Skip to content

Frontend / Terminal Experience

Version: v0.8.3 · Revision: 2026-07-04

Quorum has no graphical frontend (web, desktop or mobile). It is, by design, a CLI/Docker tool oriented toward CI/CD: "configure via flags, gate via exit code. No panel, no daemon" (cmd/quorum/root.go). Quorum's "user interface" is, therefore, the terminal experience: progress logs on stderr (as [quorum] … text or, since v0.7.4, as structured JSON via --log-format json), a summary table at the end, deterministic exit codes for gating, and the SARIF artifact — which, when consumed by GitHub Code Scanning, becomes the visual surface closest to a GUI. This document describes that UX as it exists in the code (as-is), treats what does not exist as N/A with justification, and clearly separates any future proposals.


1. Verdict: GUI / Web — N/A

Typical template item Status Technical justification (as-is)
SPA / web application (React, Vue, etc.) N/A There is no UI code in the repository. The binary is a Cobra CLI (cmd/quorum). main() merely runs the root command and exits.
HTTP server / REST API serving a UI N/A There is no server; there are no application net/http handlers. Quorum is stateless and one-shot: it runs, emits a report, exits.
Panel/dashboard, daemon, long-running N/A Explicit principle: "No panel, no daemon" (root.go). Each invocation is an ephemeral process.
Authentication / accounts / user sessions N/A There is no multi-user support nor session persistence. The only persistence is the local alias cache (~/.cache/quorum/aliases.json, created with permission 0600).
Visual components, CSS, web design system N/A Rendering is plain text on stderr/stdout. No ANSI/colors in the Go code.
Responsiveness / breakpoints / mobile N/A The viewport concept does not apply to a non-interactive TUI. See §8 Responsiveness.

Why this is a decision, not a gap. Quorum's target is the CI/CD runner and the developer's terminal. A web panel would require a server, state, authentication and attack surface — all contrary to the threat model of a security tool that runs inside the pipeline. Rich "visualization" is delegated to systems that already exist (GitHub Code Scanning, any SARIF viewer), via the standardized SARIF artifact.

The visual surface closest to a GUI is covered in §11 GitHub Code Scanning.

1.1 Available commands (CLI surface)

The CLI exposes exactly three subcommands, all non-interactive:

Command Function
quorum scan <target> Runs the scanner pool, correlates by consensus and emits the report (the focus of this document).
quorum list-scanners Lists the registered adapters and the target types each one supports (image/repo/k8s).
quorum advise-index Embeds the OWASP RAG corpus to enable semantic retrieval, preserving the digest pin (Phase 2 of the advisory layer — opt-in, off by default).

list-scanners prints one line per adapter to stdout (%-12s %v), useful for discovering which of the 12 scanners are available before narrowing the pool with --scanners.


2. Anatomy of the terminal experience

Quorum's terminal UX has three well-separated channels, which is deliberate to allow piping the report without polluting it with logs:

flowchart LR
    subgraph CLI["quorum scan <target>"]
        A["Progress / diagnostics<br/>[quorum] … text or JSON (--log-format)"] --> E2["stderr"]
        B["Summary table<br/>── quorum summary ── (always text)"] --> E2
        C["SARIF/JSON/XML report"] --> O1["stdout (or --output file)"]
        D["Exit code 0 / 1 / 2"] --> X["$?"]
    end
    E2 --> H["Human / runner logs"]
    O1 --> P["Pipe / upload (e.g. SARIF → Code Scanning)"]
    X --> G["Pipeline gate"]
Channel Content When it appears Controlled by
stdout Report (SARIF/JSON/XML) Whenever --output/-o is not given --format, --output
stderr Progress logs ([quorum] … or JSON) + summary table During the scan and at the end --quiet/-q, --log-format
exit code 0 ok, 1 gate fired, 2 usage/runtime error On exit --fail-on

Separation guarantee. Because the report goes to stdout and all the noise (progress logs and the summary table) goes to stderr, quorum scan img -f json > report.json produces clean JSON even without --quiet. The report channel is never contaminated — neither by the text logs, nor by the JSON logs of --log-format json (which also go to stderr). This also holds under --advice: advisory progress and any AI attachments are emitted through the same channels, and without --advice the output is byte-identical.


