Cortex XSOAR Tips & Tricks – Leveraging dynamic sections – number widgets
2023-2-28 16:0:0 Author: blog.nviso.eu(查看原文) 阅读量:27 收藏


Introduction

Cortex XSOAR is a security oriented automation platform, and one of the areas where it stands out is customization.

A recurring problem in a SOC is data visualization, analysts can be swarmed with information, and finding out what piece of data is currently both relevant and significant can become hard. One of our tasks as SOAR engineers is to ease the decision process for analysts, we do so by providing additional contextual information about the incidents they handle, directly within the incident layout. In this objective, we incorporate number widgets into the analyst interface, these allow us to tell more visual stories about the security incidents we manage in XSOAR. From raw and sometimes unorganized data, they let us bring up eye-catching depictions of elements that can help in assessing the impact and veracity of a detection.

Objectives

In this blogpost, we will focus on the use of number widgets.

We will show you how to make use of them for outputting information to the war room, incidents, indicators and dasbhoards. On top of that we will also cover how to add trends information and even how to integrate them into a dashboard with a dynamic query. In the previous post in the series, we looked at dynamic sections in Cortex XSOAR and how to leverage them to display text in a tree like way. If you are not familiar with Cortex XSOAR and dynamic sections, please read the previous post in the series.

We previously saw that we could use dynamic dections to display text, but there are a few other options available to us. These options are broken down here. In this post, we will:

  • Start with a simple example that runs a static query against Microsoft Sentinel and lets us display a single number widget.
  • Continue with extracting a second number from our query to populate the trend of the number we display.
  • Bring our widget to a dashboard
  • Make our dashboard widget read the date range selected by the user and modify the Sentinel query accordingly.

Let’s begin with a new automation and follow the instructions available in the number widget example of the PaloAlto documentation. When we run their example, we get the following result:

Figure 1: War Room output of the code example available in the Cortext XSOAR documentation

As expected, the example works out of the box. Let’s now go and make the widget display data from Microsoft Sentinel.

A static number from sentinel

To display data pulled from Microsoft Sentinel (Microsoft Azure’s cloud native SIEM), we first need to call an integration command. Here we use an instance of the Azure Log Analytics integration available in the Cortex XSOAR marketplace:

res = demisto.executeCommand(
	"azure-log-analytics-execute-query", {
		"query": THE_QUERY
	}
)

We need a query to run, we will develop it on Sentinel before using it from Cortex XSOAR.

We will be looking at entries in SecurityIncident, a table that holds information about the security incidents present in your Sentinel deployment. We will query that table, and count the number of distinct incidents in a given month. The query we will use for that is the following:

SecurityIncident
| where TimeGenerated between (
    datetime("2022-10-01T00:00:00+00:00")
    ..
    datetime("2022-11-01T00:00:00+00:00"))
| summarize count()
Figure 2: Screenshot of a Microsoft Sentinel query and it’s results: single value

Now that we know our query works, we will port it to Cortex XSOAR. We start by duplicating our previous automation and adding code to call the integration with the Sentinel query.

res = demisto.executeCommand("azure-log-analytics-execute-query", {
"query": """SecurityIncident
| where TimeGenerated between(
	datetime("2022-10-01T00:00:00+00:00")
	..
	datetime("2022-11-01T00:00:00+00:00"))
| summarize count()"""
})

We need to extract the count_ we could observe in the results of Sentinel, let’s inspect the res object returned to us by the integration.

Figure 3: Debug view in PyCharm

Upon inspection of the returned object, we identify that we can use the following logic to extract the count of incidents

counts = []

for result in results:
    if not (
        isinstance(result, dict)
        and
        isinstance(contents := result.get("Contents"), list)
    ):
        continue
    for content in contents:
        if (
            isinstance(content, dict)
            and
            isinstance(count := content.get("count_"), int)
        ):
            counts.append(count)

total_count = sum(counts)

With the total_count obtained, we can simply change the hardcoded number from our previous widget and replace it with the value we just fetched:

demisto.results(
    {
        "Type": 17,
        "ContentsFormat": "number",
        "Contents": {
            "stats": total_count,
            "params": {
                "name": "Incidents Last Month",
                "colors": {
                    "items": {
                        "green": {
                            "value": 40
                        }
                    }
                }
            }
        }
    }
)

In the snippet above we use demisto.results(), this function let’s us write to the standard output that will be read by Cortex XSOAR. More possibilities for returning data from an automation are available in this documentation page: Python code conventions, returning data. Here we use the type 17 in the data we return, this is the type associated to widgets, the list of all defined types is available here.

Upon running our new automation, we get the exact same number previously obtained through Sentinel:

Figure 4: War room view of the widget outputted by the “Single value from Sentinel” code snippet

Adding a trend

We already have the number of alerts from last month pulled into XSOAR and displayed as a widget, let’s continue and also pull the count for the previous month. Our query to Sentinel now becomes:

