diff --git a/README.md b/README.md index 1fc40be..d6872a5 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Designed to showcase **evidence-backed security engineering skills** through rep | PT-2026-010 | T1218.011 | Rundll32 proxy execution | Sigma + Splunk evidence | **Live validated** | | PT-2026-011 | T1218.010 | Regsvr32 proxy execution | Sigma + Splunk evidence | **Live validated** | | PT-2026-012 | T1569.002 | Service-launched command execution | Sigma + Splunk evidence | **Live validated** | +| PT-2026-013 | T1546.003 | Permanent WMI event subscription creation | Sigma + Splunk evidence | **Live validated** | **Meaning of statuses in this repo** - **Live validated**: replayed in the Mayuri lab with positive/negative evidence and cleanup confirmation. diff --git a/automation/execution/pt_2026_013_cleanup.ps1 b/automation/execution/pt_2026_013_cleanup.ps1 new file mode 100644 index 0000000..0361375 --- /dev/null +++ b/automation/execution/pt_2026_013_cleanup.ps1 @@ -0,0 +1,45 @@ +$ErrorActionPreference = 'Continue' +$names = 'AtomicRedTeam-WMIPersistence-CommandLineEventConsumer-Example','PT-2026-013-Variant' +$namespace = 'root/subscription' +$bindingResidue = @() + +function Get-NamedBindings { + param([string]$ObjectName) + $found = @() + $consumers = @(Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$ObjectName'" -ErrorAction SilentlyContinue) + foreach ($consumer in $consumers) { + $found += @(Get-WmiObject -Namespace $namespace -Query "REFERENCES OF {$($consumer.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue) + } + $filters = @(Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$ObjectName'" -ErrorAction SilentlyContinue) + foreach ($filter in $filters) { + $found += @(Get-WmiObject -Namespace $namespace -Query "REFERENCES OF {$($filter.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue) + } + @($found | Sort-Object -Property __PATH -Unique) +} + +foreach ($name in $names) { + @(Get-NamedBindings -ObjectName $name) | Remove-WmiObject -ErrorAction SilentlyContinue + Start-Sleep -Milliseconds 250 + $remainingBindings = @(Get-NamedBindings -ObjectName $name) + if ($remainingBindings.Count -eq 0) { + @(Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue) | + Remove-WmiObject -ErrorAction SilentlyContinue + @(Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue) | + Remove-WmiObject -ErrorAction SilentlyContinue + } else { + $bindingResidue += "binding:$name" + } +} +Remove-Item 'C:\Windows\Temp\pt-2026-013-original-completion.json','C:\Windows\Temp\pt-2026-013-variant-completion.json' -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 2 +$remaining = foreach ($name in $names) { + if (Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue) { "filter:$name" } + if (Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue) { "consumer:$name" } +} +$remaining = @($bindingResidue) + @($remaining) +$completionFiles = @( + 'C:\Windows\Temp\pt-2026-013-original-completion.json', + 'C:\Windows\Temp\pt-2026-013-variant-completion.json' +) | Where-Object { Test-Path $_ } +[pscustomobject]@{ Remaining=@($remaining); CompletionFiles=@($completionFiles); Clean=(@($remaining).Count -eq 0 -and @($completionFiles).Count -eq 0) } | ConvertTo-Json -Compress +if (@($remaining).Count -ne 0 -or @($completionFiles).Count -ne 0) { throw 'WMI subscription cleanup residue remains' } diff --git a/automation/execution/pt_2026_013_negative_cim_inventory.ps1 b/automation/execution/pt_2026_013_negative_cim_inventory.ps1 new file mode 100644 index 0000000..8752b5e --- /dev/null +++ b/automation/execution/pt_2026_013_negative_cim_inventory.ps1 @@ -0,0 +1,2 @@ +$ErrorActionPreference = 'Stop' +Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,LastBootUpTime | ConvertTo-Json -Compress diff --git a/automation/execution/pt_2026_013_negative_subscription_inventory.ps1 b/automation/execution/pt_2026_013_negative_subscription_inventory.ps1 new file mode 100644 index 0000000..1d3cca5 --- /dev/null +++ b/automation/execution/pt_2026_013_negative_subscription_inventory.ps1 @@ -0,0 +1,2 @@ +$ErrorActionPreference = 'Stop' +@(Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Select-Object -ExpandProperty Name) | ConvertTo-Json -Compress diff --git a/automation/execution/pt_2026_013_negative_transient_subscription.ps1 b/automation/execution/pt_2026_013_negative_transient_subscription.ps1 new file mode 100644 index 0000000..a52e8a8 --- /dev/null +++ b/automation/execution/pt_2026_013_negative_transient_subscription.ps1 @@ -0,0 +1,7 @@ +$ErrorActionPreference = 'Stop' +$id = 'PT-2026-013-Transient' +Register-CimIndicationEvent -Query "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName='definitely-not-created-pt-2026-013.exe'" -SourceIdentifier $id | Out-Null +Start-Sleep -Seconds 1 +Unregister-Event -SourceIdentifier $id +Get-Job -Name $id -ErrorAction SilentlyContinue | Remove-Job -Force +[pscustomobject]@{ SourceIdentifier=$id; Permanent=$false } | ConvertTo-Json -Compress diff --git a/automation/execution/pt_2026_013_positive_atomic_wmi_subscription.ps1 b/automation/execution/pt_2026_013_positive_atomic_wmi_subscription.ps1 new file mode 100644 index 0000000..c826fbb --- /dev/null +++ b/automation/execution/pt_2026_013_positive_atomic_wmi_subscription.ps1 @@ -0,0 +1,27 @@ +$ErrorActionPreference = 'Stop' +Set-ExecutionPolicy -Scope Process Bypass -Force +$testGuid = '3c64f177-28e2-49eb-a799-d767b24dd1e0' +$name = 'AtomicRedTeam-WMIPersistence-CommandLineEventConsumer-Example' +Import-Module Invoke-AtomicRedTeam -Force +Invoke-AtomicTest T1546.003 -TestGuids $testGuid -PathToAtomicsFolder 'C:\Tools\AtomicRedTeam\atomics' -Confirm:$false +$deadline = (Get-Date).AddSeconds(30) +do { + $filter = Get-WmiObject -Namespace root/subscription -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue + $consumer = Get-WmiObject -Namespace root/subscription -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue + $binding = if ($consumer) { + Get-WmiObject -Namespace root/subscription -Query "REFERENCES OF {$($consumer.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue + } + if ($filter -and $consumer -and $binding) { break } + Start-Sleep -Seconds 1 +} while ((Get-Date) -lt $deadline) +$result = [pscustomobject]@{ + TestGuid = $testGuid + FilterPresent = [bool]$filter + ConsumerPresent = [bool]$consumer + BindingPresent = [bool]$binding + CompletedAt = (Get-Date).ToUniversalTime().ToString('o') +} +if (-not ($result.FilterPresent -and $result.ConsumerPresent -and $result.BindingPresent)) { throw 'Atomic WMI subscription was not fully created' } +$completionPath = 'C:\Windows\Temp\pt-2026-013-original-completion.json' +$result | ConvertTo-Json -Compress | Set-Content -Path $completionPath -Encoding UTF8 +$result | ConvertTo-Json -Compress diff --git a/automation/execution/pt_2026_013_positive_variant_wmi_subscription.ps1 b/automation/execution/pt_2026_013_positive_variant_wmi_subscription.ps1 new file mode 100644 index 0000000..e4910bb --- /dev/null +++ b/automation/execution/pt_2026_013_positive_variant_wmi_subscription.ps1 @@ -0,0 +1,39 @@ +$ErrorActionPreference = 'Stop' +$name = 'PT-2026-013-Variant' +$namespace = 'root/subscription' +$filterArgs = @{ + Name = $name + EventNameSpace = 'root\CimV2' + QueryLanguage = 'WQL' + Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime > 99999999" +} +$consumerArgs = @{ + Name = $name + CommandLineTemplate = "$env:SystemRoot\System32\cmd.exe /c exit 0" +} +$filter = New-CimInstance -Namespace $namespace -ClassName __EventFilter -Property $filterArgs +$consumer = New-CimInstance -Namespace $namespace -ClassName CommandLineEventConsumer -Property $consumerArgs +$binding = New-CimInstance -Namespace $namespace -ClassName __FilterToConsumerBinding -Property @{ Filter=[Ref]$filter; Consumer=[Ref]$consumer } +$deadline = (Get-Date).AddSeconds(30) +do { + $persistedFilter = Get-WmiObject -Namespace $namespace -Class __EventFilter -Filter "Name = '$name'" -ErrorAction SilentlyContinue + $persistedConsumer = Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer -Filter "Name = '$name'" -ErrorAction SilentlyContinue + $persistedBinding = if ($persistedConsumer) { + Get-WmiObject -Namespace $namespace -Query "REFERENCES OF {$($persistedConsumer.__RELPATH)} WHERE ResultClass = __FilterToConsumerBinding" -ErrorAction SilentlyContinue + } + $filterPresent = [bool]$persistedFilter + $consumerPresent = [bool]$persistedConsumer + $bindingPresent = [bool]$persistedBinding + if ($filterPresent -and $consumerPresent -and $bindingPresent) { break } + Start-Sleep -Seconds 1 +} while ((Get-Date) -lt $deadline) +$result = [pscustomobject]@{ + FilterPresent = $filterPresent + ConsumerPresent = $consumerPresent + BindingPresent = $bindingPresent + CompletedAt = (Get-Date).ToUniversalTime().ToString('o') +} +if (-not ($result.FilterPresent -and $result.ConsumerPresent -and $result.BindingPresent)) { throw 'Variant WMI subscription was not fully created' } +$completionPath = 'C:\Windows\Temp\pt-2026-013-variant-completion.json' +$result | ConvertTo-Json -Compress | Set-Content -Path $completionPath -Encoding UTF8 +$result | ConvertTo-Json -Compress diff --git a/automation/validators/sigma_ops.py b/automation/validators/sigma_ops.py index 111f554..43ffbb5 100644 --- a/automation/validators/sigma_ops.py +++ b/automation/validators/sigma_ops.py @@ -33,10 +33,13 @@ 'Image': "", 'ParentImage': "", 'User': "", + 'EventType': "", + 'Operation': "", } BASE_SEARCH = { 'ps_script': 'search index=main source="WinEventLog:Microsoft-Windows-PowerShell/Operational" _raw="*4104*"', 'process_creation': 'search index=main source="WinEventLog:Microsoft-Windows-Sysmon/Operational" _raw="*1*"', + 'wmi_event': 'search index=main source="WinEventLog:Microsoft-Windows-Sysmon/Operational" (_raw="*19*" OR _raw="*20*" OR _raw="*21*")', } diff --git a/detections/generated/elastic/suspicious_permanent_wmi_subscription.eql b/detections/generated/elastic/suspicious_permanent_wmi_subscription.eql new file mode 100644 index 0000000..9f662f1 --- /dev/null +++ b/detections/generated/elastic/suspicious_permanent_wmi_subscription.eql @@ -0,0 +1 @@ +any where winlog.event_data.Operation:"Created" and (winlog.event_data.EventType like~ ("WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent")) \ No newline at end of file diff --git a/detections/generated/splunk/live/suspicious_permanent_wmi_subscription.spl b/detections/generated/splunk/live/suspicious_permanent_wmi_subscription.spl new file mode 100644 index 0000000..182e959 --- /dev/null +++ b/detections/generated/splunk/live/suspicious_permanent_wmi_subscription.spl @@ -0,0 +1 @@ +search index=main source="WinEventLog:Microsoft-Windows-Sysmon/Operational" (_raw="*19*" OR _raw="*20*" OR _raw="*21*") (_raw="**Created*") AND ((_raw="**WmiFilterEvent*" OR _raw="**WmiConsumerEvent*" OR _raw="**WmiBindingEvent*")) diff --git a/detections/generated/splunk/official/suspicious_permanent_wmi_subscription.spl b/detections/generated/splunk/official/suspicious_permanent_wmi_subscription.spl new file mode 100644 index 0000000..e70347a --- /dev/null +++ b/detections/generated/splunk/official/suspicious_permanent_wmi_subscription.spl @@ -0,0 +1 @@ +Operation="Created" EventType IN ("WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent") \ No newline at end of file diff --git a/detections/sigma/windows/wmi_event/suspicious_permanent_wmi_subscription.yml b/detections/sigma/windows/wmi_event/suspicious_permanent_wmi_subscription.yml new file mode 100644 index 0000000..1b40522 --- /dev/null +++ b/detections/sigma/windows/wmi_event/suspicious_permanent_wmi_subscription.yml @@ -0,0 +1,31 @@ +title: Permanent WMI Event Subscription Created +id: 45742e29-7c43-4a65-bd91-b04fa7a8d9dc +status: test +description: >- + Detects creation of a permanent WMI event filter, consumer, or filter-to-consumer binding. + Permanent WMI subscriptions can provide stealthy event-triggered persistence. +references: + - https://attack.mitre.org/techniques/T1546/003/ + - https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1546.003/T1546.003.yaml +author: mell0wx +logsource: + product: windows + category: wmi_event +detection: + selection_created: + Operation: Created + selection_type: + EventType: + - WmiFilterEvent + - WmiConsumerEvent + - WmiBindingEvent + condition: selection_created and selection_type +falsepositives: + - Legitimate endpoint-management, monitoring, or software-deployment products creating permanent WMI subscriptions + - Controlled administrative or purple-team validation activity +date: 2026-08-02 +level: high +tags: + - attack.persistence + - attack.privilege-escalation + - attack.t1546.003 diff --git a/detections/validation/live/VAL-2026-013-PT-2026-013.json b/detections/validation/live/VAL-2026-013-PT-2026-013.json new file mode 100644 index 0000000..a3c4d81 --- /dev/null +++ b/detections/validation/live/VAL-2026-013-PT-2026-013.json @@ -0,0 +1,133 @@ +{ + "scenario_id": "PT-2026-013", + "validation_run_id": "VAL-2026-013", + "technique_id": "T1546.003", + "atomic_test_guid": "3c64f177-28e2-49eb-a799-d767b24dd1e0", + "atomic_test_index": 1, + "atomic_test_name": "Persistence via WMI Event Subscription - CommandLineEventConsumer", + "validation_date_utc": "2026-08-03", + "target": "approved Windows victim", + "rollback_snapshot": "verified; identifier retained in private evidence", + "scope": { + "local_only": true, + "domain_controller_targeted": false, + "network_payload": false, + "credential_access": false, + "lateral_movement": false, + "payload_triggered": false + }, + "preflight": { + "victim_running": true, + "secure_channel": true, + "sensors_running": ["Sysmon64", "SplunkForwarder", "Velociraptor", "WazuhSvc", "WinDefend"], + "splunk_healthy": true, + "fresh_victim_telemetry_confirmed": true, + "wmi_object_name_collisions": false, + "victim_boot_time_utc": "2026-07-17T14:40:33.501419Z", + "uptime_seconds_at_original_start": 1422387.012, + "atomic_payload_trigger_window_seconds": [240, 324] + }, + "original": { + "start_time_utc": "2026-08-03T01:47:00.513788Z", + "end_time_utc": "2026-08-03T01:47:08.334013Z", + "exec_output": "Exact Atomic UUID completed; filter, CommandLineEventConsumer, and binding were verified through a durable completion record.", + "detection_results": [ + { + "_time": "2026-08-03 01:47:05.000 UTC", + "host": "approved-windows-victim", + "source": "WinEventLog:Microsoft-Windows-Sysmon/Operational", + "event_codes": [19, 20], + "event_types": ["WmiFilterEvent", "WmiConsumerEvent"], + "event_count": 3 + } + ], + "subscription_state": "filter, consumer, and binding created and verified", + "detection_fired": true, + "detection_latency_seconds": 4.486, + "payload_verification": { + "process": "notepad.exe", + "event_id": 1, + "source": "WinEventLog:Microsoft-Windows-Sysmon/Operational", + "earliest_utc": "2026-08-03T01:46:55Z", + "latest_utc": "2026-08-03T01:47:15Z", + "query_scope": "bounded Sysmon process-creation search", + "match_count": 0 + } + }, + "variant": { + "start_time_utc": "2026-08-03T01:48:46.005927Z", + "end_time_utc": "2026-08-03T01:48:49.604107Z", + "exec_output": "Modified local CommandLineEventConsumer subscription completed with a deliberately non-triggering WQL condition; durable completion record verified.", + "detection_results": [ + { + "_time": "2026-08-03 01:48:47.000 UTC", + "host": "approved-windows-victim", + "source": "WinEventLog:Microsoft-Windows-Sysmon/Operational", + "event_codes": [19, 20, 21], + "event_types": ["WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent"], + "event_count": 3 + } + ], + "subscription_state": "filter, consumer, and binding created and verified", + "detection_fired": true, + "detection_latency_seconds": 0.994 + }, + "negatives": [ + { + "name": "read-only CIM operating-system inventory", + "start_time_utc": "2026-08-03T01:49:52.263193Z", + "end_time_utc": "2026-08-03T01:49:54.749420Z", + "exec_output": "read-only operating-system inventory completed", + "detection_results": [], + "detection_fired": false, + "detection_latency_seconds": null + }, + { + "name": "read-only permanent subscription inventory", + "start_time_utc": "2026-08-03T01:50:34.234812Z", + "end_time_utc": "2026-08-03T01:50:36.844661Z", + "exec_output": "existing subscription inventory completed", + "detection_results": [], + "detection_fired": false, + "detection_latency_seconds": null + }, + { + "name": "transient in-process CIM indication subscription", + "start_time_utc": "2026-08-03T01:51:16.343586Z", + "end_time_utc": "2026-08-03T01:51:21.796557Z", + "exec_output": "temporary in-process subscription registered and removed; no permanent namespace objects created", + "detection_results": [], + "detection_fired": false, + "detection_latency_seconds": null + } + ], + "deviations": [ + { + "description": "The initial harness used direct CIM-reference string matching to verify and remove the filter-to-consumer binding. The Atomic created the subscription, but the harness could not reliably recognize the association and did not produce a completion record.", + "response": "Execution stopped. Cleanup was replaced with the upstream-style REFERENCES OF association query, a regression contract was added, and filter, consumer, binding, and completion-file counts were all verified as zero before rerun.", + "residue_after_cleanup": false + }, + { + "description": "A subsequent controlled rerun confirmed that fixed-delay polling did not correct the binding comparison because the issue was association representation rather than ingestion delay.", + "response": "The positive verification path was changed to the same REFERENCES OF association query and tested before the successful evidence-producing replay.", + "residue_after_cleanup": false + } + ], + "cleanup": { + "start_time_utc": "2026-08-03T01:52:17.025253Z", + "end_time_utc": "2026-08-03T01:52:21.487320Z", + "exec_output": "filters=0; consumers=0; bindings=0; completion_files=0; victim secure channel=true; endpoint sensors running; dcdiag quiet", + "filters_remaining": 0, + "consumers_remaining": 0, + "bindings_remaining": 0, + "completion_files_remaining": 0, + "idempotence_recheck": { + "start_time_utc": "2026-08-03T02:18:46.789298Z", + "end_time_utc": "2026-08-03T02:18:53.085340Z", + "remaining": [], + "completion_files": [], + "clean": true + } + }, + "status": "validated" +} diff --git a/detections/validation/permanent-wmi-event-subscription.md b/detections/validation/permanent-wmi-event-subscription.md new file mode 100644 index 0000000..5009956 --- /dev/null +++ b/detections/validation/permanent-wmi-event-subscription.md @@ -0,0 +1,19 @@ +# Permanent WMI Event Subscription Detection Validation + +- Scenario: `PT-2026-013` +- Validation: `VAL-2026-013` +- Technique: `T1546.003` +- Atomic UUID: `3c64f177-28e2-49eb-a799-d767b24dd1e0` +- Result: **passed live validation** + +## Assertions +- Exact Atomic `CommandLineEventConsumer` subscription: detected. +- Modified non-triggering permanent subscription: detected. +- Read-only CIM operating-system inventory: not detected. +- Read-only permanent-subscription inventory: not detected. +- Transient in-process indication subscription: not detected. +- Explicit association cleanup and postflight health: passed. + +The live Mayuri query uses raw Sysmon XML because equivalent normalized WMI fields are not yet verified in Splunk. The canonical Sigma rule remains field-based and backend-neutral. + +See the [live JSON record](live/VAL-2026-013-PT-2026-013.json), [scenario results](../../purple-team/scenarios/PT-2026-013-wmi-event-subscription/RESULTS.md), and [sanitized evidence](../../evidence/sanitized/PT-2026-013/README.md). diff --git a/docs/current-state/DETECTION_PLATFORM_READINESS.md b/docs/current-state/DETECTION_PLATFORM_READINESS.md index c5a9783..48e5947 100644 --- a/docs/current-state/DETECTION_PLATFORM_READINESS.md +++ b/docs/current-state/DETECTION_PLATFORM_READINESS.md @@ -1,7 +1,7 @@ # Detection Platform Readiness ## Summary -Critical telemetry prerequisites are currently sufficient to replay and validate the current twelve live-validated Windows scenarios represented in the repository. +Critical telemetry prerequisites are currently sufficient to replay and validate the current thirteen live-validated Windows scenarios represented in the repository. ## Proxmox and VM state | Component | Status | Evidence | Notes | @@ -11,9 +11,9 @@ Critical telemetry prerequisites are currently sufficient to replay and validate | DC01 VM 120 | Ready | `qm list`, service checks | treat as critical / non-destructive only | | SOC01 VM 140 | Ready | `qm list`, Splunk ports, sigma present | active SIEM node | | DFIR01 VM 160 | Partially ready | running in `qm list` | not exercised in this validation cycle | -| KALI VM 150 | Partially ready | stopped in `qm list` | not required for current validation | -| Snapshot capability | Ready | victim rollback snapshot `pre-pt-2026-012-t1569-002-20260729` verified | snapshots supplement but do not replace cleanup | -| Host disk capacity | Partially ready | `/` 80% used, ~19G free | enough for current work, not ideal for Elastic | +| KALI VM 150 | Partially ready | running in `qm list` | not required for current validation | +| Snapshot capability | Ready | victim rollback snapshot verified; identifier retained privately | snapshots supplement but do not replace cleanup | +| Host disk capacity | Partially ready | local storage 77% used; local LVM thin pool 72% used | enough for current work, not ideal for Elastic | | Host memory headroom | Partially ready | 62Gi total / ~1Gi free / 24Gi cache available | Elastic on SOC would be risky | ## Windows target telemetry @@ -30,7 +30,8 @@ Critical telemetry prerequisites are currently sufficient to replay and validate | Task Scheduler Operational log | Ready | present on victim (last seen event ID 332) | scenario not yet implemented | | Defender Operational log | Ready | present on victim (last seen event ID 5007) | not yet integrated into current detections | | Security log | Ready | present on victim | current Sigma live path does not yet normalize 4688 fields | -| Time alignment | Ready | fresh replays searchable in Splunk immediately after execution | manual latency still uncomputed | +| Sysmon WMI subscription events | Ready | live events 19, 20, and 21 validated during PT-2026-013 | primary source for permanent WMI subscription detection | +| Time alignment | Ready | fresh replays searchable in Splunk immediately after execution | numeric latency recorded for PT-2026-013; older records remain inconsistent | ## Splunk readiness | Capability | Status | Evidence | Notes | @@ -39,8 +40,8 @@ Critical telemetry prerequisites are currently sufficient to replay and validate | Splunk web/API/forwarding | Ready | ports listening | current primary SIEM | | Victim telemetry ingestion | Ready | 24h counts for Application / PowerShell / Sysmon / Security / System | live query | | DC01 telemetry ingestion | Ready | 24h counts for Application / PowerShell / Sysmon / Security / System | live query | -| Required sourcetypes | Ready | `XmlWinEventLog:*` sources visible | raw XML ingestion confirmed | -| Searchability during replay | Ready | `VAL-2026-001..012` | positive and negative windows validated | +| Required sources | Ready | `WinEventLog:*` sources visible | raw XML ingestion confirmed | +| Searchability during replay | Ready | `VAL-2026-001..013` | positive and negative windows validated | | Field normalization | Partially ready | official Sigma conversion returns field-based SPL, but live environment lacks equivalent extracted fields | current Mayuri live pipeline uses raw XML `_raw` matching | | Existing alerts/saved searches | Missing | no durable Splunk alert objects were verified in this cycle | validation currently query-driven | | Retention sufficiency | Partially ready | at least 24h historical queries succeeded | formal retention policy not audited | @@ -52,10 +53,10 @@ Critical telemetry prerequisites are currently sufficient to replay and validate | Controller-side sigma venv | Ready | `/root/.venvs/sigma-platform` with working `sigma check/convert` | current authoritative build environment | | Splunk backend conversion | Ready | generated official + Mayuri live SPL files | repository-local | | Elastic conversion | Ready (conversion only) | generated EQL files | no live Elastic backend | -| Fixture harness | Ready | `automation/validators/sigma_ops.py test-fixtures` passing | 12 rules / 64 fixtures | +| Fixture harness | Ready | `automation/validators/sigma_ops.py test-fixtures` passing | 13 rules / 69 fixtures | ## Readiness decision -- Critical telemetry for PT-2026-001 through PT-2026-012: **Ready** +- Critical telemetry for PT-2026-001 through PT-2026-013: **Ready** - Critical telemetry for the current Windows-safe execution/persistence set: **Ready with existing field-normalization caveats** -- Safe to replay the twelve verified scenarios individually after fresh preflight: **Yes** +- Safe to replay the thirteen verified scenarios individually after fresh preflight: **Yes** - Safe to deploy Elastic now: **No** diff --git a/docs/current-state/PORTFOLIO_METRICS.md b/docs/current-state/PORTFOLIO_METRICS.md index 11d719f..2a4a964 100644 --- a/docs/current-state/PORTFOLIO_METRICS.md +++ b/docs/current-state/PORTFOLIO_METRICS.md @@ -2,20 +2,20 @@ | Metric | Value | |---|---:| -| Purple-team scenarios | 12 | -| Canonical Sigma rules | 12 | -| Fixture files | 64 | -| Positive fixtures | 26 | -| Negative fixtures | 38 | -| Live validation records | 12 | -| ATT&CK techniques covered | 12 | -| Generated Splunk detections | 12 | -| Generated Elastic detections | 12 | +| Purple-team scenarios | 13 | +| Canonical Sigma rules | 13 | +| Fixture files | 69 | +| Positive fixtures | 28 | +| Negative fixtures | 41 | +| Live validation records | 13 | +| ATT&CK techniques covered | 13 | +| Generated Splunk detections | 13 | +| Generated Elastic detections | 13 | ## ATT&CK techniques currently represented -T1037.001, T1047, T1053.005, T1059.001, T1059.003, T1197, T1218.010, T1218.011, T1543.003, T1546.013, T1547.001, T1569.002 +T1037.001, T1047, T1053.005, T1059.001, T1059.003, T1197, T1218.010, T1218.011, T1543.003, T1546.003, T1546.013, T1547.001, T1569.002 ## Source -Generated from repository content with `python3 playbook metrics` on 2026-07-29. +Generated from repository content with `python3 playbook metrics` on 2026-08-03. diff --git a/docs/current-state/PROJECT_TIMELINE_ASSESSMENT.md b/docs/current-state/PROJECT_TIMELINE_ASSESSMENT.md index 928cc63..4f3b8c1 100644 --- a/docs/current-state/PROJECT_TIMELINE_ASSESSMENT.md +++ b/docs/current-state/PROJECT_TIMELINE_ASSESSMENT.md @@ -116,12 +116,24 @@ Status legend: Planned | Implemented | Executed | Partially tested | Validated | - systems_involved: victim, SOC01, repository; domain controller health checked but never targeted - claimed_result: service-launched shell behavior was detected in Splunk for both positives, create-without-start stayed quiet, and all services/files were removed - evidence_found: scenario YAML, exact Atomic UUID, behavioral Sigma, 64-fixture suite, live validation JSON, SCM/Sysmon correlation, hunt, investigation, and sanitized evidence -- validation_status: Validated; publication pending exact-head review +- validation_status: Validated and merged - gaps: live Splunk rule still uses raw XML matching and durable alert deployment remains unverified - recommended_action: preserve one-technique-at-a-time execution and exact-head review before merge +### 2026-08-03 — PT-2026-013 WMI Event Subscription live validated +- date: 2026-08-03 +- activity: exact upstream Atomic permanent WMI subscription plus a modified non-triggering variant and three negative controls were replayed on the approved victim +- scenario_id: PT-2026-013 +- attack_technique: T1546.003 +- systems_involved: victim, SIEM, repository; domain controller health checked but never targeted +- claimed_result: behavioral Sysmon WMI creation detection fired for both positives, all three controls stayed quiet, and all filters, consumers, bindings, and completion files were removed +- evidence_found: scenario YAML, exact Atomic UUID, behavioral Sigma, 69-fixture suite, live validation JSON, Sysmon/Splunk correlation, hunt, DFIR notes, and sanitized evidence +- validation_status: Validated +- gaps: live Splunk rule still uses raw XML matching and durable alert deployment remains unverified +- recommended_action: continue one-technique-at-a-time validation and preserve association-aware WMI cleanup + ## Verified current position - repository now supports authored Sigma + generated Splunk/Elastic + fixture tests + live validation records -- twelve scenarios are represented as live validated; PT-2026-012 publication remains branch/review gated +- thirteen scenarios are represented as live validated; PT-2026-013 publication remains branch/review gated - GitHub Actions workflow exists and is part of the repository state - project maturity is suitable for portfolio showcase as a lab-validated detection-engineering repository, not a production SIEM platform diff --git a/docs/current-state/PURPLE_TEAM_PROGRAM_STATUS.md b/docs/current-state/PURPLE_TEAM_PROGRAM_STATUS.md index 66c6b2c..c1477d6 100644 --- a/docs/current-state/PURPLE_TEAM_PROGRAM_STATUS.md +++ b/docs/current-state/PURPLE_TEAM_PROGRAM_STATUS.md @@ -5,17 +5,17 @@ |---|---|---|---|---| | Lab readiness | Partially ready | `DETECTION_PLATFORM_READINESS.md`, `PROJECT_TIMELINE_ASSESSMENT.md` | limited to current Mayuri lab assumptions | keep evidence-scoped and public-safe | | Windows telemetry | Ready | Sysmon + PowerShell + Splunk evidence in live validation JSON | field normalization incomplete | improve normalized field model | -| Splunk ingestion | Ready | generated Splunk queries + `VAL-2026-001..012` | raw XML matching still used in places | normalize fields where practical | +| Splunk ingestion | Ready | generated Splunk queries + `VAL-2026-001..013` | raw XML matching still used in places | normalize fields where practical | | Atomic execution foundation | Validated | `VAL-2026-001..004` | latency not yet standardized across historical records | add numeric latency fields | -| Windows persistence/execution expansion | Validated | `VAL-2026-005..012` | coverage is broader but still Windows-centric | continue expanding safely | -| Threat hunts | Partially ready | `HUNT-2026-001..012` | could be broader and more standardized | expand hunt packs | -| Forensics | Partially ready | `investigations/endpoint/DFIR-2026-001..012/` | not yet full DFIR suite | enrich case studies and artifacts | -| Sigma source rules | Ready | `detections/sigma/windows/process_creation/` | 12 canonical rules are live validated | add more validated rules incrementally | +| Windows persistence/execution expansion | Validated | `VAL-2026-005..013` | coverage is broader but still Windows-centric | continue expanding safely | +| Threat hunts | Partially ready | `HUNT-2026-001..013` | could be broader and more standardized | expand hunt packs | +| Forensics | Partially ready | `investigations/endpoint/DFIR-2026-001..013/` | not yet full DFIR suite | enrich case studies and artifacts | +| Sigma source rules | Ready | `detections/sigma/windows/` | 13 canonical rules are live validated | add more validated rules incrementally | | Splunk conversions | Ready | `detections/generated/splunk/official/` and `live/` | live path still partly XML-backed | improve normalization | | Offline EVTX testing | Staged | `docs/workflows/OFFLINE_EVTX_DETECTION_TESTING.md` | tooling not yet fully operationalized | add Chainsaw/Hayabusa workflow | | Positive fixtures | Ready | `tests/fixtures/` | coverage is solid for current Windows scenarios | expand with each new scenario | | Negative fixtures | Ready | `tests/fixtures/` | same as above | expand with each new scenario | -| Live detection validation | Ready | `detections/validation/live/` | 12 scenarios are live validated | continue scenario-by-scenario growth | +| Live detection validation | Ready | `detections/validation/live/` | 13 scenarios are live validated | continue scenario-by-scenario growth | | GitHub CI | Ready | `.github/workflows/detection-validation.yml` | live Mayuri connectivity intentionally excluded | keep CI offline-focused | | Elastic readiness | Deferred | `ELASTIC_READINESS_DECISION.md` | no live backend deployment | keep conversion-only for now | | Elastic deployment | Not started | none | intentionally deferred | revisit only after stronger maturity | diff --git a/evidence/sanitized/PT-2026-013/README.md b/evidence/sanitized/PT-2026-013/README.md new file mode 100644 index 0000000..7025717 --- /dev/null +++ b/evidence/sanitized/PT-2026-013/README.md @@ -0,0 +1,20 @@ +# PT-2026-013 sanitized evidence + +## Summary +On 2026-08-03 UTC, the approved Windows victim executed Atomic Red Team `T1546.003-1` using exact test UUID `3c64f177-28e2-49eb-a799-d767b24dd1e0`. A modified local, non-triggering permanent subscription provided a second positive path. + +## Correlated telemetry +| Path | UTC event/window | Sysmon evidence | Detection | +|---|---|---|---| +| Exact Atomic | 01:47:05 | Three creation events; event types included WMI filter and consumer | Fired; 4.486 s | +| Modified variant | 01:48:47 | Events 19, 20, and 21; filter, consumer, and binding creation | Fired; 0.994 s | +| CIM inventory control | 01:49:52-01:49:55 | No permanent-subscription creation | Quiet | +| Subscription inventory control | 01:50:34-01:50:37 | No permanent-subscription creation | Quiet | +| Transient subscription control | 01:51:16-01:51:22 | No permanent `root/subscription` objects | Quiet | + +Boot-time evidence placed the exact Atomic run outside its 240–324 second trigger interval. A bounded Splunk search returned zero `notepad.exe` Sysmon process-creation matches during the exact positive window. + +## Cleanup +Final validation found zero remaining test filters, consumers, bindings, and completion files. A later idempotence recheck again returned empty residue sets and `Clean=true`. Endpoint sensors, the domain secure channel, Splunk ingestion, and domain-controller health remained operational. + +Raw event XML, credentials, private addresses, management commands, process identifiers, and private infrastructure details are intentionally omitted. diff --git a/investigations/endpoint/DFIR-2026-013/README.md b/investigations/endpoint/DFIR-2026-013/README.md new file mode 100644 index 0000000..6b4dcf9 --- /dev/null +++ b/investigations/endpoint/DFIR-2026-013/README.md @@ -0,0 +1,21 @@ +# DFIR-2026-013 — Permanent WMI event subscription investigation + +## Question +Was a permanent WMI event filter, consumer, or filter-to-consumer binding created, and was the behavior approved administration or unauthorized event-triggered persistence? + +## Evidence sequence +1. Review Sysmon events 19, 20, and 21 for `WmiFilterEvent`, `WmiConsumerEvent`, and `WmiBindingEvent` creation. +2. Correlate the filter namespace and WQL query with the consumer class and destination command. +3. Confirm the binding relationship; a filter or consumer alone may be incomplete or orphaned rather than functional persistence. +4. Pivot to adjacent PowerShell and WMI Activity telemetry to identify the creating account, process, and execution window. +5. Inventory `root/subscription` directly and compare object names and consumers with approved endpoint-management and monitoring baselines. +6. Remove the binding before its consumer and filter, then verify all three object classes are clean. + +## Validated observations +- Both controlled positives created a filter, `CommandLineEventConsumer`, and binding and fired in Splunk. +- The modified positive produced Sysmon events 19, 20, and 21. +- Read-only inventory and transient in-process subscription controls remained quiet. +- Final cleanup found no test objects or completion artifacts. + +## Disposition +Expected lab activity for validation `VAL-2026-013`. Outside an approved exercise or software deployment, permanent WMI subscription creation warrants escalation because it can provide stealthy event-triggered persistence. Validate legitimate management tooling before containment, but preserve the filter, consumer, and binding relationship as evidence. diff --git a/purple-team/scenarios/PT-2026-013-wmi-event-subscription/RESULTS.md b/purple-team/scenarios/PT-2026-013-wmi-event-subscription/RESULTS.md new file mode 100644 index 0000000..352cdd9 --- /dev/null +++ b/purple-team/scenarios/PT-2026-013-wmi-event-subscription/RESULTS.md @@ -0,0 +1,49 @@ +# PT-2026-013 Results + +## Validation run +- Validation ID: `VAL-2026-013` +- Technique: `T1546.003` WMI Event Subscription +- Exact Atomic test: `3c64f177-28e2-49eb-a799-d767b24dd1e0` (`T1546.003-1`) +- Status: **live validated** on 2026-08-03 UTC +- Target: approved Windows victim only +- Rollback snapshot: verified; exact identifier retained in private evidence + +## Scope and safety +The scenario validated creation of a permanent WMI event filter, `CommandLineEventConsumer`, and filter-to-consumer binding. Boot-time evidence showed that the victim's uptime exceeded the upstream Atomic's 240–324 second trigger window, and a bounded Splunk search found zero `notepad.exe` Sysmon process-creation matches during the exact positive window. The modified variant used an intentionally unreachable uptime condition and an inert local command. No test-triggered payload execution was observed. + +The ActiveScriptEventConsumer and MOFComp Atomic tests were excluded. No domain-controller execution, remote target, credential access, lateral movement, reboot, or network payload was permitted. + +## Positive paths +1. The exact upstream Atomic UUID created the permanent filter, consumer, and binding. + - A durable completion record verified all three objects. + - The behavioral Splunk rule matched three Sysmon creation events at `2026-08-03T01:47:05Z`. + - Observed event types included `WmiFilterEvent` and `WmiConsumerEvent`. + - Detection latency was 4.486 seconds. +2. A modified local `CommandLineEventConsumer` variant used a deliberately non-triggering WQL condition. + - A durable completion record verified all three objects. + - The same rule matched Sysmon events 19, 20, and 21 at `2026-08-03T01:48:47Z`. + - Detection latency was 0.994 seconds. + +The detection is behavioral: it matches permanent WMI subscription creation fields, not scenario object names or Atomic-specific strings. + +## Negative paths +The rule remained quiet for: +- read-only CIM operating-system inventory; +- read-only inventory of existing permanent WMI subscriptions; +- a transient in-process CIM indication subscription that did not write to `root/subscription`. + +## Deviation and correction +The first validation harness compared CIM association references as ordinary strings. The Atomic created the subscription, but the harness could not reliably confirm or remove the binding, so no successful completion record was accepted. A later controlled attempt proved that a longer polling delay did not solve the representation problem. + +Execution was stopped after each failed proof. Cleanup and verification were changed to the upstream-style WMI `REFERENCES OF` association query, regression assertions were added, and filter, consumer, binding, and completion-file counts were verified as zero before the successful replay. Only the final clean positive windows above are used as validation evidence. + +## Cleanup and postflight +At `2026-08-03T01:52:21.487320Z`: +- no test filters, consumers, or bindings remained; +- no completion files remained; +- the victim domain secure channel passed; +- Sysmon, Splunk Forwarder, Velociraptor, Wazuh, and Defender were running; +- fresh victim telemetry remained searchable in Splunk; +- the domain controller remained healthy and `dcdiag /q` was quiet. + +An idempotence recheck reran the corrected cleanup against the clean victim and again returned no remaining objects or completion files with `Clean=true`. diff --git a/purple-team/scenarios/PT-2026-013-wmi-event-subscription/scenario.yaml b/purple-team/scenarios/PT-2026-013-wmi-event-subscription/scenario.yaml new file mode 100644 index 0000000..fae96ce --- /dev/null +++ b/purple-team/scenarios/PT-2026-013-wmi-event-subscription/scenario.yaml @@ -0,0 +1,93 @@ +id: PT-2026-013 +title: Permanent WMI Event Subscription Validation on Approved Windows Endpoint +description: >- + Low-risk victim-only Atomic Red Team validation of permanent WMI event subscription creation, + behavioral detection, negative controls, and explicit cleanup without reboot or payload triggering. +status: validated +created: '2026-08-02T00:00:00Z' +updated: '2026-08-03T00:00:00Z' +owner: mell0wx +attack: + framework: MITRE ATT&CK + technique_ids: + - T1546.003 + test_ids: + - 3c64f177-28e2-49eb-a799-d767b24dd1e0 + commands: + - Invoke-AtomicTest T1546.003 exact test GUID for CommandLineEventConsumer persistence + - Create a modified local CommandLineEventConsumer subscription with a non-triggering query + prerequisites: + - Windows victim is healthy and domain joined + - Sysmon, SplunkForwarder, Velociraptor, Wazuh, and Defender are running + - Sysmon WMI subscription events and Splunk telemetry are current and queryable + - Atomic Red Team repository and Invoke-AtomicRedTeam 2.3.0 are staged locally +targets: + approved_hosts: + - approved-windows-victim + approved_networks: [] +safety: + risk_level: low + snapshot_required: true + required_snapshot_name: private-rollback-reference + isolation_required: true + internet_access: none + cleanup_required: true + timeout_seconds: 300 + excluded_tests: + - ActiveScriptEventConsumer + - MOFComp + - reboot-triggered payload execution + - remote hosts or credentials +telemetry: + required: + - Sysmon Event ID 19 WmiEventFilter activity + - Sysmon Event ID 20 WmiEventConsumer activity + - Sysmon Event ID 21 WmiEventConsumerToFilter activity + optional: + - Microsoft-Windows-WMI-Activity/Operational + - Wazuh event correlation +baseline: + duration_minutes: 15 + queries: + - splunk-recent-wmi-subscription-events + - local-permanent-wmi-subscription-inventory +execution: + runner: isolated-guest-command-runner + arguments: + - Invoke-AtomicTest T1546.003 exact CommandLineEventConsumer test + - local non-triggering permanent WMI subscription variant +collection: + before: + - named filter, consumer, and binding absence + - sensor health + - current telemetry timestamps + after: + - WMI filter creation + - WMI consumer creation + - WMI binding creation +hunts: + hypothesis_ids: + - HUNT-2026-013 +forensics: + collection_profiles: + - permanent-wmi-subscriptions + - sysmon-wmi-events + - wmi-activity-events +detections: + expected_rules: + - DET-2026-013 +validation: + positive_tests: + - exact-atomic-command-line-consumer + - modified-non-triggering-command-line-consumer + negative_tests: + - read-only-cim-os-query + - read-only-subscription-inventory + - transient-in-process-cim-indication-subscription +reporting: + evidence_paths: + - evidence/sanitized/PT-2026-013/ + - investigations/endpoint/DFIR-2026-013/ + output_paths: + - purple-team/scenarios/PT-2026-013-wmi-event-subscription/ + - detections/validation/live/ diff --git a/tests/fixtures/T1546.003/negative/filter-deleted.json b/tests/fixtures/T1546.003/negative/filter-deleted.json new file mode 100644 index 0000000..604502b --- /dev/null +++ b/tests/fixtures/T1546.003/negative/filter-deleted.json @@ -0,0 +1,11 @@ +{ + "rule_id": "45742e29-7c43-4a65-bd91-b04fa7a8d9dc", + "technique_id": "T1546.003", + "scenario_id": "PT-2026-013", + "expected_match": false, + "event": { + "EventType": "WmiFilterEvent", + "Operation": "Deleted", + "Name": "RetiredSubscription" + } +} diff --git a/tests/fixtures/T1546.003/negative/process-creation.json b/tests/fixtures/T1546.003/negative/process-creation.json new file mode 100644 index 0000000..fd5b309 --- /dev/null +++ b/tests/fixtures/T1546.003/negative/process-creation.json @@ -0,0 +1,11 @@ +{ + "rule_id": "45742e29-7c43-4a65-bd91-b04fa7a8d9dc", + "technique_id": "T1546.003", + "scenario_id": "PT-2026-013", + "expected_match": false, + "event": { + "EventType": "ProcessCreate", + "Operation": "Created", + "Image": "C:\\Windows\\System32\\wbem\\WmiPrvSE.exe" + } +} diff --git a/tests/fixtures/T1546.003/negative/wmi-inventory-query.json b/tests/fixtures/T1546.003/negative/wmi-inventory-query.json new file mode 100644 index 0000000..52ebfa8 --- /dev/null +++ b/tests/fixtures/T1546.003/negative/wmi-inventory-query.json @@ -0,0 +1,11 @@ +{ + "rule_id": "45742e29-7c43-4a65-bd91-b04fa7a8d9dc", + "technique_id": "T1546.003", + "scenario_id": "PT-2026-013", + "expected_match": false, + "event": { + "EventType": "WmiQueryEvent", + "Operation": "Queried", + "Query": "SELECT * FROM Win32_OperatingSystem" + } +} diff --git a/tests/fixtures/T1546.003/positive/atomic-filter-created.json b/tests/fixtures/T1546.003/positive/atomic-filter-created.json new file mode 100644 index 0000000..65f928c --- /dev/null +++ b/tests/fixtures/T1546.003/positive/atomic-filter-created.json @@ -0,0 +1,11 @@ +{ + "rule_id": "45742e29-7c43-4a65-bd91-b04fa7a8d9dc", + "technique_id": "T1546.003", + "scenario_id": "PT-2026-013", + "expected_match": true, + "event": { + "EventType": "WmiFilterEvent", + "Operation": "Created", + "Name": "AtomicRedTeam-WMIPersistence-CommandLineEventConsumer-Example" + } +} diff --git a/tests/fixtures/T1546.003/positive/variant-binding-created.json b/tests/fixtures/T1546.003/positive/variant-binding-created.json new file mode 100644 index 0000000..7eda1d9 --- /dev/null +++ b/tests/fixtures/T1546.003/positive/variant-binding-created.json @@ -0,0 +1,11 @@ +{ + "rule_id": "45742e29-7c43-4a65-bd91-b04fa7a8d9dc", + "technique_id": "T1546.003", + "scenario_id": "PT-2026-013", + "expected_match": true, + "event": { + "EventType": "WmiBindingEvent", + "Operation": "Created", + "Consumer": "PT-2026-013-Variant" + } +} diff --git a/tests/test_pt_2026_013_contract.py b/tests/test_pt_2026_013_contract.py new file mode 100644 index 0000000..1ed0e5d --- /dev/null +++ b/tests/test_pt_2026_013_contract.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +SCENARIO = ROOT / "purple-team/scenarios/PT-2026-013-wmi-event-subscription/scenario.yaml" +RULE = ROOT / "detections/sigma/windows/wmi_event/suspicious_permanent_wmi_subscription.yml" +ATOMIC_GUID = "3c64f177-28e2-49eb-a799-d767b24dd1e0" +SNAPSHOT = "private-rollback-reference" +RULE_ID = "45742e29-7c43-4a65-bd91-b04fa7a8d9dc" + + +class PT2026013ContractTests(unittest.TestCase): + def test_scenario_records_exact_atomic_and_rollback_snapshot(self) -> None: + scenario = yaml.safe_load(SCENARIO.read_text(encoding="utf-8")) + self.assertEqual(scenario["id"], "PT-2026-013") + self.assertEqual(scenario["attack"]["technique_ids"], ["T1546.003"]) + self.assertEqual(scenario["attack"]["test_ids"], [ATOMIC_GUID]) + self.assertEqual(scenario["safety"]["required_snapshot_name"], SNAPSHOT) + self.assertTrue(scenario["safety"]["cleanup_required"]) + self.assertEqual(scenario["targets"]["approved_hosts"], ["approved-windows-victim"]) + self.assertIn("ActiveScriptEventConsumer", scenario["safety"]["excluded_tests"]) + self.assertIn("MOFComp", scenario["safety"]["excluded_tests"]) + + def test_positive_original_invokes_only_exact_atomic_guid(self) -> None: + script = (ROOT / "automation/execution/pt_2026_013_positive_atomic_wmi_subscription.ps1").read_text(encoding="utf-8") + self.assertIn("Invoke-AtomicTest T1546.003", script) + self.assertIn(ATOMIC_GUID, script) + self.assertIn("-TestGuids $testGuid", script) + self.assertNotIn("-TestNumbers 2", script) + self.assertNotIn("mofcomp", script.lower()) + + def test_variant_is_local_non_triggering_and_cleanup_is_explicit(self) -> None: + original = (ROOT / "automation/execution/pt_2026_013_positive_atomic_wmi_subscription.ps1").read_text(encoding="utf-8") + variant = (ROOT / "automation/execution/pt_2026_013_positive_variant_wmi_subscription.ps1").read_text(encoding="utf-8") + self.assertIn("CommandLineEventConsumer", variant) + self.assertIn("SystemUpTime > 99999999", variant) + self.assertNotIn("Invoke-WebRequest", variant) + self.assertIn("pt-2026-013-original-completion.json", original) + self.assertIn("pt-2026-013-variant-completion.json", variant) + self.assertIn("ConvertTo-Json", original) + self.assertIn("ConvertTo-Json", variant) + self.assertIn("AddSeconds(30)", original) + self.assertIn("AddSeconds(30)", variant) + self.assertIn("REFERENCES OF", original) + self.assertIn("REFERENCES OF", variant) + cleanup = (ROOT / "automation/execution/pt_2026_013_cleanup.ps1").read_text(encoding="utf-8") + for name in ("AtomicRedTeam-WMIPersistence-CommandLineEventConsumer-Example", "PT-2026-013-Variant"): + self.assertIn(name, cleanup) + self.assertIn("__FilterToConsumerBinding", cleanup) + self.assertIn("CommandLineEventConsumer", cleanup) + self.assertIn("__EventFilter", cleanup) + self.assertIn("pt-2026-013-original-completion.json", cleanup) + self.assertIn("pt-2026-013-variant-completion.json", cleanup) + self.assertIn("REFERENCES OF", cleanup) + self.assertIn("Remove-WmiObject", cleanup) + self.assertIn("Get-NamedBindings", cleanup) + self.assertNotIn('"$($_.Filter)" -like', cleanup) + self.assertNotIn('"$($_.Consumer)" -like', cleanup) + binding_delete = cleanup.index("@(Get-NamedBindings -ObjectName $name) | Remove-WmiObject") + binding_recheck = cleanup.index("$remainingBindings = @(Get-NamedBindings -ObjectName $name)") + endpoint_gate = cleanup.index("if ($remainingBindings.Count -eq 0)") + consumer_delete = cleanup.index( + "@(Get-WmiObject -Namespace $namespace -Class CommandLineEventConsumer", + endpoint_gate, + ) + self.assertLess(binding_delete, binding_recheck) + self.assertLess(binding_recheck, endpoint_gate) + self.assertLess(endpoint_gate, consumer_delete) + + def test_detection_is_behavioral_and_not_scenario_coupled(self) -> None: + rule = yaml.safe_load(RULE.read_text(encoding="utf-8")) + self.assertEqual(rule["id"], RULE_ID) + self.assertEqual(rule["logsource"]["category"], "wmi_event") + self.assertIn("attack.t1546.003", rule["tags"]) + serialized = json.dumps(rule["detection"]) + for event_type in ("WmiFilterEvent", "WmiConsumerEvent", "WmiBindingEvent"): + self.assertIn(event_type, serialized) + self.assertIn("Created", serialized) + self.assertNotIn("PT-2026-013", serialized) + self.assertNotIn("AtomicRedTeam", serialized) + + def test_live_spl_generator_supports_wmi_event_fields(self) -> None: + from automation.validators import sigma_ops + + self.assertIn("wmi_event", sigma_ops.BASE_SEARCH) + self.assertIn("EventType", sigma_ops.FIELD_RAW_MAP) + self.assertIn("Operation", sigma_ops.FIELD_RAW_MAP) + + def test_fixture_contract_has_two_positive_and_three_negative_cases(self) -> None: + root = ROOT / "tests/fixtures/T1546.003" + positive = sorted((root / "positive").glob("*.json")) + negative = sorted((root / "negative").glob("*.json")) + self.assertEqual(len(positive), 2) + self.assertEqual(len(negative), 3) + for path, expected in [(p, True) for p in positive] + [(p, False) for p in negative]: + fixture = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual(fixture["rule_id"], RULE_ID) + self.assertEqual(fixture["technique_id"], "T1546.003") + self.assertEqual(fixture["scenario_id"], "PT-2026-013") + self.assertIs(fixture["expected_match"], expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/threat-hunting/hypotheses/HUNT-2026-013.yaml b/threat-hunting/hypotheses/HUNT-2026-013.yaml new file mode 100644 index 0000000..f56d347 --- /dev/null +++ b/threat-hunting/hypotheses/HUNT-2026-013.yaml @@ -0,0 +1,41 @@ +id: HUNT-2026-013 +title: Hunt for Permanent WMI Event Subscription Creation +status: completed +hypothesis: >- + If an adversary establishes event-triggered persistence through WMI, Sysmon should record creation of a filter, + consumer, and binding in the permanent root/subscription namespace. +attack: + - T1546.003 +required_data: + - Sysmon Event IDs 19, 20, and 21 + - Microsoft-Windows-WMI-Activity/Operational + - permanent WMI subscription inventory +target_systems: + - approved-windows-victim +baseline_query: windows-baseline-permanent-wmi-subscriptions +hunt_query: suspicious-permanent-wmi-subscription +investigation_pivots: + - event filter namespace and WQL query + - consumer type and destination command + - filter-to-consumer binding relationship + - creating user and adjacent PowerShell activity +known_legitimate_activity: + - endpoint management agents + - monitoring products + - approved software deployment tools +success_criteria: + - exact Atomic CommandLineEventConsumer creation is visible in Sysmon and Splunk + - a modified non-triggering variant triggers the behavioral rule + - read-only and transient subscription controls stay quiet + - all temporary filters, consumers, and bindings are removed +detection_opportunity: >- + Detect creation of permanent WMI filters, consumers, and bindings rather than matching lab-specific object names. +results: + - exact Atomic CommandLineEventConsumer subscription detected in Splunk + - modified non-triggering filter, consumer, and binding detected in Splunk + - read-only CIM and subscription inventory controls remained quiet + - transient in-process subscription control remained quiet + - final filter, consumer, binding, and completion-file counts were zero +conclusion: >- + Sysmon permanent WMI subscription creation telemetry supports a behavioral detection across both + the exact Atomic and a modified variant while excluding read-only and transient controls.