3. Progress logs (stderr)

Progress logs are emitted by the logf closure in runScan. Since v0.7.4 it has two rendering modes, chosen by --log-format (default text), and it remains a no-op under --quiet:

logf := func(format string, args ...any) {
    if f.quiet {
        return
    }
    msg := fmt.Sprintf(format, args...)
    if f.logFormat == "json" {
        b, _ := json.Marshal(struct {
            TS    string `json:"ts"`
            Level string `json:"level"`
            Msg   string `json:"msg"`
        }{time.Now().UTC().Format(time.RFC3339), "info", msg})
        fmt.Fprintln(os.Stderr, string(b))
        return
    }
    fmt.Fprintf(os.Stderr, "[quorum] %s\n", msg)
}

This same logf is injected into the orchestrator via orchestrator.Options.Logf, so the whole pipeline speaks through the same channel, with the same prefix (in text) or the same envelope (in json).

3.1 Log format (--log-format text|json)

--log-format is validated right at the start of runScan — a value outside {text, json} is fatal (invalid --log-format "x" (want text|json), exit 2).

Mode Look of each line on stderr Typical use
text (default) [quorum] target=… type=image crosswalk=… offline=false Human at the terminal / direct reading of the runner log
json {"ts":"2026-07-04T12:00:00Z","level":"info","msg":"target=… type=image …"} Ingestion into log collectors (Loki, ELK, CloudWatch, Datadog)

JSON envelope (one line per event, newline-delimited / JSONL):

Field Type Content
ts string UTC timestamp in RFC 3339 (time.Now().UTC())
level string Always "info" (progress does not use distinct levels — see note)
msg string The same message as text mode, without the [quorum] prefix

Scope of --log-format. It affects only the progress events of the logf closure. It does not change: (a) the summary table (printSummary always prints readable text — see §4); (b) the report on stdout (governed by --format); nor (c) the fatal-error line from main() (quorum: <err>). For a 100% machine-structured output, combine --log-format json with --quiet? No--quiet turns logs off entirely; pick one or the other as needed (structured logs or total silence).

level is always info. In the current code all progress events — including warning: unknown scanner …, skip … and gate: … — are emitted by the same logf and serialized with "level":"info". The event's semantic severity lives in the msg text, not in the level field. Consumers that want to filter by severity should match on the msg content.

3.2 Progress message catalog

The msg text is identical in both formats; only the envelope changes.

Source Message (format) Meaning
runScan (preamble) target=… type=… crosswalk=N rules (dir) offline=bool Summary of the resolved configuration before the fan-out
orchestrator.Run warning: unknown scanner "x" ignored (known: …) Name passed in --scanners does not match a registered adapter
runOne (skip) skip <name>: does not support target <type> Adapter does not support the target type → status skipped
runOne (slow probe) skip <name>: version probe timed out after 60s (slow start / low memory?) Version probe exceeded ProbeTime → status unavailable
runOne (OOM) skip <name>: version probe killed (likely OOM — increase container memory) signal: killed on the probe → likely OOM → status unavailable
runOne (missing) skip <name>: not installed/available Binary not installed/found → status unavailable
runOne (execution) run <name> (<ver>) ... Scanner started
runOne (success) done <name>: N findings in <dur> Scanner finished with N raw findings → status ran
runOne (failure) fail <name>: <err> Scanner failed (status error) or timed out (timeout)
runScan (filter) filtered: N suppressed by baseline (M entries), K below min-severity <sev> Post-processing by .quorumignore / --min-severity
runScan (advice/enrich) advice: knowledge=N entries (dir) → K findings enriched Only under --advice: deterministic remediation templates attached (see §4.2)
runScan (advice/RAG) advice(rag): corpus=N chunks (dir, mode) → K findings gained OWASP references Only under --advice: OWASP references retrieved from the pinned corpus
runScan (advice/AI) advice(ai): provider=local\|REMOTE model=… endpoint=… … Only under --advice with --advice-provider=local\|remote: opt-in LLM recommendation
runScan (metrics) metrics written to <path> --metrics wrote the Prometheus textfile (see §6.1)
runScan (gate) gate: found <sev> finding >= --fail-on <thr> → exit 1 Gating fired; process will exit with code 1

