/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package report import ( "encoding/json" "fmt" "sort" ) // sarif format/version constants pinned to the 2.1.0 schema so the output is // ingestable by github code scanning and other sarif consumers. const ( sarifVersion = "2.1.0" sarifSchema = "https://json.schemastore.org/sarif-2.1.0.json" toolName = "sif" ) // sarifLog is the minimal valid 2.1.0 shape: one run from one tool. type sarifLog 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"` Rules []sarifRule `json:"rules"` } type sarifRule struct { ID string `json:"id"` } type sarifResult struct { RuleID string `json:"ruleId"` Level string `json:"level"` Message sarifMessage `json:"message"` Locations []sarifLocation `json:"locations"` } type sarifMessage struct { Text string `json:"text"` } type sarifLocation struct { PhysicalLocation sarifPhysicalLocation `json:"physicalLocation"` } type sarifPhysicalLocation struct { ArtifactLocation sarifArtifactLocation `json:"artifactLocation"` } type sarifArtifactLocation struct { URI string `json:"uri"` } // sarif levels this writer emits. the 2.1.0 spec also defines "none", but every // sif result is at least informational, so the floor here is "note". const ( sarifError = "error" sarifWarning = "warning" sarifNote = "note" ) // sarifLevelFor maps a normalized finding severity onto a sarif level. an empty // or unrecognized severity falls back to "warning", the neutral middle ground // that preserves the old behavior for results that carry no severity. func sarifLevelFor(severity string) string { switch severity { case "critical", "high": return sarifError case "low", "info": return sarifNote case "medium": return sarifWarning default: return sarifWarning } } // SARIF serializes results to a minimal valid sarif 2.1.0 log. Each module // result becomes one sarif result tagged with its module id (the rule) and the // target uri, with the raw module data inlined into the message for context. func SARIF(results []Result) ([]byte, error) { sarifResults := make([]sarifResult, 0, len(results)) ruleSet := make(map[string]struct{}, len(results)) for i := 0; i < len(results); i++ { res := results[i] ruleSet[res.Module] = struct{}{} sarifResults = append(sarifResults, sarifResult{ RuleID: res.Module, Level: sarifLevelFor(res.Severity), Message: sarifMessage{Text: messageFor(res)}, Locations: []sarifLocation{{ PhysicalLocation: sarifPhysicalLocation{ ArtifactLocation: sarifArtifactLocation{URI: res.Target}, }, }}, }) } // rules must list each id exactly once; build it from the set so duplicate // modules across targets don't duplicate the rule. the set itself ranges // in random map order, so sort the ids first: otherwise driver.rules // would come out in a different order on every run, making the sarif // output byte-unstable across identical scans. ruleIDs := make([]string, 0, len(ruleSet)) for id := range ruleSet { ruleIDs = append(ruleIDs, id) } sort.Strings(ruleIDs) rules := make([]sarifRule, 0, len(ruleIDs)) for _, id := range ruleIDs { rules = append(rules, sarifRule{ID: id}) } doc := sarifLog{ Schema: sarifSchema, Version: sarifVersion, Runs: []sarifRun{{ Tool: sarifTool{Driver: sarifDriver{Name: toolName, Rules: rules}}, Results: sarifResults, }}, } out, err := json.MarshalIndent(doc, "", " ") if err != nil { return nil, fmt.Errorf("marshal sarif: %w", err) } return out, nil } // messageFor builds a human-readable result message: the module id plus the raw // finding json so a sarif viewer shows what was actually found. func messageFor(res Result) string { if len(res.Data) == 0 { return fmt.Sprintf("%s finding on %s", res.Module, res.Target) } return fmt.Sprintf("%s finding on %s: %s", res.Module, res.Target, string(res.Data)) }