Skip to content

Commit aa24e59

Browse files
kryonsxclaude
andcommitted
fix(deceptive-bytes): only run the ransomware history search when its placeholders exist
The ransomware rule searches history on {{.log.process}} and {{.log.source_ip}}, but its condition did not require those fields. A missing placeholder makes the search fail, and five failures switch the rule off with a Circuit Breaker alert. Require both fields, as the data theft, advanced threat tactic and zero-day rules already require theirs. The history-guard test now covers the four rules and checks that a rule does not match without any one of the fields it needs; it fails on the unguarded ransomware rule. go-sdk v1.1.36 replay: the unguarded rule matched two lines that each lacked one field, with unresolved placeholders; the guarded rule matches only the line with both (65 of 65 checks). Full plugins/alerts suite: 48 pass, 11 skip, 0 fail; replay.py on EventProcessor 8a3ade7 still passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7ba2d4b commit aa24e59

3 files changed

Lines changed: 73 additions & 14 deletions

File tree

‎filters/audits/deceptive-bytes.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,15 @@ Three rules run a history search on `{{.origin.ip}}` (and `{{.log.tacticName}}`
6969
placeholder makes the search fail, and five failures switch a rule off with a
7070
Circuit Breaker alert, so the data theft, advanced threat tactic and zero-day
7171
conditions now also require those fields, as the other seven `origin.ip` rules of
72-
this source already do. `ransomware_behavior_patterns` still searches on
73-
`{{.log.process}}` and `{{.log.source_ip}}` without requiring them; that is unchanged.
72+
this source already do. For the same reason `ransomware_behavior_patterns`, which
73+
searches on `{{.log.process}}` and `{{.log.source_ip}}`, now requires both fields. The
74+
history-guard test checks each of the four rules: no match without the fields it needs or
75+
without any one of them, a match with them, and every placeholder resolved. Without the
76+
ransomware guard it fails; the go-sdk v1.1.36 replay over the playground events plus two
77+
copies of the ransomware line that each lack one of those fields then matched both copies
78+
with unresolved placeholders. With the guard the same replay passes 65 of 65 checks: each
79+
rule matches only its intended case, and the ransomware rule only the line that carries
80+
both fields.
7481

7582
The committed fabricated lines carry all 18 keys. On EventProcessor `8a3ade7` the KV
7683
plugin stored every one with its underscore (for example `log.event_type`,

‎plugins/alerts/deceptive_bytes_history_guard_test.go‎

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"path/filepath"
56
"strings"
67
"testing"
@@ -11,17 +12,55 @@ import (
1112
"google.golang.org/protobuf/encoding/protojson"
1213
)
1314

15+
// deceptiveBytesEvent builds an event from a log object plus dotted fields.
16+
func deceptiveBytesEvent(t *testing.T, logFields map[string]any, extra map[string]string) string {
17+
t.Helper()
18+
logCopy := map[string]any{}
19+
for k, v := range logFields {
20+
logCopy[k] = v
21+
}
22+
event := map[string]any{"dataType": "deceptive-bytes", "log": logCopy}
23+
for path, value := range extra {
24+
parts := strings.Split(path, ".")
25+
node := event
26+
for _, part := range parts[:len(parts)-1] {
27+
next, ok := node[part].(map[string]any)
28+
if !ok {
29+
next = map[string]any{}
30+
node[part] = next
31+
}
32+
node = next
33+
}
34+
node[parts[len(parts)-1]] = value
35+
}
36+
b, err := json.Marshal(event)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
return string(b)
41+
}
42+
1443
// A history search whose placeholder is missing returns an error, and five errors switch the
1544
// rule off with a Circuit Breaker alert. Each rule must only match when its placeholders exist.
1645
func TestDeceptiveBytesHistoryPlaceholdersGuarded(t *testing.T) {
1746
cache := plugins.NewCELCache("deceptive-bytes-history-guard")
1847
cases := []struct {
19-
rule string
20-
event string
48+
rule string
49+
log map[string]any // matches the condition apart from the placeholder fields
50+
needed map[string]string // the fields the history placeholders read
2151
}{
22-
{"data_theft_attempt_indicators", `{"event_type":"decoy_accessed","action":"file_copy","decoy_sensitivity":"high","decoy_file":"f"}`},
23-
{"advanced_threat_tactic_identification", `{"eventType":"advanced_threat_detected","threatLevel":"critical","tacticName":"execution","deceptionTriggered":"true","behaviorScore":95}`},
24-
{"zero_day_behavior_patterns", `{"eventType":"zero_day_suspect","threatSignature":"unknown","deceptionEnvironment":"true","memoryAnomalyScore":95,"knownMalwareFamily":"","exploitTechnique":"t","processName":"p.exe"}`},
52+
{"data_theft_attempt_indicators",
53+
map[string]any{"event_type": "decoy_accessed", "action": "file_copy", "decoy_sensitivity": "high", "decoy_file": "f"},
54+
map[string]string{"origin.ip": "192.0.2.10"}},
55+
{"advanced_threat_tactic_identification",
56+
map[string]any{"eventType": "advanced_threat_detected", "threatLevel": "critical", "tacticName": "execution", "deceptionTriggered": "true", "behaviorScore": 95},
57+
map[string]string{"origin.ip": "192.0.2.10"}},
58+
{"zero_day_behavior_patterns",
59+
map[string]any{"eventType": "zero_day_suspect", "threatSignature": "unknown", "deceptionEnvironment": "true", "memoryAnomalyScore": 95, "knownMalwareFamily": "", "exploitTechnique": "t", "processName": "p.exe"},
60+
map[string]string{"origin.ip": "192.0.2.10"}},
61+
{"ransomware_behavior_patterns",
62+
map[string]any{"event_type": "ransomware_behavior", "behavior_pattern": "mass_encryption"},
63+
map[string]string{"log.process": "example.exe", "log.source_ip": "192.0.2.10"}},
2564
}
2665
for _, tc := range cases {
2766
b, err := utils.ReadPbYaml(filepath.Join("../..", "rules/antivirus/deceptive-bytes", tc.rule+".yml"))
@@ -33,15 +72,26 @@ func TestDeceptiveBytesHistoryPlaceholdersGuarded(t *testing.T) {
3372
t.Fatal(err)
3473
}
3574
rule.Normalize()
36-
without := `{"dataType":"deceptive-bytes","log":` + tc.event + `}`
37-
with := `{"dataType":"deceptive-bytes","origin":{"ip":"192.0.2.10"},"log":` + tc.event + `}`
3875
t.Run(tc.rule, func(t *testing.T) {
39-
if got, err := cache.Eval(rule.Where, without); err != nil || got {
40-
t.Errorf("matched without origin.ip: %v (%v)", got, err)
76+
// Without the placeholder fields, and without any one of them, the rule must not match.
77+
if got, err := cache.Eval(rule.Where, deceptiveBytesEvent(t, tc.log, nil)); err != nil || got {
78+
t.Errorf("matched without %v: %v (%v)", tc.needed, got, err)
79+
}
80+
for missing := range tc.needed {
81+
partial := map[string]string{}
82+
for path, value := range tc.needed {
83+
if path != missing {
84+
partial[path] = value
85+
}
86+
}
87+
if got, err := cache.Eval(rule.Where, deceptiveBytesEvent(t, tc.log, partial)); err != nil || got {
88+
t.Errorf("matched without %s: %v (%v)", missing, got, err)
89+
}
4190
}
91+
with := deceptiveBytesEvent(t, tc.log, tc.needed)
4292
got, err := cache.Eval(rule.Where, with)
4393
if err != nil || !got {
44-
t.Fatalf("did not match with origin.ip: %v (%v)", got, err)
94+
t.Fatalf("did not match with %v: %v (%v)", tc.needed, got, err)
4595
}
4696
for _, block := range rule.Correlation {
4797
for _, expr := range block.With {

‎rules/antivirus/deceptive-bytes/ransomware_behavior_patterns.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Rule version v1.0.0
1+
# Rule version v1.0.1
22

33
dataTypes:
44
- deceptive-bytes
@@ -29,7 +29,9 @@ description: |
2929
9. Review backup integrity and availability before any restoration attempts
3030
where: |
3131
equals("log.event_type", "ransomware_behavior") &&
32-
oneOf("log.behavior_pattern", ["mass_encryption", "file_enumeration", "ransom_note_drop"])
32+
oneOf("log.behavior_pattern", ["mass_encryption", "file_enumeration", "ransom_note_drop"]) &&
33+
exists("log.process") &&
34+
exists("log.source_ip")
3335
afterEvents:
3436
- indexPattern: v11-log-deceptive-bytes-*
3537
with:

0 commit comments

Comments
 (0)