Creating the app

About this task

Make sure to create your playbook components, such as message destination, function, workflow, and rule or playbook before you start the following procedure. The URLhaus app, with the app name of fn_urlhaus, is used in the example code. Substitute it with your own app name.

Procedure

  1. If you use playbooks for your app, run codegen and specify the message destination (-m), playbook by its API name (--playbook), and any global script used by the playbook by its display name (--script). You do not need to enter any local scripts. A local script is a script that is created within the playbook and for use in that playbook only.
    resilient-sdk codegen -p fn_urlhaus -m fn_urlhaus --playbook "Playbook_URLhaus_Lookup" --script "URLHaus Script"

    The example command specifies the message destination, playbook API name, and, for this example, the only global script associated with the playbook. The --script option takes the script display name, not the API name as its parameter. The SOAR SDK finds any related customizations in the export file. For message destinations, the SDK finds the related function. For playbooks, the SDK finds the related objects, such as functions, local scripts, and tasks.

  2. If you use rules, run codegen and specify the message destination (-m) and rule (-r).
    resilient-sdk codegen -p fn_urlhaus -m fn_urlhaus -r "Example: URLhaus Lookup"

    The example command specifies the message destination (-m) and rule (-r). The SOAR SDK finds any related customizations in the export file. For message destinations, the SDK finds the related function. For rules, the SDK finds the related workflows.

  3. Open the generated boilerplate code in your code editor.
    For each function, the SDK generates a Python file under the components directory. The file contains an AppFunctionComponent.
  4. Install the generated package in editable mode.
    pip3 install -e fn_urlhaus/
  5. At the SOAR Platform, locate the app's message destination in the Destination tab. Add the SOAR API key or user account you use to run the app to the Users/API Keys field.
  6. Run Resilient® Circuits.
    resilient-circuits run
  7. Check the logs to verify that Resilient Circuits started successfully.
    2021-11-15 13:09:03,727 INFO [actions_component] resilient-circuits has started successfully and is now running...
       2021-11-15 13:09:03,728 INFO [actions_component] Subscribe to message destination 'fn_urlhaus'
       2021-11-15 13:09:03,729 INFO [stomp_component] Subscribe to message destination actions.201.fn_urlhaus
    
  8. At the SOAR Platform, start the rule then check the logs to verify that the rule was successful.
    2021-11-15 14:13:50,121 INFO [actions_component] Event: <fn_urlhaus[] (id=1, workflow=example_urlhaus_lookup, user=admin@example.com) 
    2021-11-15 14:13:49.862000> Channel: functions.fn_urlhaus
    2021-11-15 14:13:50,328 INFO [decorators] [fn_urlhaus] Validated function inputs
    2021-11-15 14:13:50,331 INFO [decorators] [fn_urlhaus] StatusMessage: Starting App Function: 'fn_urlhaus'
    2021-11-15 14:13:50,331 INFO [decorators] [fn_urlhaus] StatusMessage: Finished running App Function: 'fn_urlhaus'
    2021-11-15 14:13:50,333 INFO [decorators] [fn_urlhaus] Returning results
    
  9. Press CTRL+X to stop Resilient Circuits.
  10. Update the config_section_data method in the util/config.py file.
    config_data = u"""[fn_urlhaus]
    base_url=https://urlhaus-api.abuse.ch/v1
    """
    return config_data
  11. Run the following command to update the app.config file.
    resilient-circuits config -u
  12. Open the app.config file and confirm that a section for the app, [fn_urlhaus], was added at the end.
  13. Edit the start of the AppFunctionComponent template to access the new setting.
    # Example validating app_configs
    validate_fields([
        {"name": "base_url", "placeholder": "<api-base-url>"}],
    self.app_configs)
    ​
    yield self.status_message("base_url: '{0}'".format(self.app_configs.base_url))
  14. Run resilient-circuits and verify the results.
    1. Verify that the log contains a status message.
      2021-11-15 14:25:24,754 INFO [decorators] [fn_urlhaus] 
      StatusMessage: base_url: 'https://urlhaus-api.abuse.ch/v1'
    2. At the SOAR Platform, verify that the incident's Action shows a similar status.

  15. Validate that the function has the correct inputs.
    # Example validating required fn_inputs
        validate_fields(["urlhaus_artifact_type", "urlhaus_artifact_value"], fn_inputs)
    
  16. Add an ARTIFACT_TYPE_MAP dictionary to your AppFunctionComponent file to correctly map to the URLHaus types.
  17. Create an API call with the rc.execute method to send a request to the URLHaus API and return the results to the SOAR Platform.
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    ​
    url = "{0}/{1}".format(self.app_configs.base_url, ARTIFACT_TYPE_MAP.get(fn_inputs.urlhaus_artifact_type, ""))
    ​
    payload = {
        ARTIFACT_TYPE_MAP.get(fn_inputs.urlhaus_artifact_type, ""): fn_inputs.urlhaus_artifact_value
    }
    ​
    response = self.rc.execute(
        method="post",
        headers=headers,
        url=url,
        data=payload
    )
    ​
    results = response.json()
    ​
    yield self.status_message("Endpoint reached successfully and returning results for App Function: '{0}'".format(FN_NAME))
    ​
    yield FunctionResult(results)
    
  18. Run Resilient Circuits with the log level set to DEBUG to see the results returned by the AppFunctionComponent.
    resilient-circuits run --loglevel DEBUG
    The following example output shows the results of the URLhaus app. The results are also available in the workflow's post-process script.
    2021-11-15 14:48:09,382 DEBUG [actions_component] Result: 
    {'version': 2.0, 'success': True, 'reason': None, 'content': 
    {'query_status': 'ok', 'id': '438055', 'urlhaus_reference': 
    'https://urlhaus.abuse.ch/url/438055/', 'url': 
    'http://183.151.65.155:33955/Mozi.m', 'url_status': 'offline', 
    'host': '183.151.65.155', 'date_added': '2020-08-21 08:51:10 
    UTC', 'threat': 'malware_download', 'blacklists': 
    {'spamhaus_dbl': 'not listed', 'surbl': 'not listed'}, 
    'reporter': 'lrz_urlhaus', 'larted': 'true', 
    'takedown_time_seconds': '308582', 'tags': ['elf', 'Mozi'], 
    'payloads': [{'firstseen': '2020-08-21', 'filename': None, 
    'file_type': 'elf', 'response_size': '95268', 'response_md5': 
    '9a111588a7db15b796421bd13a949cd4', 'response_sha256': 
    'e15e93db3ce3a8a22adb4b18e0e37b93f39c495e4a97008f9b1a9a42e1fac2b0', 
    'urlhaus_download': 'https://urlhaus-api.abuse.ch/v1/download/e15e93db3ce3a8a22adb4b18e0e37b93f39c495e4a97008f9b1a9a42e1fac2b0/', 
    'signature': None, 'virustotal': 
    {'result': '41 / 56', 'percent': '73.21', 'link': 
    'https://www.virustotal.com/gui/file/e15e93db3ce3a8a22adb4b18e0e37b93f39c495e4a97008f9b1a9a42e1fac2b0/detection/f-e15e93d'}, 
    'imphash': None, 'ssdeep': '1536:pymLLU1F5kHIrIj0D6rhfd+lK3exiTCzxNtI4sZLi6UEbFEBFaW1EH6t6wfPP/Q:2F+ooxalK3exiTOijZLdUEbFlWPP/Q', 'tlsh': 
    'B2930272135417C5894772B4209409F5363AA265FCBF34FBBF93C66027834BCD49BAA2'}]}, 'raw': None, 'inputs': {'urlhaus_artifact_type': 
    'URL', 'urlhaus_artifact_value': 
    'http://183.151.65.155:33955/Mozi.m'}, 'metrics': {'version': '1.0', 'package': 'fn-urlhaus', 'package_version': '1.0.0', 'host': 'xxx.local', 'execution_time_ms': 244, 'timestamp': '2021-11-15 14:48:09'}}

