package main import ( "fmt" "os" "strings" ) // ScanContext holds all resolved scan state, built once before any check runs. // It is read-only to checks. type ScanContext struct { repoRoot string backendDir string orchestratorOnly bool includeCoreTargets bool allAny bool // --all-any: scan every source for `any`, not just the unstaged diff verbose bool // --verbose/-v: print every check, not just warn/fail/err backendTargets []backendScanTarget pluginTargets []pluginScanTarget // filtered resolvedPluginTargets []pluginScanTarget // pre-filter frontendTargets []frontendScanTarget unresolvedPluginRoots []string pluginPageDirs []string } // status is a check's verdict. Ordered by severity so it can only escalate. // statusUnset is the default before any verdict; the first call to any // OK/Skip/Warn/Fail/Fatal sets it, and later more-severe calls win. A check // that declares both Skip (some targets) and OK (others) resolves to OK. type status int const ( statusUnset status = iota // no verdict declared yet (default) statusSkip statusOK statusWarn statusFail statusErr // most severe ) func (s status) label() string { switch s { case statusSkip: return "SKIP" case statusWarn: return "WARN" case statusFail: return "FAIL" case statusErr: return "ERR " default: // statusUnset and statusOK both render as a pass return "OK " } } // checkReport accumulates one check's verdict and detail lines. type checkReport struct { seq int id string title string status status summary string // short note shown after the title (verbose, or for OK/SKIP) findings []string // detail lines, printed indented under the header } // Reporter collects every check's report and renders them once at the end. // It also tracks the process exit code. type Reporter struct { exitCode int verbose bool current *checkReport reports []*checkReport } // begin starts a fresh report for the next check. Default status is OK; checks // escalate as needed. func (r *Reporter) begin(seq int, id, title string) { r.current = &checkReport{seq: seq, id: id, title: title, status: statusUnset} } // end files the current report. func (r *Reporter) end() { if r.current != nil { r.reports = append(r.reports, r.current) r.current = nil } } // setStatus escalates the current check to the most severe verdict seen. func (r *Reporter) setStatus(s status, summary string) { if r.current == nil { return } if s >= r.current.status { r.current.status = s if summary != "" { r.current.summary = summary } } else if r.current.summary == "" && summary != "" { r.current.summary = summary } } // OK marks the current check as passing. func (r *Reporter) OK(format string, a ...any) { r.setStatus(statusOK, fmt.Sprintf(format, a...)) } // Skip marks the current check as skipped (nothing to scan). func (r *Reporter) Skip(format string, a ...any) { r.setStatus(statusSkip, fmt.Sprintf(format, a...)) } // Warn marks the current check as a warning. Does not affect the exit code. func (r *Reporter) Warn(format string, a ...any) { r.setStatus(statusWarn, fmt.Sprintf(format, a...)) } // Fail marks the current check as failed and sets exit code 1. func (r *Reporter) Fail(format string, a ...any) { r.setStatus(statusFail, fmt.Sprintf(format, a...)) r.exitCode = 1 } // Fatal reports an unrecoverable error for the current check, renders what has // been collected so far, and exits with code 2 — matching the pre-existing // hard-error behavior of checks that could not run. func (r *Reporter) Fatal(format string, a ...any) { r.setStatus(statusErr, fmt.Sprintf(format, a...)) r.exitCode = 2 r.end() r.render() os.Exit(2) } // Finding appends one detail line to the current check. func (r *Reporter) Finding(line string) { if r.current != nil { r.current.findings = append(r.current.findings, line) } } // Findingf is Finding with formatting. func (r *Reporter) Findingf(format string, a ...any) { r.Finding(fmt.Sprintf(format, a...)) } // render prints the collected reports. In concise mode (default) only // warn/fail/err checks print; --verbose prints every check. func (r *Reporter) render() { printed := false for _, c := range r.reports { show := r.verbose || c.status == statusWarn || c.status == statusFail || c.status == statusErr if !show { continue } // For warn/fail/err the summary is the specific, descriptive headline // (the check title would just duplicate it); id still identifies the // check. OK/SKIP (verbose only) show the title plus any summary note. var headline string if c.status == statusWarn || c.status == statusFail || c.status == statusErr { headline = c.summary if headline == "" { headline = c.title } } else { headline = c.title if c.summary != "" { headline += " — " + c.summary } } fmt.Printf("%s %s %s\n", c.status.label(), c.id, headline) for _, f := range c.findings { fmt.Printf(" %s\n", f) } printed = true } if printed { fmt.Println() } fmt.Println(r.summaryLine()) } // summaryLine builds the trailing one-line tally. func (r *Reporter) summaryLine() string { var ok, skip, warn, fail, errc int for _, c := range r.reports { switch c.status { case statusOK, statusUnset: ok++ case statusSkip: skip++ case statusWarn: warn++ case statusFail: fail++ case statusErr: errc++ } } parts := []string{} add := func(n int, label string) { if n > 0 { parts = append(parts, fmt.Sprintf("%d %s", n, label)) } } add(ok, "ok") add(skip, "skip") add(warn, "warn") add(fail, "fail") add(errc, "err") verdict := "OK" if r.exitCode != 0 { verdict = "FAIL" } return fmt.Sprintf("%d checks: %s -> %s", len(r.reports), strings.Join(parts, " "), verdict) } // Check is one registered safety check. Run declares its verdict via the // Reporter (rep.OK/Skip/Warn/Fail/Fatal + rep.Finding); the Reporter renders. type Check struct { Seq int ID string Title string Run func(ctx *ScanContext, rep *Reporter) } var registry []Check func register(c Check) { registry = append(registry, c) }