SecurityIncident
| where TimeGenerated between (
    datetime("2022-10-01T00:00:00+00:00")..
    datetime("2022-11-01T00:00:00+00:00"))
| extend same = 1
| union (
    SecurityIncident
    | where TimeGenerated between (
        datetime("2022-09-01T00:00:00+00:00")..
        datetime("2022-10-01T00:00:00+00:00"))
    | extend same = 2)
| summarize count() by same
Figure 5: Screenshot of a Microsoft Sentinel query and it’s results: two values

Correspondingly, our querying and extracting code becomes:

this_month_counts = list()
last_month_counts = list()

lookup = {
    1: this_month_counts,
    2: last_month_counts
}

for result in results:
    if not (
        isinstance(result, dict)
        and
        isinstance(contents := result.get("Contents"), list)
    ):
        continue
    for content in contents:
        if not isinstance(content, dict):
            continue
        if not isinstance(raw_same_target := content.get("same"), int):
            continue
        same_target = lookup.get(raw_same_target)
        if (
            same_target is not None
            and
            isinstance(count := content.get("count_"), int)
        ):
            same_target.append(count)

total_this_month_counts = sum(this_month_counts)
total_last_month_counts = sum(last_month_counts)

As for the data returned to Cortex XSOAR, the only change is on the stats key which now becomes:

"stats": {
	"prevSum": total_last_month_counts,
	"currSum": total_this_month_counts
}

The resulting widget looks as follows:

Figure 6: War room view of the widget outputted by the “Dual values from Sentinel” code snippet

Moving to incidents and indicators

Until now, we have been displaying our widgets in the war room, however we can also add them to incident and indicator layouts as well. As a reminder, the procedure to add General Purpose Dynamic sections to an incident can be found here: Add a Script to the incident Layout.

Our existing widgets are already compatible with incidents and indicators, after following the instructions above on how to add widgets to incidents, we can get the following layout tab. In a similar fashion after adding the dynamic-indicator-section tag to all three automations, you can also add them as widgets to an indicator layout:

Figure 7: Incident VS Indicator view of the three widgets

Moving to a dashboard

Rendering widgets in a dashboard is actually easier than in an incident layout, to verify this, let’s compare the methods to output a simple number widget, both for an incident and for a dashboard. For an incident, as we already saw earlier, you need to return the actual number, but it needs to be wrapped appropriately:

data = {
    "Type": 17,
    "ContentsFormat": "number",
    "Contents": {
        "stats": 53,
        "params": {
            "layout": "horizontal",
            "name": "Lala",
            "sign": "@",
            "colors": {
                "items": {
                    "#00CD33": {
                        "value": 10
                    },
                    "#FAC100": {
                        "value": 20
                    },
                    "green": {
                        "value": 40
                    }
                }
            },
            "type": "above"
        }
    }
}

demisto.results(data)

In contrast, it is much easier for a dashboard:

result = 10
demisto.results(result)

The difference here is that when building a dashboard, you can access the widget builder:

Figure 8: Dashboard widget editor view

Whereas from an incident, you need to explicitly return metadata defining the look and feel of your widget.

Therefore, if we want to make it possible for our automations to be used from a dashboard too, we need to adapt them to return either a simple value if being called from a dashboard, or a wrapped value if called from an incident or indicator.

Our first addition to the existing scripts will be to identify whether we’re being called from a dashboard, we will use the following snippet for this purpose.

is_dashboard = demisto.args().get("widgetType") is not None

This works because dashboards that have automation based widgets add a special argument when calling these automations. This special argument mentions the expected results type and can be found under the key widgetType, it’s presence is a good indication that your automation has been called from a dashboard.

We can now differentiate our outputted results depending on whether or not we are in a dashboard. For that, we separate our incident/indicator results in two, between the actual data and the wrapper. This snippet exposes the statement above applied to our first automation:

number = 53

