Files

9.6 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-output-reporting 03 execute 1
06-01
pkg/output/sarif.go
pkg/output/sarif_test.go
true
OUT-03
truths artifacts key_links
scan results can be rendered as SARIF 2.1.0 JSON suitable for GitHub Security upload
one SARIF rule is emitted per distinct provider observed in the findings
each result maps confidence to SARIF level (high=error, medium=warning, low=note)
each result has a physicalLocation with artifactLocation.uri and region.startLine
path provides contains
pkg/output/sarif.go SARIFFormatter + SARIF 2.1.0 structs SARIFFormatter
from to via pattern
pkg/output/sarif.go pkg/output/formatter.go init() Register("sarif", SARIFFormatter{}) Register("sarif"
from to via pattern
pkg/output/sarif.go SARIF 2.1.0 schema $schema + version fields 2.1.0
Implement a SARIF 2.1.0 formatter using hand-rolled structs (CLAUDE.md constraint: no SARIF library). Emits a schema-valid report that GitHub's code scanning accepts on upload.

Purpose: CI/CD integration (CICD-02 downstream in Phase 7 depends on this). Addresses OUT-03. Output: pkg/output/sarif.go, pkg/output/sarif_test.go.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/06-output-reporting/06-CONTEXT.md @.planning/phases/06-output-reporting/06-01-PLAN.md @pkg/engine/finding.go From Plan 06-01: ```go type Formatter interface { Format(findings []engine.Finding, w io.Writer, opts Options) error } type Options struct { Unmask bool ToolName string // "keyhunter" ToolVersion string // e.g. "0.6.0" } ```

SARIF 2.1.0 reference minimal shape:

{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [{
    "tool": { "driver": { "name": "...", "version": "...", "rules": [{"id":"...","name":"...","shortDescription":{"text":"..."}}] } },
    "results": [{
      "ruleId": "...",
      "level": "error|warning|note",
      "message": { "text": "..." },
      "locations": [{
        "physicalLocation": {
          "artifactLocation": { "uri": "..." },
          "region": { "startLine": 1 }
        }
      }]
    }]
  }]
}
Task 1: SARIF 2.1.0 struct definitions + SARIFFormatter pkg/output/sarif.go, pkg/output/sarif_test.go - pkg/output/formatter.go - pkg/output/json.go (for consistent encoding style) - pkg/engine/finding.go - SARIFFormatter implements Formatter. - Output JSON contains top-level $schema="https://json.schemastore.org/sarif-2.1.0.json" and version="2.1.0". - runs[0].tool.driver.name = opts.ToolName (fallback "keyhunter"), version = opts.ToolVersion (fallback "dev"). - rules are deduped by provider name; rule.id == provider name; rule.name == provider name; rule.shortDescription.text = "Leaked API key". - results: one per finding. ruleId = providerName. level: high->"error", medium->"warning", low->"note", default "warning". - message.text: "Detected key (): " where key is masked unless opts.Unmask. - locations[0].physicalLocation.artifactLocation.uri = f.Source (unchanged path). - locations[0].physicalLocation.region.startLine = max(1, f.LineNumber) (SARIF requires >= 1). - Empty findings: still emit a valid SARIF doc with empty rules and empty results. - Tests: * TestSARIF_Empty: parse output, assert version=="2.1.0", len(runs)==1, len(results)==0, len(rules)==0. * TestSARIF_DedupRules: two findings same provider -> len(rules)==1. * TestSARIF_LevelMapping: high/medium/low -> error/warning/note. * TestSARIF_LineFloor: f.LineNumber=0 -> region.startLine==1. * TestSARIF_Masking: opts.Unmask=false -> message.text contains KeyMasked, not KeyValue. * TestSARIF_ToolVersionFallback: empty Options uses "keyhunter"/"dev". Create pkg/output/sarif.go:
```go
package output

import (
    "encoding/json"
    "fmt"
    "io"

    "github.com/salvacybersec/keyhunter/pkg/engine"
)

func init() {
    Register("sarif", SARIFFormatter{})
}

// SARIFFormatter emits SARIF 2.1.0 JSON suitable for CI uploads.
type SARIFFormatter struct{}

type sarifDoc struct {
    Schema  string     `json:"$schema"`
    Version string     `json:"version"`
    Runs    []sarifRun `json:"runs"`
}

type sarifRun struct {
    Tool    sarifTool      `json:"tool"`
    Results []sarifResult  `json:"results"`
}

type sarifTool struct {
    Driver sarifDriver `json:"driver"`
}

type sarifDriver struct {
    Name    string      `json:"name"`
    Version string      `json:"version"`
    Rules   []sarifRule `json:"rules"`
}

type sarifRule struct {
    ID               string         `json:"id"`
    Name             string         `json:"name"`
    ShortDescription sarifText      `json:"shortDescription"`
}

type sarifText struct {
    Text string `json:"text"`
}

type sarifResult struct {
    RuleID    string          `json:"ruleId"`
    Level     string          `json:"level"`
    Message   sarifText       `json:"message"`
    Locations []sarifLocation `json:"locations"`
}

type sarifLocation struct {
    PhysicalLocation sarifPhysicalLocation `json:"physicalLocation"`
}

type sarifPhysicalLocation struct {
    ArtifactLocation sarifArtifactLocation `json:"artifactLocation"`
    Region           sarifRegion           `json:"region"`
}

type sarifArtifactLocation struct {
    URI string `json:"uri"`
}

type sarifRegion struct {
    StartLine int `json:"startLine"`
}

func (SARIFFormatter) Format(findings []engine.Finding, w io.Writer, opts Options) error {
    toolName := opts.ToolName
    if toolName == "" {
        toolName = "keyhunter"
    }
    toolVersion := opts.ToolVersion
    if toolVersion == "" {
        toolVersion = "dev"
    }

    // Dedup rules by provider, preserving first-seen order.
    seen := map[string]bool{}
    rules := make([]sarifRule, 0)
    for _, f := range findings {
        if seen[f.ProviderName] {
            continue
        }
        seen[f.ProviderName] = true
        rules = append(rules, sarifRule{
            ID:               f.ProviderName,
            Name:             f.ProviderName,
            ShortDescription: sarifText{Text: fmt.Sprintf("Leaked %s API key", f.ProviderName)},
        })
    }

    results := make([]sarifResult, 0, len(findings))
    for _, f := range findings {
        key := f.KeyMasked
        if opts.Unmask {
            key = f.KeyValue
        }
        startLine := f.LineNumber
        if startLine < 1 {
            startLine = 1
        }
        results = append(results, sarifResult{
            RuleID:  f.ProviderName,
            Level:   sarifLevel(f.Confidence),
            Message: sarifText{Text: fmt.Sprintf("Detected %s key (%s): %s", f.ProviderName, f.Confidence, key)},
            Locations: []sarifLocation{{
                PhysicalLocation: sarifPhysicalLocation{
                    ArtifactLocation: sarifArtifactLocation{URI: f.Source},
                    Region:           sarifRegion{StartLine: startLine},
                },
            }},
        })
    }

    doc := sarifDoc{
        Schema:  "https://json.schemastore.org/sarif-2.1.0.json",
        Version: "2.1.0",
        Runs: []sarifRun{{
            Tool: sarifTool{Driver: sarifDriver{
                Name:    toolName,
                Version: toolVersion,
                Rules:   rules,
            }},
            Results: results,
        }},
    }

    enc := json.NewEncoder(w)
    enc.SetIndent("", "  ")
    return enc.Encode(doc)
}

func sarifLevel(confidence string) string {
    switch confidence {
    case "high":
        return "error"
    case "medium":
        return "warning"
    case "low":
        return "note"
    default:
        return "warning"
    }
}
```

Create pkg/output/sarif_test.go implementing all six test cases listed in <behavior>. Use json.Unmarshal into sarifDoc to assert structural fields.
cd /home/salva/Documents/apikey && go test ./pkg/output/... -run "TestSARIF" -count=1 - All TestSARIF_* tests pass - `grep -q "Register(\"sarif\"" pkg/output/sarif.go` - `grep -q '"2.1.0"' pkg/output/sarif.go` - `grep -q "sarifLevel" pkg/output/sarif.go` and covers high/medium/low - `go build ./...` succeeds - `go test ./pkg/output/... -count=1` all green - All four formatters (table, json, csv, sarif) are registered

<success_criteria>

  • SARIFFormatter produces 2.1.0-compliant documents
  • Rules deduped per provider
  • Confidence -> level mapping is deterministic
  • Ready for CI/CD integration in Phase 7 </success_criteria>
After completion, create `.planning/phases/06-output-reporting/06-03-SUMMARY.md`.