Use custom views to render results in your app
The Splunk Phantom platform 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 Phantom renders the custom view in Splunk Mission Control.
Splunk Phantom 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 Phantom DNS app
The Splunk Phantom DNS App comes preinstalled on the Splunk Phantom OVA.
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:
- Use the app JSON to select a custom view function in the render section of the action.
- In a separate Python file, implement the view function specified in the app JSON.
- The view function returns the Django template HTML file used to render the result context.
- Use the Django template HTML file in the DNS App directory.
- Package the view, template, and other code in the app installer. The results display on the Investigation page in Splunk Phantom.
Use the render section of the app JSON
To configure a custom view function in the app JSON, follow these steps:
- In the app JSON, go to the render section.
- Select type as custom.
- Select view as the module function. The view value is made up of two parts:
- The Python file name,
dns_view
. - The function within the Python file,
display_ips
.
- The Python file name,
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:
{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': []}}
{u'record_info': u'172.217.6.78', u'cannonical_name': u'google.com.', u'total_record_infos': 1}
[{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 everyActionResult
object that is added in the action handler, usually one per action. Theget_ctx_result(...)
function converts the result object into a context dictionary by performing the following steps:
- Initialize the
ctx_result
dictionary. - Get the
param
from the result object and set it to the param key ofctx_result
. - Get the
summary
from the result object and set it to the summary key ofctx_result
. - 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:
- 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. - Make the changes required inside the
div
tag commented withMain Div
. Loop over the data you added to context in your view function. For example, if you added the results to context throughcontext['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.
Do not create new Django template tags. Use python code inside a custom view instead. If you have previously registered template tags in Django to use with your app, you should refactor that code to use a custom view.
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.
Use data paths to present data to the Splunk Phantom web interface | Use REST handlers to allow external services to call into Splunk Phantom |
This documentation applies to the following versions of Splunk® Phantom (Legacy): 4.9, 4.10, 4.10.1, 4.10.2, 4.10.3, 4.10.4, 4.10.6, 4.10.7
Feedback submitted, thanks!