Skip to main content
Skip table of contents

My First Custom Integration Command

LAST UPDATED: DEC 23, 2024

Custom Commands.drawio (2)-20241211-191240.png

Users can develop their own integration commands and utility commands, using Python scripts or playbook-driven workflows. For simplicity and ease of onboarding, the focus of this article will be on building a Python integration command for the VirusTotal v3 integration.

Developing a Python Custom Command

  1. Navigate to the VirusTotal v3 integration.

    Frame 57 (9)-20241210-191710.png
    1. Click on the Configuration navigational link.

    2. Click on the image 37 (1)-20241204-202434.png (Integrations) module.

    3. Input VirusTotal v3 in the search field, then press the Enter key.

    4. Select the integration.

  2. Click on the + Custom Command button.

    Frame 58 (4)-20241210-191835.png
  3. Configure and create the command.

    Frame 59 (4)-20241210-192111.png
    1. Enter a unique command name.

    2. Click on the + Add button.

READER NOTE

The text below the custom command name is its internal name, automatically generated from the user’s input. It is this internal name that is used as the command’s Python function name. D3 recommends using this internal name as the input parameter name for easier code matching.

  1. Replace the auto-generated custom command stub function with the following code:

PY
import requests
import json

# Retrieves the server URL, API version and API key from the runtime.
serverUrl = runtime["connector"].get("serverurl").strip("/")
version = runtime["connector"].get("version").strip()
apikey = runtime["connector"].get("apikey").strip()

# Constructs the base URL for API requests using the server URL and version.
BASE_URL = serverUrl + "/api/" + version

# Defines headers for the API requests.
HEADERS = {
    "x-apikey": apikey
}

def isJson(inputString):
    """
    Checks if the given string is in JSON format.
    :param inputString: The string to verify.
    :return: True if valid JSON, False otherwise.
    """
    try:
        json.loads(inputString)  # Tries to parse the input string as JSON.
        return True  # Returns True if the input string is a valid JSON.
    except json.JSONDecodeError:
        return False  # Returns False if parsing fails, indicating the input is not a valid JSON.

def Getdomainreport(*args):
    """
    Fetches the domain report for the provided domain name.
    :param args: Domain name (required).
    :return: Command output model.
    """
    errors = []  # Initializes a list to store error messages encountered during execution.
    returnData = "Successful"  # Sets "Successful" as the default return status.
    context = []  # Initializes an empty list to hold any additional context data.

    raw = {
        "Results": [],  # Stores the original JSON response.
        "D3Errors": []  # Stores error messages if any errors are encountered.
    }
    keyFields = {}  # Initializes key fields that might be required for subsequent commands.
    resultData = {}  # Initializes result data to store relevant extracted fields.

    try:
        if not args:  # Checks if no arguments (i.e., domain names) are provided.
            errors.append("Domain is required.")  # Appends an error message indicating that a domain name is required.
        else:
            actionUrl = f"/domains/{args[0].strip()}"  # Constructs the specific API endpoint URL for the domain report using the provided domain name.
            httpMethod = "GET"  # Sets the HTTP method used for this request.

            response = requests.request(httpMethod, f"{BASE_URL}{actionUrl}", headers=HEADERS, verify=False)  # Sends the HTTP request with the specified method, URL, headers, and query parameters.

            if not response.ok:  # Checks if the response status code indicates failure (outside the 200–299 range).
                originalResponse = response.json() if isJson(response.text) else response.text  # Assigns the Python-converted JSON to originalResponse if the response is valid JSON, else assigns the raw text.

                raw["D3Errors"].append({  # Appends error information to the raw "D3Errors" list for debugging.
                    "FailedAction": f"Failed to get domain report for domain ({args[0]}).",  # Describes the failed action.
                    "StatusCode": response.status_code,  # Logs the HTTP status code of the failed request.
                    "Reason": response.reason,  # Logs the reason phrase returned by the server.
                    "Message": originalResponse  # Logs the original response message or JSON.
                })
            else:  # Executes if the response status code indicates success (within the 200–299 range).
                responseJson = response.json()  # Assigns the Python-converted JSON to responseJson.
                attributes = responseJson.get("data", {}).get("attributes", {})  # Assigns a value associated with a key (at a certain level of nesting) from the response to attributes.

                raw["Results"].append(responseJson)  # Appends the full response JSON to the "Results" list.

                resultData = {  # Extracts relevant fields for the result data.
                    "domain": args[0],  # Sets the domain name provided in the arguments.
                    "reputation": attributes.get("reputation", ""),  # Sets the domain reputation value.
                    "suspicious": attributes.get("last_analysis_stats", {}).get("suspicious", ""),  # Sets the number of suspicious findings.
                    "malicious": attributes.get("last_analysis_stats", {}).get("malicious", "")  # Sets the number of malicious findings.
                }

    except Exception as err:
        errors.extend(err.args)  # Extends the errors list with the details of the exception.

    if errors or len(raw["D3Errors"]) > 0:  # Checks if any errors are encountered or if there are logged "D3Errors."
        returnData = "Failed"  # Sets the return status to "Failed" if any errors occur.
        errors.extend(raw["D3Errors"])  # Appends the logged "D3Errors" to the error list.

    # Returns the command output model with all the gathered information.
    return pb.returnOutputModel(
        resultData,
        returnData,
        keyFields,
        context,
        raw,
        errors
    )

READER NOTE

