FortiSOAR Knowledge Base
FortiSOAR: Security Orchestration and Response software provides innovative case management, automation, and orchestration. It pulls together all of an organization's tools, helps unify operations, and reduce alert fatigue, context switching, and the mean time to respond to incidents.
Andy_G
Staff
Staff
Article Id 193273
Description

Many of our analysts will take multiple alerts and want to group them into one incident so the relationships are done automatically, as well as their data and analysis from the multiple alerts are carried over into the Incident as a base for the incident. 


This can be achieved by setting a user prompt at one start book such as a Yes/No Picklist (make one if you don't have one), and have the variable be "groupIntoOneIncident", them set this variable to access later. Create a Decision to check if it was answered Yes/No, and have it fork to either a Map Playbook (No) or Reference Playbook (Yes). All thats left from here is to create a few steps, with one being your "Map" to set all your variables for your Incident. You can do this in your Create Record Step as well, but it can get ugly, so my preference is to add a Set Variables Step. 


Here are some example syntaxes of how you'll set your variables to be used so it takes the correct one from the source records either from loop_resource or request.data.records. What you'll want to do is just set variables for these syntaxes for all of your fields from Alerts that will map to Incidents, such as sourceip, destinationip, filename, etc. Once you've set all of your variables you want to map into Incidents, you just put in your variables into the Create Record Step. You'll have to get slightly creative to set "Messages" and "Correlations" correctly, and some sample syntaxes for those are below:


---This one is for when you want to grab the smallest time (first)

An example of this would be when you want to map the alert Create Date to a field you track that in Incidents, or you can use it to calculate Dwell Time by taking the earliest time of an Alert and the current time to figure out how long it took from the first Alert to trigger an incident. 

{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('createDate','none')|rejectattr('createDate','eq','')||map(attribute="createDate")|min}}{% else %}{{vars.loop_resource.createDate}}{% endif %}

 

---For when you expect the fields to have values you can concat

 Use this for most of your fields that you'll be brining in as strings. IPs, domains, filenames, hashes, etc. Be sure to truncate in your create record step if it the field is limited.

{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('destinationip','none')|rejectattr('destinationip','eq','')|map(attribute="destinationip")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.destinationip}}{% endif %}

 

---For picklists where you can only have one value such as severity

 So what about picklists, as you can only have one value, right? This will select the Highest severity from the group of alerts when selecting multiple alerts and set it in Incidents. If utilizing a different picklist name for Severities, just use this to map all of your severity needs either based on literal value or numerical value

{% if vars.grouped == 'Yes' %}{% set severitytest = vars.request.data.records|rejectattr('severity','none')|rejectattr('severity','eq','')|map(attribute="severity")|join(', ') %}{% if 'Critical' in severitytest %}{{ "AlertSeverity" |picklist("Critical")}}{% elif 'High' in severitytest %}{{ "AlertSeverity" |picklist("High")}}{% elif 'Moderate' in severitytest %}{{ "AlertSeverity" |picklist("Moderate")}}{% elif 'Low' in severitytest %}{{ "AlertSeverity" |picklist("Low")}}{% else %}{{ "AlertSeverity" |picklist("Informational")}}{% endif %}{% else %}{{vars.loop_resource.severity.itemValue}}{% endif %}

 

---For picklists where you can only havae one value such as severity, and you want the itemValue from the list

 Unfortunately for text fields in your Description or Incident Name you set, the syntax above provides the whole dict for the picklist, which it reads just fine, but when you want a specific field from the picklist you can do this. Its probably more complicated than necessary to get the itemValue, but it works. 

{% if vars.grouped == 'Yes' %}{% set severitytest = vars.request.data.records|rejectattr('severity','none')|rejectattr('severity','eq','')|map(attribute="severity")|join(', ') %}{% if 'Critical' in severitytest %}{% set severitylist = "AlertSeverity" |picklist("Critical") %}{{severitylist.itemValue}}{% elif 'High' in severitytest %}{% set severitylist = "AlertSeverity" |picklist("High") %}{{severitylist.itemValue}}{% elif 'Moderate' in severitytest %}{% set severitylist = "AlertSeverity" |picklist("Moderate") %}{{severitylist.itemValue}}{% elif 'Low' in severitytest %}{% set severitylist = "AlertSeverity" |picklist("Low") %}{{severitylist.itemValue}}{% else %}{% set severitylist = "AlertSeverity" |picklist("Minimal") %}{{severitylist.itemValue}}{% endif %}{% else %}{{vars.loop_resource.severity.itemValue}}{% endif %}

 

