Skip to content

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 scan runs, 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.Finding between 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 calls adapter.Register(a) in its init(); the core discovers adapters via adapter.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.
  • Supports filters by target. trivy/grype cover image+repo; dockle only image; kube-score/polaris/kubescape cover k8s (kubescape also repo); the IaC scanners (checkov/kics/terrascan/tfsec/regula) and conftest cover repo+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 from QUORUM_<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; CorrelationKey and Fingerprint are the centralized responsibility of internal/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 the links field (avdInLink), so both correlate without depending on the crosswalk.
  • Per-adapter contract test. Each adapter has versioned fixtures in internal/adapter/testdata and 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. runCmd caps output at QUORUM_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 their Match redacted (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:

scan → normalize → resolve aliases → correlate → score → report

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 by canonicalControl / 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 via quorum advise-index. scan auto-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=local queries an on-host OpenAI-compatible endpoint (e.g. Ollama) for a natural-language recommendation, and --fix=suggest proposes 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 via temperature=0 + an on-disk cache keyed by fingerprint+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=remote calls an external API (auth via QUORUM_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 the cache.Store, creates the alias.OSVClient (unless --offline), loads the crosswalk (with a fallback to /opt/quorum/crosswalk bundled in the image), builds the correlate.Correlator, and injects everything into orchestrator.Options. This keeps the orchestrator agnostic of configuration I/O.
  • Filter and gating happen after consensus, in the controller: filter.Apply removes suppressions/below-minimum from res.Merged before emitting and applying --fail-on.
  • The advisory layer runs after the filter, only under --advice: enrich.Load(...).Enrich, then rag.AttachReferences (and rag.GroundingText for the prompt), then advisor.New(...).Enrich. It only decorates the surviving res.Merged and never re-opens the gate decision.
  • Safe writes: the report uses filepath.Clean and perm 0600 (it may carry sensitive detail); metrics use 0644 (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, via signal: 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 correlate respects a CanonicalControl already filled at the source (trivy emits native AVD; tfsec derives AVD from links), 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 Correlator is nil, the orchestrator still stamps CorrelationKey/Fingerprint (via BuildKey/Fingerprint) to allow grouping — it only skips the enrichment (alias/crosswalk).
  • The advisory step (--advice only) runs after the filter/gate, on the surviving res.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_BYTES 512 MiB, QUORUM_MAX_TARGET_BYTES 20 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 --advice the metrics add quorum_advice_enriched{kind=remediation|references|recommendation}, quorum_advice_provider{provider}, and quorum_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 by sha256; third-party scanner binaries (kubescape/tfsec/terrascan/regula/conftest) verified by checksum; :full/:slim images on GHCR; see 10-infraestrutura.md.
  • Input security: a target starting with - refused (argument injection); --output with filepath.Clean and perm 0600; OSV id validated and url.PathEscape; cache 0600 with schemaVersion; secrets redacted in Match. For the advisory layer, remote egress is gated on explicit consent, blocked by --offline, and --fix refuses 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.Adapter interface (Name/Version/Supports/Capabilities/Run).
  • [ ] A new adapter calls adapter.Register in init() and does not collide with an existing name.
  • [ ] Supports restricts targets correctly (image|repo|k8s); the orchestrator never runs the adapter on an unsupported target.
  • [ ] An adapter does not compute CorrelationKey/Fingerprint (the responsibility of internal/correlate). Legitimate exception: filling CanonicalControl when the tool already emits AVD (trivy/tfsec).
  • [ ] An adapter has a versioned fixture in internal/adapter/testdata and a contract test.
  • [ ] Run translates the output to model.Finding; no business logic operates over the scanner's raw JSON; it uses runCmd/extraArgs (respecting the output cap and the QUORUM_<NAME>_ARGS passthrough).
  • [ ] 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 Unmapped rather than guessing.
  • [ ] The advisory layer stays opt-in and presentation-only: it must never touch correlationKey/fingerprint/confidence/aggregated severity or the --fail-on gate, and without --advice the 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.sock on image targets; the moving v0 tag auto-advanced on each semver release; SLSA/SBOM attestations.
  • Advisory layer (since v0.7.4): opt-in --advice with 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 the quorum advise-index subcommand, 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 :full size becomes a concern (DESIGN §12).

These proposals are roadmap, not current behavior in v0.8.3.


Assumptions

  1. I took the code on the main branch (v0.8.3) as the source of truth. Where DESIGN.md diverges from the code, I followed the code — for example the real signature of alias.Resolver.Canonical(ctx, id, knownAliases), the defaultProbeTime = 60s in orchestrator.go, the engine-family taxonomy in consensus.scannerCategory, the fact that trivy/tfsec already emit CanonicalControl AVD at the source, and the fact that the advisory layer (internal/enrich/rag/advisor) runs after the filter and only under --advice.
  2. I counted 12 adapters registered under internal/adapter (trivy, grype, checkov, kics, terrascan, tfsec, regula, conftest, kubescape, polaris, kube-score, dockle), confirmed by their respective Name()/Register(). I assumed internal/adapter/testdata holds 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.
  3. Distribution/supply-chain details (:full linux/amd64 and :slim amd64+arm64 images, cosign keyless with retry, SLSA build-provenance, attested SPDX SBOM, the knowledge-pack SLSA attestation, the composite GitHub Action, the moving v0 tag via tag-major.yml) come from the product briefing and the release manifests; this document references them but did not audit them line by line in release.yml/action.yml/.goreleaser.yaml — see 10-infraestrutura.md for the authoritative source.
  4. The crosswalk hub contents (AVD for aws/azure/gcp.yaml, C-#### for k8s.yaml, control coverage) were taken from the briefing and the presence of the files under crosswalk/; the loading mechanism (crosswalk.Load, schemaVersion, /opt/quorum/crosswalk fallback) was verified in the code.
  5. The sequence diagram represents the happy path with a non-nil Correlator and --offline off; variations (offline, nil correlator, unavailable scanner, conftest without Rego → error, and the advisory layer degrading when the model is unreachable) are described in text.