Suppressions always log. When the baseline or --min-severity discard findings, the number is recorded on stderr. This honors the transparency principle: nothing is silently removed from the report.

3.3 Typical sequence (success, without --quiet)

sequenceDiagram
    participant U as User/CI
    participant Q as quorum scan
    participant O as orchestrator
    participant S as scanners (parallel)
    U->>Q: quorum scan img:tag --fail-on high
    Q-->>U: [quorum] target=… type=image crosswalk=… offline=false
    Q->>O: Run(target, Options{Logf})
    O-->>U: [quorum] run  trivy (0.5x) ...
    O->>S: fan-out (goroutines)
    S-->>O: findings
    O-->>U: [quorum] done trivy: 42 findings in 3.1s
    O-->>Q: Result{Runs, Merged}
    Q-->>U: [quorum] filtered: 3 suppressed by baseline …
    Q-->>U: (stdout) <SARIF>
    Q-->>U: ── quorum summary ── (stderr)
    Q-->>U: exit 0 | 1 | 2

With --log-format json, each [quorum] … line above becomes, instead, a JSON object {"ts":…,"level":"info","msg":"…"} in the same order and on the same channel (stderr).


4. Summary table

At the end of each scan, printSummary writes a human-readable block to stderr. It is suppressed by --quiet (returns immediately when quiet == true) and is always text, regardless of --log-format (--log-format json does not structure the summary). Actual layout produced by the code, with the full pool of 12 scanners:

── quorum summary ───────────────────────────
  trivy      ran           42 findings
  grype      ran           38 findings
  checkov    unavailable    0 findings  (version probe killed — likely out…)
  kics       ran           12 findings
  dockle     ran            5 findings
  kubescape  timeout        0 findings  (context deadline exceeded)
  polaris    ran            9 findings
  kube-score ran            7 findings
  terrascan  ran           14 findings
  tfsec      ran           11 findings
  regula     ran            6 findings
  conftest   skipped        0 findings
  ----------------------------------------
  57 findings after consensus  (31 multi-detected)
  CRIT 4  HIGH 12  MED 28  LOW 11  INFO 2
  elapsed 7.412s
  note: 0 findings is not proof of safety — see scanner statuses above.

4.1 Structure

Block Content Data source
Per-scanner line name, status, count of raw findings, and (if any) error truncated to 60 chars res.Runs ([]ScannerRun)
Inline error (…) with truncate(err, 60) when ScannerRun.Error != "" ScannerRun.Error
Post-consensus total N findings after consensus (M multi-detected) len(res.Merged) + DetectionCount>1
Distribution CRIT … HIGH … MED … LOW … INFO … count per m.Severity
Time elapsed <dur> rounded to milliseconds res.Duration
Caution note note: 0 findings is not proof of safety … literal (DESIGN §14)

raw findings vs after consensus. The per-scanner column shows the number of findings that scanner emitted before correlation. The total line shows the findings after the consensus merge. That is why the sum of the columns is normally larger than the total — this is expected and desirable (several scanners detecting the same CVE or the same IaC control collapse into one finding with DetectionCount > 1). With 12 scanners and the crosswalk active (SCA and MISCONFIG/K8S_POSTURE), multi-scanner consensus is the rule, not the exception.

4.2 The advisory layer (--advice)

Since v0.8.3, --advice (opt-in, off by default) attaches an advisory layer to the report. It is presentation-only: it never touches correlationKey, fingerprint, confidence, aggregated severity, or the --fail-on gate. Without --advice, the output is byte-identical to earlier versions, so the terminal UX described above is unchanged for the default path. When enabled, the only terminal-visible effect is the extra advice* progress lines on stderr (see §3.2) and advisory fields carried inside the report artifact.

Phase / flag What it adds (advisory only) Model / egress
Phase 0 — --advice Curated remediation templates + OWASP references, matched by canonicalControl/ruleId/category/type. Deterministic, no model.
Phase 2 — --advice Retrieval from a versioned, digest-pinned OWASP corpus; semantic when embedded via quorum advise-index. Deterministic; lexical by default.
Phase 1 — --advice-provider=local On-host OpenAI-compatible LLM recommendation; --fix=suggest proposes a verify-the-fix patch (never auto-applied). Local endpoint (e.g. Ollama); nothing leaves host.
Phase 3 — --advice-provider=remote External API recommendation. Sends only the normalized finding, never source; refuses --fix. Egress: needs --advice-allow-egress + QUORUM_ADVICE_API_KEY; blocked by --offline.