---Use for your ID fields, where you will want an array of them for updating Correlations, or iterating through them one by one for something. Granted you don't really need the blank checks or unique, but I left them in just incase something weird happens. 

{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('@id','none')|rejectattr('@id','eq','')|map(attribute="@id")|list|unique}}{% else %}[ '{{vars.loop_resource['@id']}}' ]{% endif %}

 


It will take some time to set up your Map with the Set Variable Step, but at the end the sample playbook attached only uses a total of 8 steps across two playbooks. Another plus in the sample playbook is it will add a hyperlink to the escalated alert/incident in the applicable record to easily open the escalated alert/incident in a new window. I chose this as through the life of the Alert/Incident other records may get related and wouldn't be sure right away which one it was escalated from, so this was a quick access to those records. 


If utilizing the sample attached, ensure you check your fields, as the sample has several fields that are not default and may require a quick removal from the JSON before import if you experience issues. Example of mapped fields:

  

{
              "@type": "WorkflowStep",
              "name": "Map Incident Fields",
              "arguments": {
                "md5": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('md5','none')|rejectattr('md5','eq','')|map(attribute=\"md5\")|join(', ')}}{% else %}{{vars.loop_resource.md5}}{% endif %}",
                "sha1": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('sha1','none')|rejectattr('sha1','eq','')|map(attribute=\"sha1\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.sha1}}{% endif %}",
                "sha256": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('sha256','none')|rejectattr('sha256','eq','')|map(attribute=\"sha256\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.sha256}}{% endif %}",
                "source": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('source','none')|rejectattr('source','eq','')|map(attribute=\"source\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.source}}{% endif %}",
                "weburl": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('weburl','none')|rejectattr('weburl','eq','')|map(attribute=\"weburl\")|list|unique|join(', ')|regex_replace(\"http\",\"hxxp\")|regex_replace(\"www\\.\",\"www[.]\")}}{% else %}{{vars.loop_resource.weburl|regex_replace(\"http\",\"hxxp\")|regex_replace(\"www\\.\",\"www[.]\")}}{% endif %}",
                "emailto": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('emailto','none')|rejectattr('emailto','eq','')|map(attribute=\"emailto\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.emailto}}{% endif %}",
                "filename": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('filename','none')|rejectattr('filename','eq','')|map(attribute=\"filename\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.filename}}{% endif %}",
                "severity": "{% if vars.grouped == 'Yes' %}{% set severitytest = vars.request.data.records|rejectattr('severity','none')|rejectattr('severity','eq','')|map(attribute=\"severity\")|join(', ') %}{% if 'Critical' in severitytest %}{% set severitylist =  \"AlertSeverity\" |picklist(\"Critical\") %}{{severitylist.itemValue}}{% elif 'High' in severitytest %}{% set severitylist =  \"AlertSeverity\" |picklist(\"High\") %}{{severitylist.itemValue}}{% elif 'Moderate' in severitytest %}{% set severitylist =  \"AlertSeverity\" |picklist(\"Moderate\") %}{{severitylist.itemValue}}{% elif 'Low' in severitytest %}{% set severitylist =  \"AlertSeverity\" |picklist(\"Low\") %}{{severitylist.itemValue}}{% else %}{% set severitylist = \"AlertSeverity\" |picklist(\"Minimal\") %}{{severitylist.itemValue}}{% endif %}{% else %}{{vars.loop_resource.severity.itemValue}}{% endif %}",
                "sourceid": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('sourceid','none')|rejectattr('sourceid','eq','')|map(attribute=\"sourceid\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.sourceid}}{% endif %}",
                "sourceip": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('sourceip','none')|rejectattr('sourceip','eq','')|map(attribute=\"sourceip\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.sourceip}}{% endif %}",
                "emailfrom": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('emailfrom','none')|rejectattr('emailfrom','eq','')|map(attribute=\"emailfrom\")|list|unique|join(', ')|regex_replace(\"@\",\"[@]\")}}{% else %}{{vars.loop_resource.emailfrom|regex_replace(\"@\",\"[@]\")}}{% endif %}",
                "webdomain": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('webdomain','none')|rejectattr('webdomain','eq','')|map(attribute=\"webdomain\")|list|unique|join(', ')|regex_replace(\"http\",\"hxxp\")|regex_replace(\"www\\.\",\"www[.]\")}}{% else %}{{vars.loop_resource.webdomain|regex_replace(\"http\",\"hxxp\")|regex_replace(\"www\\.\",\"www[.]\")}}{% endif %}",
                "alert_name": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('name','none')|rejectattr('name','eq','')|map(attribute=\"name\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.name}}{% endif %}",
                "input_type": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('@type','none')|rejectattr('@type','eq','')|map(attribute=\"@type\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource['@type']}}{% endif %}",
                "weburlpath": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('weburlpath','none')|rejectattr('weburlpath','eq','')|map(attribute=\"weburlpath\")|list|unique|join(', ')|regex_replace(\"http\",\"hxxp\")|regex_replace(\"www\\.\",\"www[.]\")}}{% else %}{{vars.loop_resource.weburlpath|regex_replace(\"http\",\"hxxp\")|regex_replace(\"www\\.\",\"www[.]\")}}{% endif %}",
                "emailsubject": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('emailsubject','none')|rejectattr('emailsubject','eq','')|map(attribute=\"emailsubject\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.emailsubject}}{% endif %}",
                "emailxmailer": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('emailxmailer','none')|rejectattr('emailxmailer','eq','')|map(attribute=\"emailxmailer\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.emailxmailer}}{% endif %}",
                "severity_iri": "{% if vars.grouped == 'Yes' %}{% set severitytest = vars.request.data.records|rejectattr('severity','none')|rejectattr('severity','eq','')|map(attribute=\"severity\")|join(', ') %}{% if 'Critical' in severitytest %}{{ \"AlertSeverity\" |picklist(\"Critical\")}}{% elif 'High' in severitytest %}{{ \"AlertSeverity\" |picklist(\"High\")}}{% elif 'Moderate' in severitytest %}{{ \"AlertSeverity\" |picklist(\"Moderate\")}}{% elif 'Low' in severitytest %}{{ \"AlertSeverity\" |picklist(\"Low\")}}{% else %}{{ \"AlertSeverity\" |picklist(\"Minimal\")}}{% endif %}{% else %}{{vars.loop_resource.severity.itemValue}}{% endif %}",
                "destinationip": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('destinationip','none')|rejectattr('destinationip','eq','')|map(attribute=\"destinationip\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.destinationip}}{% endif %}",
                "assignedto_iri": "{{vars.currentUser}}",
                "input_analysis": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('analysis','none')|rejectattr('analysis','eq','')|map(attribute=\"analysis\")|list|unique|join(', ')}}{% else %}{{vars.loop_resource.analysis}}{% endif %}",
                "input_iri_path": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('@id','none')|rejectattr('@id','eq','')|map(attribute=\"@id\")|list|unique}}{% else %}[ '{{vars.loop_resource['@id']}}' ]{% endif %}",
                "input_createdepoch": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('createDate','none')|rejectattr('createDate','eq','')|map(attribute=\"createDate\")|min}}{% else %}{{vars.loop_resource.createDate}}{% endif %}",
                "input_modifiedepoch": "{% if vars.grouped == 'Yes' %}{{vars.request.data.records|rejectattr('modifyDate','none')|rejectattr('modifyDate','eq','')|map(attribute=\"modifyDate\")|max}}{% else %}{{vars.loop_resource.modifyDate}}{% endif %}",
                "assignedto_friendly_lastname": "{{vars.cu_firstname}} {{vars.cu_lastname}}"
              },

  

If you're unsure about custom fields the sample has compared to your instance, take the Create Record step out of the flow and add a new one to create an incident, then only keep the mappings you want and insert them into the newly created Step, referencing the old one for syntax as necessary. Takes a little extra time, but is more definitive you won't want extraneous fields in your create record step.


Contributors