Formatting scripts validate input and modify how indicators display.
Formatting scripts transform raw, complex data into clear, human-readable outputs. This makes the data easier to read and understand, which helps make your investigations more efficient.
Note
When an IP address (or any other indicator type like a URL, domain, hash) appears in the War Room, Cortex XSOAR's indicator extraction automatically identifies it. This extracted indicator is then clickable. When you click on it, a pop-up window appears showing the indicator's details (for example, its type, value, and reputation). This pop-up data comes from the indicator's entry in the incident context, which is typically populated by the original integration command or other enrichment processes, not directly by the formatting script's markdown output.
Using formatting scripts enables you to:
Modify how indicators appear in the War Room and reports.
The script takes raw data (such as a large JSON response from an integration command) and transforms it into a more digestible format, such as a Markdown table. This formatted table is displayed as a new entry in your War Room. This is controlled by the
demisto.results()function in the script, which specifies theTypeasMARKDOWNand provides the Contents.Validate input data
The script can include logic to check specific criteria, such as verifying if a Top-Level Domain (TLD) is valid before processing or displaying it.
After indicators are extracted according to the defined regex, the defined regex finds the indicator value and the formatting script modifies the string value, so it can be used in Cortex XSOAR. For example the IP indicator uses the UnEscapeIPs formatting script, which removes any defanged characters from an IP address, such as 127[.]0[.]0[.]1 to 127.0.0.1. In the War Room, you can click on the IP address to view the extracted IP address. This extracted indicator using the formatting script is added to the Threat Intel database.
To apply a formatting script to an indicator type:
Go to → → → .
Select the indicator type, click Edit.
Select the desired formatting script.
Formatting scripts must have the
indicator-formattag to appear in the list.
Note
Formatting scripts for out-of-the-box indicator types are system level, which means that the formatting scripts for these indicator types are not configurable. To create a formatting script for an out-of-the-box indicator type, you need to disable the existing indicator type and create a new (custom) indicator type. If you configured a formatting script before this change and updated your content, this configuration reverts to content settings (empty).
Out-of-the-Box Formatting Script Examples
In the page, there are a number of out-of-the box formatting scripts, including:
UnEscapeIPsExtractDomainAndFQDNFromUrlAndEmailThis script is used by the Domain indicator, extracts domains and FQDN from URLs and emails. It removes prefixes such as proofpoint or safelinks, removes escaped URLs, and extracts the FQDN, etc.
ExtractEmailV2
CLI Execution Examples
!UnEscapeIPs input=127.0.0[.]1!UnEscapeIPs input=127.0.0[.]1,8.8.8[.]8!UnEscapeIPs input=${contextdata.indicators}(where the keycontextdata.indicatorsin the context object, is an array)
Formatting Script Input
The formatting script requires a single input argument named input that accepts a single indicator value or an array of indicator values. The input argument should be an array to accept multiple inputs and return an entry-result per input.
Argument | Description |
|---|---|
| Accepts a string or array of strings representing the indicator value(s) to be formatted. Will be accessed within the script using In the script settings, the Is Array checkbox must be checked (see screenshot below).The script code must be able to handle a single indicator value (as string), multiple indicator values in CSV format (as string) and an array of single indicator values (array). |
Formatting Script Outputs
The indicators appear as a human readable format in Cortex XSOAR. The output should be an array of formatted indicators or an array of entry results (an entry result per indicator to be created). The entry result per input can be a JSON array to create multiple indicators. If the entry result is an empty string, it is ignored and no indicator is created.
Output Code Examples
Single-value result:
demisto.results([{
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"Contents": [format(indicator)],
"EntryContext": {"Domain": format(indicator)}
}])
Multiple-value results:
entries_list = []
for indicator in argToList(demisto.args().get('input', ‘’)):
input_entry = {
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"Contents": [format(indicator)],
"EntryContext": {"Domain": format(indicator)}
}
if input_entry.get("Contents") == [""]:
input_entry['Contents'] = []
entries_list.append(input_entry)
if entries_list:
demisto.results(entries_list)
else:
# Return empty string so it wouldn't create an empty indicator.
demisto.results('')For more information about code conventions, see https://xsoar.pan.dev/docs/integrations/code-conventions#return_results.
The following use case uses a formatting script to organize raw JSON data into a table.
In the War Room CLI, the command !my-threat-intel-get-ip ip=8.8.8.8 returns the following data:
{
"ip": "8.8.8.8",
"reputation": {
"score": 90,
"category": "Malicious"
},
"associated_campaigns": [
{"name": "APT28", "id": "C001"},
{"name": "Fancy Bear", "id": "C002"}
],
"related_indicators": [
{"type": "domain", "value": "malicious.com"},
{"type": "url", "value": "http://evil.link"}
]
}In the Scripts page, create the following Python script called FormatThreatIntelOutput that takes raw_json_data as an input.
import json # Added for json.loads
def format_threat_intel_data(raw_data):
"""
Turns raw threat data into a simple table.
"""
if not raw_data:
return "No threat intelligence data found."
markdown_output = "### Threat Intelligence Summary\n\n"
markdown_output += "| Field | Value |\n"
markdown_output += "|---|---|\n"
# Get and add key details
markdown_output += f"| IP Address | {raw_data.get('ip', 'N/A')} |\n"
markdown_output += f"| Reputation Score | {raw_data.get('reputation', {}).get('score', 'N/A')} |\n"
markdown_output += f"| Reputation Category | {raw_data.get('reputation', {}).get('category', 'N/A')} |\n"
# List campaigns
campaigns = raw_data.get('associated_campaigns', [])
if campaigns:
campaign_names = ", ".join([c.get('name', 'N/A') for c in campaigns])
markdown_output += f"| Associated Campaigns | {campaign_names} |\n"
else:
markdown_output += "| Associated Campaigns | None |\n"
# List related items
related_indicators = raw_data.get('related_indicators', [])
if related_indicators:
indicator_list = []
for ind in related_indicators:
indicator_list.append(f"{ind.get('type', 'N/A')}: {ind.get('value', 'N/A')}")
markdown_output += f"| Related Indicators | {'; '.join(indicator_list)} |\n"
else:
markdown_output += "| Related Indicators | None |\n"
return markdown_output
def main():
# Get the raw data passed to the script
args = demisto.args()
raw_json_data = args.get('raw_json_data')
if not raw_json_data:
demisto.results("Error: No raw JSON data provided to the script.")
return
# Make sure the data is in the right format (a dictionary)
try:
if isinstance(raw_json_data, str):
raw_data_dict = json.loads(raw_json_data)
else:
raw_data_dict = raw_json_data
except Exception as e:
demisto.results(f"Error parsing raw JSON data: {e}")
return
formatted_output = format_threat_intel_data(raw_data_dict)
demisto.results({
'Type': demisto.CommandResults.Type.MARKDOWN,
'Contents': formatted_output,
'ContentsFormat': demisto.CommandResults.ContentsFormat.MARKDOWN
})
if __name__ == '__main__':
main()import demisto_sdk.commands.common.tools as demisto
def format_threat_intel_data(raw_data):
"""
Turns raw threat data into a simple table.
"""
if not raw_data:
return "No threat intelligence data found."
markdown_output = "### Threat Intelligence Summary\n\n"
markdown_output += "| Field | Value |\n"
markdown_output += "|---|---|\n"
# Get and add key details
markdown_output += f"| IP Address | {raw_data.get('ip', 'N/A')} |\n"
markdown_output += f"| Reputation Score | {raw_data.get('reputation', {}).get('score', 'N/A')} |\n"
markdown_output += f"| Reputation Category | {raw_data.get('reputation', {}).get('category', 'N/A')} |\n"
# List campaigns
campaigns = raw_data.get('associated_campaigns', [])
if campaigns:
campaign_names = ", ".join([c.get('name', 'N/A') for c in campaigns])
markdown_output += f"| Associated Campaigns | {campaign_names} |\n"
else:
markdown_output += "| Associated Campaigns | None |\n"
# List related items
related_indicators = raw_data.get('related_indicators', [])
if related_indicators:
indicator_list = []
for ind in related_indicators:
indicator_list.append(f"{ind.get('type', 'N/A')}: {ind.get('value', 'N/A')}")
markdown_output += f"| Related Indicators | {'; '.join(indicator_list)} |\n"
else:
markdown_output += "| Related Indicators | None |\n"
return markdown_output
def main():
# Get the raw data passed to the script
args = demisto.args()
raw_json_data = args.get('raw_json_data')
if not raw_json_data:
demisto.results("Error: No raw JSON data provided to the script.")
return
# Make sure the data is in the right format (a dictionary)
try:
if isinstance(raw_json_data, str):
import json
raw_data_dict = json.loads(raw_json_data)
else:
raw_data_dict = raw_json_data
except Exception as e:
demisto.results(f"Error parsing raw JSON data: {e}")
return
formatted_output = format_threat_intel_data(raw_data_dict)
demisto.results({
'Type': demisto.CommandResults.Type.MARKDOWN,
'Contents': formatted_output,
'ContentsFormat': demisto.CommandResults.ContentsFormat.MARKDOWN
})
if __name__ == '__main__':
main()You can now run this script as a command in the War Room CLI: !FormatThreatIntelOutput raw_json_data="<paste_raw_json_here>"
!FormatThreatIntelOutput raw_json_data="{\"ip\": \"8.8.8.8\",
\"reputation\": {\"score\": 90,
\"category\": \"Malicious\"},
\"associated_campaigns\": [{\"name\": \"APT28\",
\"id\": \"C001\"}, {\"name\": \"Fancy Bear\",
\"id\": \"C002\"}], \"related_indicators\": [{\"type\": \"domain\",
\"value\": \"malicious.com\"}, {\"type\": \"url\", \"value\": \"http://evil.link\"}]}"Note
If your JSON string contains double quotes ("), you must escape them with a backslash (\) when pasting them directly into the War Room CLI. For example, {"key": "value"} would become {\"key\": \"value\"}
After the script runs in the War Room, instead of raw JSON, you see a table with headings such as "Field" and "Value," presenting the data in a simple, digestible format. The original IP address 8.8.8.8 is still clickable within this table, showing the standard indicator details pop-up.
The following script removes a prefix from the indicators.
def formatIndicator(indicator):
return indicator.removeprefix('/')
# argToList returns the argument as is if it's already a list so no need to check here
the_input = argToList(demisto.args().get('input'))
entries_list = []
# Otherwise assumes it's already an array
for item in the_input:
input_entry = {
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"Contents": [formatIndicator(item)],
"EntryContext": {"Domain": item}
}
if input_entry.get("Contents") == ['']:
input_entry['Contents'] = []
entries_list.append(input_entry)
if entries_list:
demisto.results(entries_list)
else:
# Return empty string so it wouldn't create an empty domain indicator.
demisto.results('')