Splunk® SOAR (Cloud)

Develop Apps for Splunk SOAR (Cloud)

The classic playbook editor will be deprecated in early 2025. Convert your classic playbooks to modern mode.
After the future removal of the classic playbook editor, your existing classic playbooks will continue to run, However, you will no longer be able to visualize or modify existing classic playbooks.
For details, see:

Use custom views to render results in your app

lets app authors use a custom view by rendering the results of an action in a tabular format without writing a single line of rendering code. The author lists the data paths in the output section of an action, specifies the column names and order for each data path of the table view, and then Splunk SOAR renders the custom view in Splunk Mission Control.

also provides a JSON view. The JSON view can be a useful placeholder that supports contextual actions based on the contains specified in the output list of the action in the app JSON. The app author doesn't need to specify the column order or names for this view. The author only sets the type as JSON in the render dictionary.

In complex scenarios, such as in a detonate file, the tabular or JSON view might not be the most effective way to render action results. The data can display, but using a custom view is often simpler.

Example: Creating a custom view in the Splunk SOAR DNS app

The Splunk SOAR DNS App comes preinstalled on .

The DNS App uses a custom view to render the results of the lookup domain action. The action results are separated into multiple tables. The first table displays key-value pairs. The second table shows IPs that the action returns. Contextual actions using contains are also implemented in custom views.

Implement a custom view

A custom view implementation uses the Django template framework. To implement a custom view, follow these steps:

  1. Use the app JSON to select a custom view function in the render section of the action.
  2. In a separate Python file, implement the view function specified in the app JSON.
  3. The view function returns the Django template HTML file used to render the result context.
  4. Use the Django template HTML file in the DNS App directory.
  5. Package the view, template, and other code in the app installer. The results display on the Investigation page in .

Use the render section of the app JSON

To configure a custom view function in the app JSON, follow these steps:

  1. In the app JSON, go to the render section.
  2. Select type as custom.
  3. Select view as the module function. The view value is made up of two parts:
    1. The Python file name, dns_view.
    2. The function within the Python file, display_ips.

Every action can have its own view function and its own HTML template file.

The following code shows what the render section of the app JSON looks like:

"render": {
    "type": "custom",
    "width": 10,
    "height": 5,
    "view": "dns_view.display_ips",
    "title": "Lookup Domain"
    },

Using the view function

The view function creates context, similar to a Python dictionary, that the template code uses to render data. The following code shows the view function section of the DNS App:

def display_ips(provides, all_app_runs, context):

    context['results'] = results = []
    for summary, action_results in all_app_runs:
        for result in action_results:

            ctx_result = get_ctx_result(result)
            if (not ctx_result):
                continue
            results.append(ctx_result)
    # print context
    return 'display_ip.html'

The following table shows the parameters used in the view function.

Field Description
provides The name of the action that was run. For example, lookup domain.
all_app_runs A list of tuples containing all the results of the action run. For example, [({u'total_objects': 1, u'total_objects_successful': 1}, [<ActionResult>])]
context The context with which your template renders. You might change the context, but you're limited to JSON-serializable data types. Before your template is rendered, the respective Django model objects override the container and app keys.
{
    u'QS': {
        u'app_run': [u'3'],
        u'container': [u'']
        },
    u'container': 1,
    u'no_connection': False,
    u'app': 19,
    u'dark_title_logo': u'dns_876ab991-313e-48e7-bccd-e8c9650c239c/logo_phantom_dark.svg',
    u'google_maps_key': False,
    u'title_logo': u'dns_876ab991-313e-48e7-bccd-e8c9650c239c/logo_phantom.svg'
}
ActionResult This field represents the results of the action run. You can use the following methods to access the data:
  • get_param(): Get the parameters used to call the action. For example,
{u'domain': u'google.com', u'type': u'A', u'context': {u'guid': u'0b8d526f-991a-4926-94d4-ca51f72b4302', u'artifact_id': 0, u'parent_action_run': []}}
  • get_summary(): Get a summary of the results. For example,
 {u'record_info': u'172.217.6.78', u'cannonical_name': u'google.com.', u'total_record_infos': 1}
  • get_data(): Get the raw result data. For example,
 [{u'record_info_objects': [{u'record_info': u'172.217.6.78'}], u'record_infos': [u'172.217.6.78']}]

To use the view function, modify the following items:

  • Change the returned HTML file to be specific to the custom view's action. In this example, it appears as display_ip.html.
  • You can either modify the get_ctx_result(...) function that is called for every result object, or you can code your own function. The result object represents every ActionResult object that is added in the action handler, usually one per action. The get_ctx_result(...) function converts the result object into a context dictionary by performing the following steps:
  1. Initialize the ctx_result dictionary.
  2. Get the param from the result object and set it to the param key of ctx_result.
  3. Get the summary from the result object and set it to the summary key of ctx_result.
  4. Get the data from the result object and set it to the data key of ctx_result. The data is converted from a list to a dictionary.

