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) } }