From a6848f2bc7adbbb255fe9e05b7527eda59227de0 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 23 Sep 2026 17:03:57 -0400 Subject: [PATCH 1/5] fix(sentinel-one): parse CEF header and full values; align rule consumers The management-console CEF records lost the event name, kept one word of values with spaces, turned header text into field names and cut rt to a fragment, so the twelve rules that need the event name never matched. Filter 3.0.1: - Parse the CEF header by position after "CEF:"; name to log.eventDescription, header severity to log.cefSeverity. The standard severity stays unset: the CEF scale is not documented. - Drop the two parsers that stored the version slot as log.syslogHost. - Run kv only when log.restData exists. - Keep the full rt in log.ruleTime; set deviceTime from it only for the observed UTC layout. - Keep whole values for nine keys, including the last key. - Copy suser to origin.user for SystemEvent records only. Rules (names, thresholds, impact, adversary and MITRE unchanged): - memory_injection_detection: drop the deleted scratch-field branch. - target.host replaces log.syslogHost in five gates and eight groupings. - kernel_level_threat and the reputation branch require target.host. - s1_policy_downgrade fires only on Protect to Detect, paired wording included. - s1_exclusion_abuse and s1_policy_downgrade group by adversary.user. - Whole-word lists in agent tampering, mitigation failures and IoT. - threat_intelligence_matches compares confidencelevel with MALICIOUS. The actionResult steps are unchanged. Co-Authored-By: Claude Opus 5.5 --- filters/antivirus/sentinel-one.yml | 260 +++++++++--------- .../sentinel-one/agent_tampering_attempts.yml | 4 +- .../behavioral_threat_detection.yml | 4 +- .../custom_detection_rule_triggers.yml | 4 +- .../deep_visibility_threat_indicators.yml | 4 +- .../endpoint_detection_response_alerts.yml | 4 +- .../iot_device_compromise_indicators.yml | 3 +- .../sentinel-one/kernel_level_threat.yml | 5 +- .../memory_injection_detection.yml | 5 +- .../rollback_operation_patterns.yml | 2 +- .../sentinel-one/s1_exclusion_abuse.yml | 2 +- .../sentinel-one/s1_policy_downgrade.yml | 15 +- .../sentinel-one/suspicious_process_tree.yml | 4 +- .../threat_intelligence_matches.yml | 4 +- .../threat_mitigation_failures.yml | 4 +- 15 files changed, 162 insertions(+), 162 deletions(-) diff --git a/filters/antivirus/sentinel-one.yml b/filters/antivirus/sentinel-one.yml index e1cf94824..38bfbb0a6 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.0.1 # 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 @@ -100,39 +100,6 @@ pipeline: pattern: '{{.greedy}}' source: raw - # Parsing raw field for log without syslog information - - grok: - patterns: - - fieldName: log.syslogPriority - pattern: '\<{{.data}}\>' - - fieldName: log.0trash - pattern: '{{.data}}Mgmt\|' - - fieldName: log.syslogHost - pattern: '{{.ipv4}}\|' - - fieldName: log.1trash - pattern: '{{.integer}}' - - fieldName: log.eventDescription - pattern: '\|{{.data}}\|' - - fieldName: log.restData - pattern: '{{.greedy}}' - source: raw - - - grok: - patterns: - - fieldName: log.syslogPriority - pattern: '\<{{.data}}\>' - - fieldName: log.0trash - pattern: '{{.data}}Mgmt\|' - - fieldName: log.syslogHost - pattern: '{{.word}}\s{{.integer}}\|' - - fieldName: log.1trash - pattern: '{{.integer}}' - - fieldName: log.eventDescription - pattern: '\|{{.data}}\|' - - fieldName: log.restData - pattern: '{{.greedy}}' - source: raw - - grok: patterns: - fieldName: log.2trash @@ -168,156 +135,189 @@ pipeline: pattern: '{{.greedy}}' source: raw + # Parse the CEF header by position, whatever syslog header precedes "CEF:". + # Header fields are pipe-delimited; a backslash escapes a pipe inside a field. + # log.restData then holds only the extension, so no header text reaches kv. + # The header severity stays vendor detail in log.cefSeverity; its scale is + # not documented, so the standard severity field is not set here. + - grok: + patterns: + - fieldName: log.0trash + pattern: '{{.data}}CEF:{{.integer}}\|' + - fieldName: log.cefDeviceVendor + pattern: '(?:[^|\\]|\\.)+' + - fieldName: log.1trash + pattern: '\|' + - fieldName: log.cefDeviceProduct + pattern: '(?:[^|\\]|\\.)+' + - fieldName: log.1trash + pattern: '\|' + - fieldName: log.cefDeviceVersion + pattern: '(?:[^|\\]|\\.)+' + - fieldName: log.1trash + pattern: '\|' + - fieldName: log.cefSignatureId + pattern: '(?:[^|\\]|\\.)+' + - fieldName: log.1trash + pattern: '\|' + - fieldName: log.eventDescription + pattern: '(?:[^|\\]|\\.)+' + - fieldName: log.1trash + pattern: '\|' + - fieldName: log.cefSeverity + pattern: '(?:[^|\\]|\\.)+' + - fieldName: log.1trash + pattern: '\|' + - fieldName: log.restData + pattern: '{{.greedy}}' + source: raw + where: 'contains("raw", "CEF:")' + - kv: fieldSplit: " " valueSplit: "=" source: log.restData + where: 'exists("log.restData")' - # Using grok to parse kv issued fields + # CEF rt=#arcsightDate(, , UTC) anywhere in the + # extension. kv keeps only its first word, so the complete value is taken here. - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}accountName=' - - fieldName: log.accountNameToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])rt=#arcsightDate\(' + - fieldName: log.rt + pattern: '[^)]+' source: log.restData + # The same value becomes deviceTime only when it has exactly the expected + # UTC layout; any other rt format is left alone and deviceTime keeps its default. - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}eventDesc=' - - fieldName: log.eventDescToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])rt=#arcsightDate\(' + - fieldName: log.deviceTimeCandidate + pattern: '[^)]+' source: log.restData + - reformat: + fields: + - log.deviceTimeCandidate + function: time + fromFormat: 'Mon, 2 Jan 2006, 15:04:05 MST' + toFormat: '2006-01-02T15:04:05Z07:00' + where: 'regexMatch("log.deviceTimeCandidate", "^[A-Z][a-z]{2}, [0-9]{1,2} [A-Z][a-z]{2} [0-9]{4}, [0-9]{2}:[0-9]{2}:[0-9]{2} UTC$")' + - rename: + from: + - log.deviceTimeCandidate + to: deviceTime + where: 'regexMatch("log.deviceTimeCandidate", "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")' + # Values that can contain spaces: take the text after "=" up to the next + # " =", or to the end when the key is last, then strip that next key. + # kv keeps only the first word of these values. - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}suser=' - - fieldName: log.suserToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])accountName=' + - fieldName: log.accName + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])accountName=\\S")' - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}duser=' - - fieldName: log.duserToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])eventDesc=' + - fieldName: log.eventDescription + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])eventDesc=\\S")' - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}endpointDeviceControlDeviceName=' - - fieldName: log.endpointDeviceControlDeviceNameToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])suser=' + - fieldName: log.sourceUser + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])suser=\\S")' - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}sourceGroupName=' - - fieldName: log.sourceGroupNameToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])duser=' + - fieldName: log.destinationUser + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])duser=\\S")' - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}sourceIpAddresses=' - - fieldName: log.sourceIpAddressesToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])endpointDeviceControlDeviceName=' + - fieldName: log.endpointDeviceName + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])endpointDeviceControlDeviceName=\\S")' - grok: patterns: - fieldName: log.3trash - pattern: '{{.data}}sourceMacAddresses=' - - fieldName: log.sourceMacAddressesToParse - pattern: '{{.data}}{{.word}}\=' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' + pattern: '{{.data}}(?:^|[\s|])sourceGroupName=' + - fieldName: log.sourceGpName + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])sourceGroupName=\\S")' - # Applying grok to remove unnecessary data - grok: patterns: - - fieldName: log.accName - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.accountNameToParse - - - grok: - patterns: - - fieldName: log.eventDescription - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.eventDescToParse - - - grok: - patterns: - - fieldName: log.sourceUser - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.suserToParse - - - grok: - patterns: - - fieldName: log.destinationUser - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.duserToParse - - - grok: - patterns: - - fieldName: log.endpointDeviceName - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.endpointDeviceControlDeviceNameToParse + - fieldName: log.3trash + pattern: '{{.data}}(?:^|[\s|])sourceIpAddresses=' + - fieldName: log.sourceIps + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' + source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])sourceIpAddresses=\\S")' - grok: patterns: - - fieldName: log.sourceGpName - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.sourceGroupNameToParse + - fieldName: log.3trash + pattern: '{{.data}}(?:^|[\s|])sourceMacAddresses=' + - fieldName: log.sourceMacs + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' + source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])sourceMacAddresses=\\S")' - grok: patterns: - - fieldName: log.sourceIps - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.sourceIpAddressesToParse + - fieldName: log.3trash + pattern: '{{.data}}(?:^|[\s|])siteName=' + - fieldName: log.siteName + pattern: '(?:.*?\s[A-Za-z0-9_.]+=|.+)' + source: log.restData + where: 'regexMatch("log.restData", "(^|[\\s|])siteName=\\S")' + - trim: + function: regex + substring: '\s+[A-Za-z0-9_.]+=$' + fields: + - log.accName + - log.eventDescription + - log.sourceUser + - log.destinationUser + - log.endpointDeviceName + - log.sourceGpName + - log.sourceIps + - log.sourceMacs + - log.siteName + + # In SentinelOne management (console) events, suser is the console account + # that made the change. Copy it to the standard actor field and keep the + # vendor value. Its meaning in other event classes is not established. - grok: patterns: - - fieldName: log.sourceMacs - pattern: '{{.greedy}}{{.space}}' - - fieldName: log.irrelevant - pattern: '{{.greedy}}' - source: log.sourceMacAddressesToParse + - fieldName: origin.user + pattern: '(?s:.+)' + source: log.sourceUser + where: 'equals("log.cat", "SystemEvent") && exists("log.sourceUser") && !oneOf("log.sourceUser", ["", "-", "N/A", "unknown"])' # Removing unnecessary characters - trim: @@ -343,7 +343,6 @@ pipeline: substring: '|' fields: - log.eventDescription - - log.syslogHost - trim: function: prefix @@ -394,6 +393,7 @@ pipeline: - log.restData - log.1rt - log.rt + - log.deviceTimeCandidate - log.irrelevant - log.accountNameToParse - log.accountName @@ -410,4 +410,4 @@ pipeline: - log.sourceIpAddressesToParse - log.sourceIpAddresses - log.sourceMacAddressesToParse - - log.sourceMacAddresses \ No newline at end of file + - log.sourceMacAddresses diff --git a/rules/antivirus/sentinel-one/agent_tampering_attempts.yml b/rules/antivirus/sentinel-one/agent_tampering_attempts.yml index 31acad24b..66f2a8792 100644 --- a/rules/antivirus/sentinel-one/agent_tampering_attempts.yml +++ b/rules/antivirus/sentinel-one/agent_tampering_attempts.yml @@ -25,8 +25,8 @@ description: | 6. Review network connections from the affected host for command and control activity 7. If tampering was successful, consider reimaging the system after forensic analysis where: | - regexMatch("log.eventDescription", ".*(tamper|disable|stop|uninstall|modify).*agent.*") || - regexMatch("log.eventDescription", ".*agent.*(tamper|disable|stop|uninstall|modify).*") || + regexMatch("log.eventDescription", "\\b(tamper(s|ed|ing)?|disabl(e|es|ed|ing)|stop(s|ped|ping)?|uninstall(s|ed|ing)?|modif(y|ies|ied|ying))\\b.*\\bagents?\\b") || + regexMatch("log.eventDescription", "\\bagents?\\b.*\\b(tamper(s|ed|ing)?|disabl(e|es|ed|ing)|stop(s|ped|ping)?|uninstall(s|ed|ing)?|modif(y|ies|ied|ying))\\b") || oneOf("log.activityType", ["agent_uninstall", "agent_disable", "agent_tamper"]) || equals("log.eventType", "AgentTamperingAttempt") || (regexMatch("log.processName", ".*(sentinelone|sentinel).*") && diff --git a/rules/antivirus/sentinel-one/behavioral_threat_detection.yml b/rules/antivirus/sentinel-one/behavioral_threat_detection.yml index 92fb58328..f996e3877 100644 --- a/rules/antivirus/sentinel-one/behavioral_threat_detection.yml +++ b/rules/antivirus/sentinel-one/behavioral_threat_detection.yml @@ -25,7 +25,7 @@ description: | 6. Review SentinelOne console for additional threat details and recommended remediation actions 7. Collect memory dumps if process injection or in-memory threats are suspected 8. Check for persistence mechanisms that may have been established -where: (regexMatch("log.eventDescription", "(?i)(behavioral|anomaly|suspicious behavior|abnormal|pattern detected|threat behavior|malicious activity)") || regexMatch("log.eventDescription", "(?i)(process injection|code injection|memory manipulation|hollowing)")) && exists("log.syslogHost") && exists("log.eventDescription") +where: (regexMatch("log.eventDescription", "(?i)(behavioral|anomaly|suspicious behavior|abnormal|pattern detected|threat behavior|malicious activity)") || regexMatch("log.eventDescription", "(?i)(process injection|code injection|memory manipulation|hollowing)")) && exists("target.host") && exists("log.eventDescription") groupBy: - lastEvent.log.eventDescription - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/custom_detection_rule_triggers.yml b/rules/antivirus/sentinel-one/custom_detection_rule_triggers.yml index ae3fb5812..313eeeb0b 100644 --- a/rules/antivirus/sentinel-one/custom_detection_rule_triggers.yml +++ b/rules/antivirus/sentinel-one/custom_detection_rule_triggers.yml @@ -27,7 +27,7 @@ where: | (contains("log.eventDescription", ["custom rule", "custom detection", "Custom Rules"]) || equals("log.detectionSource", "CustomRule") || equals("log.ruleType", "Custom")) && - exists("log.syslogHost") + exists("target.host") groupBy: - lastEvent.log.eventDescription - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/deep_visibility_threat_indicators.yml b/rules/antivirus/sentinel-one/deep_visibility_threat_indicators.yml index 12d0e7432..896853fe8 100644 --- a/rules/antivirus/sentinel-one/deep_visibility_threat_indicators.yml +++ b/rules/antivirus/sentinel-one/deep_visibility_threat_indicators.yml @@ -32,7 +32,7 @@ where: | (contains("log.eventDescription", "threat") && oneOf("log.threatClassification", ["Malware", "Exploit", "Ransomware", "Trojan", "PUP"])) || (contains("log.eventDescription", "malicious") && exists("log.threatId")) ) && - exists("log.syslogHost") + exists("target.host") groupBy: - lastEvent.log.eventDescription - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/endpoint_detection_response_alerts.yml b/rules/antivirus/sentinel-one/endpoint_detection_response_alerts.yml index 8769dc607..6da78e8fd 100644 --- a/rules/antivirus/sentinel-one/endpoint_detection_response_alerts.yml +++ b/rules/antivirus/sentinel-one/endpoint_detection_response_alerts.yml @@ -38,7 +38,7 @@ where: | exists("log.destinationUser") || exists("log.endpointDeviceName") ) && - exists("log.syslogHost") + exists("target.host") groupBy: - lastEvent.log.endpointDeviceName - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/iot_device_compromise_indicators.yml b/rules/antivirus/sentinel-one/iot_device_compromise_indicators.yml index 2f64a72fc..2758a31c3 100644 --- a/rules/antivirus/sentinel-one/iot_device_compromise_indicators.yml +++ b/rules/antivirus/sentinel-one/iot_device_compromise_indicators.yml @@ -27,8 +27,7 @@ description: | 8. Implement additional monitoring for IoT device communications where: | exists("log.eventDescription") && - contains("log.eventDescription", ["iot", "embedded", "firmware", - "industrial", "scada", "ics", "plc"]) && + regexMatch("log.eventDescription", "\\b(iot|embedded|firmware|industrial|scada|ics|plc)\\b") && contains("log.eventDescription", ["compromise", "malicious", "backdoor", "botnet", "exploit", "infection"]) && exists("log.endpointDeviceName") diff --git a/rules/antivirus/sentinel-one/kernel_level_threat.yml b/rules/antivirus/sentinel-one/kernel_level_threat.yml index 389382c28..a8091f573 100644 --- a/rules/antivirus/sentinel-one/kernel_level_threat.yml +++ b/rules/antivirus/sentinel-one/kernel_level_threat.yml @@ -27,7 +27,8 @@ description: | where: | regexMatch("log.eventDescription", "(?i)(kernel|rootkit|driver|ring.?0|system.?level)") && contains("log.eventDescription", ["threat", "malicious", "exploit", "compromise", - "detect", "block", "prevent", "isolate"]) + "detect", "block", "prevent", "isolate"]) && + exists("target.host") groupBy: - lastEvent.log.eventDescription - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/memory_injection_detection.yml b/rules/antivirus/sentinel-one/memory_injection_detection.yml index 1e85824a5..199b29110 100644 --- a/rules/antivirus/sentinel-one/memory_injection_detection.yml +++ b/rules/antivirus/sentinel-one/memory_injection_detection.yml @@ -26,8 +26,7 @@ description: | 7. Review network connections from the affected system for C2 communication 8. Ensure SentinelOne remediation actions have been applied successfully where: | - regexMatch("log.eventDescription", "(?i)(memory.injection|dll.injection|process.injection|reflective.dll|process.hollow|code.injection|thread.hijack|injection.detected|malicious.injection)") || - regexMatch("log.eventDescToParse", "(?i)(memory.injection|dll.injection|process.injection|reflective.dll|process.hollow|code.injection|thread.hijack|injection.detected|malicious.injection)") + regexMatch("log.eventDescription", "(?i)(memory.injection|dll.injection|process.injection|reflective.dll|process.hollow|code.injection|thread.hijack|injection.detected|malicious.injection)") groupBy: - - adversary.host + - target.host - adversary.user diff --git a/rules/antivirus/sentinel-one/rollback_operation_patterns.yml b/rules/antivirus/sentinel-one/rollback_operation_patterns.yml index 0265171ff..e8b3dba49 100644 --- a/rules/antivirus/sentinel-one/rollback_operation_patterns.yml +++ b/rules/antivirus/sentinel-one/rollback_operation_patterns.yml @@ -26,4 +26,4 @@ description: | where: regexMatch("log.eventDescription", ".*[Rr]ollback.*") && (equals("log.activityType", "rollback") || exists("log.rollbackStatus") || equals("log.cat", "SystemEvent")) groupBy: - lastEvent.log.endpointDeviceName - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/s1_exclusion_abuse.yml b/rules/antivirus/sentinel-one/s1_exclusion_abuse.yml index b17d700f8..6799e9835 100644 --- a/rules/antivirus/sentinel-one/s1_exclusion_abuse.yml +++ b/rules/antivirus/sentinel-one/s1_exclusion_abuse.yml @@ -32,4 +32,4 @@ where: | oneOf("log.activityType", ["exclusion_created", "whitelist_created", "exclusion_modified"]) groupBy: - lastEvent.log.activityType - - adversary.host + - adversary.user diff --git a/rules/antivirus/sentinel-one/s1_policy_downgrade.yml b/rules/antivirus/sentinel-one/s1_policy_downgrade.yml index 2e41b48b3..0417a478f 100644 --- a/rules/antivirus/sentinel-one/s1_policy_downgrade.yml +++ b/rules/antivirus/sentinel-one/s1_policy_downgrade.yml @@ -24,12 +24,13 @@ description: | 5. Review admin account access for compromise 6. Scan all affected endpoints for unmitigated threats where: | - (contains("log.eventDescription", "policy") && - ((contains("log.eventDescription", "Detect") && contains("log.eventDescription", "changed")) || - contains("log.eventDescription", "downgrade") || - (contains("log.eventDescription", "Protect") && contains("log.eventDescription", "Detect")))) || - (oneOf("log.activityType", ["policy_updated", "policy_changed"]) && - contains("log.eventDescription", "Detect")) + (regexMatch("log.eventDescription", "(?i)\\bpolicy\\b") || + oneOf("log.activityType", ["policy_updated", "policy_changed"])) && + (regexMatch("log.eventDescription", "(?i)\\bdowngrad(e|es|ed|ing)\\b") || + regexMatch("log.eventDescription", "(?i)\\bfrom\\W+(the\\W+)?protect\\b.*\\bto\\W+(the\\W+)?detect\\b") || + regexMatch("log.eventDescription", "(?i)\\bprotect\\W+(mode\\W+)?to\\W+(the\\W+)?detect\\b") || + regexMatch("log.eventDescription", "(?i)\\bprotect\\s*/\\s*(protect|detect)\\b.*\\bto\\W+(the\\W+)?detect\\s*/\\s*(protect|detect)\\b") || + regexMatch("log.eventDescription", "(?i)\\b(protect|detect)\\s*/\\s*protect\\b.*\\bto\\W+(the\\W+)?(protect|detect)\\s*/\\s*detect\\b")) groupBy: - lastEvent.log.activityType - - adversary.host + - adversary.user diff --git a/rules/antivirus/sentinel-one/suspicious_process_tree.yml b/rules/antivirus/sentinel-one/suspicious_process_tree.yml index 52a18a5be..999925d28 100644 --- a/rules/antivirus/sentinel-one/suspicious_process_tree.yml +++ b/rules/antivirus/sentinel-one/suspicious_process_tree.yml @@ -30,7 +30,7 @@ where: | "process spawn", "suspicious process", "abnormal process", "process anomaly"]) || contains("log.threatName", ["process", "injection", "hollow"]) || contains("log.mitigationMode", "process")) && - exists("log.syslogHost") + exists("target.host") groupBy: - - lastEvent.log.syslogHost + - target.host - lastEvent.log.threatId diff --git a/rules/antivirus/sentinel-one/threat_intelligence_matches.yml b/rules/antivirus/sentinel-one/threat_intelligence_matches.yml index 125cb93b0..75867b09d 100644 --- a/rules/antivirus/sentinel-one/threat_intelligence_matches.yml +++ b/rules/antivirus/sentinel-one/threat_intelligence_matches.yml @@ -29,8 +29,8 @@ description: | 10. Analyze the timeline of events leading to the threat intelligence match 11. Check for any related indicators of compromise (IoCs) in the environment 12. Verify if this is a false positive by cross-referencing with other security tools -where: exists("log.externalthreatvalue") || (exists("log.filecontenthash") && greaterOrEqual("log.confidencelevel", 90)) || contains("log.dveventtype", "threat_intel") || contains("log.eventDescription", "reputation") +where: exists("log.externalthreatvalue") || (exists("log.filecontenthash") && equalsIgnoreCase("log.confidencelevel", "MALICIOUS")) || contains("log.dveventtype", "threat_intel") || (contains("log.eventDescription", "reputation") && exists("target.host")) groupBy: - lastEvent.log.externalthreatvalue - lastEvent.log.filecontenthash - - lastEvent.log.syslogHost + - target.host diff --git a/rules/antivirus/sentinel-one/threat_mitigation_failures.yml b/rules/antivirus/sentinel-one/threat_mitigation_failures.yml index ae5f40ec9..d207244b1 100644 --- a/rules/antivirus/sentinel-one/threat_mitigation_failures.yml +++ b/rules/antivirus/sentinel-one/threat_mitigation_failures.yml @@ -25,8 +25,8 @@ description: | 6. If persistent failures occur, engage incident response team for deep forensic analysis 7. Document the threat indicators for future detection improvements where: | - (contains("log.eventDescription", "mitigation") && contains("log.eventDescription", "fail")) || - (contains("log.eventDescription", "remediation") && contains("log.eventDescription", "fail")) || + (regexMatch("log.eventDescription", "\\b(mitigation|remediation)s?\\b") && + regexMatch("log.eventDescription", "\\bfail(s|ed|ing|ure|ures)?\\b")) || contains("log.mitigationStatus", "fail") || equals("log.threatStatus", "mitigation_failed") || (equals("log.activityType", "mitigation") && equals("log.status", "failed")) From af3d7d587cd7262a33c7401f4d7bc5157eeb3c74 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 23 Sep 2026 17:04:06 -0400 Subject: [PATCH 2/5] test(sentinel-one): add fabricated raw fixtures and regression checks sentinel_one_filter_test.go checks the filter structure, compiles every pattern, runs the header, value and rt patterns over 52 fabricated lines and evaluates 41 predicate cases against the shipped rules with go-sdk v1.1.33. testdata/sentinel-one/replay.py replays the same lines through the public EventProcessor playground and checks every field and alert, optionally with a test-only step that supplies target.host. All inputs are invented. Co-Authored-By: Claude Opus 5.5 --- plugins/alerts/sentinel_one_filter_test.go | 593 +++++ .../testdata/sentinel-one/expected.json | 2101 +++++++++++++++++ .../testdata/sentinel-one/patterns.yaml | 14 + plugins/alerts/testdata/sentinel-one/raw.json | 54 + .../alerts/testdata/sentinel-one/replay.py | 191 ++ 5 files changed, 2953 insertions(+) create mode 100644 plugins/alerts/sentinel_one_filter_test.go create mode 100644 plugins/alerts/testdata/sentinel-one/expected.json create mode 100644 plugins/alerts/testdata/sentinel-one/patterns.yaml create mode 100644 plugins/alerts/testdata/sentinel-one/raw.json create mode 100644 plugins/alerts/testdata/sentinel-one/replay.py diff --git a/plugins/alerts/sentinel_one_filter_test.go b/plugins/alerts/sentinel_one_filter_test.go new file mode 100644 index 000000000..a8e33b380 --- /dev/null +++ b/plugins/alerts/sentinel_one_filter_test.go @@ -0,0 +1,593 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + "text/template" + + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "google.golang.org/protobuf/encoding/protojson" +) + +// SentinelOne regression checks. Every input is fabricated (see +// testdata/sentinel-one). These tests use the pinned go-sdk v1.1.33 for YAML +// decoding, CEL and Event conversion; they do not run the EventProcessor. +// testdata/sentinel-one/replay.py runs the same raw lines through the public +// playground. See filters/audits/sentinel-one.md for the evidence and limits. + +const s1Filter = "../../filters/antivirus/sentinel-one.yml" +const s1Rules = "../../rules/antivirus/sentinel-one" + +// kv keeps only the first word of these values; each one is re-extracted in full. +var s1MultiWord = map[string]string{ + "accountName": "log.accName", + "eventDesc": "log.eventDescription", + "suser": "log.sourceUser", + "duser": "log.destinationUser", + "endpointDeviceControlDeviceName": "log.endpointDeviceName", + "sourceGroupName": "log.sourceGpName", + "sourceIpAddresses": "log.sourceIps", + "sourceMacAddresses": "log.sourceMacs", + "siteName": "log.siteName", +} + +func s1Config(t *testing.T) *plugins.Pipeline { + t.Helper() + encoded, err := utils.ReadPbYaml(s1Filter) + if err != nil { + t.Fatal(err) + } + config := new(plugins.Config) + if err := protojson.Unmarshal(encoded, config); err != nil { + t.Fatal(err) + } + if len(config.Pipeline) != 1 || len(config.Pipeline[0].DataTypes) != 1 || + config.Pipeline[0].DataTypes[0] != "antivirus-sentinel-one" { + t.Fatalf("unexpected pipeline layout: %v", config.Pipeline) + } + return config.Pipeline[0] +} + +func s1Patterns(t *testing.T) map[string]string { + t.Helper() + encoded, err := utils.ReadPbYaml(filepath.Join("testdata", "sentinel-one", "patterns.yaml")) + if err != nil { + t.Fatal(err) + } + var file struct { + Patterns map[string]string `json:"patterns"` + } + if err := json.Unmarshal(encoded, &file); err != nil { + t.Fatal(err) + } + return file.Patterns +} + +// s1Expand fills {{.name}} the way the SDK regexp cache does for grok patterns. +func s1Expand(t *testing.T, pattern string, defs map[string]string) string { + t.Helper() + parsed, err := template.New("pattern").Option("missingkey=error").Parse(pattern) + if err != nil { + t.Fatalf("pattern %q: %v", pattern, err) + } + var out bytes.Buffer + if err := parsed.Execute(&out, defs); err != nil { + t.Fatalf("pattern %q: %v", pattern, err) + } + return out.String() +} + +func s1Writes(step *plugins.Step) []string { + var out []string + if g := step.Grok; g != nil { + for _, p := range g.Patterns { + out = append(out, p.FieldName) + } + } + if r := step.Rename; r != nil { + out = append(out, r.To) + } + if a := step.Add; a != nil { + out = append(out, a.Params["key"].GetStringValue()) + } + return out +} + +func s1Contains(list []string, want string) bool { + for _, item := range list { + if item == want { + return true + } + } + return false +} + +func TestSentinelOneFilterContract(t *testing.T) { + steps := s1Config(t).Steps + header, kv := -1, -1 + for i, step := range steps { + for _, field := range s1Writes(step) { + if field == "log.syslogHost" { + t.Errorf("step %d writes log.syslogHost; the CEF version slot is not a host", i) + } + if field == "severity" { + t.Errorf("step %d writes severity; the vendor CEF severity scale is not established", i) + } + } + if trim := step.Trim; trim != nil && s1Contains(trim.Fields, "log.syslogHost") { + t.Errorf("step %d still trims log.syslogHost", i) + } + if g := step.Grok; g != nil && g.Source == "raw" && g.Where == `contains("raw", "CEF:")` { + header = i + var named []string + for _, p := range g.Patterns { + if !strings.Contains(p.FieldName, "trash") { + named = append(named, p.FieldName) + } + } + want := []string{"log.cefDeviceVendor", "log.cefDeviceProduct", "log.cefDeviceVersion", + "log.cefSignatureId", "log.eventDescription", "log.cefSeverity", "log.restData"} + if strings.Join(named, ",") != strings.Join(want, ",") { + t.Errorf("CEF header fields %v, want %v", named, want) + } + if !strings.HasPrefix(g.Patterns[0].Pattern, "{{.data}}CEF:") { + t.Errorf("header parse is not anchored on CEF: %q", g.Patterns[0].Pattern) + } + } + if step.Kv != nil { + kv = i + if step.Kv.Source != "log.restData" || step.Kv.Where != `exists("log.restData")` { + t.Errorf("kv must read log.restData only when it exists, got %q / %q", step.Kv.Source, step.Kv.Where) + } + } + } + if header < 0 || kv < 0 || header > kv { + t.Fatalf("CEF header parse (step %d) must run before kv (step %d)", header, kv) + } + + // Complete values, including the last key, under the existing output names. + outputs := map[string]bool{} + for key, out := range s1MultiWord { + found := false + for _, step := range steps { + g := step.Grok + if g == nil || g.Source != "log.restData" || len(g.Patterns) != 2 { + continue + } + if g.Patterns[0].Pattern != `{{.data}}(?:^|[\s|])`+key+`=` { + continue + } + found = true + if g.Patterns[1].FieldName != out || !strings.HasSuffix(g.Patterns[1].Pattern, "|.+)") { + t.Errorf("%s: want a last-key-safe capture into %s, got %v", key, out, g.Patterns[1]) + } + if !strings.Contains(g.Where, key+`=\\S`) { + t.Errorf("%s: an empty value must not be captured, where=%q", key, g.Where) + } + } + if !found { + t.Errorf("no complete-value extraction for %s", key) + } + outputs[out] = false + } + for _, step := range steps { + if trim := step.Trim; trim != nil && trim.Function == "regex" && trim.Substring == `\s+[A-Za-z0-9_.]+=$` { + for _, field := range trim.Fields { + outputs[field] = true + } + } + } + for field, trimmed := range outputs { + if !trimmed { + t.Errorf("%s is not stripped of the following key", field) + } + } + + // Full rt value and a guarded deviceTime. + var rtGrok, reformat, deviceTime bool + for _, step := range steps { + if g := step.Grok; g != nil && g.Source == "log.restData" && len(g.Patterns) == 2 && + g.Patterns[0].Pattern == `{{.data}}(?:^|[\s|])rt=#arcsightDate\(` && g.Patterns[1].FieldName == "log.rt" { + rtGrok = true + } + if r := step.Reformat; r != nil && s1Contains(r.Fields, "log.deviceTimeCandidate") { + reformat = r.Function == "time" && r.FromFormat == "Mon, 2 Jan 2006, 15:04:05 MST" && + r.ToFormat == "2006-01-02T15:04:05Z07:00" && strings.Contains(r.Where, "UTC$") + } + if r := step.Rename; r != nil && r.To == "deviceTime" { + deviceTime = strings.Join(r.From, ",") == "log.deviceTimeCandidate" && strings.Contains(r.Where, "T[0-9]{2}") + } + } + if !rtGrok || !reformat || !deviceTime { + t.Errorf("rt/deviceTime steps: grok=%t reformat=%t rename=%t", rtGrok, reformat, deviceTime) + } + + // The console actor is copied, not moved, and only for management events. + writers := 0 + for i, step := range steps { + if !s1Contains(s1Writes(step), "origin.user") { + continue + } + writers++ + g := step.Grok + if g == nil || g.Source != "log.sourceUser" || len(g.Patterns) != 1 || + !strings.HasPrefix(g.Where, `equals("log.cat", "SystemEvent") && `) { + t.Errorf("step %d: origin.user must be a copy of log.sourceUser for SystemEvent only", i) + } + } + if writers != 1 { + t.Errorf("origin.user writers: %d, want 1", writers) + } + + cleanup := steps[len(steps)-1].Delete + if cleanup == nil || cleanup.Where != "" { + t.Fatal("the filter must end with an unconditional cleanup") + } + for _, kept := range []string{"log.sourceUser", "log.eventDescription", "log.cefSeverity", + "log.cefSignatureId", "log.ruleTime", "log.siteName", "origin.user"} { + if s1Contains(cleanup.Fields, kept) { + t.Errorf("cleanup deletes %s", kept) + } + for _, step := range steps { + if r := step.Rename; r != nil && s1Contains(r.From, kept) { + t.Errorf("%s is renamed away", kept) + } + } + } + for _, scratch := range []string{"log.restData", "log.deviceTimeCandidate", "log.3trash", "log.suser", "log.accountName"} { + if !s1Contains(cleanup.Fields, scratch) { + t.Errorf("cleanup keeps %s", scratch) + } + } +} + +var s1RegexCall = regexp.MustCompile(`regexMatch\("[^"]*",\s*("(?:[^"\\]|\\.)*")\)`) + +// The SDK regexMatch returns false for an invalid pattern, so a typo would +// silently disable a rule branch. Every filter pattern must compile as well. +func TestSentinelOnePatternsCompile(t *testing.T) { + defs := s1Patterns(t) + cache := plugins.NewCELCache("sentinel-one-compile") + sample := `{"dataType":"antivirus-sentinel-one","raw":"CEF:0","log":{"eventDescription":"x","restData":"x","cat":"x"}}` + checkWhere := func(where string) { + if where == "" { + return + } + if _, err := cache.Eval(where, sample); err != nil && !strings.Contains(err.Error(), "failed to evaluate program") { + t.Errorf("CEL %q: %v", where, err) + } + for _, m := range s1RegexCall.FindAllStringSubmatch(where, -1) { + pattern, err := strconv.Unquote(m[1]) + if err == nil { + _, err = regexp.Compile(pattern) + } + if err != nil { + t.Errorf("regexMatch pattern %s: %v", m[1], err) + } + } + } + for _, step := range s1Config(t).Steps { + if g := step.Grok; g != nil { + for _, p := range g.Patterns { + if _, err := regexp.Compile(s1Expand(t, p.Pattern, defs)); err != nil { + t.Errorf("grok %q: %v", p.Pattern, err) + } + } + checkWhere(g.Where) + } + if tr := step.Trim; tr != nil && tr.Function == "regex" { + if _, err := regexp.Compile(tr.Substring); err != nil { + t.Errorf("trim %q: %v", tr.Substring, err) + } + } + for _, w := range []string{step.GetKv().GetWhere(), step.GetTrim().GetWhere(), step.GetRename().GetWhere(), + step.GetReformat().GetWhere(), step.GetAdd().GetWhere(), step.GetDelete().GetWhere()} { + checkWhere(w) + } + } + for _, rule := range s1LoadRules(t) { + checkWhere(rule.Where) + } +} + +// s1Grok mirrors the parse loop of the public grok plugin at EventProcessor +// 497bf53dbd1ae096f7b2dbc7bce77a6bf9f22ce1 (plugins/grok/main.go): each pattern +// must match at the start of the trimmed remainder, an empty remainder stops +// the loop, and nothing is written unless every pattern matched. It is a model +// used to guard these patterns in CI, not a substitute for replay.py. +func s1Grok(t *testing.T, value string, g *plugins.Grok, defs map[string]string) (map[string]string, bool) { + out, size := map[string]string{}, 0 + for _, p := range g.Patterns { + value = strings.TrimSpace(value) + if value == "" { + break + } + match := regexp.MustCompile(s1Expand(t, p.Pattern, defs)).FindString(value) + if match == "" || !strings.HasPrefix(value, match) { + break + } + size++ + if p.FieldName != "" { + out[p.FieldName] = strings.TrimSpace(match) + } + value = strings.TrimPrefix(value, match) + } + return out, size == len(g.Patterns) +} + +// s1TrimRegex mirrors the regex branch of plugins/trim/main.go at the same commit. +func s1TrimRegex(value, pattern string) string { + s := strings.TrimSpace(value) + for _, m := range regexp.MustCompile(pattern).FindAllString(s, -1) { + s = strings.ReplaceAll(s, m, "") + } + return strings.TrimSpace(s) +} + +type s1Case struct { + Log map[string]string `json:"log"` + Alerts []string `json:"alerts"` +} + +func s1Fixtures(t *testing.T) (map[string]string, map[string]s1Case) { + t.Helper() + var raw map[string]string + var expected struct { + Cases map[string]s1Case `json:"cases"` + } + for name, target := range map[string]any{"raw.json": &raw, "expected.json": &expected} { + data, err := os.ReadFile(filepath.Join("testdata", "sentinel-one", name)) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(data, target); err != nil { + t.Fatalf("%s: %v", name, err) + } + } + if len(raw) == 0 || len(raw) != len(expected.Cases) { + t.Fatalf("raw fixtures %d, expectations %d", len(raw), len(expected.Cases)) + } + return raw, expected.Cases +} + +// Header, multi-word and rt captures on every fabricated CEF line, through the +// filter's own patterns. kv, rename, reformat and deletes are not modelled here. +func TestSentinelOneExtractionPatternsModel(t *testing.T) { + defs := s1Patterns(t) + steps := s1Config(t).Steps + cache := plugins.NewCELCache("sentinel-one-model") + var header, rt *plugins.Grok + valueGroks := []*plugins.Grok{} + var trim *plugins.Trim + for _, step := range steps { + switch g := step.Grok; { + case g != nil && g.Where == `contains("raw", "CEF:")`: + header = g + case g != nil && g.Source == "log.restData" && len(g.Patterns) == 2 && g.Patterns[1].FieldName == "log.rt": + rt = g + case g != nil && g.Source == "log.restData" && len(g.Patterns) == 2 && strings.HasSuffix(g.Patterns[1].Pattern, "|.+)"): + valueGroks = append(valueGroks, g) + } + if tr := step.Trim; tr != nil && tr.Function == "regex" { + trim = tr + } + } + if header == nil || rt == nil || trim == nil || len(valueGroks) != len(s1MultiWord) { + t.Fatalf("steps not found: header=%t rt=%t trim=%t values=%d", header != nil, rt != nil, trim != nil, len(valueGroks)) + } + raw, cases := s1Fixtures(t) + checked := 0 + for name, line := range raw { + if !strings.Contains(line, "CEF:") { + continue + } + t.Run(name, func(t *testing.T) { + want := cases[name].Log + got, ok := s1Grok(t, line, header, defs) + if !ok { + t.Fatal("CEF header not parsed") + } + rest := got["log.restData"] + draft, _ := json.Marshal(map[string]any{"log": map[string]string{"restData": rest}}) + for _, g := range valueGroks { + match, err := cache.Eval(g.Where, string(draft)) + if err != nil { + t.Fatal(err) + } + if !match { + continue + } + if values, ok := s1Grok(t, rest, g, defs); ok { + got[g.Patterns[1].FieldName] = s1TrimRegex(values[g.Patterns[1].FieldName], trim.Substring) + } + } + if values, ok := s1Grok(t, rest, rt, defs); ok { + got["log.rt"] = values["log.rt"] + } + for _, key := range []string{"cefDeviceVendor", "cefDeviceProduct", "cefDeviceVersion", + "cefSignatureId", "eventDescription", "cefSeverity"} { + if got["log."+key] != want[key] { + t.Errorf("log.%s = %q, want %q", key, got["log."+key], want[key]) + } + } + for _, out := range s1MultiWord { + key := strings.TrimPrefix(out, "log.") + if key == "eventDescription" { + continue + } + if value, expected := want[key]; got[out] != value || (!expected && got[out] != "") { + t.Errorf("%s = %q, want %q", out, got[out], value) + } + } + if strings.Contains(line, "rt=#arcsightDate(") && got["log.rt"] != want["ruleTime"] { + t.Errorf("log.rt = %q, want %q", got["log.rt"], want["ruleTime"]) + } + if strings.Contains(rest, "CEF:") || strings.HasPrefix(rest, "|") { + t.Errorf("header text left in log.restData: %q", rest) + } + }) + checked++ + } + if checked < 40 { + t.Fatalf("only %d CEF fixtures checked", checked) + } +} + +func s1LoadRules(t *testing.T) map[string]*plugins.Rule { + t.Helper() + files, err := filepath.Glob(filepath.Join(s1Rules, "*.y*ml")) + if err != nil || len(files) != 19 { + t.Fatalf("SentinelOne rules: %d files, error %v", len(files), err) + } + out := map[string]*plugins.Rule{} + for _, path := range files { + encoded, err := utils.ReadPbYaml(path) + if err != nil { + t.Fatal(err) + } + rule := new(plugins.Rule) + if err := protojson.Unmarshal(encoded, rule); err != nil { + t.Fatalf("%s: %v", path, err) + } + rule.Normalize() + out[strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))] = rule + } + return out +} + +func TestSentinelOneRuleConsumers(t *testing.T) { + alertPaths, eventPaths := map[string]bool{}, map[string]bool{} + contractPaths(new(plugins.Alert).ProtoReflect().Descriptor(), "", alertPaths) + contractPaths(new(plugins.Event).ProtoReflect().Descriptor(), "", eventPaths) + for stem, rule := range s1LoadRules(t) { + if rule.Adversary != "origin" || len(rule.DataTypes) != 1 || rule.DataTypes[0] != "antivirus-sentinel-one" { + t.Errorf("%s: adversary %q dataTypes %v", stem, rule.Adversary, rule.DataTypes) + } + for _, stale := range []string{"log.syslogHost", "log.eventDescToParse", `greaterOrEqual("log.confidencelevel"`} { + if strings.Contains(rule.Where, stale) { + t.Errorf("%s: condition still reads %s", stem, stale) + } + } + for _, field := range rule.GroupBy { + valid := alertPaths[field] + if strings.HasPrefix(field, "lastEvent.") { + inner := strings.TrimPrefix(field, "lastEvent.") + valid = eventPaths[inner] || strings.HasPrefix(inner, "log.") + } + if !valid || strings.Contains(field, "syslogHost") { + t.Errorf("%s: grouping path %q", stem, field) + } + } + } + rules := s1LoadRules(t) + for stem, want := range map[string]string{ + "s1_exclusion_abuse": "lastEvent.log.activityType,adversary.user", + "s1_policy_downgrade": "lastEvent.log.activityType,adversary.user", + "memory_injection_detection": "target.host,adversary.user", + } { + if got := strings.Join(rules[stem].GroupBy, ","); got != want { + t.Errorf("%s groups by %s, want %s", stem, got, want) + } + } +} + +// SDK v1.1.33 CEL on synthetic normalized events. These check the shipped +// predicates; raw extraction for the same wording is exercised by replay.py. +func TestSentinelOneRulePredicates(t *testing.T) { + rules := s1LoadRules(t) + cache := plugins.NewCELCache("sentinel-one-rules") + event := func(desc string, extra string) string { + body := `{"dataType":"antivirus-sentinel-one","log":{"eventDescription":` + strconv.Quote(desc) + if extra != "" { + body += "," + extra + } + return body + "}}" + } + withHost := func(e string) string { + return strings.TrimSuffix(e, "}") + `,"target":{"host":"endpoint-01.example.com"}}` + } + admin := func(name string) string { + return event("Administrative information - New user '"+name+"' added", `"cat":"SystemEvent"`) + } + cases := []struct { + rule, name, input string + want bool + }{ + {"memory_injection_detection", "injection text", event("Memory injection detected in example.exe", ""), true}, + {"memory_injection_detection", "removed scratch-field branch", `{"dataType":"antivirus-sentinel-one","log":{"eventDescToParse":"Memory injection detected"}}`, false}, + {"memory_injection_detection", "near miss", event("Memory scan completed on example.exe", ""), false}, + + {"behavioral_threat_detection", "endpoint", withHost(event("Behavioral anomaly detected in example.exe", "")), true}, + {"behavioral_threat_detection", "no endpoint", event("Behavioral anomaly detected in example.exe", ""), false}, + {"behavioral_threat_detection", "version slot as host", event("Behavioral anomaly detected in example.exe", `"syslogHost":"192.0.2.50"`), false}, + {"custom_detection_rule_triggers", "endpoint", withHost(event("STAR custom rule Example Watchlist triggered", "")), true}, + {"custom_detection_rule_triggers", "no endpoint", event("STAR custom rule Example Watchlist triggered", `"syslogHost":"build 23"`), false}, + {"deep_visibility_threat_indicators", "endpoint", withHost(event("Threat alert: ransomware detected in example.exe", "")), true}, + {"deep_visibility_threat_indicators", "no endpoint", event("Threat alert: ransomware detected in example.exe", ""), false}, + {"endpoint_detection_response_alerts", "endpoint", withHost(event("EDR alert: critical endpoint threat", "")), true}, + {"endpoint_detection_response_alerts", "no endpoint", event("EDR alert: critical endpoint threat", ""), false}, + {"suspicious_process_tree", "endpoint", withHost(event("Suspicious process chain started by example.exe", "")), true}, + {"suspicious_process_tree", "no endpoint", event("Suspicious process chain started by example.exe", ""), false}, + + {"kernel_level_threat", "endpoint", withHost(event("Kernel exploit blocked on example.exe", "")), true}, + {"kernel_level_threat", "console text", admin("Kernel Team (kernel.team@detect.example.com)"), false}, + {"threat_intelligence_matches", "vendor MALICIOUS", event("Example file flagged", `"filecontenthash":"0123456789abcdef","confidencelevel":"MALICIOUS"`), true}, + {"threat_intelligence_matches", "lower-case malicious", event("Example file flagged", `"filecontenthash":"0123456789abcdef","confidencelevel":"malicious"`), true}, + {"threat_intelligence_matches", "vendor SUSPICIOUS", event("Example file flagged", `"filecontenthash":"0123456789abcdef","confidencelevel":"SUSPICIOUS"`), false}, + {"threat_intelligence_matches", "0-100 score", event("Example file flagged", `"filecontenthash":"0123456789abcdef","confidencelevel":"95"`), false}, + {"threat_intelligence_matches", "reputation on an endpoint", withHost(event("File reputation lookup flagged example.exe", "")), true}, + {"threat_intelligence_matches", "reputation in console text", admin("Reputation Desk (reputation.desk@example.com)"), false}, + + {"s1_policy_downgrade", "protect to detect", event("Site policy mode changed from Protect to Detect", `"cat":"SystemEvent"`), true}, + {"s1_policy_downgrade", "downgraded", event("Policy downgraded for Example Site Alpha", ""), true}, + {"s1_policy_downgrade", "pair, malicious mode lowered", event("Site policy changed from Protect/Detect to Detect/Detect", ""), true}, + {"s1_policy_downgrade", "pair, suspicious mode lowered", event("Site policy changed from Protect/Protect to Protect/Detect", ""), true}, + {"s1_policy_downgrade", "upgrade", event("Site policy mode changed from Detect to Protect", ""), false}, + {"s1_policy_downgrade", "pair upgrade", event("Site policy changed from Detect/Detect to Protect/Detect", ""), false}, + {"s1_policy_downgrade", "pair upgrade, suspicious", event("Site policy changed from Protect/Detect to Protect/Protect", ""), false}, + {"s1_policy_downgrade", "role naming both modes", event(`Example Admin assigned role "Protect and Detect Reviewers" to user Policy Team (policy.team@example.com)`, ""), false}, + {"s1_policy_downgrade", "no policy wording", event("Mode changed from Protect to Detect", ""), false}, + + {"agent_tampering_attempts", "agent disabled", event("Example Admin disabled the agent on endpoint-01", ""), true}, + {"agent_tampering_attempts", "agents uninstalled", event("2 agents uninstalled from Example Site Alpha", ""), true}, + {"agent_tampering_attempts", "in-word stop and agent", admin("Nonstop Example (nonstop.example@agentur.example.com)"), false}, + {"agent_tampering_attempts", "stopwatch", event("agent stopwatch sync", ""), false}, + {"threat_mitigation_failures", "mitigation failed", event("Threat mitigation failed on endpoint-01", ""), true}, + {"threat_mitigation_failures", "remediation failures", event("remediation failures on endpoint-01", ""), true}, + {"threat_mitigation_failures", "failover in a name", event("Administrative information - User 'Mitigation Failover (mitigation.failover@example.com)' Deleted", ""), false}, + {"iot_device_compromise_indicators", "firmware backdoor", event("Malicious firmware backdoor detected", `"endpointDeviceName":"Example Camera 01"`), true}, + {"iot_device_compromise_indicators", "ics inside analytics", event("Example backdoor file detected in analytics service", `"endpointDeviceName":"Example Server 01"`), false}, + {"iot_device_compromise_indicators", "iot inside patriot", event("patriot backdoor detected", `"endpointDeviceName":"Example Server 01"`), false}, + } + for _, tc := range cases { + t.Run(tc.rule+"/"+tc.name, func(t *testing.T) { + rule := rules[tc.rule] + if rule == nil { + t.Fatalf("rule %s not found", tc.rule) + } + input := tc.input + ev := new(plugins.Event) + if err := utils.StringToProtoMessage(&input, ev); err != nil { + t.Fatal(err) + } + output, err := utils.ProtoMessageToString(ev) + if err != nil { + t.Fatal(err) + } + match, err := cache.Eval(rule.Where, *output) + if err != nil { + t.Fatal(err) + } + if match != tc.want { + t.Fatalf("match=%t want=%t on %s", match, tc.want, *output) + } + }) + } +} diff --git a/plugins/alerts/testdata/sentinel-one/expected.json b/plugins/alerts/testdata/sentinel-one/expected.json new file mode 100644 index 000000000..0f47c6fc6 --- /dev/null +++ b/plugins/alerts/testdata/sentinel-one/expected.json @@ -0,0 +1,2101 @@ +{ + "envelopeTimestamp": "2026-09-21T17:40:00Z", + "cases": { + "admin-user-added": { + "logKeys": [ + "accName", + "accountId", + "activityID", + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "siteId", + "siteName", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user 'Example User (example.user@example.com)' added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Mon, 21 Sep 2026, 13:47:01 UTC", + "activityID": "1000000000000000001", + "activityType": "23", + "siteId": "1000000000000000002", + "siteName": "Example Site Alpha", + "accountId": "1000000000000000003", + "accName": "Example Account", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T13:47:01Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "admin-user-deleted": { + "logKeys": [ + "accName", + "accountId", + "activityID", + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "siteId", + "siteName", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "25", + "cefSeverity": "1", + "eventDescription": "Administrative information - User 'Example User (example.user@example.com_del_0123456789abcdef0123456789abcdef)' Deleted", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Mon, 21 Sep 2026, 14:32:27 UTC", + "activityID": "1000000000000000004", + "activityType": "25", + "siteId": "1000000000000000002", + "siteName": "Example Site Alpha", + "accountId": "1000000000000000003", + "accName": "Example Account", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T14:32:27Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "admin-role-assigned": { + "logKeys": [ + "accName", + "accountId", + "activityID", + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "siteId", + "siteName", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "37", + "cefSeverity": "1", + "eventDescription": "Example Admin assigned role \"Viewer\" to user Example User (example.user@example.com)", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Mon, 21 Sep 2026, 17:04:41 UTC", + "activityID": "1000000000000000005", + "activityType": "37", + "siteId": "1000000000000000002", + "siteName": "Example Site Alpha", + "accountId": "1000000000000000003", + "accName": "Example Account", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:04:41Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "header-bsd": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Mon, 21 Sep 2026, 17:34:42 UTC", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:34:42Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "header-none": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "header-rfc5424": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser", + "syslogDeviceTime", + "syslogPriority", + "syslogVersion" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Mon, 21 Sep 2026, 17:34:42 UTC", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:34:42Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "header-ipv4-version": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "192.0.2.50", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "header-word-int-version": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "build 23", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "header-escaped-pipe": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - Group 'A\\|B' updated", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "rt-first-key": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "ruleTime": "Mon, 21 Sep 2026, 13:47:01 UTC", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T13:47:01Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "rt-epoch": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Example activity", + "sourceUser": "Example Admin", + "ruleTime": "1790012082000", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "rt-single-digit-day": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Example activity", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Thu, 1 Oct 2026, 09:05:07 UTC", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-10-01T09:05:07Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "rt-padded-day-last": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Example activity", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE", + "ruleTime": "Thu, 01 Oct 2026, 09:05:07 UTC" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-10-01T09:05:07Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "rt-non-utc": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "ruleTime", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Example activity", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "ruleTime": "Mon, 21 Sep 2026, 13:47:01 EST", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "multiword-all-keys": { + "logKeys": [ + "accName", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "destinationUser", + "endpointDeviceName", + "eventDescription", + "notificationScope", + "sourceGpName", + "sourceIps", + "sourceMacs", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Example description text", + "sourceUser": "Example Admin", + "destinationUser": "Other Example User", + "endpointDeviceName": "USB Mass Storage", + "sourceGpName": "Default Group", + "sourceIps": "192.0.2.10,192.0.2.11", + "sourceMacs": "00:00:5e:00:53:01", + "accName": "Example Account", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [] + }, + "multiword-accountname-last": { + "logKeys": [ + "accName", + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "siteName", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Example activity", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "siteName": "Example Site Beta", + "notificationScope": "SITE", + "accName": "Example Account Two" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "multiword-sourceips-last": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "sourceIps", + "sourceMacs", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Example activity", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "sourceMacs": "00:00:5e:00:53:01", + "sourceIps": "192.0.2.10,192.0.2.11" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "multiword-suser-last": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE", + "sourceUser": "Example Admin" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [] + }, + "multiword-empty-suser": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user added", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [] + }, + "identity-threat-class": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "5", + "eventDescription": "Example threat activity", + "sourceUser": "Other Example User", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [] + }, + "r1-memory-injection": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Memory injection detected in example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [ + "memory_injection_detection" + ] + }, + "r1-process-injection-eventdesc": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Process injection detected in example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [ + "memory_injection_detection" + ] + }, + "r1-memory-scan": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "3", + "eventDescription": "Memory scan completed on example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "note": "near miss for memory_injection_detection" + }, + "r3-policy-downgrade": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Site policy mode changed from Protect to Detect for Example Site Alpha", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [ + "s1_policy_downgrade" + ] + }, + "r3-policy-pair-downgrade": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Site policy changed from Protect/Detect to Detect/Detect", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [ + "s1_policy_downgrade" + ] + }, + "r3-policy-upgrade": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Site policy mode changed from Detect to Protect for Example Site Alpha", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "near miss: upgrade must not fire s1_policy_downgrade" + }, + "r3-policy-pair-upgrade": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Site policy changed from Detect/Detect to Protect/Detect", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "near miss: paired upgrade" + }, + "r3-admin-role-protect-detect": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "37", + "cefSeverity": "1", + "eventDescription": "Example Admin assigned role \"Protect and Detect Reviewers\" to user Policy Team (policy.team@example.com)", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "37", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "benign console text naming both modes" + }, + "r6-exclusion-added": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Path exclusion added for /opt/example/tool", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [ + "s1_exclusion_abuse" + ] + }, + "r6-exclusion-removed": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Path exclusion removed for /opt/example/tool", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "near miss for s1_exclusion_abuse" + }, + "r4-agent-disabled": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "5020", + "cefSeverity": "1", + "eventDescription": "Example Admin disabled the agent on endpoint-01", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "5020", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [ + "agent_tampering_attempts" + ] + }, + "r4-admin-nonstop-agentur": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user 'Nonstop Example (nonstop.example@agentur.example.com)' added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "in-word 'stop' and 'agent' must not fire agent_tampering_attempts" + }, + "r4-mitigation-failed": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Threat mitigation failed on endpoint-01", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [ + "threat_mitigation_failures" + ] + }, + "r4-admin-mitigation-failover": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "25", + "cefSeverity": "1", + "eventDescription": "Administrative information - User 'Mitigation Failover (mitigation.failover@example.com_del_0123456789abcdef0123456789abcdef)' Deleted", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "25", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "'failover' is not a failure word" + }, + "r4-iot-firmware": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "endpointDeviceName", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Malicious firmware backdoor detected on example camera", + "cat": "ThreatEvent", + "endpointDeviceName": "Example Camera 01", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [ + "iot_device_compromise_indicators" + ] + }, + "r4-iot-analytics": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "endpointDeviceName", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Example backdoor file detected in analytics service", + "cat": "ThreatEvent", + "endpointDeviceName": "Example Server 01", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "note": "'ics' inside 'analytics' must not fire iot_device_compromise_indicators" + }, + "r5-admin-reputation-desk": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user 'Reputation Desk (reputation.desk@example.com)' added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "console text must not fire the reputation branch" + }, + "r5-admin-kernel-team": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "sourceUser", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "23", + "cefSeverity": "1", + "eventDescription": "Administrative information - New user 'Kernel Team (kernel.team@detect.example.com)' added", + "sourceUser": "Example Admin", + "cat": "SystemEvent", + "activityType": "23", + "notificationScope": "SITE" + }, + "fields": { + "origin.user": "Example Admin", + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse" + ], + "alerts": [], + "note": "console text must not fire kernel_level_threat" + }, + "r7-confidence-malicious": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "confidencelevel", + "eventDescription", + "filecontenthash", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Example file flagged", + "cat": "ThreatEvent", + "filecontenthash": "0123456789abcdef0123456789abcdef01234567", + "confidencelevel": "MALICIOUS", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [ + "threat_intelligence_matches" + ] + }, + "r7-confidence-suspicious": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "confidencelevel", + "eventDescription", + "filecontenthash", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Example file flagged", + "cat": "ThreatEvent", + "filecontenthash": "0123456789abcdef0123456789abcdef01234567", + "confidencelevel": "SUSPICIOUS", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "note": "near miss: lower vendor confidence" + }, + "r7-confidence-numeric": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "confidencelevel", + "eventDescription", + "filecontenthash", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Example file flagged", + "cat": "ThreatEvent", + "filecontenthash": "0123456789abcdef0123456789abcdef01234567", + "confidencelevel": "95", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "note": "a 0-100 score is not the vendor scale" + }, + "rollback-status": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "rollbackStatus", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "3", + "eventDescription": "Rollback completed for example.exe", + "cat": "ThreatEvent", + "rollbackStatus": "completed", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [ + "rollback_operation_patterns" + ] + }, + "rollback-text-only": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "3", + "eventDescription": "Rollback completed for example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "note": "near miss for rollback_operation_patterns" + }, + "r2-behavioral": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Behavioral anomaly detected in example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "behavioral_threat_detection" + ] + }, + "r2-custom-rule": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "STAR custom rule Example Watchlist triggered on example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "custom_detection_rule_triggers" + ] + }, + "r2-deep-visibility": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Threat alert: ransomware detected in example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "deep_visibility_threat_indicators" + ] + }, + "r2-edr": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "EDR alert: critical endpoint threat on example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "endpoint_detection_response_alerts" + ] + }, + "r2-process-tree": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Suspicious process chain started by example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "suspicious_process_tree" + ] + }, + "r5-kernel": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Kernel exploit blocked on example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "kernel_level_threat" + ] + }, + "r5-reputation": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "S-99.0.0#1", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "File reputation lookup flagged example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "endpointHarnessAlerts": [ + "threat_intelligence_matches" + ] + }, + "r2-behavioral-ipv4-version": { + "logKeys": [ + "activityType", + "cat", + "cefDeviceProduct", + "cefDeviceVendor", + "cefDeviceVersion", + "cefSeverity", + "cefSignatureId", + "eventDescription", + "notificationScope", + "syslogDeviceTime", + "syslogPriority" + ], + "log": { + "cefDeviceVendor": "SentinelOne", + "cefDeviceProduct": "Mgmt", + "cefDeviceVersion": "192.0.2.50", + "cefSignatureId": "3608", + "cefSeverity": "8", + "eventDescription": "Behavioral anomaly detected in example.exe", + "cat": "ThreatEvent", + "activityType": "3608", + "notificationScope": "SITE" + }, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "severity", + "log.syslogHost", + "log.restData", + "log.eventDescToParse", + "origin.user" + ], + "alerts": [], + "note": "the version slot is not a host" + }, + "non-cef-line": { + "logKeys": [], + "log": {}, + "fields": { + "deviceTime": "2026-09-21T17:40:00Z" + }, + "absent": [ + "origin.user", + "severity", + "log" + ], + "alerts": [] + } + } +} diff --git a/plugins/alerts/testdata/sentinel-one/patterns.yaml b/plugins/alerts/testdata/sentinel-one/patterns.yaml new file mode 100644 index 000000000..9c9c4ef41 --- /dev/null +++ b/plugins/alerts/testdata/sentinel-one/patterns.yaml @@ -0,0 +1,14 @@ +# The shared grok definitions this filter uses, copied from +# backend/src/main/resources/config/liquibase/changelog/20250616001_insert_utm_regex_pattern.xml. +patterns: + data: '(.*?)' + greedy: '.*' + integer: '(?:[+-]?(?:[0-9]+))' + ipv4: '(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.)){3}((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))' + monthDay: '(?:(?:0[1-9])|(?:[12][0-9])|(?:3[01])|[1-9])' + monthName: '\b(?:[Jj]an(?:uary|uar)?|[Ff]eb(?:ruary|ruar)?|[Mm](?:a|รค)?r(?:ch|z)?|[Aa]pr(?:il)?|[Mm]a(?:y|i)?|[Jj]un(?:e|i)?|[Jj]ul(?:y|i)?|[Aa]ug(?:ust)?|[Ss]ep(?:tember)?|[Oo](?:c|k)?t(?:ober)?|[Nn]ov(?:ember)?|[Dd]e(?:c|z)(?:ember)?)\b' + monthNumber: '(?:0[1-9]|1[0-2])' + space: '\s+' + time: '((([01][0-9])|2[0-4]):(?:[0-5][0-9])(?::(?:(?:[0-5]?[0-9]|60)(?:[:.,][0-9]+)?)))' + word: '\b\w+\b' + year: '(([1-9])[0-9]{1,3})' diff --git a/plugins/alerts/testdata/sentinel-one/raw.json b/plugins/alerts/testdata/sentinel-one/raw.json new file mode 100644 index 000000000..01aca034c --- /dev/null +++ b/plugins/alerts/testdata/sentinel-one/raw.json @@ -0,0 +1,54 @@ +{ + "admin-user-added": "<14>2026-09-21 13:48:07,689 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user 'Example User (example.user@example.com)' added|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Mon, 21 Sep 2026, 13:47:01 UTC) activityID=1000000000000000001 activityType=23 siteId=1000000000000000002 siteName=Example Site Alpha accountId=1000000000000000003 accountName=Example Account notificationScope=SITE", + "admin-user-deleted": "<14>2026-09-21 14:34:02,650 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|25|Administrative information - User 'Example User (example.user@example.com_del_0123456789abcdef0123456789abcdef)' Deleted|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Mon, 21 Sep 2026, 14:32:27 UTC) activityID=1000000000000000004 activityType=25 siteId=1000000000000000002 siteName=Example Site Alpha accountId=1000000000000000003 accountName=Example Account notificationScope=SITE", + "admin-role-assigned": "<14>2026-09-21 17:08:42,643 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|37|Example Admin assigned role \"Viewer\" to user Example User (example.user@example.com)|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Mon, 21 Sep 2026, 17:04:41 UTC) activityID=1000000000000000005 activityType=37 siteId=1000000000000000002 siteName=Example Site Alpha accountId=1000000000000000003 accountName=Example Account notificationScope=SITE", + "header-bsd": "<14>Sep 21 17:36:27 example-console CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user added|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Mon, 21 Sep 2026, 17:34:42 UTC) activityType=23 notificationScope=SITE", + "header-none": "CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user added|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "header-rfc5424": "<14>1 2026-09-21T14:36:27.589-03:00 example-console - - - - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user added|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Mon, 21 Sep 2026, 17:34:42 UTC) activityType=23 notificationScope=SITE", + "header-ipv4-version": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|192.0.2.50|23|Administrative information - New user added|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "header-word-int-version": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|build 23|23|Administrative information - New user added|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "header-escaped-pipe": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - Group 'A\\|B' updated|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "rt-first-key": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user added|1|rt=#arcsightDate(Mon, 21 Sep 2026, 13:47:01 UTC) suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "rt-epoch": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Example activity|1|suser=Example Admin rt=1790012082000 cat=SystemEvent activityType=23 notificationScope=SITE", + "rt-single-digit-day": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Example activity|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Thu, 1 Oct 2026, 09:05:07 UTC) activityType=23 notificationScope=SITE", + "rt-padded-day-last": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Example activity|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE rt=#arcsightDate(Thu, 01 Oct 2026, 09:05:07 UTC)", + "rt-non-utc": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Example activity|1|suser=Example Admin cat=SystemEvent rt=#arcsightDate(Mon, 21 Sep 2026, 13:47:01 EST) activityType=23 notificationScope=SITE", + "multiword-all-keys": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Example activity|1|suser=Example Admin duser=Other Example User eventDesc=Example description text endpointDeviceControlDeviceName=USB Mass Storage sourceGroupName=Default Group sourceIpAddresses=192.0.2.10,192.0.2.11 sourceMacAddresses=00:00:5e:00:53:01 accountName=Example Account notificationScope=SITE", + "multiword-accountname-last": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Example activity|1|suser=Example Admin cat=SystemEvent activityType=5020 siteName=Example Site Beta notificationScope=SITE accountName=Example Account Two", + "multiword-sourceips-last": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Example activity|1|suser=Example Admin cat=SystemEvent activityType=5020 sourceMacAddresses=00:00:5e:00:53:01 sourceIpAddresses=192.0.2.10,192.0.2.11", + "multiword-suser-last": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user added|1|cat=SystemEvent activityType=23 notificationScope=SITE suser=Example Admin", + "multiword-empty-suser": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user added|1|suser= cat=SystemEvent activityType=23 notificationScope=SITE", + "identity-threat-class": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Example threat activity|5|suser=Other Example User cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r1-memory-injection": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Memory injection detected in example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r1-process-injection-eventdesc": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Threat detected|8|cat=ThreatEvent eventDesc=Process injection detected in example.exe activityType=3608 notificationScope=SITE", + "r1-memory-scan": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Memory scan completed on example.exe|3|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r3-policy-downgrade": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Site policy mode changed from Protect to Detect for Example Site Alpha|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r3-policy-pair-downgrade": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Site policy changed from Protect/Detect to Detect/Detect|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r3-policy-upgrade": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Site policy mode changed from Detect to Protect for Example Site Alpha|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r3-policy-pair-upgrade": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Site policy changed from Detect/Detect to Protect/Detect|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r3-admin-role-protect-detect": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|37|Example Admin assigned role \"Protect and Detect Reviewers\" to user Policy Team (policy.team@example.com)|1|suser=Example Admin cat=SystemEvent activityType=37 notificationScope=SITE", + "r6-exclusion-added": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Path exclusion added for /opt/example/tool|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r6-exclusion-removed": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Path exclusion removed for /opt/example/tool|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r4-agent-disabled": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|5020|Example Admin disabled the agent on endpoint-01|1|suser=Example Admin cat=SystemEvent activityType=5020 notificationScope=SITE", + "r4-admin-nonstop-agentur": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user 'Nonstop Example (nonstop.example@agentur.example.com)' added|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "r4-mitigation-failed": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Threat mitigation failed on endpoint-01|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r4-admin-mitigation-failover": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|25|Administrative information - User 'Mitigation Failover (mitigation.failover@example.com_del_0123456789abcdef0123456789abcdef)' Deleted|1|suser=Example Admin cat=SystemEvent activityType=25 notificationScope=SITE", + "r4-iot-firmware": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Malicious firmware backdoor detected on example camera|8|cat=ThreatEvent endpointDeviceControlDeviceName=Example Camera 01 activityType=3608 notificationScope=SITE", + "r4-iot-analytics": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Threat detected|8|cat=ThreatEvent eventDesc=Example backdoor file detected in analytics service endpointDeviceControlDeviceName=Example Server 01 activityType=3608 notificationScope=SITE", + "r5-admin-reputation-desk": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user 'Reputation Desk (reputation.desk@example.com)' added|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "r5-admin-kernel-team": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|23|Administrative information - New user 'Kernel Team (kernel.team@detect.example.com)' added|1|suser=Example Admin cat=SystemEvent activityType=23 notificationScope=SITE", + "r7-confidence-malicious": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Example file flagged|8|cat=ThreatEvent filecontenthash=0123456789abcdef0123456789abcdef01234567 confidencelevel=MALICIOUS activityType=3608 notificationScope=SITE", + "r7-confidence-suspicious": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Example file flagged|8|cat=ThreatEvent filecontenthash=0123456789abcdef0123456789abcdef01234567 confidencelevel=SUSPICIOUS activityType=3608 notificationScope=SITE", + "r7-confidence-numeric": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Example file flagged|8|cat=ThreatEvent filecontenthash=0123456789abcdef0123456789abcdef01234567 confidencelevel=95 activityType=3608 notificationScope=SITE", + "rollback-status": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Rollback completed for example.exe|3|cat=ThreatEvent rollbackStatus=completed activityType=3608 notificationScope=SITE", + "rollback-text-only": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Rollback completed for example.exe|3|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r2-behavioral": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Behavioral anomaly detected in example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r2-custom-rule": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|STAR custom rule Example Watchlist triggered on example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r2-deep-visibility": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Threat alert: ransomware detected in example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r2-edr": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|EDR alert: critical endpoint threat on example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r2-process-tree": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Suspicious process chain started by example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r5-kernel": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|Kernel exploit blocked on example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r5-reputation": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|S-99.0.0#1|3608|File reputation lookup flagged example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "r2-behavioral-ipv4-version": "<14>2026-09-21 17:36:27,589 example-console - CEF:0|SentinelOne|Mgmt|192.0.2.50|3608|Behavioral anomaly detected in example.exe|8|cat=ThreatEvent activityType=3608 notificationScope=SITE", + "non-cef-line": "<134>Sep 21 17:36:27 example-host example plain text line" +} diff --git a/plugins/alerts/testdata/sentinel-one/replay.py b/plugins/alerts/testdata/sentinel-one/replay.py new file mode 100644 index 000000000..b4a8ac0c3 --- /dev/null +++ b/plugins/alerts/testdata/sentinel-one/replay.py @@ -0,0 +1,191 @@ +"""Replay fabricated SentinelOne raw lines through separately built EventProcessor binaries. + +Requires PyYAML. This does not build or deploy anything and uses only local file +writers. It stages the current filter and all 19 SentinelOne rules, runs the playground +and checks every event against expected.json and every local alert against the expected +rule. With --endpoint-harness it also stages a test-only step that adds target.host to +marked copies of the endpoint-gated lines; that step stands in for the endpoint mapping +this filter does not have yet and is never shipped. See filters/audits/sentinel-one.md. +""" +import argparse +import errno +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +import yaml + +PLUGINS = ("add", "cel", "delete", "grok", "kv", "reformat", "rename", "saw", "sew", "trim") +TENANT = "00000000-0000-4000-8000-000000000001" +HARNESS_SOURCE = "endpoint-harness" +HARNESS_HOST = "endpoint-01.example.com" +HARNESS_STAGE = f"""# Test-only stage, never shipped. It stands in for the deferred endpoint mapping. +pipeline: + - dataTypes: + - antivirus-sentinel-one + steps: + - add: + function: string + params: + key: target.host + value: {HARNESS_HOST} + where: 'equals("dataSource", "{HARNESS_SOURCE}")' +""" +LOG_FAILURES = ("failed to unmarshal", "failed to evaluate rule", "plugin not found", + "failed to compile regexp", "failed to start plugin", "failed to parse time") + + +def records(path): + # The playground writers can append adjacent JSON objects before newlines. + content = path.read_text() if path.exists() else "" + decoder = json.JSONDecoder() + result, offset = [], 0 + while offset < len(content): + while offset < len(content) and content[offset].isspace(): + offset += 1 + if offset < len(content): + record, offset = decoder.raw_decode(content, offset) + result.append(record) + return result + + +def require(condition, message): + if not condition: + raise RuntimeError(message) + + +def get(value, path): + for part in path.split("."): + if not isinstance(value, dict) or part not in value: + return None, False + value = value[part] + return value, True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--playground", required=True, type=Path) + parser.add_argument("--plugins", required=True, type=Path) + parser.add_argument("--endpoint-harness", action="store_true") + args = parser.parse_args() + fixture_dir = Path(__file__).resolve().parent + root = fixture_dir.parents[3] + os.umask(0o077) + work = Path(tempfile.mkdtemp(prefix="s1-pg-", dir="/tmp")) + print(f"Local evidence directory: {work}", flush=True) + for part in ("input", "output", "pipeline/filters", "rules", "plugins", "sockets", "geolocation"): + (work / part).mkdir(parents=True, exist_ok=True) + binaries = {"playground": args.playground.resolve()} + for name in PLUGINS: + source = (args.plugins / f"{name}.plugin").resolve() + require(source.is_file(), f"Missing binary: {source}") + try: + os.link(source, work / "plugins" / source.name) + except OSError as error: + if error.errno != errno.EXDEV: + raise + shutil.copy2(source, work / "plugins" / source.name) + binaries[name] = source + config = { + "tenants": [{"id": TENANT, "name": "fixture"}], + "plugins": { + "analysis": {"order": ["sew", "cel"]}, + "correlation": {"order": ["saw"]}, + "notification": {"order": []}, + # CEL initializes a client; no SentinelOne rule has a history query. + "org.opensearch": {"opensearch": "http://127.0.0.1:19200"}, + }, + } + (work / "pipeline/config.yaml").write_text(yaml.safe_dump(config)) + shutil.copy2(fixture_dir / "patterns.yaml", work / "pipeline/patterns.yaml") + filter_path = root / "filters/antivirus/sentinel-one.yml" + shutil.copy2(filter_path, work / "pipeline/filters/sentinel-one.yaml") + if args.endpoint_harness: + (work / "pipeline/filters/zz-endpoint-harness.yaml").write_text(HARNESS_STAGE) + rule_paths = sorted((root / "rules/antivirus/sentinel-one").glob("*.y*ml")) + require(len(rule_paths) == 19, f"Expected 19 SentinelOne rules, found {len(rule_paths)}") + stems = {} + for offset, path in enumerate(rule_paths): + rule = yaml.safe_load(path.read_text()) + require(isinstance(rule, dict) and "id" not in rule, f"Unexpected rule shape: {path.name}") + require(not rule.get("afterEvents") and not rule.get("correlation"), f"{path.name} needs history") + rule["id"] = 9001 + offset # the playground loader needs unique non-zero ids + stems[rule["name"]] = path.stem + (work / "rules" / f"{rule['id']}-{path.stem}.yaml").write_text(yaml.safe_dump([rule])) + cases = json.loads((fixture_dir / "raw.json").read_text()) + spec = json.loads((fixture_dir / "expected.json").read_text()) + expected = spec["cases"] + require(set(cases) == set(expected), "raw.json and expected.json disagree") + inputs = {} + for name, raw in cases.items(): + inputs[f"sentinel-one-{name}"] = (name, "fixture-console", raw) + if args.endpoint_harness and "endpointHarnessAlerts" in expected[name]: + inputs[f"sentinel-one-{name}-endpoint"] = (name, HARNESS_SOURCE, raw) + for number, (event_id, (name, source, raw)) in enumerate(sorted(inputs.items())): + event = {"id": event_id, "dataType": "antivirus-sentinel-one", "dataSource": source, + "@timestamp": spec["envelopeTimestamp"], "tenantId": TENANT, "raw": raw} + (work / "input" / f"{number:03d}.json").write_text(json.dumps(event)) + hashed = [filter_path, *rule_paths] + [fixture_dir / n for n in ("patterns.yaml", "raw.json", "expected.json")] + manifest = { + "provenance": "fabricated raw inputs; no customer data", + "endpointHarness": args.endpoint_harness, + "sourceHashes": {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() for p in hashed}, + "binaries": {name: { + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "buildInfo": subprocess.check_output(["go", "version", "-m", str(path)], text=True), + } for name, path in binaries.items()}, + } + (work / "manifest.json").write_text(json.dumps(manifest, indent=2)) + env = dict(os.environ, WORK_DIR=str(work), MODE="playground") + with (work / "execution.log").open("w") as log: + subprocess.run([str(binaries["playground"])], env=env, stdout=log, + stderr=subprocess.STDOUT, check=True, timeout=900) + log_text = (work / "execution.log").read_text() + for marker in LOG_FAILURES: + require(marker not in log_text, f"Execution log reports: {marker}") + + parsed = records(work / "output/resulting_log.json") + events = {r.get("id"): r for r in parsed} + require(len(parsed) == len(inputs) == len(events), f"Events {len(parsed)} for {len(inputs)} inputs") + for event_id, (name, source, raw) in inputs.items(): + event, want = events[event_id], expected[name] + require(event.get("raw") == raw, f"Raw input changed: {event_id}") + require(not event.get("errors"), f"Parser errors: {event_id}: {event.get('errors')}") + log = event.get("log") or {} + require(sorted(log) == want["logKeys"], + f"{event_id}: log keys {sorted(set(log) ^ set(want['logKeys']))} differ") + for key, value in want["log"].items(): + require(log.get(key) == value, f"{event_id}: log.{key}={log.get(key)!r}, want {value!r}") + for path, value in want["fields"].items(): + require(get(event, path)[0] == value, f"{event_id}: {path}={get(event, path)[0]!r}, want {value!r}") + for path in want["absent"]: + require(not get(event, path)[1], f"{event_id}: {path} should be absent") + host = get(event, "target.host")[0] + require(host == (HARNESS_HOST if source == HARNESS_SOURCE else None), f"{event_id}: target.host={host!r}") + + alerts = records(work / "output/resulting_alert.json") + fired = {} + for alert in alerts: + require(not alert.get("errors") and not alert.get("name", "").startswith("Circuit Breaker"), + f"Rule evaluation failure: {alert.get('name')}") + ids = [e.get("id") for e in alert.get("events", [])] + require(len(ids) == 1 and ids[0] in inputs, f"Unexpected alert events: {alert.get('name')} {ids}") + require(alert.get("name") in stems, f"Unknown alert: {alert.get('name')}") + fired.setdefault(ids[0], []).append(stems[alert["name"]]) + if inputs[ids[0]][1] == HARNESS_SOURCE: + require((alert.get("target") or {}).get("host") == HARNESS_HOST, f"{ids[0]}: alert target") + for event_id, (name, source, _) in inputs.items(): + want = expected[name]["endpointHarnessAlerts"] if source == HARNESS_SOURCE else expected[name]["alerts"] + got = sorted(fired.get(event_id, [])) + require(got == sorted(want), f"{event_id}: alerts {got}, want {sorted(want)}") + result = {"passed": True, "events": len(parsed), "alerts": len(alerts), "endpointHarness": args.endpoint_harness} + (work / "assertions.json").write_text(json.dumps(result)) + print(f"PASS: {len(parsed)} raw events, zero parser errors, {len(alerts)} local alerts, each from its intended rule") + + +if __name__ == "__main__": + main() From 47e3cdb8658d3fb14af206c6fbf6316ce713ab9a Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 23 Sep 2026 17:04:06 -0400 Subject: [PATCH 3/5] docs(sentinel-one): add filter and rule audit Evidence basis, each change and its proof level, validation results, deferred items and known limits. Co-Authored-By: Claude Opus 5.5 --- filters/audits/sentinel-one.md | 201 +++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 filters/audits/sentinel-one.md diff --git a/filters/audits/sentinel-one.md b/filters/audits/sentinel-one.md new file mode 100644 index 000000000..72d0923ab --- /dev/null +++ b/filters/audits/sentinel-one.md @@ -0,0 +1,201 @@ +# SentinelOne v11 filter and rule review + +The SentinelOne filter did not parse the management-console records observed in the field. +Every record lost the CEF event name, values containing spaces kept only their first word, +header text was glued into field names, the CEF time was cut to a fragment and no standard +field was set. Twelve of the 19 rules require the event name, so none of them could match a +real record. This revision parses the CEF header by position, keeps whole +values, converts the CEF time, maps the console actor to `origin.user` for console events, +and changes 14 rules so they consume what the filter now produces without firing on +ordinary console administration. The schema is ThreatWinds go-sdk **v1.1.33**, as pinned +by `plugins/alerts/go.mod`. + +## Evidence basis + +- **Real records, described without identifying data.** Only five genuine SentinelOne + records were found on the 28 v11 instances whose indices could be searched (three could + not be searched). All five are on one instance, from one console build and one day: + CEF `SentinelOne|Mgmt` records for a console user added, a console user deleted and a + role assigned (activity types 23, 25 and 37). All five are benign administration. They + were examined and replayed privately and are not reproduced here. The same data type also + held hand-typed test lines without CEF and synthetic load records, used only as controls. + No SentinelOne threat, mitigation, Deep Visibility or STAR record was found on any + searchable instance. +- **What those records showed.** The header is `CEF:0|SentinelOne|Mgmt|||||`. The version slot holds a console build + string such as `S-..#`, never an IPv4 address or a + "word number", so none of the three header parsers that captured the event name matched. + Only the general parser matched, and it left the signature id, event name and severity in + the text given to `kv`. The signature id always equals the `activityType` extension key. + `rt` is `#arcsightDate(,
, UTC)` and appears after + other keys. In these records `suser` is the console account that made the change and + `cat` is `SystemEvent`. +- **Engine behaviour.** The public EventProcessor at commit + `497bf53dbd1ae096f7b2dbc7bce77a6bf9f22ce1`: the + [grok plugin](https://github.com/utmstack/EventProcessor/blob/497bf53dbd1ae096f7b2dbc7bce77a6bf9f22ce1/plugins/grok/main.go) + matches each pattern only at the start of the remaining text and writes nothing unless + every pattern matches; the + [kv plugin](https://github.com/utmstack/EventProcessor/blob/497bf53dbd1ae096f7b2dbc7bce77a6bf9f22ce1/plugins/kv/main.go) + splits on every space, keeps the first word of a value and fails when its source is + missing; the + [reformat plugin](https://github.com/utmstack/EventProcessor/blob/497bf53dbd1ae096f7b2dbc7bce77a6bf9f22ce1/plugins/reformat/main.go) + records an error when a time does not parse. +- **Schema and semantics.** [Event and Side fields](https://github.com/threatwinds/go-sdk/blob/v1.1.33/plugins/plugins.proto) + (`origin.user`, `target.host`, `deviceTime`, `severity` are strings) and the + [standard field meanings](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) + (`deviceTime` is the source's original time; origin is the actor). +- **Vendor documentation.** SentinelOne's syslog/CEF reference could not be read from the + allowed documentation hosts, so the header layout, extension key catalog, activity codes + and CEF severity scale are **not** documented here. Two public SentinelOne pages were + used: the [Static AI engine case study](https://www.sentinelone.com/blog/decrypting-sentinelones-detection-an-in-depth-look-at-our-real-time-cwpp-static-ai-engine/) + (section "Case Study": there are two confidence levels, `SUSPICIOUS` and `MALICIOUS`, + and the agent policy per confidence level is Detect or Protect) together with the + [threat intelligence engine example](https://www.sentinelone.com/blog/decrypting-sentinelone-cloud-detection-the-threat-intelligence-engine-in-real-time-cwpp/) + (section "Example: Shellshock Detection": `MALICIOUS` is the highest level), and the + [rollback demo](https://www.sentinelone.com/blog/ransomware-mitigation-sentinelones-rollback-demo-rsac-2018/) + (section "Demo": policy written as a pair such as `Detect/Detect` or `Protect/Detect`). + +## Filter changes (version 3.0.1) + +| Change | Why | Proof | +|---|---|---| +| Parse the CEF header by position after `CEF:`, whatever syslog header precedes it. Vendor, product, version and signature id go to `log.cefDeviceVendor`, `log.cefDeviceProduct`, `log.cefDeviceVersion`, `log.cefSignatureId`; the name to `log.eventDescription`, which rules already read; the header severity to `log.cefSeverity`; only the extension to `log.restData`. | The event name was lost on every real record and header text reached `kv`. The standard `severity` is **not** set: the CEF severity scale is not documented. | Real records (5 of 5) and playground-validated for the recorded build | +| Remove the two header parsers that stored the version slot as `log.syslogHost`. | The slot is the console build, not a host; real records never matched these parsers. Their only consumers were rules, changed below. | Real records; playground | +| Run `kv` only when `log.restData` exists. | Lines without CEF recorded a `source does not exists` error carrying the serialized pipeline. | Playground (zero errors afterwards) | +| Take the whole `rt=#arcsightDate(...)` value into `log.ruleTime`; convert it to RFC 3339 `deviceTime` only when it has exactly the observed UTC layout. | `log.ruleTime` held `#arcsightDate(Mon,`; `deviceTime` was the ingestion time. Other formats are left alone and keep the default `deviceTime`. | Real records (5 of 5 converted); playground | +| Re-extract `accountName`, `eventDesc`, `suser`, `duser`, `endpointDeviceControlDeviceName`, `sourceGroupName`, `sourceIpAddresses`, `sourceMacAddresses` and `siteName` up to the next key or to the end, then strip the next key. Output names are unchanged. | `siteName` kept its first word; the old two-step extraction lost any of these values when its key was last. | Real records (`siteName`, `suser`, `accountName`); fabricated last-key and all-keys lines in the playground | +| Copy `log.sourceUser` to `origin.user` only when `cat` is `SystemEvent` and the value is not empty or a placeholder. `log.sourceUser` is kept. | In console events `suser` is the account that made the change. Its meaning in other event classes is not established, so they are unchanged. | Real records (5 of 5); playground | + +The `actionResult` steps are unchanged; a separate correction owns them. No rule reads +`actionResult`. + +## Rule changes + +Names, ids, thresholds, windows, impact, adversary side and MITRE labels are unchanged. + +| Rule | Change | Why | +|---|---|---| +| `memory_injection_detection` | Remove the branch reading `log.eventDescToParse`; group by `target.host` and `adversary.user` instead of `adversary.host`. | The filter deletes that scratch field before rules run; event `origin.host` is never set. | +| `behavioral_threat_detection`, `custom_detection_rule_triggers`, `deep_visibility_threat_indicators`, `endpoint_detection_response_alerts`, `suspicious_process_tree` | Require `target.host` instead of `log.syslogHost`; group by `target.host`. | `log.syslogHost` was the console build and is no longer produced. The endpoint is the affected side of a detection. | +| `kernel_level_threat`, `rollback_operation_patterns`, `threat_intelligence_matches` | Group by `target.host` instead of `log.syslogHost`. | Same producer change. | +| `kernel_level_threat` and the reputation branch of `threat_intelligence_matches` | Also require `target.host`. | With the event name parsed, console text such as a new user named "Kernel Team" or "Reputation Desk" would fire them. | +| `s1_policy_downgrade` | Fire only on Protect to Detect: the word `downgrade` (any form), "from Protect ... to Detect", "Protect to Detect", or the vendor's paired wording where either mode goes from Protect to Detect (for example `Protect/Detect` to `Detect/Detect`). `policy` is matched as a whole word, in any case. The unchanged activity-name branch now also needs this direction. | The old condition also fired on upgrades (Detect to Protect) and on a role named "Protect and Detect Reviewers". | +| `s1_exclusion_abuse`, `s1_policy_downgrade` | Group by `adversary.user` instead of `adversary.host`. | These are console changes; the console account now reaches `origin.user`. | +| `agent_tampering_attempts`, `threat_mitigation_failures`, `iot_device_compromise_indicators` | Match the word lists as whole words (with the usual inflections). Case is unchanged. | "nonstop" contains "stop", "agentur" contains "agent", "failover" contains "fail", "analytics" contains "ics". | +| `threat_intelligence_matches` | Replace `greaterOrEqual("log.confidencelevel", 90)` with `equalsIgnoreCase("log.confidencelevel", "MALICIOUS")`; the key name is unchanged. | SentinelOne documents two confidence levels, `SUSPICIOUS` and `MALICIOUS`, not a 0-100 score. The syslog spelling of the key is not established. | + +No SentinelOne rule has a history query, so no history search is affected. Filter and +rules must ship together: with the new filter and the old rules, benign console text fires +seven old rules (measured below). + +## Validation + +**Fabricated regression, committed.** `plugins/alerts/testdata/sentinel-one/` holds 52 +invented raw lines (`raw.json`), their expected fields and alerts (`expected.json`), the +eleven shared grok definitions this filter uses (`patterns.yaml`, copied from the +repository changelog `20250616001_insert_utm_regex_pattern.xml`) and `replay.py`. The lines +reuse the observed record shape with made-up names, `example.com` addresses, RFC 5737 +addresses and invented ids. + +**Playground.** A clean build of the EventProcessor commit above ran the original and the +corrected filter and rules on the same 64 inputs: the 52 fabricated lines, two more +fabricated console lines for the deferred false positives, and, privately, the five +genuine records and five non-SentinelOne control records. Its parser and writer plugins +link go-sdk v1.1.26 and its CEL plugin v1.1.34. + +| Run | Result | +|---|---| +| Original filter and rules | 64 events, 4 alerts. Each genuine record lost its event name, kept one word of its site name and a fragment of `rt`, carried header text as field names and kept the ingestion time as `deviceTime`. The IPv4 and "word number" version slots became `log.syslogHost`, and one such line fired the behavioral rule on that value. Six lines without a recognised header each carried a `kv` error. | +| Corrected filter, original rules | Normalized events identical to the corrected run; 22 alerts. Benign console text fired seven rules, and the policy rule also fired on both upgrades. | +| Corrected filter and rules | 64 events, zero parser errors, exact field sets and values on every fabricated and genuine record, no alert on any genuine record. 12 alerts: the ten fabricated positives and the two deferred false positives, each exactly once. | +| Same, with the test-only `target.host` step | 71 events, 19 alerts: each of the seven endpoint-gated rules alerted exactly once, on its marked copy only, with that host as the alert target. | + +All 4,304 assertions of these four runs passed. The committed `replay.py` repeats the last +two runs with the fabricated lines only: 52 events and 10 alerts, then 59 events and 17 +alerts with `--endpoint-harness`, zero parser errors and every alert from its intended rule. + +**SDK predicate checks.** The go-sdk v1.1.33 replay evaluated the 14 changed rules, in +original and corrected form, and nine single-branch probes over 94 documents: the corrected +run's 64 events, the seven marked copies and the 23 documents previously stored for this +data type. Each corrected rule matched exactly its intended fabricated positives and no +genuine or stored document; each original rule matched exactly what it alerted on in the +playground; the removed memory-injection branch matched nothing; no corrected event carries +`log.syslogHost` or `severity`; `origin.user` is set on exactly the 36 console events that +name an actor. All 68 checks passed. + +**Go tests.** `sentinel_one_filter_test.go` checks the filter structure, compiles every grok, +trim and `regexMatch` pattern, runs the header, value and `rt` patterns over the 52 fabricated +lines with a model of the grok step, checks rule grouping paths, and evaluates 41 synthetic +normalized cases against the shipped rules with go-sdk v1.1.33. Against the original filter +and rules, four of its five tests fail. The full `plugins/alerts` suite passes; the same +private-evidence tests of other technologies skip as they do on the base commit. + +## Deferred + +These need SentinelOne's syslog/CEF reference or real threat records, and are unchanged: + +- Activity-code name lists in five rules (`policy_updated`, `exclusion_created`, + `agent_uninstall`, `rollback`, `mitigation`). SentinelOne sends numeric codes; only 23, + 25 and 37 are known. Until the codes are documented, a console user named "Exception + Queue" still fires `s1_exclusion_abuse` and a role named "Rollback Approvers" still + fires `rollback_operation_patterns` (both reproduced with fabricated lines). +- `duser` to `target.user`, the endpoint key to `target.host`, and any address mapping. + `endpointDeviceControlDeviceName` most likely names a USB or Bluetooth device, not the + endpoint. Until an endpoint key is mapped, the rules that require `target.host` cannot + fire; before this change they required a field real records never had. +- The standard `severity` (CEF scale not documented) and `action` (activity catalog not + available). +- Threat extension keys and value formats (`threatName`, `engines`, message key `msg` or + `message`, `confidencelevel` spelling), and case: most rule word lists still match only + lower-case wording. +- Retiring or redefining the IoT and Storyline rules, whose premises the vendor pages do + not support. +- MITRE relabelling: ATT&CK v19 revoked T1562 and T1562.001 in favour of T1685; this is a + repository-wide change. +- New rules for console users added and roles assigned (the only SentinelOne activity + observed so far). +- The other Stage 3 grouping drafts (`agent_tampering_attempts`, `container_security_alerts`, + `storyline_correlation`) and the `log.syslogHost` mentions left in four rule descriptions. + +## Known limits + +- No threat record was available, so threat-event parsing, keys and rule wording are + verified only with invented lines. The syslog wording of a policy-mode change is not + documented either; the direction test covers the wordings listed above. +- The playground parser plugins link go-sdk v1.1.26 and its CEL plugin v1.1.34; the alerts + module pins v1.1.33. Predicates were also checked with v1.1.33. Neither build is asserted + to match a customer deployment. `reformat` is already used by the ESET and Sophos XG + filters. +- The playground `saw` writer only records alerts. Grouping, deduplication, indexing, + notifications and production alerts were not tested. +- A header missing one of the seven CEF fields, or with an empty extension, falls back to + the old header parsers. `kv` still mishandles quoted values and an escaped `=` inside a + value (it can manufacture a key); only the nine re-extracted keys are kept whole. +- A separate `actionResult` correction edits the same file. This change keeps that block + byte-identical; combining the two needs a rebase at the version comment line. + +## Reproduce + +Build the EventProcessor commit above without changing its dependencies. With `EP` set to +that checkout's absolute path: + +```sh +mkdir -p "$EP/test-bin" "$EP/test-plugins" +(cd "$EP" && go build -mod=readonly -o "$EP/test-bin/playground" ./cmd/playground) +for plugin in add cel delete grok kv reformat rename saw sew trim; do + (cd "$EP/plugins/$plugin" && go build -mod=readonly -o "$EP/test-plugins/$plugin.plugin" .) +done +``` + +From this UTMStack checkout, with PyYAML installed: + +```sh +python3 plugins/alerts/testdata/sentinel-one/replay.py \ + --playground "$EP/test-bin/playground" --plugins "$EP/test-plugins" +python3 plugins/alerts/testdata/sentinel-one/replay.py --endpoint-harness \ + --playground "$EP/test-bin/playground" --plugins "$EP/test-plugins" +(cd plugins/alerts && go test ./... -count=1) +``` + +Each playground run takes about three minutes. The Go suite alone does not execute raw +extraction. From b8ed46aecaf15b33f049edc82aae5f5f65f94db7 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 24 Sep 2026 15:44:11 -0400 Subject: [PATCH 4/5] docs(sentinel-one): record re-validation on the latest v11, go-sdk and engine Official v11 d2479c1a (go-sdk v1.1.36 in plugins/alerts) and EventProcessor main 8a3ade7 (every playground plugin on v1.1.36). Field names now keep underscores and regexMatch matches strings only; neither changes this draft, because the filter writes only letters and digits in names, no kv key in the fixtures or genuine records has an underscore, and every text-search call reads a string field. Full plugins/alerts suite: 48 pass, 11 skip, 0 fail. replay.py: 52 events and 10 alerts, 59 and 17 with the endpoint harness, all as expected. v1.1.36 rule replay matches the playground alerts exactly. The private runs repeat all 4,304 assertions. Co-Authored-By: Claude Opus 5.5 --- filters/audits/sentinel-one.md | 60 ++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/filters/audits/sentinel-one.md b/filters/audits/sentinel-one.md index 72d0923ab..98fa99931 100644 --- a/filters/audits/sentinel-one.md +++ b/filters/audits/sentinel-one.md @@ -7,8 +7,10 @@ field was set. Twelve of the 19 rules require the event name, so none of them co real record. This revision parses the CEF header by position, keeps whole values, converts the CEF time, maps the console actor to `origin.user` for console events, and changes 14 rules so they consume what the filter now produces without firing on -ordinary console administration. The schema is ThreatWinds go-sdk **v1.1.33**, as pinned -by `plugins/alerts/go.mod`. +ordinary console administration. The schema is ThreatWinds go-sdk **v1.1.36**, as pinned +by `plugins/alerts/go.mod` since official `v11` (`d2479c1a`) was merged into this branch. +The review itself used v1.1.33, whose `plugins.proto` is identical. The draft was checked +again on the latest versions; see [Re-validation on the latest versions](#re-validation-on-the-latest-versions). ## Evidence basis @@ -39,8 +41,10 @@ by `plugins/alerts/go.mod`. splits on every space, keeps the first word of a value and fails when its source is missing; the [reformat plugin](https://github.com/utmstack/EventProcessor/blob/497bf53dbd1ae096f7b2dbc7bce77a6bf9f22ce1/plugins/reformat/main.go) - records an error when a time does not parse. -- **Schema and semantics.** [Event and Side fields](https://github.com/threatwinds/go-sdk/blob/v1.1.33/plugins/plugins.proto) + records an error when a time does not parse. The latest EventProcessor, `main` at + `8a3ade72bd9d12db21f6b273200588fb49540f14`, changes only these plugins' go-sdk version + (to v1.1.36), so this behaviour is the same there. +- **Schema and semantics.** [Event and Side fields](https://github.com/threatwinds/go-sdk/blob/v1.1.36/plugins/plugins.proto) (`origin.user`, `target.host`, `deviceTime`, `severity` are strings) and the [standard field meanings](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) (`deviceTime` is the source's original time; origin is the actor). @@ -88,8 +92,43 @@ No SentinelOne rule has a history query, so no history search is affected. Filte rules must ship together: with the new filter and the old rules, benign console text fires seven old rules (measured below). +## Re-validation on the latest versions + +On 2026-09-24 official `v11` moved to `d2479c1a3705eec6a00016689c2bf5fbcc1814f2`, whose +`plugins/alerts` pins go-sdk v1.1.36, and EventProcessor `main` moved to +`8a3ade72bd9d12db21f6b273200588fb49540f14`, whose playground and parser, writer and CEL +plugins all link go-sdk v1.1.36. `v11` was merged into this branch. No file overlaps this +draft, so nothing conflicted. + +What changed in the SDK, and what it means here: + +- Since v1.1.35, `utils.SanitizeField` keeps `_` in the field names that the `json` + (top-level keys), `kv`, `grok`, `csv`, `xml`, `add` and `rename` plugins write. Other + characters are still removed. This filter only writes names made of letters and digits, + and no `kv` key in the 52 fabricated lines or the five genuine records contains `_`. So + every stored name stays the same. +- v1.1.36 makes `regexMatch` match string values only again. Since v1.1.34, `contains`, + `containsAll`, `startsWith` and `endsWith` also search the JSON text of objects and lists. + Every such call in this filter and its rules reads a text field, so no result changes. + `plugins.proto`, `plugins/cel.go` and `plugins/rules.go` are identical in v1.1.33 and v1.1.36. +- No filter, rule or fixture needed a change. + +| Check on the latest versions | Result | +|---|---| +| Full `plugins/alerts` suite, go-sdk v1.1.36 | 48 tests pass, 11 skip, none fail (2,342 passing results with subtests). The five SentinelOne tests pass. The skipped tests need other technologies' private evidence and skip on the base commit too. | +| `replay.py` on EventProcessor 8a3ade7 | 52 events, zero parser errors, every key set and value as in `expected.json`, 10 alerts, each from its intended rule. With `--endpoint-harness`: 59 events and 17 alerts. | +| go-sdk v1.1.36 rule replay | All 19 rules over those 52 and 59 events: no compile or evaluation error. The matches are exactly the 10 and 17 playground alerts, and exactly the alerts in `expected.json`. | +| The four private playground runs described below, same 64 and 71 inputs | Same results as before. Original filter and rules: 4 alerts. Corrected filter with the original rules: 22. Corrected filter and rules: 12, none on a genuine record. With the test-only step: 19. All 4,304 assertions pass. | + +At 8a3ade7 the CEL plugin reads its OpenSearch address from separate `host`, `port`, `user` +and `password` settings. `replay.py` still gives one URL, so the client gets an empty +address. No SentinelOne rule has a history search, so no result depends on it. + ## Validation +These are the original review's results, on EventProcessor `497bf53` and go-sdk v1.1.33. +The section above repeats them on the latest versions. + **Fabricated regression, committed.** `plugins/alerts/testdata/sentinel-one/` holds 52 invented raw lines (`raw.json`), their expected fields and alerts (`expected.json`), the eleven shared grok definitions this filter uses (`patterns.yaml`, copied from the @@ -162,10 +201,11 @@ These need SentinelOne's syslog/CEF reference or real threat records, and are un - No threat record was available, so threat-event parsing, keys and rule wording are verified only with invented lines. The syslog wording of a policy-mode change is not documented either; the direction test covers the wordings listed above. -- The playground parser plugins link go-sdk v1.1.26 and its CEL plugin v1.1.34; the alerts - module pins v1.1.33. Predicates were also checked with v1.1.33. Neither build is asserted - to match a customer deployment. `reformat` is already used by the ESET and Sophos XG - filters. +- The latest check used EventProcessor `8a3ade7`, whose playground and plugins link go-sdk + v1.1.36, the version the alerts module now pins; predicates were also checked with + v1.1.36. The original review used `497bf53` (parser and writer plugins v1.1.26, CEL plugin + v1.1.34) and v1.1.33 predicates. Neither build is asserted to match a customer deployment. + `reformat` is already used by the ESET and Sophos XG filters. - The playground `saw` writer only records alerts. Grouping, deduplication, indexing, notifications and production alerts were not tested. - A header missing one of the seven CEF fields, or with an empty extension, falls back to @@ -176,8 +216,8 @@ These need SentinelOne's syslog/CEF reference or real threat records, and are un ## Reproduce -Build the EventProcessor commit above without changing its dependencies. With `EP` set to -that checkout's absolute path: +Build EventProcessor `8a3ade72bd9d12db21f6b273200588fb49540f14` (the latest check) without +changing its dependencies. With `EP` set to that checkout's absolute path: ```sh mkdir -p "$EP/test-bin" "$EP/test-plugins" From 9458a322befc25fbca0463c1521d3e0780866c01 Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Thu, 24 Sep 2026 17:07:26 -0400 Subject: [PATCH 5/5] docs(sentinel-one): name the newest engine image behind the re-validation build The newest published engine image, eventprocessor:v11.2.14 (built 2026-09-24 19:13 UTC), carries EventProcessor revision 8a3ade7 with go-sdk v1.1.36, built with go1.26.8 for linux/amd64. The local build used for the re-validation is the same source compiled natively for darwin/arm64 with go1.25.7. Co-Authored-By: Claude Opus 5.5 --- filters/audits/sentinel-one.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/filters/audits/sentinel-one.md b/filters/audits/sentinel-one.md index 98fa99931..8bf730dde 100644 --- a/filters/audits/sentinel-one.md +++ b/filters/audits/sentinel-one.md @@ -100,6 +100,13 @@ On 2026-09-24 official `v11` moved to `d2479c1a3705eec6a00016689c2bf5fbcc1814f2` plugins all link go-sdk v1.1.36. `v11` was merged into this branch. No file overlaps this draft, so nothing conflicted. +The newest published engine image, `ghcr.io/utmstack/utmstack/eventprocessor:v11.2.14` +(built 2026-09-24 19:13 UTC on base image `eventprocessor/base:1.1.7`), embeds Go build +information showing that its playground and plugin binaries come from the same +EventProcessor revision `8a3ade7` with go-sdk v1.1.36, built with go1.26.8 for linux/amd64. +The local build used here is that source revision compiled natively for darwin/arm64 with +go1.25.7; only the Go toolchain and platform differ. + What changed in the SDK, and what it means here: - Since v1.1.35, `utils.SanitizeField` keeps `_` in the field names that the `json`