The get_ctx_result(...) function defined in the dns_view.py file looks like the following:

def get_ctx_result(result):

    ctx_result = {}
    param = result.get_param()
    summary = result.get_summary()
    data = result.get_data()

    ctx_result['param'] = param

    if (data):
        ctx_result['data'] = data[0]

    if (summary):
        ctx_result['summary'] = summary

     return ctx_result

The function performs the following steps:

ctx_result['data'] = data[0]

You can add data in the action handler by using the ActionResult::add_data(...) API. This interface always keeps data as a list which makes rendering code in the template more straightforward. The get_ctx_result(...) function converts data from a list of one item to a dictionary.

Use the template file

The template file is an HTML file given to the context dictionary, which it parses using Django template language and generates code that is responsible for rendering the data. Search for Django's template language documentation for more information on how to access context variables and interact with them. In its most basic form, the template file consists of the following:

View an example of the template file.

{% extends 'widgets/widget_template.html' %}
{% load custom_template %}
​
{% block custom_title_prop %}{% if title_logo %}style="background-size: auto 60%; background-position: 50%; background-repeat: no-repeat; background-image: url('/app_resource/{{ title_logo }}');"{% endif %}{% endblock %}
{% block title1 %}{{ title1 }}{% endblock %}
{% block title2 %}{{ title2 }}{% endblock %}
{% block custom_tools %}
{% endblock %}
​
{% block widget_content %} <!-- Main Start Block -->
​
<!-- File: display_ip.html
  Copyright (c) 2016-2020 Splunk Inc.
​
  SPLUNK CONFIDENTIAL - Use or disclosure of this material in whole or in part
  without a valid written license from Splunk Inc. is PROHIBITED.
-->
​
<style>
​
​
.dns-display-ip a:hover {
  text-decoration:underline;
}
​
​
.dns-display-ip .wf-table-vertical {
  width: initial;
  font-size: 12px;
}
​
.dns-display-ip .wf-table-vertical td {
  padding: 5px;
  border: 1px solid;
}
​
.dns-display-ip .wf-table-horizontal {
  width: initial;
  border: 1px solid;
  font-size: 12px;
}
​
.dns-display-ip .wf-table-horizontal th {
  text-align: center;
  border: 1px solid;
  text-transform: uppercase;
  font-weight: normal;
  padding: 5px;
}
​
.dns-display-ip .wf-table-horizontal td {
  border: 1px solid;
  padding: 5px;
  padding-left: 4px;
}
​
.dns-display-ip .wf-h3-style {
  font-size : 20px
}
​
.dns-display-ip .wf-h4-style {
  font-size : 16px
}
​
.dns-display-ip .wf-h5-style {
  font-size : 14px
}
.dns-display-ip .wf-subheader-style {
  font-size : 12px
}
​
</style>
<div class="dns-display-ip" style="overflow: auto; width: 100%; height: 100%; padding-left:10px; padding-right:10px"> <!-- Main Div -->
  {% for result in results %} <!-- loop for each result -->
<br>
​
<!------------------- For each Result ---------------------->
​
<h3 class="wf-h3-style">Info</h3>
<table class="wf-table-vertical">
{% if result.param.domain %}
<tr>
  <td><b>Domain</b></td>
  <td>
    <a href="javascript:;" onclick="context_menu(this, [{'contains': ['domain'], 'value': '{{ result.param.domain|escapejs }}' }], 0, {{ container.id }}, null, false);">
    {{ result.param.domain }}
     <span class="fa fa-caret-down" style="font-size: smaller;"></span>
    </a>
  </td>
</tr>
<tr>
  <td><b>Type</b></td>
  <td>
    {{ result.param.type }}
  </td>
</tr>
{% endif %}
</table>
​
<br>
​
    <!-- IPs -->
      {% if result.data.record_infos %}
      <table class="wf-table-horizontal">
        {% if result.param.type == 'A' or result.param.type == 'AAAA' %}
          <tr><th>IP</th></tr>  
        {% else %}
          <tr><th>Record Info</th></tr>
        {% endif %}
        {% if result.param.type == 'A' or result.param.type == 'AAAA' %}
          {% for record_info in result.data.record_infos %}
          <tr>
            <td><a href="javascript:;" onclick="context_menu(this, [{'contains': ['ip', 'ipv6'], 'value': '{{ record_info|escapejs }}' }], 0, {{ container.id }}, null, false);">
                {{ record_info }}
                 <span class="fa fa-caret-down" style="font-size: smaller;"></span>
            </a></td>
          </tr>
          {% endfor %}
        {% else %}
          {% for record_info in result.data.record_infos %}
            <tr>
              <td>{{ record_info }}</td>
            </tr>
          {% endfor %}
        {% endif %}
​
      </table>
      <br>
      {% else %}
      <p> No Record Info in results </p>
      {% endif %}
