Observability¶
Quorum (quorum-sec-scan, v0.8.3) is an ephemeral CLI/Docker tool: every run is born with quorum scan <target>, runs a pool of scanners in parallel, emits a report and dies. There is no daemon, server, database or dashboard — root.go itself sums up the philosophy: "Built for CI/CD: configure via flags, gate via exit code. No panel, no daemon." For that reason, Quorum's observability does not follow the long-running-service model (push metrics, distributed tracing, health checks, live dashboards). Instead, it leans on process signals: structured logs on stderr (text or json), per-scanner status, exit codes, an opt-in Prometheus metrics file and the report (SARIF/JSON/XML) as an auditable artifact.
This document describes precisely what exists today (as-is), explicitly declares what is N/A for an ephemeral CLI (with a technical rationale) and offers concrete future proposals clearly separated from the current state.
New since v0.2.3: the two features that were once proposals —
--log-format json(NDJSON logs on stderr) and--metrics <file>(Prometheus textfile) — are now real and implemented. They left §4 (proposals) and joined the current state (§2.1 and §2.4).New since v0.7.4: the opt-in advisory layer (
--advice) adds its own, clearly-scoped telemetry — thequorum_advice_*metric series (§2.8) — which is emitted only when--adviceis on. Without--advice, the metrics file and every other output are byte-identical to before. The advisory layer is presentation-only and never touchescorrelationKey/fingerprint/confidence/ aggregated severity / the--fail-ongate.
1. Mental model: observability of an ephemeral CLI vs. a service¶
Most classic observability pillars (Logs, Metrics, Traces) assume a long-running process that can be scraped, queried and correlated over time. Quorum is not that process. It is a batch job: it runs in seconds to minutes, usually inside a CI step or a docker run, and terminates.
flowchart LR
subgraph "Long-running service (NOT Quorum)"
S[Always-on process] --> P[/metrics scrape/]
S --> T[traces to collector]
S --> H[/healthz/]
end
subgraph "Quorum: ephemeral job"
I[quorum scan] --> L[logs on stderr text/json]
I --> ST[per-scanner status]
I --> EC[exit code]
I --> MF[.prom file via --metrics]
I --> R[SARIF/JSON/XML report]
end
L --> CI[CI logs]
ST --> R
EC --> Gate[pipeline gate]
MF --> TF[textfile collector / Pushgateway]
R --> CS[Code Scanning / artifact]
Practical consequence: Quorum's "telemetry" is consumed by the platform that invokes it (CI runner, job orchestrator, shell), not by an observability stack of its own. The signals are designed to be captured by the step logs, by artifact uploads, by SARIF ingestion (e.g. GitHub Code Scanning) and — when --metrics is used — by a textfile collector or Pushgateway at the end of the job.
2. Current state (as-is)¶
2.1 Structured logs on stderr (text or json)¶
All progress output goes to stderr, keeping stdout clean for the report (when --output is not used, the report goes to stdout — separating the streams is what allows quorum scan img > report.sarif without pollution).
Since v0.7.4 the log format is selectable via the --log-format text|json flag (default text). In text format each event is prefixed with [quorum]; in json format each event becomes one JSON line (NDJSON) on stderr. The logger is defined in cmd/quorum/scan.go (runScan):
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 logf is injected into the orchestrator via orchestrator.Options.Logf, so that CLI and pipeline share the same log channel and format. An invalid --log-format value is rejected with a usage error (invalid --log-format …) before any scanner runs.
Example output in json format:
{"ts":"2026-07-04T10:00:01Z","level":"info","msg":"run trivy (0.52.2) ..."}
{"ts":"2026-07-04T10:00:04Z","level":"info","msg":"done trivy: 12 findings in 3.14s"}
Events logged today (real lines from the code; the same message is emitted in both formats, only the envelope changes):
| Origin | Event | Example message (msg field / text) |
|---|---|---|
scan.go |
initial context | target=… type=… crosswalk=N rules (…) offline=false |
scan.go |
filtering/suppression | filtered: N suppressed by baseline (M entries), K below min-severity … |
scan.go |
metrics written | metrics written to <path> |
scan.go |
gate triggered | gate: found HIGH finding >= --fail-on high → exit 1 |
scan.go |
advisory (Phase 0) | advice: knowledge=N entries (<dir>) → M findings enriched |
scan.go |
advisory (RAG) | advice(rag): corpus=N chunks (…) → M findings gained OWASP references |
scan.go |
advisory (AI) | advice(ai): provider=local model=… endpoint=… fix=suggest |
scan.go |
advisory (AI result) | advice(ai): N findings gained an AI recommendation (fixes: V verified / P proposed) |
orchestrator.go |
unknown scanner | warning: unknown scanner "foo" ignored (known: …) |
orchestrator.go |
skip for non-support | skip <name>: does not support target <type> |
orchestrator.go |
version probe | skip <name>: version probe timed out after 60s … |
orchestrator.go |
probe OOM | skip <name>: version probe killed (likely OOM …) |
orchestrator.go |
unavailable | skip <name>: not installed/available |
orchestrator.go |
run start | run <name> (<ver>) ... |
orchestrator.go |
run failure | fail <name>: <error> |
orchestrator.go |
completion | done <name>: N findings in <dur> |
Characteristics and limits (honest):
- In
jsonformat, each line's envelope is minimal and stable: onlyts(RFC 3339 UTC),level(today always"info") andmsg(the human text). There are no per-event typed fields such asscanner,eventordurationMs— that data lives in the free-formmsgand in the report. In other words,--log-format jsonmakes the stream easy to parse line by line, but it is not yet a rich per-field event schema. - In
textformat, messages carry the stable[quorum]prefix and a reasonably parseable format, but in free text — no guaranteed key=value nor per-line timestamps. --quiet/-qsilences all progress and the summary (controlled byf.quiet), orthogonal to--log-format: it silences both formats. There is no--verbose/--debugmode with finer granularity.- There is no log rotation, sampling or correlation — unnecessary for an ephemeral process; the CI runner is what persists.
2.2 Per-scanner status (the central "it really ran" signal)¶
The design principle "0 findings is not proof of safety" requires that Quorum never conflates "zero vulnerabilities" with "the scanner did not run". This is materialized by ScannerRun.Status (in internal/orchestrator/orchestrator.go). With 12 scanners available in v0.8.3 (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula and conftest), this signal matters even more: each target triggers only the subset that supports it, and the status distinguishes "does not apply" from "should have run and failed".
| Status | Meaning | How it is determined |
|---|---|---|
ran |
ran and produced a result | a.Run returned without error |
skipped |
does not apply to the target | !a.Supports(target) |
unavailable |
binary missing, probe timed out or was killed (OOM) | failure in a.Version during the probe |
error |
ran but failed | a.Run returned a non-deadline error |
timeout |
exceeded the per-scanner --timeout |
runCtx.Err() == DeadlineExceeded |
The version probe (Options.ProbeTime, default 60s) is an observability diagnostic in itself: it distinguishes timeout from killed (OOM) from not installed, and produces actionable messages (e.g. "raise the container's memory limit", "scope --scanners"). See also the scanner-status section in DESIGN.md §14.
2.3 Terminal summary¶
After emitting the report, printSummary (scan.go) writes a human-readable summary to stderr (suppressed by --quiet). The summary is always tabular text — it is not affected by --log-format (--log-format json covers the logf progress stream, not the summary block):
── quorum summary ───────────────────────────
trivy ran 12 findings
grype ran 9 findings
checkov unavailable 0 findings (version probe killed — likely OOM …)
----------------------------------------
18 findings after consensus (7 multi-detected)
CRIT 1 HIGH 4 MED 9 LOW 3 INFO 1
elapsed 8.412s
note: 0 findings is not proof of safety — see scanner statuses above.
Signals embedded in the summary: status + count per scanner, post-consensus total, how many were multi-detected (DetectionCount > 1), the severity distribution, and the total duration (res.Duration). The closing note reinforces the anti-false-negative principle.
2.4 Prometheus metrics via --metrics <file> (implemented)¶
True to the ephemeral model, Quorum does not expose a /metrics endpoint. Instead, the --metrics <path> flag (off by default) makes the binary write a file in Prometheus text format at the end of the run — ready to be collected by node_exporter --collector.textfile or pushed to a Pushgateway when the CI job wraps up. This way numeric telemetry exists without a server or daemon.
The file is written by writeMetricsFile (cmd/quorum/scan.go), which calls report.WriteMetrics (internal/report/metrics.go). Relevant implementation details:
- Execution order: the file is written after the report and the summary and before the gate — so the
.promis produced even when--fail-onis about to triggeros.Exit(1). - After writing, a
metrics written to <path>event is logged vialogf. - Permissions:
0644(metrics are non-sensitive counts, meant to be scraped), unlike the report, which is0600because it may carry sensitive finding detail. The path goes throughfilepath.Cleanand intermediate directories are created withos.MkdirAll(0755). - Failure to write the file is fatal to the run (
writing metrics: …), because the operator explicitly asked for the telemetry.
Example .prom file generated (labels and names exactly as emitted by the code):
# HELP quorum_scan_duration_seconds Total scan wall-clock time in seconds.
# TYPE quorum_scan_duration_seconds gauge
quorum_scan_duration_seconds 8.412
# HELP quorum_scanner_up 1 if the scanner ran, else 0 (skipped/unavailable/error/timeout).
# TYPE quorum_scanner_up gauge
quorum_scanner_up{scanner="trivy",status="ran"} 1
quorum_scanner_up{scanner="checkov",status="unavailable"} 0
# HELP quorum_scanner_findings Raw findings reported per scanner (pre-consensus).
# TYPE quorum_scanner_findings gauge
quorum_scanner_findings{scanner="trivy"} 12
# HELP quorum_scanner_duration_seconds Per-scanner duration in seconds.
# TYPE quorum_scanner_duration_seconds gauge
quorum_scanner_duration_seconds{scanner="trivy"} 3.14
# HELP quorum_findings_after_consensus Findings remaining after consensus merge.
# TYPE quorum_findings_after_consensus gauge
quorum_findings_after_consensus 18
# HELP quorum_findings_total Consensus findings by severity.
# TYPE quorum_findings_total gauge
quorum_findings_total{severity="CRITICAL"} 1
quorum_findings_total{severity="HIGH"} 4
quorum_findings_total{severity="MEDIUM"} 9
quorum_findings_total{severity="LOW"} 3
quorum_findings_total{severity="INFO"} 1
# HELP quorum_multi_detected Consensus findings corroborated by more than one scanner.
# TYPE quorum_multi_detected gauge
quorum_multi_detected 7
Metrics emitted by report.WriteMetrics (all of type gauge), with the exact source in the code:
| Metric | Labels | Source in the code |
|---|---|---|
quorum_scan_duration_seconds |
— | res.Duration.Seconds() |
quorum_scanner_up |
scanner, status |
1 if ScannerRun.Status == "ran", else 0 (one point per scanner) |
quorum_scanner_findings |
scanner |
ScannerRun.Findings (raw, pre-consensus findings) |
quorum_scanner_duration_seconds |
scanner |
ScannerRun.Duration.Seconds() |
quorum_findings_after_consensus |
— | len(res.Merged) |
quorum_findings_total |
severity |
count of res.Merged by Severity (labels CRITICAL/HIGH/MEDIUM/LOW/INFO; UNKNOWN is not emitted) |
quorum_multi_detected |
— | how many res.Merged have DetectionCount > 1 |
Important fidelity notes:
- The
severitylabel uses the upper-case values ofmodel.Severity(CRITICAL,HIGH,MEDIUM,LOW,INFO), because it is the literal serialization of the enum. - There is a deliberate distinction between
quorum_scanner_findings(raw, per scanner, before consensus) andquorum_findings_after_consensus/quorum_findings_total(after the consensus merge). Do not conflate the two when building dashboards. - Each scanner's health is exposed as
quorum_scanner_up(boolean) plus the textualstatuslabel, allowing bothsum(quorum_scanner_up)and the detail of the reason (status="timeout", etc.). - There is no dedicated gate metric (e.g.
quorum_gate_triggered); the gate outcome is observable through the exit code (§2.5) and thegate: …log event.
2.5 Exit codes as a machine signal¶
Exit codes are the primary signal consumed by pipelines (contract in scan.go and root.go):
| Code | Meaning |
|---|---|
0 |
OK — run completed and no finding reached --fail-on |
1 |
Gate — some finding >= --fail-on triggered (os.Exit(1)) |
2 |
Usage/runtime error |
This is the cheapest and most reliable "monitoring" of a job: the pipeline decides pass/fail by reading a single integer. See 06-interfaces-cli-e-formatos.md for the details of the gate flags (--fail-on, --min-severity).
2.6 Report as an auditable artifact¶
The report is the persistent audit trail of each run. In SARIF (primary format) and JSON, each scanner's status is embedded in the document itself — it does not live only in the volatile log:
- SARIF (
internal/report/sarif.go): the scanner list (scannerSummary→name/status/version) is written toproperties.scannersof therun; each result carriespartialFingerprints["quorum/v1"](=Fingerprint = sha256(correlationKey)), enabling deduplication and stable tracking across runs and upstream tools (e.g. GitHub Code Scanning). - JSON (
internal/report/json.go): includesscanners(theScannerRuns, withstatus,version,findings,durationMs,error) and the detailed canonical findings. - XML (
internal/report/xml.go): exposes per-scannerstatusas an attribute.
Because the report carries status + version + duration per scanner + a deterministic fingerprint, it is self-sufficient for post-mortem auditing, even if the runner's logs have already expired. See 06-interfaces-cli-e-formatos.md.
2.7 Advisory-layer metrics (only under --advice)¶
When the opt-in advisory layer runs (--advice), report.WriteMetrics receives a non-nil *report.AdviceMetrics and appends a quorum_advice_* block to the same .prom file. These series appear only when --advice is on; without it the advice metrics are absent and the whole output is byte-identical. They are strictly observational — the advisory layer is presentation-only and never affects correlationKey, fingerprint, confidence, aggregated severity or the --fail-on gate.
quorum_advice_enrichedis emitted whenever--adviceis on (even with the purely deterministic Phase 0/2, no model). Its counts are derived from the merged findings: a finding contributes toremediationif it has aRemediation, toreferencesif it has anyReferences, and torecommendationif it has an AIAdviceattachment.quorum_advice_providerandquorum_advice_fixare emitted only when an AI provider was actually used (--advice-provider=localorremote, i.e.Provideris neither empty nornone). With Phase 0/2 alone there is onlyquorum_advice_enriched.quorum_advice_fixreports the verify-the-fix loop:proposedis how many patches the model suggested,verifiedis how many survived the re-scan (applied to a temp copy, re-scanned with the same scanner, kept only if the finding is gone and the file still parses). Theverified/proposedratio is the verify-the-fix success rate.--fixnever auto-applies, and it is refused with--advice-provider=remote(it would upload source).
Example advisory block appended to the .prom file (only with --advice --advice-provider=local --fix=suggest):
# HELP quorum_advice_enriched Findings that gained an advisory attachment, by kind.
# TYPE quorum_advice_enriched gauge
quorum_advice_enriched{kind="remediation"} 14
quorum_advice_enriched{kind="references"} 11
quorum_advice_enriched{kind="recommendation"} 9
# HELP quorum_advice_provider The AI recommendation provider used (1 = active).
# TYPE quorum_advice_provider gauge
quorum_advice_provider{provider="local"} 1
# HELP quorum_advice_fix AI suggested fixes by verify-the-fix stage.
# TYPE quorum_advice_fix gauge
quorum_advice_fix{stage="proposed"} 3
quorum_advice_fix{stage="verified"} 2
Advisory metrics emitted by report.WriteMetrics (all gauge), with the exact source in the code:
| Metric | Labels | Emitted when | Source in the code |
|---|---|---|---|
quorum_advice_enriched |
kind = remediation | references | recommendation |
--advice on |
count of res.Merged with a Remediation / non-empty References / an Advice attachment |
quorum_advice_provider |
provider = local | remote |
AI provider used | constant 1 for the active provider (AdviceMetrics.Provider) |
quorum_advice_fix |
stage = proposed | verified |
AI provider used | AdviceMetrics.FixProposed / AdviceMetrics.FixVerified (the verify-the-fix rate) |
Fidelity notes:
- The advisory block is appended after
quorum_multi_detected; it does not change any of the core metrics above it. A consumer that predates the advisory layer keeps working unchanged. quorum_advice_provideris a single point (value1) for the provider actually used — it is a presence marker, not a count.- Every AI attachment is labeled "AI-generated, advisory only" in the report; the metrics simply count how many findings carry each kind of attachment. If the model is unreachable the run degrades gracefully — the report ships without AI advice, the counters stay at
0, and the scan never fails because of the advisory layer. - The AI recommendation is reproducible (
temperature=0+ an on-disk cache keyed byfingerprint+provider+model), so repeated runs over the same findings produce stable advice counts.
2.8 Map of current signals¶
flowchart TD
Run[quorum scan] --> Orq[orchestrator.Run]
Orq -->|Logf text/json| Stderr["stderr: [quorum] … / NDJSON"]
Orq --> Runs["ScannerRun{status,version,findings,duration,error}"]
Runs --> Sum[printSummary → stderr]
Runs --> Met["--metrics → .prom file (0644)"]
Runs --> Rep[SARIF/JSON/XML report]
Adv["--advice (opt-in)"] -.->|AdviceMetrics| Met
Adv -.-> Rep
Run --> Exit[exit code 0/1/2]
Met --> TF[textfile collector / Pushgateway]
Rep --> Art[artifact / Code Scanning]
Stderr --> CILogs[CI runner logs]
3. What is N/A (and why)¶
For an ephemeral binary/container that terminates in seconds, the pillars below do not apply to Quorum's runtime. We declare N/A with a technical rationale; where an equivalent output compatible with the ephemeral model already exists (e.g. --metrics textfile), that is pointed out.
| Pillar | Status | Technical rationale |
|---|---|---|
Scrapable metrics endpoint (/metrics HTTP scrape) |
N/A (runtime) — but see §2.4 | There is no persistent process to expose /metrics nor to be scraped; the lifecycle (seconds) is shorter than a typical scrape interval. Numeric export does exist, however, via --metrics <file> in textfile format (collected by node_exporter/Pushgateway). |
| Distributed tracing (OpenTelemetry traces) | N/A (runtime) | Local, single-process execution, fan-out in goroutines within the same process. There are no network hops between services to correlate. |
Health-check endpoint (/healthz, readiness/liveness) |
N/A | There is no server to health-check. The "health" analog is the exit code, the per-scanner status and the quorum_scanner_up metric. list-scanners serves as a capability check. |
| Live dashboards (Grafana) | N/A (runtime) | No time series emitted in real time. Trend visualization is the responsibility of the platform that ingests the SARIF or the metrics textfile. |
| Alerting (Alertmanager/PagerDuty) | N/A (runtime) | The native alert is the exit-code gate (--fail-on) interpreted by CI. Alert rules can be built on top of the --metrics series in Prometheus, but they are not internal to Quorum. |
| Own log aggregation (ELK/Loki) | N/A | Quorum writes to stderr (text or json); aggregation is delegated to the runner. --log-format json eases ingestion, but reimplementing log shippers would reinvent the host platform. |
| APM / continuous profiling | N/A | No live process to profile continuously; one-off profiling is a development task (go test -bench, ad-hoc pprof), not a production concern. |
Important: N/A means "not applicable to the current execution model", not "impossible". §2.4 already shows how to export metrics without becoming a service; §4 details the evolutions not yet implemented.
4. Future proposals (NOT implemented)¶
Everything in this section is a proposal, clearly separated from the current state. Nothing below exists in the code today. They are evolution designs compatible with Quorum's ephemeral, CLI-first nature (principle: opt-in telemetry, no daemon).
Graduated to the current state (§2): the former
--log-format json(now §2.1),--metrics <file>(now §2.4) and the advisory-layer metrics (now §2.7). They were implemented and therefore left this list.
4.1 OpenTelemetry for the pipeline (opt-in spans via OTLP)¶
Even though it is single-process, the pipeline scan → normalize → resolve aliases → correlate → score → report has well-defined phases and the scanner fan-out is naturally a set of sibling spans. An opt-in export via OTLP (enabled by standard OTel env vars and --otel) would let a Quorum run be correlated with the trace of the CI pipeline that invoked it (via an inherited traceparent).
- [ ] Flag
--otel+ honorOTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SERVICE_NAME,traceparent. - [ ] Root span
quorum.scan; child spans per scanner (runOne) and per correlator phase. - [ ] Export on shutdown (synchronous flush — an ephemeral process cannot rely on async batching).
gantt
title Proposed trace of a quorum scan (spans)
dateFormat X
axisFormat %S
section scan
quorum.scan :a, 0, 9
section scanners (fan-out)
trivy.run :0, 4
grype.run :0, 5
checkov.probe (OOM) :0, 1
section post
correlate+alias :5, 7
consensus.score :7, 8
report.write :8, 9
4.2 Trend dashboards via SARIF (no server)¶
Trend analysis (findings over time, MTTR, multi-detection rate) should be delegated to the platform that ingests the SARIF — typically GitHub Code Scanning, which already offers history, charts and alerts from the partialFingerprints["quorum/v1"]. Alternatively, the metrics from the --metrics file can feed an external Prometheus/Grafana. Quorum does not need a Grafana of its own.
- [ ] Document the
quorum scan -f sarif→upload-sarif→ Code Scanning flow as the recommended trend path. - [ ] (Optional) a reporting job that collects the
--metricsfiles from several runs and plots trends in Prometheus. - [ ] Guarantee fingerprint stability across versions (already
sha256(correlationKey)).
4.3 Opt-in post-run "alerts"¶
Keep the exit-code gate as the primary mechanism, and offer an optional notification hook (e.g. --notify-webhook <url> sending the summary JSON), executed only at the end of the process. No polling, no daemon.
- [ ] Flag
--notify-webhook(off by default; respects--offline). - [ ] Payload = structured summary (severities, per-scanner status, gate).
- [ ] Send failure is non-fatal (graceful degradation, like the OSV alias).
4.4 Proposal summary¶
| Feature | Flag/mechanism | Ephemeral-compatible? | Status |
|---|---|---|---|
| JSON logs | --log-format json |
Yes (stderr) | Implemented (§2.1) |
| Textfile metrics | --metrics <path> |
Yes (file, no server) | Implemented (§2.4) |
| Advisory metrics | --advice → quorum_advice_* |
Yes (same textfile) | Implemented (§2.7) |
| OTel pipeline | --otel + OTLP env |
Yes (flush on shutdown) | Proposal (§4.1) |
| SARIF trend | Code Scanning / Prometheus | Yes (delegated) | Proposal (§4.2) |
| Webhook | --notify-webhook |
Yes (one-shot) | Proposal (§4.3) |
5. How to observe Quorum today (practical guide)¶
Checklist to integrate the existing observability into a pipeline:
- [ ] Capture stdout and stderr separately. Route the report with
-o report.sarifand leave the[quorum]/NDJSON logs on stderr for the runner. - [ ] Choose the log format according to the consumer:
--log-format textfor human reading,--log-format jsonwhen an aggregator (Loki/ELK/CloudWatch) will parse the stream line by line. - [ ] Use
--fail-onso that the exit code is your primary alert signal (gate). - [ ] Enable
--metrics quorum.promif you have anode_exportertextfile collector or a Pushgateway — the.promfile is written even when the gate fails, so you never lose telemetry on red builds. - [ ] If you use
--advice, watch thequorum_advice_*series:quorum_advice_enrichedfor coverage of remediation/references/recommendation, and — with an AI provider — theverified/proposedratio ofquorum_advice_fixas the verify-the-fix success rate. Remember they are absent without--adviceand never affect the gate. - [ ] Upload the report as an artifact (and/or ingest the SARIF into Code Scanning) — it is your durable audit trail.
- [ ] Inspect the per-scanner status (
quorum_scanner_up, the summary column,properties.scanners), not just the finding count: anunavailable/timeout/errormeans reduced coverage, not absence of risk. - [ ] Do not conflate
quorum_scanner_findings(raw) withquorum_findings_after_consensus/quorum_findings_total(post-consensus) when building dashboards. - [ ] Do not use
--quietin CI unless you already persist the JSON/SARIF report (and/or the.prom) —--quietremoves the summary and progress in both formats. - [ ] Treat exit code
2as an infrastructure failure (usage/runtime), distinct from the1gate. - [ ] In low-memory environments, watch the probe messages (OOM/timeout) and adjust memory or
--scanners.
Example (GitHub Actions, conceptual):
- name: Quorum scan
run: >
quorum scan . -f sarif -o quorum.sarif
--fail-on high --log-format json --metrics quorum.prom
# exit 1 => gate; exit 2 => runtime error
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: quorum.sarif }
- name: Upload raw report + metrics (audit/telemetry)
if: always()
uses: actions/upload-artifact@v4
with: { name: quorum-report, path: "quorum.sarif\nquorum.prom" }
6. Cross-references¶
- 06-interfaces-cli-e-formatos.md — flags (
--metrics,--log-format,--fail-on,--min-severity,--advice*,--fix), gating and exit codes. - 06-interfaces-cli-e-formatos.md — SARIF structure,
partialFingerprints, scanners inproperties. - 13-ia.md — honest framing of the AI/advisory layer (the core has no AI; the local/remote advisory layer is opt-in and off by default).
DESIGN.md§14 (scanner status) and §6 (correlation/consensus matrix).- Code:
cmd/quorum/scan.go(logfwith--log-format,printSummary,writeMetricsFile, advisory wiring, exit codes),internal/report/metrics.go(WriteMetrics,AdviceMetrics),internal/orchestrator/orchestrator.go(ScannerRun, probe, status),internal/report/sarif.go(scannerSummary, fingerprints).
Assumptions¶
- Reference version: v0.8.3 (revision 2026-07-04); claims verified in
cmd/quorum/scan.go,internal/report/metrics.go,cmd/quorum/root.go,internal/orchestrator/orchestrator.goandinternal/report/sarif.goin the current state of the repository. --log-format jsonproduces a minimal and stable envelope per line (ts,level,msg), withleveltoday always"info"; I did not assume additional typed fields (scanner,event,durationMs) that do not exist in the current struct.--metricswrites a Prometheus textfile with permission0644; the metric names and labels were copied literally frominternal/report/metrics.go(includingseverityin upper case and the absence of aquorum_gate_triggered). I assumeUNKNOWNis not emitted because the severity loop inWriteMetricsdoes not include it.- The
quorum_advice_*series are emitted only when--adviceran (WriteMetricsreceives a non-nil*AdviceMetrics);quorum_advice_providerandquorum_advice_fixrequire an actual AI provider (local/remote, notnone). Names and labels (kind,provider,stage) were copied literally frominternal/report/metrics.go. Without--advicethe output is byte-identical. - I assume the telemetry consumer is the invoking platform (CI runner,
docker run, shell) or an external collector (textfile/Pushgateway), since Quorum has no observability stack of its own. - The field names in SARIF (
properties.scanners,partialFingerprints["quorum/v1"]) and in JSON (scanners,durationMs) were taken from the code; future schema changes may alter them. - The remaining flags in §4 (
--otel,--notify-webhook) are hypothetical; none exists today and the names are design suggestions, not commitments. - The output examples (summary,
.prom, NDJSON, advisory block) are illustrative; numeric values do not come from a real run. - The version-probe default is 60s (
defaultProbeTime) and the per-scanner--timeoutis 5m, per the code; I assume these are the effective defaults whenOptions.ProbeTime/PerScannerTimeare not overridden.