From 2cb35d50ac7d07a31fe18af0801af7ff766ffb84 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Sun, 5 Apr 2026 23:30:38 +0300 Subject: [PATCH] test(06-03): add failing tests for SARIF 2.1.0 formatter --- pkg/output/sarif_test.go | 166 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 pkg/output/sarif_test.go diff --git a/pkg/output/sarif_test.go b/pkg/output/sarif_test.go new file mode 100644 index 0000000..6be772c --- /dev/null +++ b/pkg/output/sarif_test.go @@ -0,0 +1,166 @@ +package output + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/salvacybersec/keyhunter/pkg/engine" +) + +// decodeSARIF runs the formatter and unmarshals the output into a sarifDoc +// for structural assertions. +func decodeSARIF(t *testing.T, findings []engine.Finding, opts Options) sarifDoc { + t.Helper() + var buf bytes.Buffer + if err := (SARIFFormatter{}).Format(findings, &buf, opts); err != nil { + t.Fatalf("Format returned error: %v", err) + } + var doc sarifDoc + if err := json.Unmarshal(buf.Bytes(), &doc); err != nil { + t.Fatalf("failed to unmarshal SARIF output: %v\npayload: %s", err, buf.String()) + } + return doc +} + +func TestSARIF_Empty(t *testing.T) { + doc := decodeSARIF(t, nil, Options{}) + if doc.Version != "2.1.0" { + t.Errorf("version: got %q want 2.1.0", doc.Version) + } + if doc.Schema != "https://json.schemastore.org/sarif-2.1.0.json" { + t.Errorf("schema: got %q", doc.Schema) + } + if len(doc.Runs) != 1 { + t.Fatalf("runs: got %d want 1", len(doc.Runs)) + } + if len(doc.Runs[0].Results) != 0 { + t.Errorf("results: got %d want 0", len(doc.Runs[0].Results)) + } + if len(doc.Runs[0].Tool.Driver.Rules) != 0 { + t.Errorf("rules: got %d want 0", len(doc.Runs[0].Tool.Driver.Rules)) + } +} + +func TestSARIF_DedupRules(t *testing.T) { + findings := []engine.Finding{ + {ProviderName: "openai", KeyMasked: "sk-aaaa...bbbb", Confidence: "high", Source: "a.go", LineNumber: 10}, + {ProviderName: "openai", KeyMasked: "sk-cccc...dddd", Confidence: "high", Source: "b.go", LineNumber: 20}, + {ProviderName: "anthropic", KeyMasked: "sk-ant-x...y", Confidence: "medium", Source: "c.go", LineNumber: 5}, + } + doc := decodeSARIF(t, findings, Options{}) + rules := doc.Runs[0].Tool.Driver.Rules + if len(rules) != 2 { + t.Fatalf("rules: got %d want 2", len(rules)) + } + seen := map[string]bool{} + for _, r := range rules { + seen[r.ID] = true + if r.Name != r.ID { + t.Errorf("rule name %q != id %q", r.Name, r.ID) + } + if !strings.Contains(r.ShortDescription.Text, "Leaked") { + t.Errorf("shortDescription missing 'Leaked': %q", r.ShortDescription.Text) + } + } + if !seen["openai"] || !seen["anthropic"] { + t.Errorf("expected openai and anthropic rules, got %+v", seen) + } + if got := len(doc.Runs[0].Results); got != 3 { + t.Errorf("results: got %d want 3", got) + } +} + +func TestSARIF_LevelMapping(t *testing.T) { + cases := map[string]string{ + "high": "error", + "medium": "warning", + "low": "note", + "unknown": "warning", + } + for conf, wantLevel := range cases { + findings := []engine.Finding{{ProviderName: "p", KeyMasked: "k", Confidence: conf, Source: "f", LineNumber: 1}} + doc := decodeSARIF(t, findings, Options{}) + if got := doc.Runs[0].Results[0].Level; got != wantLevel { + t.Errorf("confidence %q: got level %q want %q", conf, got, wantLevel) + } + } +} + +func TestSARIF_LineFloor(t *testing.T) { + findings := []engine.Finding{ + {ProviderName: "p", KeyMasked: "k", Confidence: "high", Source: "f", LineNumber: 0}, + {ProviderName: "p", KeyMasked: "k", Confidence: "high", Source: "f", LineNumber: -5}, + {ProviderName: "p", KeyMasked: "k", Confidence: "high", Source: "f", LineNumber: 42}, + } + doc := decodeSARIF(t, findings, Options{}) + results := doc.Runs[0].Results + if results[0].Locations[0].PhysicalLocation.Region.StartLine != 1 { + t.Errorf("zero line should floor to 1, got %d", results[0].Locations[0].PhysicalLocation.Region.StartLine) + } + if results[1].Locations[0].PhysicalLocation.Region.StartLine != 1 { + t.Errorf("negative line should floor to 1, got %d", results[1].Locations[0].PhysicalLocation.Region.StartLine) + } + if results[2].Locations[0].PhysicalLocation.Region.StartLine != 42 { + t.Errorf("line 42 should pass through, got %d", results[2].Locations[0].PhysicalLocation.Region.StartLine) + } + if results[0].Locations[0].PhysicalLocation.ArtifactLocation.URI != "f" { + t.Errorf("artifactLocation.uri not preserved") + } +} + +func TestSARIF_Masking(t *testing.T) { + findings := []engine.Finding{{ + ProviderName: "openai", + KeyValue: "sk-SECRETVALUE1234", + KeyMasked: "sk-SECRE...1234", + Confidence: "high", + Source: "a.go", + LineNumber: 1, + }} + + // Default: masked. + doc := decodeSARIF(t, findings, Options{}) + msg := doc.Runs[0].Results[0].Message.Text + if !strings.Contains(msg, "sk-SECRE...1234") { + t.Errorf("masked message should contain KeyMasked, got %q", msg) + } + if strings.Contains(msg, "SECRETVALUE1234") { + t.Errorf("masked message leaked full key: %q", msg) + } + + // Unmasked. + doc = decodeSARIF(t, findings, Options{Unmask: true}) + msg = doc.Runs[0].Results[0].Message.Text + if !strings.Contains(msg, "sk-SECRETVALUE1234") { + t.Errorf("unmasked message should contain KeyValue, got %q", msg) + } +} + +func TestSARIF_ToolVersionFallback(t *testing.T) { + doc := decodeSARIF(t, nil, Options{}) + d := doc.Runs[0].Tool.Driver + if d.Name != "keyhunter" { + t.Errorf("default tool name: got %q want keyhunter", d.Name) + } + if d.Version != "dev" { + t.Errorf("default tool version: got %q want dev", d.Version) + } + + doc = decodeSARIF(t, nil, Options{ToolName: "custom", ToolVersion: "1.2.3"}) + d = doc.Runs[0].Tool.Driver + if d.Name != "custom" || d.Version != "1.2.3" { + t.Errorf("explicit name/version not honored: %+v", d) + } +} + +func TestSARIF_RegisteredInRegistry(t *testing.T) { + f, err := Get("sarif") + if err != nil { + t.Fatalf("sarif formatter not registered: %v", err) + } + if _, ok := f.(SARIFFormatter); !ok { + t.Errorf("registered formatter is not SARIFFormatter: %T", f) + } +}