​
​
<!------------------- For each Result END ---------------------->
​
{% endfor %} <!-- loop for each result end -->
</div> <!-- Main Div -->
​
{% endblock %} <!-- Main Start Block -->

You can use the supplied display_ip.html template as the basis for your own custom view template. However, you must make the following changes:

  1. Put any custom styles your app might require inside <style></style> tags and before the code that renders the actual results. Do not set any colors for text because they might not render properly.
  2. Make the changes required inside the div tag commented with Main Div. Loop over the data you added to context in your view function. For example, if you added the results to context through context['results'], change the following {% for result in results %}...{% endfor %} in the template. For the context menu to work in your template, include the following tag in your template with the appropriate data:

<a href="javascript:;" onclick="context_menu(this, [{'contains': ['ip', 'ipv6'], 'value': '{{ record_info|escapejs }}' }], 0, {{ container.id }}, null, false);"> {{ record_info }}  <span class="fa fa-caret-down" style="font-size: smaller;"></span> </a> For more information, see Use the contains parameter to configure contextual actions.

Modify the render code for each result

The DNS App renders two tables as shown in the following example.

Here is the code from the first table:

     <h3 class="wf-h3-style">Info</h3>
     <table class="wf-table-vertical">
     {% if result.param.domain %}
     <tr>
          <td><b>Domain</b></td>
          <td>
               <a href="#" onclick="context_menu(this, [{'contains': ['domain'], 'value': '{{ result.param.domain|escapejs }}' }], 0, {{ container.id }}, null, false); return false;">
     {{ result.param.domain }}
      
               </a>
          </td>
     </tr>
     <tr>
          <td><b>Type</b></td>
          <td>
               {{ result.param.type }}
     </td>
     </tr>
     {% endif %}
     </table>

The second table is made up of one column that lists all the IPs that the domain resolves in. The code for this table looks like this:

     <!-- IPs -->
          {% if result.data.ips %}
          <table class="wf-table-horizontal">
               <tr><th>IP</th></tr>
               {% for curr_ip in result.data.ips %}
              <tr>
                    <td><a href="#" onclick="context_menu(this, [{'contains': ['ip'], 'value': '{{ curr_ip|escapejs }}' }], 0, {{ container.id }}, null, false); return false;">
                         {{ curr_ip }}
                          <span class="fa fa-caret-down" style="font-size: smaller;"></span>
                    </a></td>
               </tr>
               {% endfor %}
          </table>
          <br>
          {% else %}
          <p> No IPs in results </p>
          {% endif %}

The following statements describe the tables :

  • The first if statement only creates the table if IPs are present in the data dictionary
  • Next is the table definition.
  • The first row sets the header name IP.
  • The other rows are added in a loop for the number of items in the result.data.ips.
  • The anchor for the contextual actions for each IP are added in its own cell.

Use the app_resource endpoints to include files in custom views

If your custom views need to include other assets, the app_resource endpoints are a good way to retrieve files from an app by using HTTP. These endpoints can retrieve any files that belong to an app, including custom HTML files, the app logo, or other custom resources your app can define. You can then include these files in your custom view. The following endpoint shows what a general app_resource looks like. Make sure you include the app name, and the app's Globally Unique Identifier (GUID) when using the endpoint.

https://<instance_ip>/app_resource/<appname>_<appguid>/path/to/file 

In the following example request, the endpoint is used to request the MaxMind app logo.

https://<instance_ip>/app_resource/maxmind_c566e153-3118-4033-abda-14dd9748c91a/logo_maxmind_dark.svg 

Debug custom views

Your custom view is executed by an app interface module and all errors are recorded in the /var/log/phantom/app_interface.log file, unless your function was incorrectly defined or your template is invalid, in which case it is logged to /var/log/phantom/wsgi.log.

Using Python logging to debug a custom view

You can use Python logging in your REST handler to debug a custom view. Any logging at level WARNING or higher is logged into the /var/log/phantom/app_interface.log file. Use the following code to define your logger:

import logging

logger = logging.getLogger(__name__)

After you define your logger, use the following code to log a message: logger.warning('Example warning')

Debug a ViewDoesNotExist exception

The ViewDoesNotExist exception occurs due to the following issues:

  • The __init__.py file is missing from the app folder.
  • The view file name doesn't match the name specified in the JSON file.
  • The function name doesn't match the name specified in the JSON file.
Last modified on 18 September, 2024
Use datapaths to present data to the web interface   Use REST handlers to allow external services to call into Splunk SOAR

This documentation applies to the following versions of Splunk® SOAR (Cloud): current


Was this topic useful?







You must be logged into splunk.com in order to post comments. Log in now.

Please try to keep this discussion focused on the content covered in this documentation topic. If you have a more general question about Splunk functionality or are experiencing a difficulty with Splunk, consider posting a question to Splunkbase Answers.

0 out of 1000 Characters