Honest framing. The deterministic core has no AI. The AI parts (Phases 1 and 3) are strictly opt-in and off by default; if the model is unreachable the report ships without AI advice and the scan never fails. Every AI attachment is labeled "AI-generated, advisory only". See 13-ia.md and 21-proposta-ia.md for the full design.


5. Scanner states (status)

Each scanner ends in exactly one of five states, defined in orchestrator.ScannerRun.Status. They are the centerpiece of the UX's transparency — "0 vulns" must never look like "the scan did not run" (DESIGN §14).

stateDiagram-v2
    [*] --> Supports?
    Supports? --> skipped: does not support the target
    Supports? --> Probe
    Probe --> unavailable: probe failed\n(timeout 60s / OOM / not installed)
    Probe --> Run
    Run --> ran: finished OK
    Run --> timeout: exceeded --timeout
    Run --> error: execution failure
    ran --> [*]
    skipped --> [*]
    unavailable --> [*]
    timeout --> [*]
    error --> [*]
Status When it occurs Diagnostic to the user
ran Supports==true, probe OK, Run returned without error Findings count in the table
skipped Supports(target)==false "does not support target …" — not a failure, it just does not apply
unavailable Version() (probe) failed: timeout (60s), signal: killed (OOM), missing Message distinguishes slow start, OOM and missing binary, with a fix suggestion
timeout Run exceeded --timeout (default 5m) → context.DeadlineExceeded status timeout + inline error
error Run returned an error that is not a deadline status error + inline error (truncated to 60 chars in the summary)

60s version probe. The orchestrator runs a version probe before each scanner with ProbeTime = 60s (defaultProbeTime, generous on purpose, because Python tools like checkov have slow cold start and may be SIGKILLed on low-memory runners). It is this probe that distinguishes unavailable/timeout/OOM from a real run. With 12 scanners in the pool, this probe also prevents a single missing binary from bringing down the whole scan — the missing adapter becomes unavailable and the rest continues.


6. Exit codes and gating

Exit codes are the machine API of the UX — it is how the pipeline reads the result.

Code Meaning Origin in the code
0 OK — no finding reached --fail-on (or no --fail-on) runScan returns nil
1 Gate fired — some finding ≥ --fail-on os.Exit(1) in runScan after severity.AtLeast(worst, thr)
2 Usage or runtime error (invalid flag, bad target, etc.) main(): fmt.Fprintln(os.Stderr, "quorum:", err); os.Exit(2)

Gate decision flow:

flowchart TD
    A[scan finished] --> B{--fail-on set?}
    B -- no --> Z[exit 0]
    B -- yes --> C[worst = highest severity among Merged]
    C --> D{worst >= threshold?}
    D -- no --> Z
    D -- yes --> E["[quorum] gate: … → exit 1"]
    E --> F[exit 1]

Checklist for using gating in CI:

  • [ ] Set --fail-on with the severity threshold (critical|high|medium|low).
  • [ ] Consider --min-severity to remove noise below a floor (affects report and gating).
  • [ ] Treat exit 2 as a configuration/infra error (not as "findings were found").
  • [ ] Do not confuse exit 0 with "safe" — check the scanner statuses (unavailable/timeout mask coverage).
  • [ ] On low-RAM runners, if you see unavailable (OOM), increase the container memory or narrow --scanners.

Careful with exit 0. An exit 0 only guarantees that no finding reached the threshold; it does not guarantee that all scanners ran. An unavailable/timeout scanner may have left an entire class of findings out. The 0 findings is not proof of safety note exists precisely for that.

6.1 Optional telemetry (--metrics)

Orthogonal to the exit codes, the --metrics <file> flag writes metrics in the Prometheus text format (via writeMetricsFilereport.WriteMetrics), with permission 0644 (non-sensitive counts, meant for scraping by a node exporter textfile collector). The gate and the report do not depend on it; when writing, Quorum logs metrics written to <path>. If the write fails, the error is fatal (writing metrics: …, exit 2).

