Skip to content

v12 dashboard: SentinelOne #2711

Description

@kryonsx

Part of: #2696 · Needs first: #2697 (click-through to the Log Explorer and the value/count table)
Related: #1723 (parsers and rules for this integration, owned by the detection team)

Goal

Ship a built-in SentinelOne dashboard that shows, at a glance, how much SentinelOne is sending, whether it is still sending, and what kinds of events they are. The heart of it is the list of SentinelOne logs grouped by activity types (widget W7): click one and the Log Explorer opens on exactly those logs.

Where the data comes from

Integration (catalog name) SENTINEL_ONE
Data type antivirus-sentinel-one
How the logs arrive The SentinelOne management console's Syslog integration streams CEF over TCP or TLS on port 7012 to the UTMStack collector, which passes each line unchanged to the server.
Parser definitions/filters/antivirus/sentinel-one.yaml
What dataSource holds The IP address the syslog message came from, which is the SentinelOne management console (for the cloud console, one of SentinelOne's sending addresses), or the collector's hostname when the sender is 127.0.0.1. Proof: collectors/forwarder/collector/syslog/listener.go and handler.go set DataSource: resolveRemoteAddr(addr); log-input/ingest/server.go applyDefaults only fills 'unknown'; the parser never writes dataSource. So W6 is 'Logs by console IP'. The endpoint name is not available as a field (see caveats).
Grouped by log.activityType (activity types)

Why log.activityType: Every SentinelOne CEF record carries the console activity code in 'activityType=' (the same number is the CEF signature ID). The kv step writes it to log.activityType as text, and it exists on every parsed record because it is never the first extension key in public samples. It is a number, so the list shows codes; the readable name (the CEF 'Name' slot) is written to log.eventDescription only for a few header shapes and is lost for current console records, which carry a build string in the CEF version slot. A code-to-name lookup in the frontend would make W7 and W8 readable. Codes and meanings below come from public SentinelOne records (Rapid7 and Sekoia documentation and test data).

Typical values: 19 New active threat, 4003 Threat with confidence level suspicious detected, 2001 Agent killed the threat, 2004 Agent quarantined the threat, 2009 Quarantine failed, 4008 Threat status changed (for example Not mitigated to Mitigated), 25 Console user deleted, 27 Console user logged in

Widgets

Standard layout from the parent issue; rows W2, W3 and W9 onward are specific to this integration.

# Title Shown as Query Why
W1 Total logs number logs: count All SentinelOne logs in the selected time range.
W2 New threats number logs: count; filter log.activityType is one of [19, 4003] Threat activities: 19 'New active threat' and 4003 'Threat with confidence level suspicious detected'.
W3 Quarantine failed number logs: count; filter log.activityType is one of [2009] Activity 2009: the agent could not quarantine the threat, so it may still be on the endpoint.
W4 Alerts number alerts: count Alerts raised from SentinelOne logs.
W5 Log volume over time area chart logs: count over time Shows gaps in the syslog stream and bursts of activity.
W6 Logs by console IP bar chart logs: top 10 values of dataSource dataSource is the IP address the console sends from, so this shows which console (or sending address) is busiest or silent.
W7 Top activity types value and count table logs: top 25 values of log.activityType; filter log.activityType exists The main list: SentinelOne activity codes by count; click one to open those logs. The exists filter hides lines the parser could not read.
W8 Activity types over time (top 5) line chart logs: count over time, one line per value of log.activityType (top 5); filter log.activityType exists When each of the five most common activity types happened.
W9 Top users bar chart logs: top 10 values of log.sourceUser; filter log.sourceUser exists Users named in the records (mostly console users who made changes).
W10 Top accounts bar chart logs: top 10 values of log.accName; filter log.accName exists SentinelOne accounts with the most activity (useful when one console serves several accounts).
W11 Top threat file hashes bar chart logs: top 10 values of log.fileHash; filter log.fileHash exists The same malicious file seen again and again, whatever its path (paths are cut at spaces, hashes are not).
W13 Alerts by rule bar chart alerts: top 10 values of name Which SentinelOne detection rules fire most.
W14 Alerts by severity bar chart alerts: top 50 values of severity Split of SentinelOne alerts into low, medium and high.
W15 Latest logs table of latest logs logs: latest 20 records; columns @timestamp, dataSource, log.activityType, log.sourceUser, log.accName, log.fileHash, raw The newest records; raw is included because the readable event name (and the endpoint name) only survive in the original line.

Fields used and where they come from

  • log.activityType: SentinelOne activity code, as text. Examples: 19, 2004, 2009, 23. Source: kv step on log.restData (fieldSplit ' ', valueSplit '='), key activityType, never renamed.
  • log.sourceUser: User named in suser=: the console user for console activities. Examples: Example Admin. Source: rescue grok '{{.data}}suser=' -> log.suserToParse -> grok '{{.greedy}}{{.space}}' -> log.sourceUser (keeps spaces).
  • log.accName: SentinelOne account name. Examples: Rapid 7 Institute of Institutionary Research, Example Account. Source: rescue grok for accountName= -> log.accountNameToParse -> log.accName.
  • log.fileHash: SHA1 of the threat file on threat activities. Examples: 841be03a8cd3ea0b928b78057938c80cee381ef7. Source: kv step (key fileHash).
  • log.filePath: Threat file path, cut at the first space. Examples: \Device\Disk\Downloads\WinPython-64bit-1.2.3.4\Python.exe. Source: kv step (key filePath); not in the rescue list.
  • log.eventDescription: CEF event name; only for a few header shapes (see caveats). Examples: New active threat - machine ZXCVPOIU4209. Source: header groks with '|{{.data}}|' after the version slot, or the eventDesc= rescue grok.

Watch out for

  • The readable event name is usually lost. Current console records carry a build string in the CEF version slot (for example 'S-...'), and only header shapes with an IP address or 'word number' (such as 'Windows 10') there write log.eventDescription. That is why the event-name field is the numeric log.activityType.
  • Some header shapes are not parsed at all, and those lines keep only raw: 'CEF:' with no syslog time in front and a version such as 'OS X' (a real record in Rapid7's documentation: '<11>CEF:0|SentinelOne|Mgmt|OS X|2009|Quarantine failed|1|...'), BSD headers ('<14>Sep 21 17:36:27 host CEF:...'), and CEF without a priority. W1 counts them; W7 hides them.
  • W2 and W3 use activity codes confirmed in public SentinelOne records: 19 (New active threat), 4003 (suspicious threat detected) and 2009 (Quarantine failed). SentinelOne has more threat and mitigation codes that could not be confirmed, so W2 and W3 may undercount. SentinelOne's own activity-code list is not public.
  • No field holds the endpoint name: public CEF records only mention it inside the event name text ('New active threat - machine ZXCVPOIU4209'), which is lost. There is therefore no 'Top endpoints' widget; W15 shows raw instead.
  • The kv step reads the CEF header remainder too, so the first extension key after the header is glued to the header's last word and severity (for example cat becomes 'log.endpoint018cat'). log.cat (SystemEvent, ThreatEvent) is lost whenever it comes first, so no widget uses it; the real 'New active threat' record in Rapid7's documentation also carries cat=SystemEvent.
  • Multi-word values are kept only for suser, duser, accountName, eventDesc, endpointDeviceControlDeviceName, sourceGroupName, sourceIpAddresses and sourceMacAddresses. filePath and siteName are cut at the first space, so W11 uses the hash instead of the path.
  • actionResult is only set when the record carries threatStatus, status or mitigationStatus, which none of the public CEF records do, so no widget uses it. The CEF header severity is dropped, so the standard severity column stays empty.
  • dataSource is the address the console sends from. For the SentinelOne cloud console that may be several SentinelOne addresses, not one customer device.
  • No field used here depends on the engine's underscore rule; the replay gave the same result with both rules. Checked with a step-by-step replay of this filter over 54 lines: the 52 lines from v11 pull request fix(sentinel-one): parse CEF header and full values; align rule consumers #2677 (fabricated, modeled on genuine console records) and 2 real lines from Rapid7's documentation.

Parser problems found while designing this

These are not dashboard work, but they limit what the dashboard can show. They belong to #1723; raise them there rather than working around them in the dashboard.

  • The CEF name and severity are lost for current console records: the header groks that capture log.eventDescription expect an IPv4 address or 'word number' in the CEF version slot, but real records carry a console build string. Fix: parse the CEF header by position after 'CEF:' (vendor|product|version|signature|name|severity|extension), as v11 pull request fix(sentinel-one): parse CEF header and full values; align rule consumers #2677 does.
  • Records whose version slot is a plain word pair such as 'OS X' and that have no syslog time are not parsed at all, and the kv step then errors on the missing log.restData.
  • The kv step runs on text that still contains the end of the CEF header, so the first extension key becomes a junk key made of the header's last word, the severity and the key name (for example log.endpoint018cat, log.added1suser), and its real key (often cat or suser) is missing.
  • No step writes the endpoint name (target.host) or maps the console user to origin.user; several SentinelOne rules and any per-endpoint view need them.
  • filePath, siteName and other keys outside the rescue list are cut at the first space.
  • actionResult depends on keys (threatStatus, status, mitigationStatus) that public CEF records do not contain, so it is effectively never set.

How to build it

Before you start: parser field names are changing while the parsers are updated for the engine's new underscore handling (see "Field names are about to move" in #2696). Check every field in this issue against the v12 parser at that moment and against real logs, and build against what you find.

  1. Add definitions/dashboards/integration-sentinel-one.yaml. Keep it in the top folder: the test that checks shipped dashboards (TestEveryShippedDashboardDefinitionIsValid) only reads the top folder.
  2. Start from the file below; it follows the table above and passes the same checks as the backend (domain.Spec.Validate).
  3. W7 is a value/count table: a category query shown with chart type table. It already renders; v12 dashboards: groundwork for integration dashboards (click-through to the Log Explorer, value/count table, Agents dashboard fix) #2697 makes its rows clickable and its headers readable.
  4. Load real logs from this technology on a v12 test server (or replay samples) and check every widget before opening the pull request.
Starting dashboard file
# Dashboard version v1.0.0
#
# System-owned default dashboard for the SentinelOne integration, seeded by
# backend/modules/dashboards/repository/dashboard_bootstrap.go.
# Field names come from definitions/filters/antivirus/sentinel-one.yaml.
# Verify every widget against real logs before shipping.
name: "SentinelOne"
description: "What SentinelOne is sending: volume, event types, and the activity worth a look."
widgets:
  - layout: { x: 0, y: 0, w: 3, h: 2 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: metric
      metric:
        agg: count
    config:
      __builder:
        chartType: metric
        title: "Total logs"

  - layout: { x: 3, y: 0, w: 3, h: 2 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: metric
      metric:
        agg: count
      filters:
        - field: "log.activityType"
          op: in
          value: ["19", "4003"]
    config:
      __builder:
        chartType: metric
        title: "New threats"

  - layout: { x: 6, y: 0, w: 3, h: 2 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: metric
      metric:
        agg: count
      filters:
        - field: "log.activityType"
          op: in
          value: ["2009"]
    config:
      __builder:
        chartType: metric
        title: "Quarantine failed"

  - layout: { x: 9, y: 0, w: 3, h: 2 }
    spec:
      dataset: alerts
      dataType: "antivirus-sentinel-one"
      chart: metric
      metric:
        agg: count
    config:
      __builder:
        chartType: metric
        title: "Alerts"

  - layout: { x: 0, y: 2, w: 8, h: 4 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: time
      metric:
        agg: count
    config:
      __builder:
        chartType: area
        title: "Log volume over time"

  - layout: { x: 8, y: 2, w: 4, h: 4 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "dataSource"
      limit: 10
    config:
      __builder:
        chartType: bar
        title: "Logs by console IP"

  - layout: { x: 0, y: 6, w: 6, h: 6 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "log.activityType"
      filters:
        - field: "log.activityType"
          op: exists
      limit: 25
    config:
      __builder:
        chartType: table
        title: "Top activity types"

  - layout: { x: 6, y: 6, w: 6, h: 6 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: time
      metric:
        agg: count
      dimension: "log.activityType"
      filters:
        - field: "log.activityType"
          op: exists
      limit: 5
    config:
      __builder:
        chartType: line
        title: "Activity types over time (top 5)"

  - layout: { x: 0, y: 12, w: 4, h: 4 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "log.sourceUser"
      filters:
        - field: "log.sourceUser"
          op: exists
      limit: 10
    config:
      __builder:
        chartType: bar
        title: "Top users"

  - layout: { x: 4, y: 12, w: 4, h: 4 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "log.accName"
      filters:
        - field: "log.accName"
          op: exists
      limit: 10
    config:
      __builder:
        chartType: bar
        title: "Top accounts"

  - layout: { x: 8, y: 12, w: 4, h: 4 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "log.fileHash"
      filters:
        - field: "log.fileHash"
          op: exists
      limit: 10
    config:
      __builder:
        chartType: bar
        title: "Top threat file hashes"

  - layout: { x: 0, y: 16, w: 6, h: 4 }
    spec:
      dataset: alerts
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "name"
      limit: 10
    config:
      __builder:
        chartType: bar
        title: "Alerts by rule"

  - layout: { x: 6, y: 16, w: 6, h: 4 }
    spec:
      dataset: alerts
      dataType: "antivirus-sentinel-one"
      chart: category
      metric:
        agg: count
      dimension: "severity"
    config:
      __builder:
        chartType: bar
        title: "Alerts by severity"

  - layout: { x: 0, y: 20, w: 12, h: 6 }
    spec:
      dataset: logs
      dataType: "antivirus-sentinel-one"
      chart: table
      metric:
        agg: count
      limit: 20
      columns: ["@timestamp", "dataSource", "log.activityType", "log.sourceUser", "log.accName", "log.fileHash", "raw"]
    config:
      __builder:
        chartType: table
        title: "Latest logs"

Done when

  • definitions/dashboards/integration-sentinel-one.yaml is merged to release/v12.0.0 and go test ./modules/dashboards/... passes in backend/.
  • With SentinelOne logs flowing on a v12 test server, every widget shows data. A widget that stays empty while logs arrive means a wrong field or value: fix it, don't ship it.
  • Clicking a row, bar or line point opens the Log Explorer on the same logs, and the Log Explorer count matches the widget.
  • A time range with no logs shows empty states, not errors.
  • A screenshot of the finished dashboard is attached to this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions