Skip to content

Commit 55cc830

Browse files
aksOpsclaude
andauthored
test(graphrag): add Drain miner fuzz target for Scorecard Fuzzing (#115)
Add FuzzDrainMatch, a Go native fuzz test over the Drain log-template miner's Match() — the ingestion hot path every untrusted log body flows through (LogsServer.Export -> LogCallback -> Drain.Match). It asserts Match never panics on malformed input and holds two invariants: empty or whitespace-only tokenization yields a nil template, and any non-nil result is well-formed (non-empty tokens, ID == templateID(tokens)). It also checks Preprocess+tokenize determinism. Seeds cover empty, whitespace, IPs/UUIDs/timestamps/emails/hex pointers, NUL + high-byte binary, multibyte UTF-8, and an over-depth token run. Satisfies OpenSSF Scorecard's Fuzzing check (detects a native Go fuzz function). Verified: go build/vet/test green (101 tests), and go test -fuzz=FuzzDrainMatch -fuzztime=10s found no crashers. Claude-Session: https://claude.ai/code/session_01LDQVJrixs2nJoea67a8pEG Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ccf7958 commit 55cc830

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package graphrag
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
// FuzzDrainMatch feeds arbitrary log lines into the Drain template miner.
9+
// Match() is the ingestion hot path that every untrusted log body flows
10+
// through (LogsServer.Export -> LogCallback -> Drain.Match), so it must
11+
// never panic on malformed input and must satisfy two invariants:
12+
//
13+
// - empty/whitespace-only input returns nil (no template);
14+
// - any non-nil result is a well-formed template (non-empty token slice,
15+
// stable ID matching its tokens).
16+
//
17+
// We also assert determinism: Preprocess + tokenize are pure functions, so
18+
// masking the same line twice must yield the identical token sequence.
19+
func FuzzDrainMatch(f *testing.F) {
20+
seeds := []string{
21+
"",
22+
" \t\n",
23+
"connected to 10.0.0.1:8080 successfully",
24+
"trace 550e8400-e29b-41d4-a716-446655440000 done",
25+
"event at 2025-01-02T03:04:05Z completed",
26+
"notify alice@example.com now",
27+
"pointer 0xdeadbeef freed -1 times",
28+
"Database connection failed: timeout after 30s",
29+
"\x00\x01\x02 binary garbage \xff\xfe",
30+
"日本語 のログ メッセージ 42",
31+
"a b c d e f g h i j k l m n o p", // long token run, exceeds depth
32+
}
33+
for _, s := range seeds {
34+
f.Add(s)
35+
}
36+
37+
ts := time.Unix(0, 0)
38+
f.Fuzz(func(t *testing.T, line string) {
39+
// Preprocessing must be deterministic: same input -> same masked tokens.
40+
m1 := tokenize(Preprocess(line))
41+
m2 := tokenize(Preprocess(line))
42+
if len(m1) != len(m2) {
43+
t.Fatalf("Preprocess non-deterministic token count: %d vs %d for %q", len(m1), len(m2), line)
44+
}
45+
for i := range m1 {
46+
if m1[i] != m2[i] {
47+
t.Fatalf("Preprocess non-deterministic token %d: %q vs %q for input %q", i, m1[i], m2[i], line)
48+
}
49+
}
50+
51+
d := NewDrain()
52+
tpl := d.Match(line, ts)
53+
54+
if len(m1) == 0 {
55+
if tpl != nil {
56+
t.Fatalf("Match(%q) = non-nil template for empty/whitespace tokenization, want nil", line)
57+
}
58+
return
59+
}
60+
61+
if tpl == nil {
62+
t.Fatalf("Match(%q) = nil for non-empty tokenization %v", line, m1)
63+
}
64+
if len(tpl.Tokens) == 0 {
65+
t.Fatalf("Match(%q) produced a template with no tokens", line)
66+
}
67+
// ID must be the stable hash of the (possibly generalized) tokens.
68+
if got := templateID(tpl.Tokens); got != tpl.ID {
69+
t.Fatalf("template ID %d does not match templateID(tokens)=%d for %q", tpl.ID, got, line)
70+
}
71+
})
72+
}

0 commit comments

Comments
 (0)