Architecture¶
This document describes the architecture of Quorum (v0.8.3), a CLI/Docker consensus security scanning tool. Quorum is not a scanner: it orchestrates a pool of 12 OSS scanners (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest), normalizes all output to a canonical model (model.Finding), resolves vulnerability aliases, correlates equivalent findings by a deterministic key, computes a confidence (consensus) score, and emits a unified report (SARIF/JSON/XML). The chosen architectural style is a Modular Monolith (a single Go binary) organized as Ports & Adapters (hexagonal) and structured as a deterministic Pipeline. This document justifies those choices, maps the layers to the repository's real packages, and describes the execution flow with component and sequence diagrams.
Revision: 2026-07-04 · product version v0.8.3. Related documents: Overview · Data model / Design · CLI and flags · Supply chain and distribution. When a link points to a document not yet written, treat it as a forward reference.
1. Executive summary¶
| Attribute | Value |
|---|---|
| Primary style | Modular Monolith (a single Go binary) |
| Integration pattern | Ports & Adapters (hexagonal) — the adapter.Adapter interface |
| Processing pattern | Deterministic pipeline (scan → normalize → alias → correlate → score → report) |
| Concurrency | Parallel fan-out (goroutines), one per scanner, with a per-scanner timeout |
| State | No persistent state beyond on-disk caches (aliases, grype DB, AI advice) |
| Language / runtime | Go 1.26, CLI with cobra |
| Integrated scanners | 12 adapters (SCA, IaC/misconfig, policy-as-code, K8s posture, image hardening) |
| Commands | scan <target>, list-scanners, advise-index |
| Distribution | Docker images :full (linux/amd64) and :slim (amd64+arm64) on GHCR + native binaries via GoReleaser |
| Design principle | False split > false merge — when in doubt, do not merge findings |
| Advisory layer | Opt-in (--advice), presentation-only; the deterministic core stays AI-free |
2. Architectural style¶
2.1 Modular Monolith (single binary)¶
Quorum compiles to a single Go executable (cmd/quorum). Every module — orchestration, adapters, correlation, consensus, alias, crosswalk, filter, report, metrics, and the optional advisory layer — lives in the same process and communicates via in-process function calls, not over the network. Modularity is enforced by package boundaries (internal/*) with single responsibilities and unidirectional dependencies, not by splitting into services.
Why a modular monolith:
- The unit of work is a short, batch run. A
quorum scanruns, produces an artifact (a report), and exits. There is no continuous traffic, sessions, or multitenancy that would justify long-lived processes. - CI/CD is the target environment. The binary must be easy to download, pin by digest, and run in a runner or container. A single signed artifact (cosign + SLSA) is trivial to audit; a swarm of services is not.
- Latency and simplicity. Passing
[]model.Findingbetween stages by function call costs nanoseconds and zero serialization. Correlation needs all findings in memory at once (grouping by key) — distributing them would only add cost. - Trivial operation. No runtime container orchestration, no service discovery, no internal network. The user runs one command. Even with 12 scanners, each stays a subprocess invoked in-process, not a service.
2.2 Ports & Adapters (hexagonal)¶
The heart of extensibility is the adapter.Adapter interface (internal/adapter/adapter.go), the port that isolates the core (orchestrator, correlation, consensus) from the external tools. Each OSS scanner is an adapter that knows how to (a) invoke the tool's CLI and (b) translate the native output into model.Finding. Adding a scanner = adding a file under internal/adapter/; nothing in the core changes. That is exactly how the pool grew from 6 to 12 adapters between v0.2.3 and v0.8.3.
// internal/adapter/adapter.go
type Adapter interface {
Name() string
Version(ctx context.Context) (string, error) // probe: detects a missing/slow tool
Supports(target Target) bool // does this adapter cover this target?
Capabilities() []Capability // types/targets it produces
Run(ctx context.Context, target Target) ([]model.Finding, error)
}
The 12 adapters registered today, by engine family (the same taxonomy used by consensus — see §6):
Family (scannerCategory) |
Adapters | Predominant finding type |
|---|---|---|
sca |
trivy, grype | VULN |
iac |
checkov, kics, terrascan, tfsec, regula | MISCONFIG |
policy |
conftest | MISCONFIG (evaluates YOUR Rego from ./policy) |
k8s |
kubescape, polaris, kube-score | K8S_POSTURE |
hardening |
dockle | IMG_HARDENING |
Key hexagonal points, verified in the code:
- Registration via
init(). Each adapter callsadapter.Register(a)in itsinit(); the core discovers adapters viaadapter.All()/adapter.Get(name)without knowing concrete types. Duplicate registration is a panic (a programming error). - The core depends on the abstraction, not the implementation. The orchestrator operates over
[]adapter.Adapter. Trivy, grype, tfsec, conftest, etc. are pluggable details. Supportsfilters by target. trivy/grype coverimage+repo; dockle onlyimage; kube-score/polaris/kubescape coverk8s(kubescape alsorepo); the IaC scanners (checkov/kics/terrascan/tfsec/regula) and conftest coverrepo+k8s. The orchestrator never invokes an adapter on a target it does not support.- Per-scanner passthrough. Each adapter injects extra operator arguments via
extraArgs(name), read fromQUORUM_<NAME>_ARGS(e.g.QUORUM_CHECKOV_ARGS="--bc-api-key <key>"unlocks Prisma Cloud/Bridgecrew policies;QUORUM_CONFTEST_ARGS="--policy <dir>"points to the Rego). The value carries the same trust level as the flags — it is controlled by whoever runs the container. - Adapters do NOT compute identity. They emit raw/normalized
Finding;CorrelationKeyandFingerprintare the centralized responsibility ofinternal/correlate— this guarantees cross-tool consistency (DESIGN §5/§6). The faithful-to-code exception is the already-canonical CanonicalControl at the source: trivy emits AVD ids natively and tfsec derives the AVD from thelinksfield (avdInLink), so both correlate without depending on the crosswalk. - Per-adapter contract test. Each adapter has versioned fixtures in
internal/adapter/testdataand a test (adapter_test.go,realdata_test.go) that breaks when the scanner's output format changes (before production, not after). - I/O hardening shared by the port.
runCmdcaps output atQUORUM_MAX_OUTPUT_BYTES(512 MiB by default) to avoid OOM from an output bomb, and a non-zero exit with stdout is treated as success (several scanners exit non-zero precisely because they found problems). Secret findings have theirMatchredacted (redactSecretText) so the value is not leaked.
2.3 Deterministic pipeline¶
Processing is a pipeline of well-defined stages, documented in the orchestrator's package comment and in DESIGN §3:
Each stage is a pure (or near-pure) transformation over the previous stage's data. Determinism is an explicit principle: the CorrelationKey is a pure function of the normalized data (DESIGN principle 4), consensus orders the output stably, and the Fingerprint is sha256(correlationKey). Same input ⇒ same output ⇒ temporal dedup for free (via partialFingerprints["quorum/v1"] in SARIF).
2.4 Advisory layer (opt-in, presentation-only)¶
Since v0.7.4 there is an advisory layer that attaches remediation guidance, OWASP references, and — when explicitly enabled — natural-language recommendations to the findings. It is opt-in via --advice and runs after consensus and gating, as a post-consensus presentation stage. It never touches correlationKey, fingerprint, confidence, aggregated severity, or the --fail-on gate. Without --advice, the output is byte-identical to before. The deterministic core has no AI; the AI parts are strictly opt-in and off by default. It is realized in three packages and four phases (all implemented):
- Phase 0 —
internal/enrich(deterministic, no model). Curated remediation templates + OWASP references, matched bycanonicalControl/ruleId/category/type. The data lives in a versioned knowledge pack (knowledge/*.yaml: aws/azure/gcp/k8s/image/categories). On a miss, nothing is attached (the same "never guess a match" rule as the crosswalk). - Phase 2 —
internal/rag(RAG-as-artifact, deterministic). Retrieval from a versioned, DIGEST-PINNED OWASP corpus (knowledge/owasp/corpus.yaml). Lexical retrieval by default (no model, fully air-gapped); semantic (embeddings) when the corpus is embedded viaquorum advise-index.scanauto-picks semantic when the corpus carries vectors. A tampered or truncated corpus fails the digest pin and is refused. - Phase 1 —
internal/advisor(opt-in local LLM).--advice-provider=localqueries an on-host OpenAI-compatible endpoint (e.g. Ollama) for a natural-language recommendation, and--fix=suggestproposes a patch that must pass a verify-the-fix re-scan (apply to a temp copy, re-scan with the same scanner, keep only if the finding is gone and the file still parses; it never auto-applies). Reproducible viatemperature=0+ an on-disk cache keyed byfingerprint+provider+model. Graceful degradation: 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". - Phase 3 —
internal/advisor(opt-in remote provider).--advice-provider=remotecalls an external API (auth viaQUORUM_ADVICE_API_KEY). Data leaves the host, so it is gated on explicit consent (--advice-allow-egress), BLOCKED by--offline, and REFUSES--fix(that would upload source). Only the normalized finding is sent — never source code.
These phases attach the new MergedFinding fields Remediation, References, and Advice (types model.Remediation/DocRef/Advice/Fix). They are covered by an evaluation harness (internal/evals) that measures deterministic remediation coverage, OWASP reference relevance, and the verify-the-fix rate in CI (no heavy model). See AI approach and AI proposal.
3. Why NOT microservices / serverless / event-driven / CQRS¶
The template requires an explicit trade-off justification. Each style below was considered and declared N/A with a technical rationale.
| Style | Verdict | Technical rationale |
|---|---|---|
| Microservices | N/A | The workload is batch, short-lived, and single-tenant. Splitting correlation/consensus/report into services would introduce networking, serialization, and service discovery with no gain in scale or isolation — and would break the central requirement of shipping one signable artifact (cosign + SLSA). Correlation needs all findings from the 12 scanners in memory simultaneously; distributing them would be counterproductive. |
| Serverless (FaaS) | N/A | Heavy scanners (checkov is a Python process; grype needs a pre-cached vulnerability DB of hundreds of MB — in Quorum it is embedded in the image with GRYPE_DB_VALIDATE_AGE=false so it does not expire) violate cold-start, package-size, and execution-time limits of functions. The target environment is the CI runner, where the binary already runs; FaaS would add latency and cost. The default scan timeout is 5m and the version probe tolerates up to 60s of cold start — incompatible with typical FaaS. |
| Event-driven / messaging | N/A | There are no asynchronous producers/consumers or event flow. The scanners' parallel fan-out is already done in-process with goroutines + sync.WaitGroup; a broker (Kafka/NATS/SQS) would be purposeless infrastructure for a job that starts and ends. |
| CQRS | N/A | CQRS separates read and write models over a mutable datastore. Quorum has no relational database and no commands that mutate shared state: the only "writes" are the report file and the optional metrics file (--metrics), and the only persistence is a read-through cache (aliases, AI advice). With no write domain, there is nothing to segregate. |
| Event Sourcing | N/A | There is no domain-event history to reconstruct; each scan is independent and idempotent. |
Where a legitimate future need arises — for example a runtime/streaming mode (Falco/Tetragon) — DESIGN §2 already classifies it as a separate product with a stream model, outside the scope of this batch binary. See "Future proposals" at the end.
4. Layers and package map¶
Dependencies flow in one direction: the CLI layer (controller) orchestrates the pipeline; the pipeline depends on the canonical model and the abstractions; the adapters depend only on the model. internal/model is the core with no dependencies.
| Layer | Package(s) | Responsibility |
|---|---|---|
| CLI / Controller | cmd/quorum (main.go, root.go, scan.go, advise_index.go, advisor.go) |
Flag parsing (cobra), target validation (rejects - → argument injection; QUORUM_MAX_TARGET_BYTES cap of 20 GiB), target/crosswalk/baseline resolution, dependency wiring, exit codes, stderr summary, Prometheus metrics (--metrics), log format (--log-format text\|json), advisory-layer wiring (--advice*, --fix) |
| Orchestration | internal/orchestrator |
Adapter selection by target, parallel fan-out, version probe, per-scanner timeout, per-scanner status, findings collection |
| Adapters (port) | internal/adapter (12 files: trivy.go, grype.go, checkov.go, kics.go, terrascan.go, tfsec.go, regula.go, conftest.go, kubescape.go, polaris.go, kubescore.go, dockle.go) |
Invoke the tool CLI and translate to model.Finding; registration; Version probe; Supports/Capabilities; QUORUM_<NAME>_ARGS passthrough |
| Identity / Correlation | internal/correlate (correlate.go, key.go) |
Enrich (alias + crosswalk), stamp CorrelationKey + Fingerprint |
| Alias resolution | internal/alias (resolver.go, osv.go) |
CVE/GHSA → canonical form (CVE preferred); local→cache→OSV chain (id validated + url.PathEscape) |
| Crosswalk | internal/crosswalk |
Load YAML rule→canonical-control (AVD hub for cloud, C-#### hub for k8s); versioned schemaVersion; resolve scanner\|ruleID |
| Consensus | internal/consensus |
Group by CorrelationKey, aggregate severity, detectionCount, confidence (weighting engine-family diversity), stable ordering |
| Filter / Gating | internal/filter |
Baseline (.quorumignore), --min-severity, logged suppressions |
| Advisory layer | internal/enrich, internal/rag, internal/advisor (+ internal/evals) |
Opt-in, presentation-only, post-consensus: curated remediation + OWASP references (Phase 0), digest-pinned OWASP retrieval (Phase 2, lexical/semantic), opt-in local/remote AI recommendations + verify-the-fix (Phases 1/3). Attaches Remediation/References/Advice; never touches key/fingerprint/confidence/gate |
| Report | internal/report (sarif.go, json.go, xml.go, metrics.go) |
Serialize Result/[]MergedFinding to SARIF (primary), JSON, XML; Prometheus-text metrics (including the --advice-only advisory metrics) |
| Support | internal/cache, internal/purl, internal/severity, internal/model |
Alias/advice cache (0600, schemaVersion); PURL parsing/normalization; severity normalization; canonical types (incl. Remediation/DocRef/Advice/Fix) |
Faithful-to-code notes:
- The controller (
scan.go) is what wires the dependencies: it opens thecache.Store, creates thealias.OSVClient(unless--offline), loads thecrosswalk(with a fallback to/opt/quorum/crosswalkbundled in the image), builds thecorrelate.Correlator, and injects everything intoorchestrator.Options. This keeps the orchestrator agnostic of configuration I/O. - Filter and gating happen after consensus, in the controller:
filter.Applyremoves suppressions/below-minimum fromres.Mergedbefore emitting and applying--fail-on. - The advisory layer runs after the filter, only under
--advice:enrich.Load(...).Enrich, thenrag.AttachReferences(andrag.GroundingTextfor the prompt), thenadvisor.New(...).Enrich. It only decorates the survivingres.Mergedand never re-opens the gate decision. - Safe writes: the report uses
filepath.Cleanand perm0600(it may carry sensitive detail); metrics use0644(non-sensitive counts, meant to be scraped).
5. Component diagram (Mermaid)¶
flowchart TB
user([User / CI runner]) -->|quorum scan target flags| CLI
subgraph controller["cmd/quorum — CLI / Controller (cobra)"]
CLI["scan.go<br/>validate target · resolve crosswalk/baseline<br/>wire dependencies · exit codes<br/>--metrics · --log-format · --advice"]
end
CLI -->|orchestrator.Options| ORCH
subgraph core["Core (in-process, single binary)"]
ORCH["internal/orchestrator<br/>select adapters by target · parallel fan-out<br/>version probe · per-scanner timeout · status"]
subgraph ports["Adapters — Ports & Adapters (port: adapter.Adapter) — 12 scanners"]
SCA["sca:<br/>trivy · grype"]
IAC["iac:<br/>checkov · kics · terrascan<br/>tfsec · regula"]
POL["policy:<br/>conftest (your Rego)"]
K8S["k8s:<br/>kubescape · polaris · kube-score"]
HARD["hardening:<br/>dockle"]
end
ORCH --> SCA & IAC & POL & K8S & HARD
CORR["internal/correlate<br/>enrich + CorrelationKey + Fingerprint"]
CONS["internal/consensus<br/>group · detectionCount · confidence<br/>(engine-family diversity)"]
FILT["internal/filter<br/>baseline + min-severity"]
ADV["Advisory layer (--advice only)<br/>internal/enrich · internal/rag · internal/advisor<br/>presentation-only · post-consensus"]
REP["internal/report<br/>SARIF · JSON · XML · metrics"]
SCA & IAC & POL & K8S & HARD -->|"[]model.Finding"| ORCH
ORCH -->|"[]Finding"| CORR
CORR -->|"keyed []Finding"| CONS
CONS -->|"[]MergedFinding"| FILT
FILT -->|"kept"| ADV
ADV -.->|"--advice: Remediation/References/Advice"| REP
FILT -->|"without --advice: byte-identical"| REP
end
subgraph deps["correlate dependencies"]
ALIAS["internal/alias<br/>canonical CVE/GHSA"]
CW["internal/crosswalk<br/>rule → canonical control<br/>AVD hub (cloud) · C-#### hub (k8s)"]
CACHE[("alias cache<br/>~/.cache/quorum/aliases.json (0600)")]
OSV{{"OSV.dev<br/>(off with --offline)"}}
end
CORR --> ALIAS
CORR --> CW
ALIAS --> CACHE
ALIAS -.->|graceful fallback| OSV
subgraph advdeps["advisory dependencies (--advice)"]
KB[("knowledge/*.yaml<br/>remediation + OWASP refs")]
OWASP[("knowledge/owasp/corpus.yaml<br/>digest-pinned")]
LLM{{"local/remote LLM<br/>(off by default)"}}
end
ADV --> KB
ADV --> OWASP
ADV -.->|"--advice-provider local\|remote"| LLM
REP -->|file / stdout| OUT[["report.sarif|json|xml"]]
REP -.->|Prometheus file| MET[["metrics.prom (--metrics)"]]
REP -.->|exit code 0/1/2| user
MODEL["internal/model<br/>(canonical types, no deps)"]
MODEL -.-> ports
MODEL -.-> CORR
MODEL -.-> CONS
MODEL -.-> ADV
Reading the diagram: the controller is the only layer with configuration I/O; the orchestrator is the concurrency coordinator; the 12 adapters (grouped by engine family) are pluggable through the adapter.Adapter interface; correlation/consensus/filter/report are sequential pipeline stages; the advisory layer sits after the filter and only decorates the report under --advice; internal/model is the core that everything depends on but that depends on nothing.
6. Pipeline sequence diagram (Mermaid)¶
The scan → normalize → alias → correlate → score → report flow for a typical scan.
sequenceDiagram
autonumber
actor U as User/CI
participant C as cmd/quorum (scan.go)
participant O as orchestrator
participant A as adapters (up to 12 in parallel)
participant R as correlate
participant AL as alias
participant X as crosswalk
participant K as consensus
participant F as filter
participant ADV as advisory (--advice)
participant P as report
U->>C: quorum scan target --type ... --format sarif
C->>C: validate target (reject '-', size cap)
C->>C: resolve crosswalk dir (fallback /opt/quorum/crosswalk), baseline
C->>C: wire cache + OSV (if !offline) + Correlator
C->>O: Run(ctx, target, Options{Scanners, PerScannerTime, Correlator})
O->>O: selectAdapters(target, scanners) — filter by Supports
note over O,A: fan-out: 1 goroutine per adapter, WaitGroup
par scan (parallel)
O->>A: Supports(target)? Version(ctx) [probe 60s]
note right of A: distinguishes timeout / killed(OOM) / not-installed
A-->>O: status = ran|skipped|unavailable|error|timeout
O->>A: Run(ctx, target) [per-scanner timeout + output cap]
A->>A: normalize: native output → []model.Finding
A-->>O: []model.Finding (canonical)
end
O->>O: join all findings + ScannerRun[] (status)
O->>R: Enrich(ctx, allFindings)
loop per finding
alt Type == VULN
R->>AL: Canonical(id, knownAliases)
AL->>AL: 1) local aliases → 2) cache → 3) OSV (CVE preferred)
AL-->>R: canonical id (degrades gracefully if the network fails)
else MISCONFIG / K8S_POSTURE / IMG_HARDENING
R->>R: CanonicalControl already filled? (trivy AVD, tfsec AVD via links) → keep
R->>X: else Resolve(scanner, ruleID)
X-->>R: canonical control (AVD/C-####) or Unmapped=true: never guesses a match
end
R->>R: CorrelationKey = BuildKey(f); Fingerprint = sha256(key)
end
R-->>O: []Finding with key/fingerprint
O->>K: Merge(findings)
K->>K: group by CorrelationKey
K->>K: detectionCount = distinct scanners, aggregated severity (max)
K->>K: confidence = f(count, family diversity, severity, authoritative)
K->>K: stable sort (severity, confidence, count, key)
K-->>O: []MergedFinding
O-->>C: Result{Runs, Findings, Merged, Duration}
C->>F: Apply(merged, minSeverity, baseline)
F->>F: suppress by fingerprint/correlationKey + below min-severity
F-->>C: kept (suppressions always logged)
opt --advice (presentation-only, after gating inputs are fixed)
C->>ADV: enrich.Enrich → rag.AttachReferences → advisor.Enrich
ADV->>ADV: Phase 0 templates + OWASP refs (deterministic)
ADV->>ADV: Phase 2 retrieve from digest-pinned corpus (lexical/semantic)
ADV->>ADV: Phase 1/3 local/remote recommendation (labeled, graceful) + verify-the-fix
ADV-->>C: Remediation/References/Advice attached (key/fingerprint/gate untouched)
end
C->>P: Write(buf, result, format) [+ WriteMetrics if --metrics]
P-->>C: SARIF/JSON/XML
C->>U: write file / stdout + summary (stderr)
C->>U: exit 0 (ok) | 1 (--fail-on tripped) | 2 (error)
Faithful-to-code points that the diagram reflects:
- The version probe runs with its own timeout (
Options.ProbeTime, default 60s) and classifies the failure: timeout (slow/out of memory), killed (likely OOM, viasignal: killed), or not-installed. The status never confuses "0 findings" with "did not run" (DESIGN §14, "0 findings is not proof of safety"). - A scanner's non-zero exit with output on stdout is treated as success (
runCmd): several scanners exit non-zero precisely because they found problems. - Control resolution in
correlaterespects aCanonicalControlalready filled at the source (trivy emits native AVD; tfsec derives AVD fromlinks), consulting the crosswalk only when the adapter did not bring a canonical control. That makes tfsec and trivy correlate without a crosswalk entry. - The crosswalk resolves via two hubs, DERIVED from real output (the false split > false merge rule): AVD for cloud (
aws.yaml/azure.yaml/gcp.yaml: S3/IAM/EBS/SG/RDS/KMS/CloudTrail/VPC-flow-logs, Azure Storage/Key Vault, GCP bucket/firewall/SQL) and C-#### (kubescape controls) for k8s (k8s.yaml: privilege-escalation, privileged, non-root, cpu/mem limits, probes, read-only-fs, linux-hardening, automount-SA, network-policy, host-network, host-PID/IPC, capabilities, secrets — crossing kubescape × polaris × kube-score). - RBAC stays single-engine: kubescape's RBAC analysis requires live cluster context with no equivalent cross-engine counterpart, so it does not enter the k8s crosswalk (documented; a match is never forced).
- If the
Correlatorisnil, the orchestrator still stampsCorrelationKey/Fingerprint(viaBuildKey/Fingerprint) to allow grouping — it only skips the enrichment (alias/crosswalk). - The advisory step (
--adviceonly) runs after the filter/gate, on the survivingres.Merged; it degrades gracefully (an unreachable model yields no advice and never fails the scan) and never mutates key/fingerprint/confidence/severity or the gate.
7. Recorded decisions and trade-offs¶
| Decision | Rejected alternative | Accepted trade-off |
|---|---|---|
| Single binary (modular monolith) | Microservices/FaaS | Less failure isolation between stages; gains simplicity, signability, and latency |
| Ports & Adapters via interface | Coupling scanners into the core | A little more boilerplate per adapter; gains extensibility — that is how it went from 6 to 12 scanners without touching the core |
| Fan-out with goroutines + per-scanner timeout | Sequential execution | Higher memory peak (up to 12 scanners at once); gains wall-clock time |
| Generous 60s probe | Short probe | The scan takes longer to mark a tool missing; avoids a false "unavailable" on cold start / low-RAM runners |
| False split > false merge | Aggressive merging | More apparent duplicate findings; never hides risk through a wrong merge. The crosswalk itself is derived from real output under this rule |
| Consensus by engine-family diversity | Raw detection count | Two engines of the same family count less than two distinct families; more nuance in confidence, more complexity in the score |
| Multi-hub crosswalk (AVD for cloud, C-#### for k8s) | A single universal hub | Must maintain per-domain mappings; gains real correlation across IaC and posture engines |
CanonicalControl at the source (trivy/tfsec AVD) |
Always going through the crosswalk | Two sources of truth for the control; gains tfsec↔trivy correlation without a mapping entry |
| Read-through alias cache | Always querying OSV | Possible cache staleness; gains idempotency and speed in CI, and works offline |
Crosswalk with Unmapped flag |
Inferring a match | Isolated findings when unmapped; never invents a correlation |
| User-supplied policy-as-code (conftest/Rego) | Built-in policy rules | No default policy (conftest without Rego reports error, as expected); gains full flexibility for the operator |
| Opt-in, presentation-only advisory layer | Baking AI into the core, or no guidance at all | An extra opt-in surface (flags, knowledge pack, optional model); the deterministic core stays AI-free and byte-identical without --advice, while operators who want it get remediation/OWASP/AI guidance |
8. Quality attributes (mapping)¶
- Extensibility: a new scanner = a new file under
internal/adapter+ a contract fixture; zero change to the core (proven: 6→12 adapters). - Determinism/Idempotency: keys and fingerprints are pure functions; consensus orders stably; same input ⇒ same SARIF. The advisory layer is deterministic in Phases 0/2 and reproducible in Phase 1 (
temperature=0+ fingerprint-keyed cache). - Resilience: graceful degradation on network failure (alias/OSV, and the AI advisor); explicit per-scanner status; an isolated per-scanner timeout does not take down the others; DoS caps (
QUORUM_MAX_OUTPUT_BYTES512 MiB,QUORUM_MAX_TARGET_BYTES20 GiB) against output/target bombs. - Observability: progress logs on stderr (
--log-format text|json, silenceable with--quiet), a per-scanner summary with status, suppressions always logged, optional Prometheus metrics (--metrics, textfile collector). Under--advicethe metrics addquorum_advice_enriched{kind=remediation|references|recommendation},quorum_advice_provider{provider}, andquorum_advice_fix{stage=proposed|verified}(the verify-the-fix rate). - Supply-chain security: a single artifact signed keyless (cosign/OIDC, with retry) + SLSA build-provenance attestation and attested SPDX SBOM (per image and per binary); the knowledge pack + crosswalk also get a SLSA build-provenance attestation each release (verify with
gh attestation verify knowledge/owasp/corpus.yaml); bases pinned bysha256; third-party scanner binaries (kubescape/tfsec/terrascan/regula/conftest) verified by checksum;:full/:slimimages on GHCR; see 10-infraestrutura.md. - Input security: a target starting with
-refused (argument injection);--outputwithfilepath.Cleanand perm0600; OSV id validated andurl.PathEscape; cache0600withschemaVersion; secrets redacted inMatch. For the advisory layer, remote egress is gated on explicit consent, blocked by--offline, and--fixrefuses remote (no source upload). - Testability: contract tests per adapter; dependency injection in the controller allows stubbing OSV/cache in tests; an evals harness (
internal/evals) measures advisory quality in CI; test coverage reported in CI.
9. Architectural compliance checklist¶
Use when adding/changing components to keep the architecture sound.
- [ ] A new scanner implements the whole
adapter.Adapterinterface (Name/Version/Supports/Capabilities/Run). - [ ] A new adapter calls
adapter.Registerininit()and does not collide with an existing name. - [ ]
Supportsrestricts targets correctly (image|repo|k8s); the orchestrator never runs the adapter on an unsupported target. - [ ] An adapter does not compute
CorrelationKey/Fingerprint(the responsibility ofinternal/correlate). Legitimate exception: fillingCanonicalControlwhen the tool already emits AVD (trivy/tfsec). - [ ] An adapter has a versioned fixture in
internal/adapter/testdataand a contract test. - [ ]
Runtranslates the output tomodel.Finding; no business logic operates over the scanner's raw JSON; it usesrunCmd/extraArgs(respecting the output cap and theQUORUM_<NAME>_ARGSpassthrough). - [ ] Network failures (OSV) degrade gracefully, never failing the whole scan.
- [ ] Pipeline changes preserve determinism (key = pure function of the normalized data).
- [ ] Scanner status reported correctly (
ran|skipped|unavailable|error|timeout); "0 findings" never masks "did not run". - [ ] A new crosswalk entry maps to the correct hub (AVD for cloud, C-#### for k8s) and is derived from real output; without a match, it marks
Unmappedrather than guessing. - [ ] The advisory layer stays opt-in and presentation-only: it must never touch
correlationKey/fingerprint/confidence/aggregated severity or the--fail-ongate, and without--advicethe output stays byte-identical. Remote providers stay gated on--advice-allow-egress, blocked by--offline, and refuse--fix. - [ ] No new dependency reintroduces a web frontend, a relational database, a REST API, or a long-running runtime; any AI stays in the opt-in advisory layer (never in the deterministic core).
10. Non-goals and future proposals (clearly separated)¶
Non-goals (N/A by design): a web frontend, a relational database, an HTTP REST API, authentication/user accounts, and a long-running runtime/cloud. Rationale: Quorum is a single-tenant CLI/Docker batch job whose contract is "target in, report + exit code out". These components would require an operating model incompatible with the single signable artifact and with running in a CI runner. This boundary has held from v0.2.3 to v0.8.3 — the product grew in depth (more scanners, multi-cloud/k8s consensus, a hardened supply chain, and an opt-in advisory layer), not in architectural surface.
On AI: AI is not a non-goal anymore, but it is deliberately kept off the deterministic core. The advisory layer (§2.4) is opt-in via
--advice, presentation-only, and off by default — without it the output is byte-identical and contains no AI. The core scanning/correlation/consensus/gating stays fully deterministic and model-free.
Already implemented since v0.2.3 (was roadmap, now current behavior):
- Consensus beyond SCA: correlation of MISCONFIG/IaC and K8s posture via a multi-hub crosswalk (AVD for cloud, C-#### for k8s), crossing checkov/kics/terrascan/tfsec/regula and kubescape/polaris/kube-score.
- Optional policy-as-code (Conftest/OPA): the user brings their Rego (
./policy), integrated into the same report and consensus — once planned for v1.0, already delivered. - Hardened distribution: a composite GitHub Action that cosign-verifies the image and auto-mounts
/var/run/docker.sockonimagetargets; the movingv0tag auto-advanced on each semver release; SLSA/SBOM attestations. - Advisory layer (since v0.7.4): opt-in
--advicewith deterministic remediation templates + OWASP references (Phase 0), digest-pinned OWASP retrieval (Phase 2), and an opt-in local/remote AI recommendation + verify-the-fix (Phases 1/3), plus thequorum advise-indexsubcommand, the advisory metrics, and an evals harness. The GitHub Action (action.yml) now exposes all advisory inputs and the knowledge pack gets its own SLSA attestation.
Future proposals (not implemented today):
- A separate runtime module (Falco or Tetragon): a stream model, outside this batch binary — it would be a separate product (DESIGN §2/§13).
- Image profiles (
:sca,:iac,:k8s) if the:fullsize becomes a concern (DESIGN §12).
These proposals are roadmap, not current behavior in v0.8.3.
Assumptions¶
- I took the code on the
mainbranch (v0.8.3) as the source of truth. Where DESIGN.md diverges from the code, I followed the code — for example the real signature ofalias.Resolver.Canonical(ctx, id, knownAliases), thedefaultProbeTime = 60sinorchestrator.go, the engine-family taxonomy inconsensus.scannerCategory, the fact that trivy/tfsec already emitCanonicalControlAVD at the source, and the fact that the advisory layer (internal/enrich/rag/advisor) runs after the filter and only under--advice. - I counted 12 adapters registered under
internal/adapter(trivy, grype, checkov, kics, terrascan, tfsec, regula, conftest, kubescape, polaris, kube-score, dockle), confirmed by their respectiveName()/Register(). I assumedinternal/adapter/testdataholds the contract fixtures cited in DESIGN §5/§14; the presence of the tests (adapter_test.go,realdata_test.go) confirms the pattern without inspecting each fixture. - Distribution/supply-chain details (
:fulllinux/amd64 and:slimamd64+arm64 images, cosign keyless with retry, SLSA build-provenance, attested SPDX SBOM, the knowledge-pack SLSA attestation, the composite GitHub Action, the movingv0tag viatag-major.yml) come from the product briefing and the release manifests; this document references them but did not audit them line by line inrelease.yml/action.yml/.goreleaser.yaml— see 10-infraestrutura.md for the authoritative source. - The crosswalk hub contents (AVD for
aws/azure/gcp.yaml, C-#### fork8s.yaml, control coverage) were taken from the briefing and the presence of the files undercrosswalk/; the loading mechanism (crosswalk.Load,schemaVersion,/opt/quorum/crosswalkfallback) was verified in the code. - The sequence diagram represents the happy path with a non-nil
Correlatorand--offlineoff; variations (offline, nil correlator, unavailable scanner, conftest without Rego →error, and the advisory layer degrading when the model is unreachable) are described in text.