Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 14 additions & 17 deletions filters/antivirus/sentinel-one.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -410,4 +407,4 @@ pipeline:
- log.sourceIpAddressesToParse
- log.sourceIpAddresses
- log.sourceMacAddressesToParse
- log.sourceMacAddresses
- log.sourceMacAddresses
96 changes: 96 additions & 0 deletions plugins/alerts/sentinel_one_action_result_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
116 changes: 116 additions & 0 deletions plugins/alerts/testdata/sentinel_one_action_result.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
Loading