Advisory metrics. Under --advice, Quorum additionally exports quorum_advice_enriched{kind=remediation|references|recommendation}, quorum_advice_provider{provider} and quorum_advice_fix{stage=proposed|verified} (the verified/proposed ratio is the verify-the-fix rate). These appear only when the advisory layer is enabled and never influence gating. See 14-observabilidade.md.


7. Quiet mode (--quiet / -q)

--quiet turns off everything that goes to stderr, in both log formats:

  1. The progress logs (the logf closure returns at the start, be it text or json).
  2. The summary table (printSummary returns at the start if quiet).

What is not affected by --quiet:

  • The report on stdout/--output (still emitted normally).
  • The exit code (gating keeps working).
  • The --metrics file (still written, if requested).
  • The fatal-error line in main() (quorum: <err> with exit 2).
Scenario Without --quiet With --quiet
Progress (text or JSON) yes (stderr) no
Summary table yes (stderr) no
Report (stdout/-o) yes yes
--metrics file yes (if requested) yes (if requested)
Exit code / gate yes yes
Fatal usage/runtime error yes (exit 2) yes (exit 2)

CI recommendation. Use --quiet -o report.sarif --fail-on high. You still get the gate and the SARIF file to upload, without polluting the runner log. If, instead of silence, you want ingestible logs by a collector, swap --quiet for --log-format json (mutually exclusive in practice: one silences, the other structures).


8. Responsiveness — N/A

Concept Status Justification
Breakpoints N/A There is no web layout; nothing reflows by viewport width.
Fluid layout N/A The summary table uses fixed-width columns (%-10s %-12s %3d), designed for ≥ ~50 terminal columns.
Mobile/tablet N/A The target is the CI/developer terminal; there is no mobile client.

The only "width" consideration is the summary: errors are truncated to 60 characters (truncate(r.Error, 60)) so they do not overflow the terminal. There is no terminal-width detection (COLUMNS) in the code. Scanner names fit in the 10-character column — including the longest, kube-score (10 chars).


9. Accessibility

The UX is plain text, which already removes many barriers (compatible with screen readers and braille displays via the terminal). Current state (as-is):

Aspect State (as-is)
Color / ANSI No color. No ANSI escape code is emitted by the Go code. So there is no dependence on color to understand the output.
NO_COLOR There is no need to handle NO_COLOR: since there is already no color, the output is stable regardless of the variable.
Text without color Status (ran/unavailable/…) and severities (CRIT/HIGH/…) are textual labels, never color alone.
Quiet mode --quiet offers a minimal/deterministic output for those who want only the file + exit code.
Structured logs --log-format json offers a machine-readable stream (JSONL) on stderr, useful for assistive tools and collectors that prefer data over free text.
Unicode The header uses box-drawing (──) and ellipsis (). Requires a UTF-8 terminal; the informative content is ASCII.
Message language Runtime messages are in English (in the code); this documentation is the English default (a Brazilian-Portuguese version exists as 08-frontend.pt.md).

Accessibility checklist (continuous validation):

  • [ ] Ensure no information depends on color (already satisfied: no ANSI).
  • [ ] Keep textual labels for severity and status (do not replace with icons-only).
  • [ ] Keep the severity/counts content in ASCII for terminals without UTF-8.
  • [ ] Keep --quiet as a predictable path for automation and assistive tools.
  • [ ] Keep the JSON envelope of --log-format json stable (ts/level/msg) for programmatic consumers.

Future proposal (clearly separated — does not exist today). If colors are ever added, they must (a) respect NO_COLOR and --no-color, (b) activate only when stderr is a TTY, and (c) never encode meaning by color alone. None of this is currently implemented.


10. Error messages and validation

Validation happens early, in runScan, and errors return through the RunEmain() chain, which prints quorum: <err> on stderr and exits with exit 2. Because the root command uses SilenceUsage/SilenceErrors, there is no help dump on every error — just the objective message.