data = {
    "Type": 17,
    "ContentsFormat": "number",
    "Contents": {
      "stats": number,
......
if is_dashboard:
    demisto.results(number)
else:
    demisto.results(data)

We do this with our three automations and also add the widget tag to them to make them selectable as source for automation-based dashboard widgets. Once added to a dashboard, our widgets look as follows:

Figure 9: Dashboard view of the three widgets

Getting timeframe data from the dashboard

At this point we are powering our widgets with data from Sentinel, but we are always looking at data from the same timeframe. Because dashboards have a time picker, we can instead start to use that data to determine the timeframe we are querying Sentinel for. Extraction of timeframe data from dashboards was covered in this previous blogpost.

We start by adding this line to our automation:

FromDate, ToDate = (
    NitroDateFactory
    .from_regular_xsoar_date_range_args(
        demisto.args()
    )
)

This gives us two NitroDates we can use to craft our Sentinel queries. In our second script which queries a single timeframe, the code becomes:

from_ = "2022-10-01T00:00:00+00:00"
to_ = "2022-11-01T00:00:00+00:00"

if is_dashboard:
    if isinstance(FromDate, NitroRegularDate):
        from_ = FromDate.to_iso8601()
    else:
        from_ = None
    if isinstance(ToDate, NitroRegularDate):
        to_ = ToDate.to_iso8601()
    else:
        to_ = None

query = "SecurityIncident"

tmp_query_list = list()

if from_ is not None:
    tmp_query_list.append(f'TimeGenerated >= datetime("{from_}")')

if to_ is not None:
    tmp_query_list.append(f'TimeGenerated >= datetime("{to_}")')

if tmp_query_list:
    query += "\n| where " + " and ".join(tmp_query_list)

query += """
| extend same = 1
| summarize count() by same"""

The logic we are modifying is the one describing how we craft our Kusto query (the query langage used in Microsoft Sentinel). We previously always had at our disposal a from_ and a to_ string representing the beginning and end of the timeframe we were interested in. With the selection dashboard date range selector, this is not the case anymore, we may get only a start date if the selector is on “3 days ago to now”, or only an end date if the selector is on “up to 3 days ago”. We must then change the logic we use to craft our query in a way that reflects this change. To accomodate this, we replace the between statement with >= and <= statements used to compare the TimeGenerated of an incident to the dates transmitted by the dashboard.

In a similar fashion, we modify the 3rd automation to calculate both the initial timeframe, and the previous timeframe from the dates passed down by the dashboard.

from_ = "2022-10-01T00:00:00+00:00"
to_ = "2022-11-01T00:00:00+00:00"

from_2 = "2022-09-01T00:00:00+00:00"
to_2 = "2022-10-01T00:00:00+00:00"

if is_dashboard:
    if isinstance(FromDate, NitroRegularDate):
        if isinstance(ToDate, NitroRegularDate):
            td = ToDate.date
        else:
            td = datetime.now(timezone.utc)
        delta = td - FromDate.date
        from2 = NitroRegularDate(date=FromDate.date - delta)
        to2 = FromDate
    else:
        from2 = FromDate
        to2 = ToDate

    if isinstance(FromDate, NitroRegularDate):
        from_ = FromDate.to_iso8601()
    else:
        from_ = None
    if isinstance(ToDate, NitroRegularDate):
        to_ = ToDate.to_iso8601()
    else:
        to_ = None
    if isinstance(from2, NitroRegularDate):
        from_2 = from2.to_iso8601()
    else:
        from_2 = None
    if isinstance(to2, NitroRegularDate):
        to_2 = to2.to_iso8601()
    else:
        to_2 = None

query = "SecurityIncident"

tmp_query_list = list()

if from_ is not None:
    tmp_query_list.append(f"TimeGenerated >= datetime(\"{from_}\")")

if to_ is not None:
    tmp_query_list.append(f"TimeGenerated < datetime(\"{to_}\")")

if tmp_query_list:
    query += "\n| where " + " and ".join(tmp_query_list)

query += """
| extend same = 1
| union (
SecurityIncident"""

tmp_query_list2 = list()

if from_2 is not None:
    tmp_query_list2.append(f"TimeGenerated >= datetime(\"{from_2}\")")

if to_2 is not None:
    tmp_query_list2.append(f"TimeGenerated < datetime(\"{to_2}\")")

if tmp_query_list2:
    query += "\n| where " + " and ".join(tmp_query_list2)

query += """
| extend same = 2)
| summarize count() by same"""

Our dashboard is now fully dynamic, with two widgets presenting data corresponding to the selected timeframe:

Figure 10: Dashboard view of the three widgets, data in third widget corresponds to the selected timeframe: “Today”
Figure 11: Dashboard view of the three widgets, data in third widget corresponds to the selected timeframe: “Last 7 days”

Looking back

We have covered the use of number widgets throughout Cortex XSOAR in pretty much every scenario, and have managed to make use of all the inputs available to us. Although the process used in this post was centered around number widgets, it should be noted that it can be applied to all other types of widgets.

References

Cortex XSOAR documentation: script based widget examples

Cortex XSOAR documentation: script based widget example 2

Microsoft Azure: Sentinel

Cortex XSOAR marketplace: Azure Log Analytics Integration

Cortex XSOAR documentation: Python code conventions

GIthub: Cortex XSOAR source – EntryTypes

Cortex XSOAR documentation: adding a script to an incident layout

About the author

Benjamin Danjoux

Benjamin Danjoux

Benjamin is a senior engineer in NVISO’s SOAR engineering team.
As the SOAR engineering design lead, he is responsible for the overall architecture and organization of the automated workflows running on Palo Alto Cortex XSOAR, which enables the NVISO SOC analysts to detect attackers in customer environments.


文章来源: https://blog.nviso.eu/2023/02/28/cortex-xsoar-tips-tricks-leveraging-dynamic-sections-number-widgets/
如有侵权请联系:admin#unsafe.sh