Skip to content

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 — the quorum_advice_* metric series (§2.8) — which is emitted only when --advice is on. Without --advice, the metrics file and every other output are byte-identical to before. The advisory layer is presentation-only and never touches correlationKey / fingerprint / confidence / aggregated severity / the --fail-on gate.


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 json format, each line's envelope is minimal and stable: only ts (RFC 3339 UTC), level (today always "info") and msg (the human text). There are no per-event typed fields such as scanner, event or durationMs — that data lives in the free-form msg and in the report. In other words, --log-format json makes the stream easy to parse line by line, but it is not yet a rich per-field event schema.
  • In text format, messages carry the stable [quorum] prefix and a reasonably parseable format, but in free text — no guaranteed key=value nor per-line timestamps.
  • --quiet / -q silences all progress and the summary (controlled by f.quiet), orthogonal to --log-format: it silences both formats. There is no --verbose/--debug mode 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 .prom is produced even when --fail-on is about to trigger os.Exit(1).
  • After writing, a metrics written to <path> event is logged via logf.
  • Permissions: 0644 (metrics are non-sensitive counts, meant to be scraped), unlike the report, which is 0600 because it may carry sensitive finding detail. The path goes through filepath.Clean and intermediate directories are created with os.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 severity label uses the upper-case values of model.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) and quorum_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 textual status label, allowing both sum(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 the gate: … 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 (scannerSummaryname/status/version) is written to properties.scanners of the run; each result carries partialFingerprints["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): includes scanners (the ScannerRuns, with status, version, findings, durationMs, error) and the detailed canonical findings.
  • XML (internal/report/xml.go): exposes per-scanner status as 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_enriched is emitted whenever --advice is on (even with the purely deterministic Phase 0/2, no model). Its counts are derived from the merged findings: a finding contributes to remediation if it has a Remediation, to references if it has any References, and to recommendation if it has an AI Advice attachment.
  • quorum_advice_provider and quorum_advice_fix are emitted only when an AI provider was actually used (--advice-provider=local or remote, i.e. Provider is neither empty nor none). With Phase 0/2 alone there is only quorum_advice_enriched.
  • quorum_advice_fix reports the verify-the-fix loop: proposed is how many patches the model suggested, verified is 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). The verified/proposed ratio is the verify-the-fix success rate. --fix never 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_provider is a single point (value 1) 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 by fingerprint+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 + honor OTEL_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 sarifupload-sarif → Code Scanning flow as the recommended trend path.
  • [ ] (Optional) a reporting job that collects the --metrics files 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 --advicequorum_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.sarif and leave the [quorum]/NDJSON logs on stderr for the runner.
  • [ ] Choose the log format according to the consumer: --log-format text for human reading, --log-format json when an aggregator (Loki/ELK/CloudWatch) will parse the stream line by line.
  • [ ] Use --fail-on so that the exit code is your primary alert signal (gate).
  • [ ] Enable --metrics quorum.prom if you have a node_exporter textfile collector or a Pushgateway — the .prom file is written even when the gate fails, so you never lose telemetry on red builds.
  • [ ] If you use --advice, watch the quorum_advice_* series: quorum_advice_enriched for coverage of remediation/references/recommendation, and — with an AI provider — the verified/proposed ratio of quorum_advice_fix as the verify-the-fix success rate. Remember they are absent without --advice and 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: an unavailable/timeout/error means reduced coverage, not absence of risk.
  • [ ] Do not conflate quorum_scanner_findings (raw) with quorum_findings_after_consensus/quorum_findings_total (post-consensus) when building dashboards.
  • [ ] Do not use --quiet in CI unless you already persist the JSON/SARIF report (and/or the .prom) — --quiet removes the summary and progress in both formats.
  • [ ] Treat exit code 2 as an infrastructure failure (usage/runtime), distinct from the 1 gate.
  • [ ] 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 in properties.
  • 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 (logf with --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.go and internal/report/sarif.go in the current state of the repository.
  • --log-format json produces a minimal and stable envelope per line (ts, level, msg), with level today always "info"; I did not assume additional typed fields (scanner, event, durationMs) that do not exist in the current struct.
  • --metrics writes a Prometheus textfile with permission 0644; the metric names and labels were copied literally from internal/report/metrics.go (including severity in upper case and the absence of a quorum_gate_triggered). I assume UNKNOWN is not emitted because the severity loop in WriteMetrics does not include it.
  • The quorum_advice_* series are emitted only when --advice ran (WriteMetrics receives a non-nil *AdviceMetrics); quorum_advice_provider and quorum_advice_fix require an actual AI provider (local/remote, not none). Names and labels (kind, provider, stage) were copied literally from internal/report/metrics.go. Without --advice the 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 --timeout is 5m, per the code; I assume these are the effective defaults when Options.ProbeTime/PerScannerTime are not overridden.