Results

The resulting AppFunctionComponent code is available and is similar to the following example code.
# -*- coding: utf-8 -*-
​
"""AppFunction implementation"""
​
from resilient_circuits import AppFunctionComponent, app_function, FunctionResult
from resilient_lib import IntegrationError, validate_fields
​
PACKAGE_NAME = "fn_urlhaus"
FN_NAME = "fn_urlhaus"
​
ARTIFACT_TYPE_MAP = {
    "DNS Name": "host",
    "IP Address": "host",
    "Malware MD5 Hash": "payload:md5_hash",
    "Malware SHA-256 Hash": "payload:sha256_hash",
    "Server Name": "host",
    "String": "tag",
    "URL": "url"
}
​
​class FunctionComponent(AppFunctionComponent):
    """Component that implements function 'fn_urlhaus'"""
​
    def __init__(self, opts):
        super(FunctionComponent, self).__init__(opts, PACKAGE_NAME)
​
    @app_function(FN_NAME)
    def _app_function(self, fn_inputs):
        """
        Function: Perform a lookup on several artifacts of types
        Inputs:
            -   fn_inputs.urlhaus_artifact_value
            -   fn_inputs.urlhaus_artifact_type
        """
​
        yield self.status_message("Starting App Function: '{0}'".format(FN_NAME))
​
        # Example validating app_configs
        validate_fields([
            {"name": "base_url", "placeholder": "<api-base-url>"}],
            self.app_configs)
​
        yield self.status_message("base_url: '{0}'".format(self.app_configs.base_url))
​
        # Example validating required fn_inputs
        validate_fields(["urlhaus_artifact_type", "urlhaus_artifact_value"], fn_inputs)
​
        ##############################################
        # PUT YOUR FUNCTION IMPLEMENTATION CODE HERE #
        ##############################################
​
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
​
        url = "{0}/{1}".format(self.app_configs.base_url, ARTIFACT_TYPE_MAP.get(fn_inputs.urlhaus_artifact_type, ""))
​
        payload = {
            ARTIFACT_TYPE_MAP.get(fn_inputs.urlhaus_artifact_type, ""): fn_inputs.urlhaus_artifact_value
        }
​
        response = self.rc.execute(
            method="post",
            headers=headers,
            url=url,
            data=payload
        )
​
        results = response.json()
​
        yield self.status_message("Endpoint reached successfully and returning results for App Function: '{0}'".format(FN_NAME))
​
        yield FunctionResult(results)