Validation / error Message Exit
Invalid --type invalid --type "x" (want image\|repo\|k8s) 2
Invalid --log-format invalid --log-format "x" (want text\|json) 2
Invalid --fail-on invalid --fail-on "x" (want critical\|high\|medium\|low) 2
Invalid --min-severity invalid --min-severity "x" (want critical\|high\|medium\|low) 2
Invalid --format unknown format "x" (want sarif\|json\|xml) 2
Invalid --advice-provider invalid --advice-provider "x" (want none\|local\|remote) 2
Invalid --fix invalid --fix "x" (want off\|suggest) 2
--advice-provider=remote under --offline --advice-provider=remote is disabled by --offline …; use --advice-provider=local 2
--advice-provider=remote without egress consent --advice-provider=remote sends your findings … re-run with --advice-allow-egress to consent … 2
--advice-provider=remote without API key --advice-provider=remote needs an API key in QUORUM_ADVICE_API_KEY 2
--fix=suggest with --advice-provider=remote --fix is not allowed with --advice-provider=remote: it would upload file source … 2
Target starting with - invalid target "-x": must not start with '-' (use "./-x" for a path) 2
Target (repo/k8s) above the size cap target "…" exceeds the N-byte size cap (raise or disable with QUORUM_MAX_TARGET_BYTES) 2
Non-numeric QUORUM_MAX_TARGET_BYTES invalid QUORUM_MAX_TARGET_BYTES "x" 2
Explicit --baseline but nonexistent baseline file not found: <path> 2
Failure loading baseline/crosswalk loading baseline: … / loading crosswalk: … 2
Failure writing --metrics writing metrics: … 2
Wrong number of arguments Cobra error (cobra.ExactArgs(1)) 2
Unknown scanner in --scanners warning: unknown scanner "x" ignored (known: …) (warning, not error)

Principle: configuration errors are fatal and explicit (exit 2); degraded but recoverable situations (missing scanner, OSV offline, unknown scanner in the list, unreachable advisory model) are warnings that do not interrupt the scan. See graceful degradation in 09-backend.md where applicable.

Security note (as-is). Several input validations appear in the table because they are visible to the user at the terminal: the refusal of targets that start with - (defense against argument injection in downstream scanners), the target size cap (QUORUM_MAX_TARGET_BYTES, default 20 GiB — an anti-DoS guard against pointing Quorum at a pathologically large tree), and the egress consent gate for --advice-provider=remote (data leaves the host only with explicit --advice-allow-egress).


11. GitHub Code Scanning as the visual surface

Quorum's richest visual representation is not its own — it is GitHub's Code Scanning UI, fed by the SARIF that Quorum emits as its primary format (internal/report/sarif.go).

flowchart LR
    Q["quorum scan -f sarif -o results.sarif"] --> A["actions/upload-sarif\n(or Quorum's composite GitHub Action)"]
    A --> CS["GitHub Code Scanning"]
    CS --> UI["Security tab → Code scanning alerts\n(visual, dedup by fingerprint, inline PR annotations)"]

What Quorum's SARIF carries into that UI:

SARIF field Quorum content Effect in the GitHub UI
level error (CRIT/HIGH), warning (MED), note (rest) Alert severity/icon
ruleId VulnID (CVE), CanonicalControl, RuleID or correlationKey (fallback) Grouping by rule
partialFingerprints["quorum/v1"] m.Fingerprint = sha256(correlationKey) Stable dedup of alerts across runs
locations file + region (lines) per member with Location.File Inline annotation in the PR diff / navigation to the spot
properties.detectedBy / detectionCount which and how many scanners detected Consensus evidence (visible when expanding the alert)
properties.confidence consensus score (2 decimals) Confidence signal
properties.correlationKey / unmapped deterministic key and unmapped flag Traceability and triage
run.properties.scanners name/status/version of each scanner Coverage transparency in the artifact

partialFingerprints is what makes the UI usable over time: GitHub uses this fingerprint to recognize "the same alert" across commits, preventing each run from creating duplicate alerts. Since Quorum's fingerprint is deterministic (sha256(correlationKey)), the dedup is stable. The advisory layer never touches this fingerprint, so --advice does not disturb dedup.

