feat(06-06): wire scan --output to formatter registry and exit-code contract
- Replace inline jsonFinding switch with output.Get() dispatch - Add renderScanOutput helper used by RunE and tests - Introduce version var + versionString() for SARIF tool metadata - Update --output help to list table, json, sarif, csv - Change root Execute to os.Exit(2) on RunE errors per OUT-06 (exit 0=clean, 1=findings, 2=tool error)
This commit is contained in:
66
cmd/scan.go
66
cmd/scan.go
@@ -3,11 +3,12 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/salvacybersec/keyhunter/pkg/config"
|
||||
@@ -172,36 +173,13 @@ var scanCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
// Output
|
||||
switch flagOutput {
|
||||
case "json":
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
type jsonFinding struct {
|
||||
Provider string `json:"provider"`
|
||||
KeyMasked string `json:"key_masked"`
|
||||
Confidence string `json:"confidence"`
|
||||
Source string `json:"source"`
|
||||
Line int `json:"line"`
|
||||
}
|
||||
out := make([]jsonFinding, 0, len(findings))
|
||||
for _, f := range findings {
|
||||
out = append(out, jsonFinding{
|
||||
Provider: f.ProviderName,
|
||||
KeyMasked: f.KeyMasked,
|
||||
Confidence: f.Confidence,
|
||||
Source: f.Source,
|
||||
Line: f.LineNumber,
|
||||
})
|
||||
}
|
||||
if err := enc.Encode(out); err != nil {
|
||||
return fmt.Errorf("encoding JSON output: %w", err)
|
||||
}
|
||||
default:
|
||||
output.PrintFindings(findings, flagUnmask)
|
||||
// Output via the formatter registry (OUT-01..04).
|
||||
if err := renderScanOutput(findings, flagOutput, flagUnmask, os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Exit code semantics (CLI-05 / OUT-06): 0=clean, 1=found, 2=error
|
||||
// OUT-06 exit codes: 0=clean, 1=findings, 2=error (errors returned via
|
||||
// RunE -> root.Execute -> os.Exit(2)).
|
||||
if len(findings) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -209,6 +187,34 @@ var scanCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// renderScanOutput dispatches findings through the formatter registry. It is
|
||||
// the single entry point used by scan RunE (and tests) to format output.
|
||||
//
|
||||
// Returns an error wrapping output.ErrUnknownFormat when name does not match a
|
||||
// registered formatter; the error message includes the valid format list.
|
||||
func renderScanOutput(findings []engine.Finding, name string, unmask bool, w io.Writer) error {
|
||||
formatter, err := output.Get(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w (valid: %s)", err, strings.Join(output.Names(), ", "))
|
||||
}
|
||||
if err := formatter.Format(findings, w, output.Options{
|
||||
Unmask: unmask,
|
||||
ToolName: "keyhunter",
|
||||
ToolVersion: versionString(),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("rendering %s output: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// version is the compiled tool version. Override with:
|
||||
//
|
||||
// go build -ldflags "-X github.com/salvacybersec/keyhunter/cmd.version=1.2.3"
|
||||
var version = "dev"
|
||||
|
||||
// versionString returns the compiled tool version (used by SARIF output).
|
||||
func versionString() string { return version }
|
||||
|
||||
// sourceFlags captures the CLI inputs that control source selection.
|
||||
// Extracted into a struct so selectSource is straightforward to unit test.
|
||||
type sourceFlags struct {
|
||||
@@ -324,7 +330,7 @@ func init() {
|
||||
scanCmd.Flags().IntVar(&flagWorkers, "workers", 0, "number of worker goroutines (default: CPU*8)")
|
||||
scanCmd.Flags().BoolVar(&flagVerify, "verify", false, "actively verify found keys (opt-in, Phase 5)")
|
||||
scanCmd.Flags().BoolVar(&flagUnmask, "unmask", false, "show full key values (default: masked)")
|
||||
scanCmd.Flags().StringVar(&flagOutput, "output", "table", "output format: table, json (full JSON output in Phase 6)")
|
||||
scanCmd.Flags().StringVar(&flagOutput, "output", "table", "output format: table, json, sarif, csv")
|
||||
scanCmd.Flags().StringSliceVar(&flagExclude, "exclude", nil, "extra glob patterns to exclude (e.g. *.min.js)")
|
||||
|
||||
// Phase 4 source-selection flags.
|
||||
|
||||
Reference in New Issue
Block a user