Skip to content

Backend

The "backend" of Quorum (v0.8.3) is the set of Go packages that make up the CLI/Docker. There is no application server, no long-running process (daemon), and no HTTP API: the backend is a library of internal/* packages orchestrated by a command-line binary (cmd/quorum). Each run of quorum scan is an ephemeral process that fans out to 12 external scanners (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest), normalizes everything into the canonical model model.Finding, resolves vulnerability aliases, correlates equivalent findings, computes a consensus score, and emits a SARIF/JSON/XML report (optionally Prometheus metrics too). The design principle that permeates the whole codebase is "false split > false merge" (prefer never merging two distinct findings over incorrectly merging two different findings) and "0 findings is not proof of safety" (absence of results is never the same as proven safety).

Since v0.8.3 there is also an opt-in advisory layer (--advice): a strictly presentational set of packages (internal/enrich, internal/rag, internal/advisor) that attach remediation templates, OWASP references, and — optionally — an AI-generated recommendation. It is off by default and never touches correlationKey / fingerprint / confidence / aggregated severity / the --fail-on gate; without --advice the output is byte-identical. The deterministic core still has no AI.

This document maps the enterprise "Backend" template (Layers, Services, Repositories, Controllers, Middlewares, Workers, Jobs, Cache, Queue/Messaging) onto the reality of the code. When the template asks for a concept typical of web/distributed applications that does not exist in this product, the item is marked N/A with a technical justification.

Related documents: Architecture · Data model · CLI / Commands · AI framing · this file is the canonical reference for the Go code.

Revision: 2026-07-04 · reference version: v0.8.3.


1. Layered overview

The backend is strictly layered, with dependencies pointing always inward (from the CLI binary toward the domain). No domain package imports cmd/quorum, and the model package imports no one — it is the stable core that everyone shares.

flowchart TD
    subgraph CLI["Entry Layer (Controllers) — cmd/quorum"]
        MAIN[main.go]
        ROOT[root.go<br/>cobra root + list-scanners + advise-index]
        SCAN[scan.go<br/>scan command + DoS guards]
    end

    subgraph APP["Application / Orchestration Layer"]
        ORCH[orchestrator<br/>fan-out + per-scanner status]
    end

    subgraph SVC["Domain Services Layer"]
        CORR[correlate<br/>identity + key]
        CONS[consensus<br/>grouping + score]
        ALIAS[alias<br/>CVE/GHSA resolution]
        CW[crosswalk<br/>rule -> canonical control]
        FILT[filter<br/>baseline + min-severity]
        REP[report<br/>SARIF/JSON/XML + metrics]
    end

    subgraph ADV["Advisory Layer (opt-in, presentation-only) — --advice"]
        ENR[enrich<br/>Phase 0: remediation templates + OWASP refs]
        RAG[rag<br/>Phase 2: digest-pinned OWASP corpus]
        ADVI[advisor<br/>Phase 1/3: local/remote LLM + verify-the-fix]
    end

    subgraph GW["Gateways Layer (Repositories)"]
        AD[adapter<br/>wrappers for the 12 scanner CLIs]
        CACHE[cache<br/>file-based JSON KV]
        OSV[alias/osv<br/>OSV.dev HTTP client]
    end

    subgraph CORE["Core / pure utilities"]
        MODEL[model<br/>Finding / MergedFinding]
        SEV[severity<br/>normalization table]
        PURL[purl<br/>package identity]
    end

    EXT[(OSS scanners<br/>trivy/grype/checkov/kics/<br/>dockle/kubescape/polaris/<br/>kube-score/terrascan/tfsec/<br/>regula/conftest)]
    NET[(OSV.dev API)]
    AIEP[(OpenAI-compatible<br/>endpoint: Ollama/remote)]
    FS[(Filesystem:<br/>cache, crosswalk YAML,<br/>knowledge pack, OWASP corpus,<br/>policy Rego, .quorumignore,<br/>output, metrics)]

    MAIN --> ROOT --> SCAN
    SCAN --> ORCH
    SCAN --> CORR & FILT & REP & CW & ALIAS & CACHE
    SCAN -.opt-in.-> ENR & RAG & ADVI
    ORCH --> AD
    ORCH --> CORR
    ORCH --> CONS
    CORR --> ALIAS & CW
    ALIAS --> CACHE & OSV
    AD --> EXT
    OSV --> NET
    ADVI --> AIEP
    RAG --> AIEP
    CACHE --> FS
    CW --> FS
    ENR & RAG --> FS
    REP --> FS
    SCAN -.shared model.-> MODEL
    CORR & CONS & FILT & REP & AD --> MODEL
    ENR & RAG & ADVI --> MODEL
    AD & CONS --> SEV
    AD & CORR --> PURL

Layers table

Layer Package(s) Responsibility Depends on
Entry (Controllers) cmd/quorum Flag parsing (cobra), target validation/inference, DoS guards (target size, argument injection), dependency wiring, exit codes the whole application/services layer
Application / Orchestration internal/orchestrator Select adapters, parallel fan-out, timeout/probe, collect findings, trigger enrich+consensus adapter, correlate, consensus, model
Domain services correlate, consensus, alias, crosswalk, filter, report Business logic: identity, score, alias/control resolution, suppression, serialization (report + metrics) model, severity, purl, cache, osv
Advisory (opt-in) enrich, rag, advisor Presentation-only: remediation templates + OWASP references + optional LLM recommendation/fix. Never affects gating model, cache; optional OpenAI-compatible endpoint
Gateways (Repositories) adapter, cache, alias/osv Access to external resources: CLI processes, cache file, OSV HTTP model, severity, purl
Core / utilities model, severity, purl Canonical types and pure, side-effect-free functions nothing (model is a leaf)

Repo golden rule (internal/model/model.go): "Nothing in the pipeline operates on a scanner's raw JSON — every adapter normalizes into Finding, and every later stage speaks Finding and MergedFinding only." The Raw map[string]any is kept but marked json:"-" (not serialized by default).


2. Execution pipeline (from command to report)

The full path of a scan run, with the packages responsible for each stage:

sequenceDiagram
    participant U as User/CI
    participant C as cmd/quorum (scan.go)
    participant O as orchestrator
    participant A as adapters (goroutines)
    participant E as correlate (Correlator)
    participant K as consensus
    participant F as filter
    participant V as advisory (enrich/rag/advisor)
    participant R as report

    U->>C: quorum scan <target> [flags]
    C->>C: validateTargetRef / resolveTargetType / checkTargetSize
    C->>C: Parse(fail-on,min-sev) / validate log-format / validate advice flags
    C->>C: LoadBaseline / crosswalk.Load / cache.Open
    C->>C: correlate.New(alias.New(cache,osv), crosswalk)
    C->>O: orchestrator.Run(ctx, target, Options)
    par parallel fan-out
        O->>A: probe Version() (timeout 60s)
        A-->>O: ran|skipped|unavailable|error|timeout
        O->>A: Run() (timeout = --timeout, capped output)
        A-->>O: []model.Finding
    end
    O->>E: Enrich(ctx, allFindings)
    E->>E: resolveVuln (alias) / resolveControl (crosswalk)
    E->>E: BuildKey + Fingerprint (sha256)
    O->>K: consensus.Merge(allFindings)
    K-->>O: []MergedFinding (score + ordering)
    O-->>C: Result{Runs, Findings, Merged}
    C->>F: filter.Apply(min-severity, baseline)
    opt --advice (presentation-only, after gating inputs are fixed)
        C->>V: enrich.Enrich / rag.AttachReferences / advisor.Enrich
        V-->>C: Remediation / References / Advice attached in place
    end
    C->>R: report.Write(SARIF|JSON|XML) [+ WriteMetrics]
    C->>U: report + summary + exit code (0/1/2)

Stages in prose:

  1. Controller (scan.go) validates flags, rejects malicious targets (leading -), applies the target-size cap, infers the target type, loads baseline/crosswalk/cache and injects the dependencies into the Correlator. It also validates the advisory flags (--advice-provider, --fix, egress consent) before doing any work.
  2. Orchestrator selects the adapters that support the target, runs each in a goroutine, does the version probe and the run with a timeout (and a stdout cap), and collects the canonical Findings.
  3. Correlate enriches (resolves aliases for VULN, controls for MISCONFIG/K8S_POSTURE/ IMG_HARDENING) and stamps CorrelationKey + Fingerprint.
  4. Consensus groups by CorrelationKey and computes Confidence/DetectionCount.
  5. Filter applies --min-severity and the .quorumignore baseline.
  6. Advisory (only if --advice) runs after consensus and gating inputs are fixed: it attaches remediation templates, OWASP references, and optionally an AI recommendation/fix — strictly presentational, never mutating the key/fingerprint/confidence/severity/gate.
  7. Report serializes the report and, if --metrics was passed, writes the Prometheus metrics (including the advisory series when --advice is on); the controller decides the exit code via --fail-on.

3. Services (domain logic)

The template's "services" correspond to the domain packages. None of them holds long-lived mutable global state; they are functions/objects instantiated per run.

3.1 internal/orchestrator — orchestration service

Key file: orchestrator.go. Exposes Run(ctx, target, Options) (*Result, error).

  • Options carries: Scanners []string, PerScannerTime (per-scanner timeout), ProbeTime (version-probe timeout, default 60s via defaultProbeTime), Correlator, and Logf.
  • Result aggregates Runs []ScannerRun, Findings []model.Finding (raw, for JSON detail), Merged []model.MergedFinding, Duration.
  • ScannerRun.Status takes ran | skipped | unavailable | error | timeout — status transparency is deliberate: "0 vulns must never look like scan didn't run".
  • runOne distinguishes probe failure causes: timeout (DeadlineExceeded), killed by signal/OOM (killedSignal detects "signal: killed"), and missing binary, emitting actionable error messages (e.g. "raise the container's memory limit").
  • When Correlator == nil, the orchestrator still computes CorrelationKey/Fingerprint via correlate.BuildKey/Fingerprint so consensus grouping works even without enrichment.

3.2 internal/correlate — identity service

Files: correlate.go (enrichment) and key.go (deterministic key).

  • Correlator{Alias, Crosswalk} — both dependencies may be nil (degrades to id-as-is / unmapped).
  • Enrich iterates over the findings: TypeVulnresolveVuln (alias); TypeMisconfig/ TypeK8sPosture/TypeImgHardeningresolveControl (crosswalk). Then applies BuildKey + Fingerprint.
  • BuildKey is pure and deterministic, with a per-type key (there is no universal key):
Type CorrelationKey shape
VULN VULN\|<uppercase VULNID>\|<name@version from PURL>
MISCONFIG MISCONFIG\|<file basename>\|<resource type>\|<control>
K8S_POSTURE K8S\|<ns/kind/name>\|<control>
IMG_HARDENING IMGH\|<control>
SECRET SECRET\|<normalized path>\|<line>\|<ruleId>
others OTHER\|<scanner>\|<title>
  • Cross-engine granularity note: the MISCONFIG key uses basename + resource type + control (engines disagree on path and resource identity); the K8S_POSTURE key uses object (ns/kind/name) + control, deliberately without the container, because kubescape reports at the workload level and polaris per container — including the container would block cross-engine consensus (documented in key.go).
  • Fingerprint(key) = sha256(key) in hex — it is the partialFingerprints["quorum/v1"] in SARIF.
  • controlKey prefers the resolved canonical control; when unmapped, it uses UNMAPPED:<scanner>:<ruleId> to never silently merge distinct findings.

3.3 internal/consensus — score service

File: consensus.go. Exposes Merge([]Finding) []MergedFinding.

  • Groups by CorrelationKey, preserving first-appearance order.
  • For each group it computes: DetectedBy (distinct scanners), Severity (aggregated max), DetectionCount, Unmapped (any member), Confidence.
  • Confidence formula (DESIGN §9), weights: count 0.35, diversity 0.25, severity 0.25, authoritative 0.15.
  • count: log(1+n)/log(5) — diminishing returns.
  • diversity: distinct engine families (scannerCategory: sca, iac, k8s, hardening, policy); 1 family ≈ 0.33, 2 ≈ 0.66, 3+ = 1.0 — two different engines are worth more than two identical ones.
  • authoritative: 1.0 if Confirmed or if it is a CVE with CVSS > 0.
  • Sorts by (severity, confidence, detectionCount, correlationKey) descending for stable, useful output.

Current scannerCategory map (12 scanners → 5 families):

Family Scanners
sca trivy, grype
iac checkov, kics, terrascan, tfsec, regula
k8s kubescape, polaris, kube-score
hardening dockle
policy conftest

3.4 internal/alias — identifier-resolution service

Files: resolver.go (chain) and osv.go (OSV.dev client).

  • chainResolver resolves a vuln id to the canonical form, preferring CVE, in 3 layers:
  • aliases already present in the finding (preferCVE),
  • local cache (cache.Store),
  • OSV.dev arbitration (osvSource).
  • Never returns an error: degrades gracefully to the best available id on a network failure.
  • --offline passes osv = nil, disabling layer 3 (uses only local aliases + cache).
  • OSVClient has retry with exponential backoff, an HTTP timeout, and classifies failures as retryable (network, 429, 5xx) or not.

3.5 internal/crosswalk — control-mapping service

File: crosswalk.go. Loads *.yaml/*.yml from a directory and indexes "scanner|ruleID"Resolution{Control, Category, CWE, Title}.

  • A missing directory is not an error (runs without a custom crosswalk).
  • Resolve returns ok=false when there is no mapping — the caller keeps the finding isolated and marks it Unmapped ("never guess a match", DESIGN §6).
  • The default directory is ./crosswalk with automatic fallback to /opt/quorum/crosswalk (the Docker image bundle), via resolveCrosswalkDir in scan.go.
  • Consensus enabled beyond SCA: the mappings are derived from real scanner output (applying "false split > false merge"). The bundle includes:
  • crosswalk/aws.yaml, azure.yaml, gcp.yamlAVD hub; covers S3/IAM/EBS/SG/RDS/KMS/ CloudTrail/VPC-flow-logs (AWS), Storage/Key Vault (Azure), bucket/firewall/SQL (GCP).
  • crosswalk/k8s.yaml — kubescape C-#### hub; correlates kubescape × polaris × kube-score on controls such as privilege-escalation, privileged, non-root, cpu/mem limits, probes, read-only-fs, linux-hardening, service-account automount, network-policy, host-network, host-PID/IPC, capabilities, and secrets.
  • tfsec auto-correlates with trivy without a crosswalk: it emits native AVD (extracts AVD-<PROV>-<n> from the link and stores it in CanonicalControl). See tfsec.go.
  • RBAC stays single-engine (kubescape's RBAC requires cluster context; documented as a limitation, not merged with other engines).

3.6 internal/filter — post-processing/gating service

File: filter.go. Applies the minimum-severity cut and the baseline before reporting/gating.

  • Baseline matches by Fingerprint OR CorrelationKey (the user can copy either from the report); the .quorumignore file (default), # comments, blank lines ignored.
  • LoadBaseline distinguishes "missing" (ok=false) from "present but empty" — the controller requires existence only when --baseline was passed explicitly.
  • Apply returns Result{Kept, SuppressedBaseline, SuppressedSeverity} — it always logs suppressions (a suppressed finding is still a finding, DESIGN §14).

3.7 internal/report — serialization service

Files: report.go (format dispatch), sarif.go, json.go, xml.go, metrics.go.

  • Write(w, res, format) dispatches to SARIF (primary), JSON, or XML.
  • ParseFormat validates --format.
  • SARIF carries partialFingerprints["quorum/v1"] derived from Fingerprint.
  • WriteMetrics(w, res, adv) (metrics.go) emits the result in Prometheus text format, suited to node_exporter's textfile collector or a Pushgateway — exportable telemetry for a CLI with no long-running process to scrape. Series emitted: quorum_scan_duration_seconds, quorum_scanner_up{scanner,status}, quorum_scanner_findings{scanner}, quorum_scanner_duration_seconds{scanner}, quorum_findings_after_consensus, quorum_findings_total{severity}, and quorum_multi_detected. When --advice is on, it also emits the advisory series (see §3.8 and §5.1). Triggered by the --metrics <file> flag (see §5.1).

3.8 Advisory services (internal/enrich, internal/rag, internal/advisor) — opt-in

The advisory layer is presentation-only and off by default. It runs after consensus and the gating inputs are fixed, attaching fields to MergedFinding in place without ever touching CorrelationKey / Fingerprint / Confidence / aggregated severity / the --fail-on gate. Without --advice, the report is byte-identical. The deterministic core has no AI; the AI parts (Phases 1 and 3) are strictly opt-in and off by default. Every AI attachment is labeled "AI-generated, advisory only". See AI framing and the AI proposal.

3.8.1 internal/enrich — Phase 0 (deterministic, no model)

File: enrich.go. Load(dir) reads the curated knowledge pack (knowledge/*.yaml: aws/azure/gcp/k8s/image/categories) into a KB; KB.Enrich(merged) attaches a model.Remediation (curated fix template) plus model.DocRef OWASP references, matched by canonicalControl / ruleId / category / type. No model, fully deterministic. KB.Len() reports how many entries were loaded. The default directory is ./knowledge with fallback to the image-bundled /opt/quorum/knowledge (via resolveKnowledgeDir), configurable with --knowledge.

3.8.2 internal/rag — Phase 2 (RAG-as-artifact, deterministic)

Files: rag.go, lexical.go, embed.go. Retrieval from a versioned, digest-pinned OWASP corpus (knowledge/owasp/corpus.yaml). Load(dir) reads the corpus and verifies its ContentDigest; a tampered corpus fails the pin and is refused. Retrieval is lexical by default (no model) via NewLexical; semantic (embeddings, NewSemantic) only when the corpus ships vectors and a local endpoint is configured. scan auto-picks semantic when corpus.HasEmbeddings() and --advice-provider=local. AttachReferences(merged, retriever, k) attaches the top-k OWASP passages as model.DocRefs; GroundingText produces the grounding string that feeds the Phase 1 prompt. Embeddings are excluded from the content digest, so the pin is preserved after quorum advise-index.

3.8.3 internal/advisor — Phase 1 (local LLM) and Phase 3 (remote provider), opt-in

Files: advisor.go, client.go, prompt.go, verify.go. Advisor.Enrich(ctx, merged) queries an OpenAI-compatible endpoint for a natural-language recommendation, attaching a model.Advice and returning Stats{Advised, FixProposed, FixVerified}. It never returns a scan-failing error: on the first connectivity failure it logs once and stops (graceful degradation — the report ships without AI advice and the scan never fails).

  • Phase 1, local (NewLocalClient, --advice-provider=local): queries an on-host endpoint (e.g. Ollama). Reproducible via temperature=0 plus an on-disk cache keyed by fingerprint+provider+model.
  • --fix=suggest: proposes a patch that must pass a verify-the-fix re-scan (verify.go): apply to a temp copy, re-scan with the same scanner (via the Rescanner interface, backed by the adapter registry in cmd/quorum/advisor.go), and keep the patch only if the finding is gone and the file still parses. It never auto-applies. Fixes are scoped to repo/k8s targets (not image targets).
  • Phase 3, remote (NewRemoteClient, --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 (which would upload source). Only the normalized finding is sent — never source code.

4. Repositories (external-resource access gateways)

Quorum has no relational-database repository. The "repository" role (an abstraction over access to a persistent or I/O external resource) is played by three gateways:

Gateway Package External resource Pattern
Scanner adapters internal/adapter CLI processes (12 scanners) Interface + Registry + exec.CommandContext
Alias cache internal/cache On-disk JSON file KV store with atomic flush
OSV client internal/alias/osv.go OSV.dev HTTP API HTTP client with retry/backoff

The alias cache is also reused (with a distinct default path) by the advisory layer as the AI-advice cache — see §3.8.3 and §9.

4.1 Adapters as scanner gateways

The adapter package is the gateway to the "outside world" of the scanners. Each adapter implements the Adapter interface:

type Adapter interface {
    Name() string
    Version(ctx context.Context) (string, error)
    Supports(target Target) bool
    Capabilities() []Capability
    Run(ctx context.Context, target Target) ([]model.Finding, error)
}
  • Registry: each adapter calls Register(&xxx{}) in its init(); Get/All/Names expose the registry. A duplicate name panics (a programming error).
  • Process execution: runCmd runs the binary, folds stderr into the error, and treats non-zero exit with non-empty stdout as success (several scanners exit != 0 precisely because they found problems). toolVersion does the version probe and detects a missing binary.
  • Normalization: each adapter has its own parser (e.g. trivy.parse) that translates the native JSON into []model.Finding, using severity.FromLabel/FromCVSS/FromDockle and purl.Build. Adapters never compute CorrelationKey — that is centralized in the correlator.
  • Contract tests: each adapter has a contract test against fixtures in internal/adapter/testdata (see adapter_test.go, realdata_test.go).

Registered adapters (12): trivy, grype (sca family); checkov, kics, terrascan, tfsec, regula (iac / MISCONFIG); kubescape, polaris, kube-score (k8s / K8S_POSTURE); dockle (hardening / IMG_HARDENING); conftest (policy — policy-as-code).

Per-adapter notes relevant to the backend:

  • conftest has no built-in rules: it evaluates your own Rego (in ./policy by default, or via QUORUM_CONFTEST_ARGS="--policy <dir>"). With no policies it errors and the scanner is reported as error — expected, since policy-as-code is opt-in.
  • tfsec derives the AVD from the link and stores it in CanonicalControl, correlating with trivy without a crosswalk.
  • kubescape writes the report to a temp file (--output) and treats non-zero exit with a valid report as success.

Package shared helpers (adapter.go)

Helper Role
maxOutputBytes() / capWriter DoS cap on scanner stdout. runCmd buffers into capWriter (default 512 MiB, defaultMaxOutputBytes, override via QUORUM_MAX_OUTPUT_BYTES); if exceeded, it aborts with an error instead of OOM. capWriter still returns len(p) so the child process's pipe never blocks, marking over and detecting truncation after the run.
extraArgs(name) / splitArgs(s) Per-scanner argument passthrough via env QUORUM_<NAME>_ARGS (e.g. QUORUM_CHECKOV_ARGS="--bc-api-key <key>" unlocks Prisma Cloud/Bridgecrew policies; QUORUM_CONFTEST_ARGS="--policy <dir>"). splitArgs does a shell-like split (respects single/double quotes, no variable expansion). The value is operator-controlled — same trust level as the flags. Applied by all adapters.
redactSecretText(s) Secret redaction: masks long tokens ([A-Za-z0-9+/=_-]{12,}), preserving the first 4 chars + …REDACTED…. Used by trivy to carry a SECRET's context (line) without leaking the value ("matched (redacted): " + redactSecretText(s.Match)).
runCmd / toolVersion Process execution and version probe (described above).

4.2 Cache as a repository

internal/cache/store.go is a map[string]string KV persisted to a JSON file — a deliberate choice to give alias resolution idempotency/speed without pulling in a CGO database.

  • Open(path) loads; a missing/corrupt file becomes an empty cache (never breaks a scan).
  • Put writes with an atomic rename (.tmp → destination, perm 0600) and creates parent directories on demand; flush errors are swallowed on purpose (the cache is an optimization, not a source of failure). The file carries a schemaVersion for format invalidation.
  • Safe for concurrent use (sync.RWMutex) — relevant because the adapters run in parallel and share the resolver.
  • Default path: ~/.cache/quorum/aliases.json (via os.UserCacheDir), configurable with --cache.
  • The same store type backs the AI-advice cache (--advice-cache, default ~/.cache/quorum/advice.json), keyed by fingerprint+provider+model for reproducible CI runs.

4.3 OSV client as a remote repository

internal/alias/osv.go is the HTTP gateway to OSV.dev (GET /v1/vulns/<id>), the "arbiter of last resort" in the alias chain. The id is validated and passes through url.PathEscape before the request. Details in §3.4.


5. Controllers (cobra commands)

The entry layer (cmd/quorum) is Quorum's "controller": it translates arguments/flags into service calls, wires dependencies, and defines exit codes.

File Role
main.go Entry point; runs the root, prints the error and exits with exit 2 on usage/runtime failure
root.go Defines the root quorum command (cobra) and the list-scanners + advise-index subcommands
scan.go Defines the scan command, validates/guards the target, wires all dependencies, and runs the pipeline
advisor.go Wires the advisory layer for scan: the adapterRescanner (verify-the-fix re-scan via the adapter registry) and the AI-advice cache path
advise_index.go Defines the advise-index command (embeds the OWASP corpus, preserving the digest pin)

5.1 Commands

  • scan <target> (ExactArgs(1)): the main command. Core flags: --type (image|repo|k8s, inferred if omitted), --scanners, --format/-f (sarif|json|xml), --output/-o, --fail-on, --min-severity, --baseline (.quorumignore), --crosswalk (./crosswalk + fallback /opt/quorum/crosswalk), --cache, --metrics <file> (writes Prometheus textfile metrics), --log-format (text|json, progress log format on stderr), --timeout (default 5m), --offline, --quiet/-q.

Advisory flags (all opt-in; without --advice the output is byte-identical):

Flag Default Meaning
--advice false Attach deterministic remediation templates + OWASP references (advisory; does not affect gating)
--advice-provider none AI recommendation provider: none|local|remote
--advice-endpoint http://localhost:11434/v1 OpenAI-compatible base URL (local by default; set the remote URL for remote)
--advice-model qwen2.5-coder:7b Model id for --advice-provider=local
--advice-embed-model nomic-embed-text Embedding model for semantic OWASP retrieval (only when the corpus ships embeddings)
--advice-cache ~/.cache/quorum/advice.json AI-advice cache file (keyed by fingerprint+model)
--advice-max 50 Max findings sent to the AI provider per run (0 = no cap)
--advice-allow-egress false Consent to send findings off-host; required for --advice-provider=remote
--fix off AI suggested-fix mode: off|suggest (suggest emits a verify-the-fix patch; never auto-applies)
--knowledge ./knowledge Directory of advisory knowledge-pack files (remediation templates + OWASP refs)

Advisory-flag validation (in scan.go): --advice-provider must be none|local|remote and --fix must be off|suggest; --advice-provider=remote is blocked by --offline, requires --advice-allow-egress and QUORUM_ADVICE_API_KEY, and refuses --fix (it would upload source).

  • advise-index (NoArgs): embeds the OWASP RAG corpus, turning the default lexical retrieval into semantic. It reads the corpus, embeds every chunk via a local OpenAI-compatible embeddings endpoint (e.g. Ollama), and writes it back with per-chunk vectors. Embeddings are excluded from the content digest, so the pin is preserved. Flags: --corpus, --out, --advice-endpoint, --advice-embed-model.
  • list-scanners: lists the registered adapters and their capabilities (Capabilities().Type).

Beyond the flags, behavior is tunable via environment variables (operator trust level): QUORUM_<NAME>_ARGS (per-scanner passthrough, §4.1), QUORUM_MAX_OUTPUT_BYTES (stdout cap, default 512 MiB), QUORUM_MAX_TARGET_BYTES (on-disk target-size cap, default 20 GiB; 0 disables), and QUORUM_ADVICE_API_KEY (auth for --advice-provider=remote).

5.2 Target inference and validation

  • validateTargetRef: refuses a target starting with - (avoids argument injection into a downstream scanner; steer ./-name to a literal path).
  • checkTargetSize: for repo/k8s targets, does a walk that stops as soon as the cap (QUORUM_MAX_TARGET_BYTES, default 20 GiB) is crossed; image targets are skipped.
  • resolveTargetType: if --type is omitted, an existing on-disk path → repo; otherwise → image. Accepts aliases (fs/dir → repo; kubernetes/manifests → k8s).

5.3 Exit codes (the gating contract)

Exit Meaning Origin in code
0 OK — no finding reached --fail-on normal runScan return
1 Gate tripped — a finding ≥ --fail-on os.Exit(1) in runScan
2 Usage/runtime error os.Exit(2) in main.go

The advisory layer never changes the exit code: a scan with --advice yields the exact same gate result as without it.

5.4 Progress output, summary, and artifacts

  • The controller injects a Logf that writes to stderr (silenceable via --quiet) in two formats: text (prefix [quorum]) or json (line {ts,level,msg}), per --log-format. Under --advice it also logs the advisory stages (advice: knowledge=…, advice(rag): corpus=…, advice(ai): provider=… fixes: N verified / M proposed).
  • It prints a human summary (per-scanner status, counts by severity, multi-detected, time) — always ending with "0 findings is not proof of safety".
  • --output is normalized with filepath.Clean and written with perm 0600 (it may carry sensitive finding detail). The --metrics file is written with 0644 (non-sensitive counts, meant to be scraped).

6. Middlewares — N/A (with equivalents)

N/A. There is no HTTP middleware chain nor interception framework, because there is no HTTP server. The roles middlewares would fill in a web application are covered by CLI/Go-runtime mechanisms:

Typical middleware role Quorum equivalent
Authentication/authorization N/A — no accounts/users (local CI tool). The one credential, QUORUM_ADVICE_API_KEY, is an outbound API key for the opt-in remote provider, not inbound auth
Request logging Logf injected by the controller (stderr, text/json via --log-format)
Timeout/cancellation context.Context propagated; context.WithTimeout per scanner and on the probe
Central error handling runCmd/toolVersion fold stderr; main.go centralizes exit 2
Panic recovery No global recover; panics are programming errors (e.g. duplicate registration)
Rate limiting Partial — backoff/retry in the OSV client (osv.go) and in the advisor HTTP client
Abuse/DoS protection validateTargetRef (argument injection), checkTargetSize and capWriter (size caps); egress consent gate for the remote advisor

7. Workers (fan-out concurrency)

Quorum's "workers" are ephemeral goroutines created by the orchestrator for the parallel fan-out of scanners. There is no persistent worker pool nor work queue.

flowchart LR
    R[orchestrator.Run] -->|wg.Add per adapter| G1[goroutine: trivy]
    R --> G2[goroutine: grype]
    R --> G3[goroutine: checkov]
    R --> Gn[goroutine: ... up to 12]
    G1 -->|mu.Lock| AGG[(all slice + Runs)]
    G2 --> AGG
    G3 --> AGG
    Gn --> AGG
    AGG -->|wg.Wait| ENR[Enrich -> Merge]

Mechanics (in orchestrator.go):

  • One goroutine per selected adapter; a sync.WaitGroup synchronizes completion.
  • Results are aggregated under a sync.Mutex (res.Runs and the all slice).
  • Each worker (runOne) does: Supports → version probe with a timeout (ProbeTime, 60s default) → Run with a timeout (PerScannerTime = --timeout) → classify status.
  • The context.Context flows from the controller; each worker derives sub-contexts with WithTimeout and cancels at the end (cancelVer() / defer cancel).
  • There is no explicit parallelism limit (concurrency = number of supported adapters, today ≤ 12).

Concurrency-guarantee checklist:

  • [x] Shared access protected by a mutex (res.Runs, all).
  • [x] Alias cache safe for concurrency (cache.Store with RWMutex).
  • [x] Per-scanner and per-probe timeouts isolated via context.
  • [x] Results deterministically ordered after wg.Wait (sort.Slice on Runs).
  • [x] Per-scanner stdout cap (capWriter) to avoid OOM under pathological output.
  • [ ] Configurable parallelism limit (does not exist — see Assumptions / future proposal).

The advisory layer runs after the fan-out (single-threaded, in the controller), so it adds no concurrency to this section. The AI provider is queried sequentially per finding, capped by --advice-max.


8. Jobs / scheduling — N/A

N/A. There is no scheduler, internal cron, background job, nor recurring task inside the process. Quorum is one-shot: one process per invocation, terminating when it emits the report.

Scheduling, when desired, is external to the backend: GitHub Actions schedules/triggers the CLI (via the action.yml composite or the :full/:slim image). Warm-ups like the grype database cache happen at build time of the Docker image, not as a runtime job. Likewise, quorum advise-index (embedding the OWASP corpus) is a one-off pre-processing command, not a scheduled job.

Future proposal (clearly separated): a --watch mode or integration with external schedulers could be added without changing the core, but does not exist today.


9. Cache

There are two cache layers, both backed by the same internal/cache store (§4.2): the alias cache and the opt-in AI-advice cache.

Aspect Alias cache AI-advice cache (--advice only)
Backend JSON file (map[string]string + schemaVersion) same store type
Key / value vuln id → canonical id (CVE preferred) fingerprint+provider+model → recommendation
Default location ~/.cache/quorum/aliases.json ~/.cache/quorum/advice.json
Flag --cache <path> --advice-cache <path>
Write Atomic (.tmp + os.Rename), perm 0600, errors swallowed same
Concurrency sync.RWMutex same
Invalidation No TTL; schemaVersion invalidates on format change keyed by model, so a model change is a new key
Failure tolerance Missing/corrupt file → empty cache, scan proceeds same; also degrades to a live query

There is no scan-result cache, no OSV HTTP cache beyond that KV, and no distributed in-memory cache. The grype database pre-cache is an artifact of the :full image (build time, with GRYPE_DB_VALIDATE_AGE=false so it does not expire), not a cache layer managed by the Go backend.


10. Queue / Messaging — N/A

N/A. There is no broker (Kafka/RabbitMQ/SQS), no internal message queue, and no asynchronous inter-process communication. The technical justification:

  • Quorum is a single, stateless, ephemeral process; all internal coordination uses Go's in-process primitives (goroutines + WaitGroup + Mutex + context).
  • The execution model is synchronous fan-out/fan-in within a single invocation — there is no decoupled producer/consumer and no need for message durability.
  • The "messaging" between stages is simply passing []model.Finding slices in memory down the pipeline (orchestrator → correlate → consensus → filter → [advisory] → report).

There is no messaging proposal: introducing it would contradict the principle of being a lightweight, daemonless CLI ("No panel, no daemon" — root.go).


11. Responsibility of each internal/* package

Quick-reference table (one line per package), faithful to the code read:

Package Template role Single responsibility Files Depends on
model Core Canonical types Finding/MergedFinding (incl. advisory fields Remediation/References/Advice), Severity (with Rank), Resource, Location, Remediation/DocRef/Advice/Fix model.go — (leaf)
severity Utility Severity normalization (from label, CVSS, Dockle), Max, AtLeast, Parse severity.go model
purl Utility Build/extract package identity (name@version) for the VULN key purl.go
adapter Repository/Gateway Adapter interface + registry + runCmd/toolVersion + helpers (capWriter/maxOutputBytes, extraArgs/splitArgs, redactSecretText); one file per scanner with a parser adapter.go, trivy.go, grype.go, checkov.go, kics.go, dockle.go, kubescape.go, polaris.go, kubescore.go, terrascan.go, tfsec.go, regula.go, conftest.go model, severity, purl
cache Repository (cache) Persistent JSON KV with atomic flush (0600), schemaVersion, thread-safe store.go
alias Service + gateway Id resolution chain (aliases→cache→OSV); OSV HTTP client (validated id + PathEscape) resolver.go, osv.go cache
crosswalk Service Maps scanner\|ruleID → canonical control (YAML derived from real output) crosswalk.go yaml.v3
correlate Service Enrichment + deterministic CorrelationKey/Fingerprint correlate.go, key.go alias, crosswalk, model, purl
consensus Service Groups by key and computes Confidence/DetectionCount; sorts; scannerCategory (5 families) consensus.go model, severity
filter Service Baseline (.quorumignore) + --min-severity cut filter.go model, severity
report Service Serializes Result into SARIF/JSON/XML + Prometheus metrics (incl. advisory series) report.go, sarif.go, json.go, xml.go, metrics.go orchestrator
orchestrator Application Parallel fan-out, probe/timeout, per-scanner status, orchestrates the pipeline orchestrator.go adapter, correlate, consensus, model
enrich Advisory (opt-in) Phase 0: loads the knowledge pack and attaches curated remediation templates + OWASP refs (deterministic, no model) enrich.go model, yaml.v3
rag Advisory (opt-in) Phase 2: lexical/semantic retrieval from the digest-pinned OWASP corpus; AttachReferences/GroundingText; embedder rag.go, lexical.go, embed.go model
advisor Advisory (opt-in) Phase 1/3: local/remote LLM recommendation + verify-the-fix patch; graceful degradation; labeled attachments advisor.go, client.go, prompt.go, verify.go model, cache, adapter (via Rescanner)
evals Advisory (testing) Offline harness measuring deterministic remediation coverage, OWASP reference relevance, and the verify-the-fix rate (runs in CI, no heavy model) evals.go enrich, rag, model

And the command packages:

Package Role Responsibility
cmd/quorum Controller main.go (entry/exit), root.go (cobra root + list-scanners + advise-index), scan.go (scan command + wiring + DoS guards + exit codes), advisor.go (advisory wiring + verify-the-fix rescanner), advise_index.go (embed the OWASP corpus)

12. Dependency graph (acyclic)

flowchart BT
    model
    severity --> model
    purl
    cache
    crosswalk
    alias --> cache
    correlate --> alias
    correlate --> crosswalk
    correlate --> model
    correlate --> purl
    consensus --> model
    consensus --> severity
    filter --> model
    filter --> severity
    adapter --> model
    adapter --> severity
    adapter --> purl
    orchestrator --> adapter
    orchestrator --> correlate
    orchestrator --> consensus
    orchestrator --> model
    report --> orchestrator
    report --> model
    enrich --> model
    rag --> model
    advisor --> model
    advisor --> cache
    evals --> enrich
    evals --> rag
    cmdquorum["cmd/quorum"] --> orchestrator
    cmdquorum --> correlate
    cmdquorum --> alias
    cmdquorum --> cache
    cmdquorum --> crosswalk
    cmdquorum --> filter
    cmdquorum --> report
    cmdquorum --> severity
    cmdquorum --> model
    cmdquorum --> adapter
    cmdquorum --> enrich
    cmdquorum --> rag
    cmdquorum --> advisor

Graph characteristics: acyclic, model is a leaf, and the whole domain is testable without a network (the OSV client is injected via the osvSource interface, stubbable in tests; the advisor's Client and Rescanner are likewise interfaces). The advisory packages sit at the same "leaf-ish" level — they depend only on model (plus cache/adapter via interfaces), never the other way around.


13. Extension checklist (actionable)

Add a new scanner:

  • [ ] Create internal/adapter/<scanner>.go implementing the Adapter interface.
  • [ ] Call Register(&<scanner>{}) in init().
  • [ ] Implement Version, Supports, Capabilities, Run + a parser to model.Finding.
  • [ ] Use runCmd (output cap) and extraArgs("<scanner>") for operator passthrough.
  • [ ] Map the engine family in consensus.scannerCategory (sca/iac/k8s/hardening/policy).
  • [ ] Add fixtures in internal/adapter/testdata and the contract test.
  • [ ] (If IaC/k8s) add YAML crosswalk rules derived from real output for the scanner's ruleIDs (or emit a native CanonicalControl, as tfsec does with AVD).

Add a new report format:

  • [ ] Add a Format in report/report.go and the case in Write/ParseFormat.
  • [ ] Create report/<format>.go with the write<Format>(w, res) function.
  • [ ] Update the --format flag help in scan.go.

Extend the advisory layer:

  • [ ] Phase 0: add curated entries to the knowledge pack (knowledge/*.yaml), matched by canonicalControl/ruleId/category/type; enrich.Load picks them up.
  • [ ] Phase 2: add passages to knowledge/owasp/corpus.yaml; re-run quorum advise-index to refresh the semantic index (the content digest is preserved). Add a case to internal/evals.
  • [ ] Keep every AI attachment labeled "AI-generated, advisory only" and ensure it never mutates key/fingerprint/confidence/severity/gate.

Assumptions

  1. Reference version: the document describes the code of the current tree (v0.8.3). The version variable in root.go is "dev" as the build default (overridden by -ldflags "-X main.version=..." in GoReleaser); the "real" release version comes from the ldflag, not from the code.
  2. "Backend" = Go packages: I interpreted "backend" as the set cmd/quorum + internal/*, since there is no server/long-running service. Web-oriented template items (HTTP middlewares, jobs/scheduler, queue/messaging) were treated as N/A with a justification, per the writing rules.
  3. Engine families: the scannerCategory table in consensus.go covers the 12 adapters currently registered, grouped into 5 families (sca, iac, k8s, hardening, policy).
  4. Parallelism limit: I assumed the fan-out concurrency equals the number of supported adapters (≤ 12 today), since there is no configurable limiter in the code.
  5. SARIF/JSON/XML serialization details: I read report.go, sarif.go, metrics.go and the use of partialFingerprints["quorum/v1"]; the field-by-field detail of the SARIF schema is left to Data model to avoid duplication.
  6. grype pre-cache: I assumed the grype database in the :full image is populated at build time (Dockerfile.full, with GRYPE_DB_VALIDATE_AGE=false), and therefore is not a cache layer managed by the Go backend.
  7. conftest's ./policy directory: the conftest adapter evaluates operator Rego in ./policy (or via QUORUM_CONFTEST_ARGS="--policy <dir>"). With no policies, the scanner is reported as error by design (policy-as-code is opt-in); no versioned Rego ships in the repo by default.
  8. Advisory layer is presentation-only: I verified in scan.go that --advice runs after consensus and gating inputs are fixed and attaches fields to MergedFinding in place; it never touches CorrelationKey/Fingerprint/Confidence/severity/--fail-on. The deterministic core has no AI; Phases 1 (local) and 3 (remote) are strictly opt-in, off by default, with the remote provider gated on explicit egress consent, blocked by --offline, and refusing --fix.