Code Scanning integration checklist:

  • [ ] Run quorum scan … -f sarif -o results.sarif (SARIF is the default of --format).
  • [ ] Upload via github/codeql-action/upload-sarif (or Quorum's composite Action, which verifies the image with cosign first and auto-mounts /var/run/docker.sock on an image scan).
  • [ ] Confirm the security-events: write permission in the workflow.
  • [ ] Use --quiet (or --log-format json) in the scan step to keep the runner log under control.
  • [ ] Optional: combine with --fail-on to block the PR via exit code, independently of the UI.

JSON and XML exist as alternative machine formats (see json.go and xml.go), useful for custom tooling — but none of them is a "screen".


12. How to test / verify the UX

# 1) Full summary + progress, SARIF report on stdout
quorum scan alpine:3.18 --fail-on high

# 2) Clean output for CI: SARIF to a file, no noise, with gate
quorum scan alpine:3.18 -q -o results.sarif --fail-on critical
echo "exit=$?"

# 3) Confirm channel separation: clean stdout, stderr discarded
quorum scan . -f json 2>/dev/null | jq '.summary'

# 4) Structured logs (JSONL on stderr) for ingestion into a collector
quorum scan . --log-format json 2> quorum.log
jq -c 'select(.msg | startswith("done"))' quorum.log

# 5) Force a validation error (exit 2) and see the message
quorum scan x --fail-on banana     # → quorum: invalid --fail-on "banana" (...)
quorum scan x --log-format yaml    # → quorum: invalid --log-format "yaml" (want text|json)

# 6) Inspect scanner states (narrowing the pool)
quorum scan . --scanners trivy,nonexistent   # → warning: unknown scanner ...

# 7) Discover the 12 scanners and what each supports
quorum list-scanners

# 8) Prometheus telemetry to a file
quorum scan . --metrics quorum.prom && cat quorum.prom

# 9) Opt-in advisory layer (byte-identical output without --advice)
quorum scan . --advice                       # deterministic remediation + OWASP refs
quorum scan . --advice --advice-provider=local --fix=suggest   # opt-in local LLM + verified patch

Manual verification checklist:

  • [ ] stdout contains only the report (no [quorum] nor log JSON).
  • [ ] stderr contains the progress and the summary table when without --quiet.
  • [ ] With --log-format json, each progress line on stderr is a valid JSON object (ts/level/msg); the summary table remains text.
  • [ ] With --quiet, stderr is empty (except a fatal error), including under --log-format json.
  • [ ] --fail-on produces exit 1 when there is a finding at the threshold; exit 0 otherwise.
  • [ ] Invalid flags produce exit 2 with a quorum: … message.
  • [ ] Each scanner appears in the summary with one of the 5 statuses.
  • [ ] Without --advice, the output is byte-identical; with --advice, only extra advice* lines appear on stderr and advisory fields inside the report.

Assumptions

  1. The source of truth is the code. Everything here was verified in cmd/quorum/{main.go,root.go,scan.go}, internal/orchestrator/orchestrator.go, internal/adapter/*.go (12 adapters), internal/report/{report,sarif,json,metrics}.go and internal/severity/severity.go in the current repository version (v0.8.3).
  2. Absence of color/ANSI was inferred from a search for color/NO_COLOR/ isatty/IsTerminal in the Go code, which did not return a coloring implementation (the only occurrence of color is in action.yml, which is the Action badge color on the Marketplace, not terminal output). If color is added in the future, the Accessibility section will need revision.
  3. The summary table example in §4 is illustrative (fictional values), but the layout and the scanner names follow exactly the Fprintf of printSummary and the Name() of the adapters.
  4. --log-format only governs the progress logs of the logf closure. The summary (printSummary), the report (--format) and the fatal error from main() are independent of --log-format, as verified in the code.
  5. Graceful-degradation details (OSV offline, alias cache, crosswalk fallback, --metrics, unreachable advisory model) are touched here only where they affect the terminal UX; the full treatment belongs to the orchestration/alias/telemetry documents.
  6. The advisory layer is opt-in and off by default. Without --advice, the output is byte-identical and the AI-free deterministic core is unchanged; only Phases 1 and 3 involve a model, and both degrade gracefully. Every AI attachment is labeled "AI-generated, advisory only".
  7. Runtime messages are in English in the code; this is the default documentation and translates the meaning, not the literal emitted strings. A Brazilian-Portuguese version exists as 08-frontend.pt.md.
  8. Cross-links to other documents in the docs/ directory (e.g. alias, supply chain) use the NN-file.md pattern; some targets may not yet exist at the time this file is written.