From 502803f69885183da7291f978910146a7aa4a24c Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 23 Sep 2026 21:00:30 -0400 Subject: [PATCH] fix(sentinel-one): set failure only from an explicit final status The three actionResult steps wrote the nonstandard value `failed`, matched `fail` as a substring of log.mitigationStatus, missed upper-case values and labelled contradictory statuses as failures. One step now sets the standard `failure` only for an exact, case-insensitive failed threat, operation or mitigation status, and leaves the result unset when the same record also reports success. Successful remediation and console events keep no outcome. Adds ten fabricated raw lines in the observed console CEF framing and an ordered-step CEL test with go-sdk v1.1.33. Co-Authored-By: Claude Opus 5.5 --- filters/antivirus/sentinel-one.yml | 31 +++-- .../alerts/sentinel_one_action_result_test.go | 96 +++++++++++++++ .../testdata/sentinel_one_action_result.json | 116 ++++++++++++++++++ 3 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 plugins/alerts/sentinel_one_action_result_test.go create mode 100644 plugins/alerts/testdata/sentinel_one_action_result.json diff --git a/filters/antivirus/sentinel-one.yml b/filters/antivirus/sentinel-one.yml index e1cf94824..9fe44d157 100644 --- a/filters/antivirus/sentinel-one.yml +++ b/filters/antivirus/sentinel-one.yml @@ -1,4 +1,4 @@ -# SentinelOne filter, version 3.0.0 +# SentinelOne filter, version 3.1.0 # Supports CEF Syslog format and [something@number something="xxxx"] format # Based on https://docs.centrify.com/Content/IntegrationContent/SIEM/arcsight-cef/arcsight-cef-format.htm # and https://docs.fortinet.com/document/fortisiem/6.1.0/external-systems-configuration-guide/298395/sentinelone @@ -363,25 +363,22 @@ pipeline: - log.rt to: log.ruleTime - # Adding action result + # Adding action result. Only an explicit failed mitigation or operation + # status is a final outcome: exact values, not substrings, and + # contradictory statuses stay unknown. A successful remediation is not an + # allowed connection, and console events have no final result, so + # neither sets one. - add: function: string params: key: actionResult - value: "failed" - where: 'equals("log.threatStatus", "mitigation_failed")' - - add: - function: string - params: - key: actionResult - value: "failed" - where: 'equals("log.status", "failed") && !exists("actionResult")' - - add: - function: string - params: - key: actionResult - value: "failed" - where: 'contains("log.mitigationStatus", "fail") && !exists("actionResult")' + value: "failure" + where: >- + (equalsIgnoreCase("log.threatStatus", "mitigation_failed") || + equalsIgnoreCase("log.status", "failed") || + regexMatch("log.mitigationStatus", "(?i)^(failed|failure|mitigation_failed)$")) && + !regexMatch("log.mitigationStatus", "(?i)^(success|succeeded|mitigated)$") && + !regexMatch("log.status", "(?i)^(success|succeeded)$") # Removing unused fields - delete: @@ -410,4 +407,4 @@ pipeline: - log.sourceIpAddressesToParse - log.sourceIpAddresses - log.sourceMacAddressesToParse - - log.sourceMacAddresses \ No newline at end of file + - log.sourceMacAddresses diff --git a/plugins/alerts/sentinel_one_action_result_test.go b/plugins/alerts/sentinel_one_action_result_test.go new file mode 100644 index 000000000..d56b6e199 --- /dev/null +++ b/plugins/alerts/sentinel_one_action_result_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "google.golang.org/protobuf/encoding/protojson" +) + +// The raw fixture is also run through the isolated EventProcessor playground. +// This test checks the ordered verdict steps with the branch's actual CEL SDK. +func TestSentinelOneActionResultContract(t *testing.T) { + var cases []struct { + Name string `json:"name"` + Result string `json:"result"` + Expected map[string]string `json:"expected"` + } + data, err := os.ReadFile("testdata/sentinel_one_action_result.json") + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(data, &cases); err != nil { + t.Fatal(err) + } + if len(cases) < 8 { + t.Fatal("missing outcome regression classes") + } + filterYAML, err := utils.ReadPbYaml("../../filters/antivirus/sentinel-one.yml") + if err != nil { + t.Fatal(err) + } + filter := new(plugins.Config) + if err := protojson.Unmarshal(filterYAML, filter); err != nil { + t.Fatal(err) + } + cache := plugins.NewCELCache("sentinel-one-final-outcome") + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + logFields := map[string]any{} + event := map[string]any{"log": logFields} + for path, value := range tc.Expected { + if strings.HasPrefix(path, "log.") { + logFields[strings.TrimPrefix(path, "log.")] = value + } + } + seenAdd := false + for _, stage := range filter.Pipeline { + if len(stage.DataTypes) != 1 || stage.DataTypes[0] != "antivirus-sentinel-one" { + continue + } + for _, step := range stage.Steps { + if step.Add == nil || step.Add.Params["key"].GetStringValue() != "actionResult" { + continue + } + seenAdd = true + state, err := json.Marshal(event) + if err != nil { + t.Fatal(err) + } + matched, err := cache.Eval(step.Add.Where, string(state)) + if err != nil { + t.Fatal(err) + } + if matched { + event["actionResult"] = step.Add.Params["value"].GetStringValue() + } + } + } + if !seenAdd { + t.Fatal("missing final result mapping") + } + got, exists := event["actionResult"] + if tc.Result == "" { + if exists { + t.Fatalf("unexpected actionResult: %v", got) + } + } else if !exists || got != tc.Result { + t.Fatalf("actionResult = %v, want %q", got, tc.Result) + } + state, err := json.Marshal(event) + if err != nil { + t.Fatal(err) + } + for _, result := range []string{"success", "failure", "denied"} { + matched, err := cache.Eval(`equals("actionResult","`+result+`")`, string(state)) + if err != nil || matched != (tc.Result == result) { + t.Errorf("%s predicate = %v (%v)", result, matched, err) + } + } + }) + } +} diff --git a/plugins/alerts/testdata/sentinel_one_action_result.json b/plugins/alerts/testdata/sentinel_one_action_result.json new file mode 100644 index 000000000..70dab3d16 --- /dev/null +++ b/plugins/alerts/testdata/sentinel_one_action_result.json @@ -0,0 +1,116 @@ +[ + { + "name": "mitigation-status-failed", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic mitigation failed|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) mitigationStatus=failed", + "result": "failure", + "expected": { + "log.mitigationStatus": "failed", + "log.cat": "Threat" + } + }, + { + "name": "threat-status-mitigation-failed", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic mitigation failed|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) threatStatus=mitigation_failed", + "result": "failure", + "expected": { + "log.threatStatus": "mitigation_failed", + "log.cat": "Threat" + } + }, + { + "name": "operation-status-failed", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic operation failed|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) status=failed", + "result": "failure", + "expected": { + "log.status": "failed", + "log.cat": "Threat" + } + }, + { + "name": "mitigation-status-failure-uppercase", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic mitigation failed|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) mitigationStatus=FAILURE", + "result": "failure", + "expected": { + "log.mitigationStatus": "FAILURE", + "log.cat": "Threat" + } + }, + { + "name": "composite-status-not-a-verdict", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic status|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) mitigationStatus=not_failed", + "result": "", + "expected": { + "log.mitigationStatus": "not_failed" + }, + "absent": [ + "actionResult" + ] + }, + { + "name": "conflicting-statuses-remain-unknown", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic conflict|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) threatStatus=mitigation_failed mitigationStatus=mitigated", + "result": "", + "expected": { + "log.threatStatus": "mitigation_failed", + "log.mitigationStatus": "mitigated" + }, + "absent": [ + "actionResult" + ] + }, + { + "name": "successful-remediation-not-an-allowed-connection", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|9999|Synthetic remediation|1|suser=analyst@example.com cat=Threat rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) status=success mitigationStatus=mitigated", + "result": "", + "expected": { + "log.status": "success", + "log.mitigationStatus": "mitigated" + }, + "absent": [ + "actionResult" + ] + }, + { + "name": "administrative-event-has-no-outcome", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|37|Synthetic role assigned|1|suser=admin@example.com cat=SystemEvent rt=#arcsightDate(Wed, 23 Sep 2026, 12:34:56 UTC) activityID=100000000000000001 activityType=37 siteId=100000000000000002 siteName=Example Site accountId=100000000000000003 accountName=Example notificationScope=SITE", + "result": "", + "expected": { + "log.activityType": "37", + "log.cat": "SystemEvent" + }, + "absent": [ + "actionResult", + "origin.ip", + "target.ip" + ] + }, + { + "name": "console-notification-address-not-a-peer", + "raw": "<14>2026-09-23 12:34:56,000 testhost - CEF:0|SentinelOne|Mgmt|S-99.1.1#10|3660|Synthetic notification triggered|1|value=true issuer=console user_id=100000000000000004 username=Synthetic User online_agents=3 offline_agents=2 cat=SystemEvent deviceAddress=203.0.113.10 deviceHostFqdn=console.example.com deviceHostName=console.example.com notificationScope=SITE siteId=100000000000000002 siteName=Example Site accountId=100000000000000003 accountName=Example vendor=SentinelOne eventID=3660 eventDesc=Synthetic notification triggered eventSeverity=1 activityID=100000000000000005 activityType=3660", + "result": "", + "expected": { + "log.activityType": "3660", + "log.cat": "SystemEvent", + "log.deviceAddress": "203.0.113.10" + }, + "absent": [ + "actionResult", + "origin.ip", + "target.ip" + ] + }, + { + "name": "transport-line-without-outcome", + "raw": "<134>1 2026-09-23T12:34:56-04:00 testhost synthetic-load - - - SYNTHETIC-0001 seq=1", + "result": "", + "expected": { + "log.seq": "1", + "log.syslogPriority": "134" + }, + "absent": [ + "actionResult", + "origin.ip", + "target.ip" + ] + } +]