9.6 KiB
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 |
|
|
true |
|
|
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 }
}
}]
}]
}]
}
```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>