Frame 60 (4)-20241210-195754.png
  1. Add a Domain input parameter.

    Frame 65 (15)-20241210-211807.png
    1. Navigate to the Overview tab.

    2. Click on the Inputs tab.

    3. Click on the + New Input Parameter button.

    4. Input the parameter details, then click on the + Add button.

      • Set the Parameter Name to Getdomainreport

      • Set the Display Name to Domain

      • Set the Parameter Type to Text

      • Set the Is Required? field to Yes

      • Set the Description to The domain to analyze

      • Set the Sample Data to xmr.pool.minergate.com

  2. Verify that the Domain parameter has been added, then click on the button.

    Frame 66 (9)-20241210-212051.png
  3. Test the command.

    Frame 67 (5)-20241210-212843.png
    1. Create a new connection or select an existing one. See the integration’s documentation for details.

    2. Paste in the domain from the sample data in step 6, or enter a different domain.

    3. Click on the Test Command button, then observe the test results.

Raw Data

Users can view the raw data by selecting the Raw Data tab.

Frame 68 (7)-20241210-213909.png
  1. Enable this command for use in specific areas within the D3 platform.

    Frame 76 (3)-20241211-005401.png

    For now, ensure the following options are selected:
    Command Task
    Conditional Task
    Ad-hoc Command

Examples of Command Usage Areas

Command Task

Frame 71 (4)-20241210-233707.png

Conditional Task

Frame 73 (3)-20241210-233811.png

Ad-hoc Command (Incident Workspace)

Frame 93 (3)-20241223-180116.png
  1. Click on the Submit button.

    Frame 61 (5)-20241210-200330.png
  2. Click on the Submit button within the Submit Command popup.

    Frame 64 (11)-20241210-202543.png

The custom command is now live and ready to be used.

Frame 69 (5)-20241210-214410.png

FAQs

How can users debug their Python code?

Users can use pb.log(), analogous to console.log() in JavaScript. Both are used to output messages, variables, or data structures for debugging, troubleshooting and validating command behavior.

These logs appear in the Test Result popup, under the Custom Log tab.

EXAMPLE pb.log(actionUrl) can be inserted after the line actionUrl = f"/domains/{args[0].strip()}" to log the actionUrl value. If gmail.com is provided as the Domain input parameter, the log will display the following:

Why are there commented code above the header of the new custom command function?

The commented code provides context to help users better understand the underlying logic when customizing a cloned command.

Besides the connector, what else should I know about the runtime dictionary?
  • For the basic example in this article, there is nothing else required.

  • For advanced usage, users should understand runtime libraries like updateTable, passDown, and others.

Why might I use the context dictionary, and is it still recommended?

The context data is reserved for specific tasks with specialized use cases. In most scenarios, D3 advises against using context data for return data.

Why might I use key fields, and how does its use differ from context data?

The keyFields dictionary is meant for collecting critical values for subsequent tasks, differing from the context field, which has reserved and specific use cases.

Why am I seeing the "Error! Command function(s) not found in Python script" alert?

Ensure that your custom commands' internal names (not display names) are used for their function names.

Frame 62 (13)-20241210-201202.png

Submitting a Python script without matching internal name will result in the following alert:

Is pb.returnOutputModel() responsible for generating the HTML table within the result data?

Yes, if resultData contains a valid JSON or JSON array, the D3 system converts it into an HTML table automatically. If resultData is plain text or HTML, it will be displayed directly as it is, without conversion into a table.

In the sample code provided in step 4, the structure of resultData was as follows:

PY
resultData = {
    "domain": args[0], 
    "reputation": attributes.get("reputation", ""),  
    "suspicious": attributes.get("last_analysis_stats", {}).get("suspicious", ""), 
    "malicious": attributes.get("last_analysis_stats", {}).get("malicious", "") 
}

This would eventually resolve into something like:

JSON
{
    "domain": "xmr.pool.minergate.com",
    "reputation": -2,
    "suspicious": 1,
    "malicious": 8
}

Since this is a JSON object, the rendered HTML table in the Result tab will look something like this:

Domain

Reputation

Suspicious

Malicious

example.com

-2

1

8

Regarding the line atts = r.json().get("data", {}).get("attributes", {}), is it the user's responsibility to refer to the API documentation for JSON response structures?

Yes, most of D3's integration documents include relevant API references. For the example in this article, you may refer to the VirusTotal Domains Object API documentation.

Does the order of arguements passed in pb.returnOutputModel() matter?

Yes, the order of arguments is always result data, followed by return data, then key fields, context data, raw data, and finally errors.

Are all six arguments required for calling pb.returnOutputModel()?

No. None of the parameters are mandatory. If no parameters are provided, nothing will be displayed in the UI.

What are the "Transform Command" and "Allow command to be run on agent" features used for?

Transform Command

  • This feature was designed for earlier versions of the D3 vSOC platform, and has been deprecated since version 14. Data transformations are now primarily handled by the Data Formatter task. The Transform Command UI components are subject to removal in future releases.

Allow command to be run on agent

  • D3’s vSOC server supports HTTP API requests by default, as they address the majority of command use cases. For specific scenarios, such as commands that require non-HTTP protocols like SMTP or LDAP, or cloud-based integrations that whitelist only the client’s IP instead of D3’s IP, the Allow command to be run on agent feature enables the command to execute locally on the machine where the D3 Proxy Agent is installed, instead of on D3’s vSOC server.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.