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
stdoutand all the noise (progress logs and the summary table) goes tostderr,quorum scan img -f json > report.jsonproduces 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 tostderr). This also holds under--advice: advisory progress and any AI attachments are emitted through the same channels, and without--advicethe 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 thelogfclosure. It does not change: (a) the summary table (printSummaryalways prints readable text — see §4); (b) the report onstdout(governed by--format); nor (c) the fatal-error line frommain()(quorum: <err>). For a 100% machine-structured output, combine--log-format jsonwith--quiet? No —--quietturns logs off entirely; pick one or the other as needed (structured logs or total silence).
levelis alwaysinfo. In the current code all progress events — includingwarning: unknown scanner …,skip …andgate: …— are emitted by the samelogfand serialized with"level":"info". The event's semantic severity lives in themsgtext, not in thelevelfield. Consumers that want to filter by severity should match on themsgcontent.
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-severitydiscard findings, the number is recorded onstderr. 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 findingsvsafter 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 withDetectionCount > 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 likecheckovhave slow cold start and may be SIGKILLed on low-memory runners). It is this probe that distinguishesunavailable/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 becomesunavailableand 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-onwith the severity threshold (critical|high|medium|low). - [ ] Consider
--min-severityto remove noise below a floor (affects report and gating). - [ ] Treat
exit 2as a configuration/infra error (not as "findings were found"). - [ ] Do not confuse
exit 0with "safe" — check the scanner statuses (unavailable/timeoutmask coverage). - [ ] On low-RAM runners, if you see
unavailable (OOM), increase the container memory or narrow--scanners.
Careful with
exit 0. Anexit 0only guarantees that no finding reached the threshold; it does not guarantee that all scanners ran. Anunavailable/timeoutscanner may have left an entire class of findings out. The0 findings is not proof of safetynote 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 writeMetricsFile → report.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 exportsquorum_advice_enriched{kind=remediation|references|recommendation},quorum_advice_provider{provider}andquorum_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:
- The progress logs (the
logfclosure returns at the start, be ittextorjson). - The summary table (
printSummaryreturns at the start ifquiet).
What is not affected by --quiet:
- The report on
stdout/--output(still emitted normally). - The exit code (gating keeps working).
- The
--metricsfile (still written, if requested). - The fatal-error line in
main()(quorum: <err>withexit 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--quietfor--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
--quietas a predictable path for automation and assistive tools. - [ ] Keep the JSON envelope of
--log-format jsonstable (ts/level/msg) for programmatic consumers.
Future proposal (clearly separated — does not exist today). If colors are ever added, they must (a) respect
NO_COLORand--no-color, (b) activate only whenstderris 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 RunE → main() 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 |
partialFingerprintsis 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--advicedoes 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.sockon an image scan). - [ ] Confirm the
security-events: writepermission in the workflow. - [ ] Use
--quiet(or--log-format json) in the scan step to keep the runner log under control. - [ ] Optional: combine with
--fail-onto 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:
- [ ]
stdoutcontains only the report (no[quorum]nor log JSON). - [ ]
stderrcontains the progress and the summary table when without--quiet. - [ ] With
--log-format json, each progress line onstderris a valid JSON object (ts/level/msg); the summary table remains text. - [ ] With
--quiet,stderris empty (except a fatal error), including under--log-format json. - [ ]
--fail-onproducesexit 1when there is a finding at the threshold;exit 0otherwise. - [ ] Invalid flags produce
exit 2with aquorum: …message. - [ ] Each scanner appears in the summary with one of the 5 statuses.
- [ ] Without
--advice, the output is byte-identical; with--advice, only extraadvice*lines appear onstderrand advisory fields inside the report.
Assumptions¶
- 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}.goandinternal/severity/severity.goin the current repository version (v0.8.3). - Absence of color/ANSI was inferred from a search for
color/NO_COLOR/isatty/IsTerminalin the Go code, which did not return a coloring implementation (the only occurrence ofcoloris inaction.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. - The summary table example in §4 is illustrative
(fictional values), but the layout and the scanner names follow
exactly the
FprintfofprintSummaryand theName()of the adapters. --log-formatonly governs the progress logs of thelogfclosure. The summary (printSummary), the report (--format) and the fatal error frommain()are independent of--log-format, as verified in the code.- 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. - 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". - 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. - Cross-links to other documents in the
docs/directory (e.g. alias, supply chain) use theNN-file.mdpattern; some targets may not yet exist at the time this file is written.