Files
keyhunter/pkg/output/sarif_test.go
2026-04-05 23:30:38 +03:00

167 lines
5.4 KiB
Go

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