From 77453c05565d5440e8a216c769fe8bd9b5e9e2a7 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 18 Jun 2026 08:16:35 +0000 Subject: [PATCH] test(graphrag): add Drain miner fuzz target for Scorecard Fuzzing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LDQVJrixs2nJoea67a8pEG --- internal/graphrag/drain_fuzz_test.go | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 internal/graphrag/drain_fuzz_test.go diff --git a/internal/graphrag/drain_fuzz_test.go b/internal/graphrag/drain_fuzz_test.go new file mode 100644 index 0000000..628dc84 --- /dev/null +++ b/internal/graphrag/drain_fuzz_test.go @@ -0,0 +1,72 @@ +package graphrag + +import ( + "testing" + "time" +) + +// FuzzDrainMatch feeds arbitrary log lines into the Drain template miner. +// Match() is the ingestion hot path that every untrusted log body flows +// through (LogsServer.Export -> LogCallback -> Drain.Match), so it must +// never panic on malformed input and must satisfy two invariants: +// +// - empty/whitespace-only input returns nil (no template); +// - any non-nil result is a well-formed template (non-empty token slice, +// stable ID matching its tokens). +// +// We also assert determinism: Preprocess + tokenize are pure functions, so +// masking the same line twice must yield the identical token sequence. +func FuzzDrainMatch(f *testing.F) { + seeds := []string{ + "", + " \t\n", + "connected to 10.0.0.1:8080 successfully", + "trace 550e8400-e29b-41d4-a716-446655440000 done", + "event at 2025-01-02T03:04:05Z completed", + "notify alice@example.com now", + "pointer 0xdeadbeef freed -1 times", + "Database connection failed: timeout after 30s", + "\x00\x01\x02 binary garbage \xff\xfe", + "日本語 のログ メッセージ 42", + "a b c d e f g h i j k l m n o p", // long token run, exceeds depth + } + for _, s := range seeds { + f.Add(s) + } + + ts := time.Unix(0, 0) + f.Fuzz(func(t *testing.T, line string) { + // Preprocessing must be deterministic: same input -> same masked tokens. + m1 := tokenize(Preprocess(line)) + m2 := tokenize(Preprocess(line)) + if len(m1) != len(m2) { + t.Fatalf("Preprocess non-deterministic token count: %d vs %d for %q", len(m1), len(m2), line) + } + for i := range m1 { + if m1[i] != m2[i] { + t.Fatalf("Preprocess non-deterministic token %d: %q vs %q for input %q", i, m1[i], m2[i], line) + } + } + + d := NewDrain() + tpl := d.Match(line, ts) + + if len(m1) == 0 { + if tpl != nil { + t.Fatalf("Match(%q) = non-nil template for empty/whitespace tokenization, want nil", line) + } + return + } + + if tpl == nil { + t.Fatalf("Match(%q) = nil for non-empty tokenization %v", line, m1) + } + if len(tpl.Tokens) == 0 { + t.Fatalf("Match(%q) produced a template with no tokens", line) + } + // ID must be the stable hash of the (possibly generalized) tokens. + if got := templateID(tpl.Tokens); got != tpl.ID { + t.Fatalf("template ID %d does not match templateID(tokens)=%d for %q", tpl.ID, got, line) + } + }) +}