Skip to content

Improvements and Recommendations

This document gathers evolution suggestions for Quorum (quorum-sec-scan, v0.8.3), prioritized by axis (Architecture, Security, UX, Performance, Costs, Scalability, Quality, DevOps and Automation). Every recommendation is anchored in the real code of the repository (files and functions cited explicitly), following the product principle that "false split > false merge" and "0 findings is not proof of safety". Each item carries status → problem → recommendation → effort → impact, and the document closes with a table prioritized by impact × effort, a suggested roadmap and an assumptions section.

Revision: 2026-07-04 · product version v0.8.3 (document originally written at v0.2.3 and now updated). Between those two versions the pool grew from 6 to 12 scanners (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest — adapters in internal/adapter/), consensus started to apply beyond SCA (multi-cloud + k8s crosswalk) and much of the recommendations below was implemented. Implemented items are kept for traceability, with the marker [IMPLEMENTED v0.8.3]; partial items carry [PARTIAL] and pending ones [OPEN].

Scope: Quorum is a CLI/Docker consensus security scanning tool. Items that would touch a web frontend, relational database or runtime REST API are marked as N/A with justification, and any idea in that direction appears as a Future proposal clearly separated. Since v0.8.3 an opt-in advisory layer (--advice) also exists — it is presentation-only and off by default, so the deterministic consensus core still has no AI. See DESIGN.md and the README.


0. What has already been delivered since v0.2.3 (summary)

These recommendations from the original document are already in the v0.8.3 code — the details stay inline in each axis, with the marker [IMPLEMENTED v0.8.3]:

  • [x] Execution observability. --metrics <file> emits metrics in Prometheus text-format (report.WriteMetrics, internal/report/metrics.go) and --log-format text|json structures the progress log on stderr (scan.go, logf). (AU1, U1-partial)
  • [x] DoS caps. QUORUM_MAX_OUTPUT_BYTES (default 512 MiB, internal/adapter/adapter.go capWriter/maxOutputBytes) aborts scanners that dump a giant output; QUORUM_MAX_TARGET_BYTES (default 20 GiB, scan.go checkTargetSize) refuses pathological trees before scanning. (mitigates A1/A2)
  • [x] Hardened supply chain. SLSA build-provenance and attested SPDX SBOM (actions/attest-build-provenance, actions/attest-sbom) for the image and per-binary (GoReleaser + syft), plus BuildKit's sbom: true; keyless cosign with retry; bases pinned by @sha256:; kubescape/tfsec/terrascan/regula/conftest verified by checksum; the knowledge pack + crosswalk now also carry a SLSA build-provenance attestation each release (release.yml knowledge job); THIRD_PARTY_NOTICES.md. (S3/D2, S1-partial, S2-partial)
  • [x] Multi-cloud and k8s consensus. Crosswalk derived from real output covers AWS/Azure/GCP (crosswalk/{aws,azure,gcp}.yaml, AVD hub) and Kubernetes (crosswalk/k8s.yaml, kubescape's C-#### hub); tfsec auto-correlates with trivy (native AVD). (fulfills the E2 promise)
  • [x] Docker socket auto-mount. The Action (action.yml) mounts /var/run/docker.sock automatically on type: image, avoiding the false-zero when scanning a local image. (closes a U4 class)
  • [x] Automated moving v0 tag. tag-major.yml advances v0 on every semver release. (D4)
  • [x] I/O hardening. --output with filepath.Clean and perm 0600; aliases.json cache 0600 + schemaVersion (internal/cache/store.go); OSV id validated + url.PathEscape (internal/alias/osv.go); target starting with - refused (validateTargetRef); secret redaction (redactSecretText); Grype DB with GRYPE_DB_VALIDATE_AGE=false (does not expire).
  • [x] Opt-in advisory layer (--advice). A presentation-only layer that never touches correlationKey/fingerprint/confidence/aggregated severity or the --fail-on gate (without --advice the output is byte-identical). Four phases, all implemented: Phase 0 — deterministic curated remediation templates + OWASP references (internal/enrich, knowledge/*.yaml); Phase 2 — RAG-as-artifact from a digest-pinned OWASP corpus (knowledge/owasp/corpus.yaml, internal/rag), lexical by default, semantic once embedded via quorum advise-index; Phase 1 — opt-in local LLM (--advice-provider=local) plus --fix=suggest gated by a verify-the-fix re-scan (internal/advisor); Phase 3 — opt-in remote provider (--advice-provider=remote, gated by --advice-allow-egress). New evals (internal/evals) run in CI. (see §11, 13-ia.md, 21-proposta-ia.md)
  • [x] Test coverage in CI (ci.yml, go test -race -coverprofile) and docs on GitHub Pages (MkDocs Material, docs.yml).

Still open (detailed below): RBAC consensus (single-engine — kubescape RBAC needs cluster context), Checkov pip pin with hashes, more crosswalk coverage, a consensus peer for Dockle (image posture without a second engine), capped concurrency (A1), --format table (U2), --require-scanners (U4), pinning actions by SHA (D1) and a non-root image (S4).


1. How to read this document

Each recommendation uses this convention:

  • Status: [IMPLEMENTED v0.8.3], [PARTIAL] or [OPEN].
  • Effort: S (small, ≤ 1 day), M (medium, 2–5 days), L (large, > 1 week).
  • Impact: High, Medium, Low — effect on reliability, chain security, CI experience or operational cost.
  • Code anchor: file/function where the change lands.
quadrantChart
    title Prioritization impact x effort (open backlog v0.8.3)
    x-axis "Low effort" --> "High effort"
    y-axis "Low impact" --> "High impact"
    quadrant-1 "Plan"
    quadrant-2 "Do now (quick wins)"
    quadrant-3 "Backlog"
    quadrant-4 "Weigh cost/benefit"
    "Limit concurrency (A1)": [0.30, 0.82]
    "require-scanners (U4)": [0.18, 0.80]
    "Pin Checkov pip (S1)": [0.30, 0.78]
    "Pin actions by SHA (D1)": [0.20, 0.72]
    "Validate empty output (A2)": [0.40, 0.75]
    "NO_COLOR / TTY (U1)": [0.15, 0.45]
    "Batched cache flush (P1)": [0.25, 0.55]
    "--format table (U2)": [0.45, 0.60]
    "Concurrent alias (P3)": [0.55, 0.66]
    "RBAC consensus": [0.70, 0.62]
    "More crosswalk (E2)": [0.55, 0.68]
    "Non-root image (S4)": [0.45, 0.55]

2. Architecture

A1. Limit the concurrency of heavy scanners (uncapped fan-out)

  • Status. [OPEN] (partial mitigation via DoS caps). The orchestrator still fans out without a cap.
  • Problem. orchestrator.Run (internal/orchestrator/orchestrator.go) still launches one goroutine per adapter at once (for _, a := range adapters { go func() {...} }, with no MaxConcurrency/semaphore). With the 12 scanners of :full — now including three heavy Python engines (Checkov, KICS+assets, and the IaC ecosystem itself) — memory pressure on a small runner is higher than at v0.2.3. The defaultProbeTime = 60s comment still acknowledges "while every scanner launches at once on a memory-constrained runner", and runOne still diagnoses OOM post-mortem (killedSignal, "version probe killed — likely out of memory"). What changed: the QUORUM_MAX_OUTPUT_BYTES/QUORUM_MAX_TARGET_BYTES caps already block a class of OOM (giant output/target), but they do not limit the number of simultaneous processes.
  • Recommendation. Introduce Options.MaxConcurrency (default = runtime.NumCPU(), configurable via flag --max-concurrency or env QUORUM_MAX_CONCURRENCY) and use a buffered semaphore (chan struct{}) or golang.org/x/sync/errgroup with SetLimit. Treat OOM mitigation as admission control, not only as a post-mortem diagnosis.
  • Effort: S–M. Impact: High (stability of :full with 12 scanners on small runners — the primary CI use case).

A2. Validate each scanner's output before parsing (runtime contract)

  • Status. [PARTIAL]. Truncation by overflow is already detected; empty JSON still passes.
  • Problem. runCmd (internal/adapter/adapter.go) treats "exit != 0 but with stdout" as success (findings-found convention), which is correct. v0.8.3 advance: capWriter flags over when output exceeds the cap and runCmd fails cleanly ("output exceeded N bytes — aborting to avoid OOM"), avoiding parsing an overflow-truncated dump. Still open: a syntactically valid but empty JSON ({}) — or one killed mid-dump without exceeding the cap — produces 0 findings with status ran, indistinguishable from "the scanner ran and found nothing".
  • Recommendation. Add lightweight per-adapter validation: (a) check the output decodes into the expected structure and that anchor fields exist (e.g. Results present in Trivy); (b) when exit was != 0 and the JSON lacks the expected shape, mark the run as error with a clear message instead of ran/0 findings. Reuse the contract fixtures (internal/adapter/testdata, realdata_test.go) as golden files for this validation — now with 12 adapters covered.
  • Effort: M. Impact: High (avoids the silent false negative — the product's declared anti-goal, DESIGN §14).

A3. Make the enrichment pipeline streamable and observable

  • Status. [PARTIAL]. Pipeline metrics already exist via --metrics; parallelizing enrichment does not.
  • Problem. Correlator.Enrich (internal/correlate/correlate.go) walks all findings in a single sequential loop and, for VULN, calls Alias.Canonical one id at a time, each possibly hitting the network (OSV). Alias resolution, crosswalk and keying happen together. What changed: the --metrics reporter already exposes per-scanner and consensus aggregates, providing the instrumentation point that was missing.
  • Recommendation. Separate "alias resolution" (I/O-bound, parallelizable — see P3) from "crosswalk + keying" (pure CPU, deterministic). Extend --metrics with resolution counters (cache vs OSV vs local) and unmapped.
  • Effort: M. Impact: Medium.

A4. Image profiles (:sca, :iac, :k8s)

  • Status. [OPEN]. Only :full and :slim exist.
  • Problem. Today there is :full (all 12 scanners, linux/amd64) and :slim (orchestrator, amd64+arm64), both on GHCR. :full embeds Python+Checkov+KICS, terrascan/tfsec/regula/ conftest, a pre-cached Grype DB etc., resulting in a large image (DESIGN §12 "avoid the monster image"). Many pipelines only do SCA.
  • Recommendation. Add per-profile variants in release.yml (matrix) that embed only the needed subset of binaries, reusing the same Dockerfile.full parameterized by stage. Each profile maps to the adapter set of that type (SCA = trivy+grype; IaC = checkov+kics+terrascan+tfsec+regula+ conftest; K8s = kubescape+polaris+kube-score).
  • Effort: M. Impact: Medium (pull/storage cost in CI; see Costs).

3. Security (of the chain itself)

S1. Pin ALL :full tools by immutable digest/checksum

  • Status. [PARTIAL]. Bases and most scanners already pinned/verified; Checkov (pip) still pending.
  • Problem. v0.8.3 advance: the Dockerfile.full bases are pinned by @sha256: and kubescape, tfsec, terrascan, regula and conftest are now verified by a fixed checksum (in addition to Trivy/KICS already pinned at v0.2.3). Still open: Checkov is installed via pip install "checkov==..." without --require-hashes, so pip's transitive resolution remains mutable — a compromised package enters the trust boundary (DESIGN §12).
  • Recommendation.
  • [x] Replace curl | sh with a versioned artifact + checksum verification for Grype/Syft/Kubescape/tfsec/terrascan/regula/conftest.
  • [ ] For Checkov, pin checkov==<ver> with hashes (pip install --require-hashes + requirements.txt generated by pip-compile --generate-hashes).
  • [x] Document digest re-resolution (docker buildx imagetools inspect).
  • Effort: S–M (remainder). Impact: High (integrity of the chain distributed to users).

S2. Cosign-verify the scanner binaries at build (do not just trust the installer)

  • Status. [PARTIAL]. Integrity by checksum covered; provenance by upstream signature, not.
  • Problem. v0.8.3 advance: the fixed build checksum guarantees integrity of the downloaded binaries. Still open: Anchore (Grype/Syft) and Kubescape publish their own cosign signatures that the build does not verify — the upstream provenance gate is missing.
  • Recommendation. In the runtime stage of Dockerfile.full, after downloading each binary, run cosign verify-blob/cosign verify with the upstream project's OIDC identity, failing the build if verification fails. Complements S1 (checksum = integrity; cosign = provenance).
  • Effort: M. Impact: Medium–High.

S3. Extend SLSA/attestation to GoReleaser artifacts (and the knowledge pack)

  • Status. [IMPLEMENTED v0.8.3].
  • What was done. release.yml now generates SLSA build-provenance and an attested SPDX SBOM both for the image (actions/attest-build-provenance@v2, actions/attest-sbom@v2, anchore/sbom-action, plus BuildKit's sbom: true/provenance: true) and per-binary on the GoReleaser channel (anchore/sbom-action/download-syft + sboms: in .goreleaser.yaml), re-verified end-to-end with gh attestation verify. In addition, the knowledge pack + crosswalk now get their own SLSA build-provenance attestation each release (the knowledge job in release.yml), verifiable with gh attestation verify knowledge/owasp/corpus.yaml. The asymmetry between the distribution channels was closed.
  • Follow-up. Keep parity when new artifacts enter the release.
  • Effort: — (done). Impact: Medium. (See Q4 / DevOps.)

S4. Harden the image at runtime (non-root user, capabilities, read-only FS)

  • Status. [OPEN]. Dockerfile.full still does not declare USER (runs as root).
  • Problem. Dockerfile.full ends as root, with docker-cli installed (for image scanning). Running as root widens the blast radius if an embedded scanner has an RCE while processing hostile input. With 12 scanners the parsing surface has grown.
  • Recommendation. Create a non-root user and USER quorum; document running with --read-only, --cap-drop ALL and the minimum of bind-mounts. When the Docker socket is needed for --type image, document the trade-off (the Action already auto-mounts the socket — make the trust contract of that mount explicit).
  • Effort: M. Impact: Medium.

S5. Self-scan Quorum itself in CI

  • Status. [PARTIAL]. Test coverage in CI already runs; a vulnerability scanner over its own repo/image, not.
  • Problem. v0.8.3 advance: ci.yml now runs go test -race -covermode=atomic -coverprofile=coverage.out ./... and publishes coverage in the summary. Still open: CI does not run govulncheck, golangci-lint, nor Quorum/Trivy itself against the repository/image. "The security tool that does not scan itself" remains a bad sign.
  • Recommendation. Add a step that runs Trivy/govulncheck against the repo and image, and uploads the SARIF to GitHub code scanning — dogfooding Quorum itself.
  • Effort: S. Impact: Medium. (See Q2 and Automation.)

S6. RBAC consensus (single-engine) — a known, documented gap

  • Status. [OPEN] (documented as a limitation of the current design).
  • Problem. The k8s crosswalk (crosswalk/k8s.yaml) already crosses kubescape × polaris × kube-score for workload posture (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). But RBAC remains single-engine: kubescape's RBAC consensus needs an active cluster context (not derivable from static manifests alone), so there is no second engine to corroborate — RBAC findings ship without multi-detection.
  • Recommendation. Explicitly document the gap in the report (a single-engine mark for the RBAC family) and, as a proposal, evaluate an adapter with optional cluster context (--type cluster) that enables the cross-check — keeping the static mode as default.
  • Effort: M–L. Impact: Medium (transparency of the k8s consensus).

4. UX (CI/command-line experience)

U1. Respect NO_COLOR and TTY detection in the progress output

  • Status. [PARTIAL]. The progress log is already structurable (--log-format json); a color/TTY policy, not.
  • Problem. v0.8.3 advance: --log-format text|json (scan.go, logf) lets you emit the progress as structured JSON in CI, which helps separate the stderr noise of 12 scanners. Still open: the human output (logf prefix [quorum], printSummary) uses plain text with no color policy, and printSummary draws Unicode box-drawing (──) that can render illegibly on legacy/ non-UTF Windows terminals.
  • Recommendation. Centralize the human output behind a helper that: (a) honors NO_COLOR (https://no-color.org) and --no-color; (b) detects whether stderr is a TTY (golang.org/x/term.IsTerminal); (c) falls back to plain ASCII when non-TTY or NO_COLOR. Adopt it before introducing colors.
  • Effort: S. Impact: Medium (log readability in CI and on Windows).

U2. Human-readable output beyond SARIF/JSON/XML (--format table)

  • Status. [OPEN]. report.ParseFormat still accepts only sarif|json|xml.
  • Problem. The only human summary is printSummary on stderr (count per severity + status per scanner). For interactive/local use there is no readable findings table on stdout.
  • Recommendation. Add --format table (and maybe markdown) that lists the MergedFindings with severity, detectedBy, detectionCount, confidence and location — great for PR comments and local runs (enables AU3).
  • Effort: M. Impact: Medium.

U3. Actionable error messages at more points in the flow

  • Status. [PARTIAL]. Some points improved; overall standardization, not.
  • Problem. v0.8.3 advance: the probe already has excellent messages (OOM, slow start) and the crosswalk gained a fallback to /opt/quorum/crosswalk when ./crosswalk is missing (resolveCrosswalkDir), avoiding the silent "0 rules loaded" when running docker run … scan .. Still open: a failing crosswalk.Load still returns raw "loading crosswalk: %w"; LoadBaseline returns the raw error.
  • Recommendation. Standardize runtime/usage errors (exit 2) with the triad what failed → why → how to fix, in the probe's style. E.g.: an empty crosswalk warning "0 rules loaded — misconfigs will be unmapped".
  • Effort: S. Impact: Medium.

U4. Explicit warning when no scanner ran

  • Status. [OPEN] in the CLI; partially covered in the Action.
  • Problem. printSummary already prints "0 findings is not proof of safety". But if all scanners ended unavailable/skipped/timeout, the report ships with 0 findings and exit 0. v0.8.3 advance: the Action (action.yml) auto-mounts /var/run/docker.sock on type: image and warns when the socket is missing, closing the most common cause of the local-image false-zero. Still open: there is no CLI --require-scanners that turns "0 scanners ran" into a failure.
  • Recommendation. When count(status==ran) == 0, emit a highlighted warning and offer an opt-in flag --require-scanners (or --fail-if-no-scanner) that returns exit 2 — without changing the default.
  • Effort: S. Impact: High (fixes the most dangerous false-negative class).

5. Performance

P1. Persist the alias cache in batch (avoid rewriting the whole file per key)

  • Status. [OPEN] (the cache hardening landed; the batch flush did not).
  • Problem. cache.Store.Put (internal/cache/store.go) still serializes and rewrites the entire JSON on every call (full snapshot → MarshalIndent → write-temp → rename). On a large image scan with hundreds of new CVEs it is O(n²) in I/O. v0.8.3 advance: the file gained schemaVersion and perm 0600 (atomic write-temp+rename kept) — more robust, but still flush-per-Put.
  • Recommendation. Add a batched mode: accumulate Puts in memory and do a single flush at the end (Store.Flush() called in runScan after Enrich), keeping flush-per-Put as a fallback. Preserves rename atomicity and the schemaVersion.
  • Effort: S. Impact: Medium.

P2. Reuse the version already obtained in the probe (avoid Version() twice)

  • Status. [OPEN].
  • Problem. runOne calls a.Version(verCtx) in the probe; then several adapters call Version again inside Run to stamp ScannerVersion. Two exec of the binary per scan — now multiplied by 12 scanners in :full.
  • Recommendation. Pass the already-resolved version into Run (via Target/RunContext) or cache the version per adapter within one execution.
  • Effort: S. Impact: Low–Medium.

P3. Resolve VULN aliases concurrently (with a bounded pool)

  • Status. [OPEN].
  • Problem. Enrich resolves aliases one id at a time and each may hit OSV (OSVClient.Aliases, timeout + retries with backoff; now with validated id + url.PathEscape). Hundreds of unique uncached CVEs = hundreds of sequential round-trips. In --offline the point is moot; online (default) it is the pipeline's biggest bottleneck.
  • Recommendation. Deduplicate ids before resolution, resolve the unique set in parallel with a bounded pool (respecting the OSV rate-limit) and map back. Combine with P1 (single flush).
  • Effort: M. Impact: Medium–High (latency of online scans in CI).

P4. Reuse the Grype/Trivy DB across runs

  • Status. [PARTIAL]. Solved in :full; guidance for :slim/native binary, not.
  • Problem. v0.8.3 advance: :full pre-caches the Grype DB in /opt/grype/db with GRYPE_DB_AUTO_UPDATE=false and GRYPE_DB_VALIDATE_AGE=false (the frozen DB does not expire, so it does not force a re-download on an offline runner). Still open: in :slim/ native-binary use each runner may re-download the DB, with no guidance in the doc to mount/persist that cache.
  • Recommendation. Document (and maybe expose via flag/env) reusing GRYPE_DB_CACHE_DIR and the Trivy DB cache mounted as a CI cache.
  • Effort: S (doc). Impact: Medium (cold-start latency).

6. Costs

C1. Reduce :full size (aggressive multi-stage / profiles)

  • Status. [OPEN].
  • Problem. :full carries Python+pip+venv (Checkov), the IaC stack (kics, terrascan, tfsec, regula, conftest), git, docker-cli, tar/curl and the frozen Grype DB. With 12 scanners the image grew; it weighs on GHCR egress and pull time on every job.
  • Recommendation. (a) Remove build tools (curl, tar, pip headers) from the final stage via multi-stage; (b) offer profiles (A4) so SCA pipelines do not pull the Python stack; (c) clean up *.pyc/--no-cache-dir.
  • Effort: M. Impact: Medium (recurring CI cost).

C2. Cache build layers in the release (already partially done)

  • Status. [PARTIAL].
  • Problem. release.yml already uses cache-from/cache-to: type=gha. The remaining cost is the grype db on every build.
  • Recommendation. Cache the Grype DB as a stable layer (pinned version) so rebuilds without a bump do not re-download; document the refresh cadence (relates to AU4).
  • Effort: S. Impact: Low–Medium.

7. Scalability

E1. Cap and prioritize the fan-out by scanner class

  • Status. [OPEN].
  • Problem. Even with A1 (global cap), the 12 scanners have very different resource profiles: Checkov/KICS (Python) are slow to start and memory-heavy; Dockle and kube-score are light. A single cap treats them all the same.
  • Recommendation. Allow weights per adapter (via Capabilities/metadata) and a scheduler that never runs two "heavy" scanners at once on small runners. Start simple: a heavy|light class and a separate cap for heavy.
  • Effort: M. Impact: Medium.

E2. Crosswalk as a scalable asset (validation + coverage)

  • Status. [PARTIAL]. Coverage grew a lot; validation/report automation, not.
  • Problem. v0.8.3 advance: the crosswalk stopped being SCA-only and now enables the multi-cloud and k8s consensus, derived from real output with the "false split > false merge" bias: crosswalk/{aws,azure,gcp}.yaml (AVD hub: S3/IAM/EBS/SG/RDS/KMS/CloudTrail/VPC-flow-logs, Azure Storage/Key Vault, GCP bucket/firewall/SQL) and crosswalk/k8s.yaml (kubescape's C-#### hub, crossing kubescape × polaris × kube-score). tfsec auto-correlates with trivy (native AVD). Still open: coverage is known debt (DESIGN §14) and keeping YAML consistency by hand does not scale; a CI validator, a coverage report and the Dockle gap (see E3) are missing.
  • Recommendation. (a) A crosswalk schema validator in CI (required fields, unique IDs, well-formed AVD/C-####); (b) a coverage report (% of each scanner's rules with a mapping, generated from the real fixtures); (c) an unmapped metric exposed in --metrics.
  • Effort: M. Impact: Medium (quality of MISCONFIG/K8S consensus).

E3. Consensus peer for Dockle (single-engine image posture)

  • Status. [OPEN] (known gap).
  • Problem. Image configuration posture (CIS Docker Benchmark) is today covered by Dockle only — there is no second engine to corroborate those findings, so they ship without multi-detection (analogous to RBAC in S6). Unlike MISCONFIG/IaC and k8s workload, which already have 2–3 crossed engines.
  • Recommendation. Evaluate a peer for the image-posture family (e.g. Trivy config/image checks or hadolint for the Dockerfile) and map it in the crosswalk against Dockle's controls; until then, mark the family as single-engine in the report.
  • Effort: M. Impact: Medium (transparency of the image consensus).

E4. Output for targets with a high volume of findings

  • Status. [OPEN].
  • Problem. Today report.Write builds everything in a bytes.Buffer before writing (emit in scan.go). For images with thousands of vulns, it keeps the whole report in memory.
  • Recommendation. Optional streaming of the writer to the destination io.Writer (JSON Lines / SARIF streaming) when the volume warrants; keep the buffer as default.
  • Effort: M. Impact: Low (tail case).

8. Quality

Q1. Error coverage in the parsers (fuzz + golden of malformed output)

  • Status. [OPEN]. The contract-test base grew to 12 adapters; hostile input is missing.
  • Problem. There are per-adapter contract tests against real fixtures (internal/adapter/testdata, realdata_test.go) — now covering the 12 scanners. Missing is hostile/malformed input coverage (truncated JSON, missing fields, out-of-range numbers).
  • Recommendation. Add go test -fuzz in the parsers and golden tests with deliberately broken fixtures, paired with the A2 validation.
  • Effort: M. Impact: Medium.

Q2. golangci-lint and govulncheck in CI

  • Status. [PARTIAL]. Test coverage landed; a comprehensive linter and vuln check, not.
  • Problem. v0.8.3 advance: ci.yml runs go test -race -coverprofile and publishes coverage. Still open: there is no golangci-lint nor govulncheck of the Go dependencies.
  • Recommendation.
  • [ ] Add golangci-lint (errcheck, staticcheck, gocritic, gosec).
  • [ ] Add govulncheck ./... as a required step.
  • [x] Report coverage (go test -coverprofile) in the summary.
  • Effort: S. Impact: Medium.

Q3. Deterministic end-to-end test of consensus (beyond e2e.yml)

  • Status. [OPEN].
  • Problem. There is e2e.yml (consensus). It is worth ensuring the consensus math (confidence, detectionCount, aggregateSeverity) has table tests covering DESIGN §9 weights and the limits (clamp 0..1, normalized log), now with more engine families (consensus.scannerCategory crossing 12 scanners).
  • Recommendation. Explicit unit tests for confidence(...) with anchor cases (3 linters on the same line vs SCA+IaC agreeing; kubescape+polaris+kube-score on the same k8s control).
  • Effort: S–M. Impact: Medium.

Q4. Document and version the output schema (JSON/SARIF) as a contract

  • Status. [OPEN].
  • Problem. The JSON is a "direct dump of []MergedFinding"; consumers (DefectDojo, scripts) depend on that shape. There is no explicit schema versioning beyond partialFingerprints["quorum/v1"].
  • Recommendation. Publish a JSON Schema of the output and a test that fails if the shape changes without a schema-version bump. Treat the output as a public API (even without REST).
  • Effort: M. Impact: Medium.

Q5. Advisory-layer evals as a quality gate (delivered)

  • Status. [IMPLEMENTED v0.8.3].
  • What was done. The internal/evals harness measures deterministic remediation coverage, OWASP reference relevance and the verify-the-fix rate, and runs in CI with no heavy model (Phase 0/Phase 2 are deterministic, so the eval is reproducible). This keeps the opt-in advisory layer honest without coupling CI to an external LLM.
  • Follow-up. Extend the eval corpus as the knowledge pack grows; wire the verify-the-fix rate to the quorum_advice_fix{stage=verified|proposed} metric for trend tracking.
  • Effort: — (done). Impact: Medium.

9. DevOps

D1. Pin third-party actions by SHA

  • Status. [OPEN].
  • Problem. The workflows still use moving tags (actions/checkout@v4, actions/setup-go@v5, docker/build-push-action@v6, sigstore/cosign-installer@v3, actions/attest-*@v2, etc.). Action tags are mutable — the same supply-chain risk the product fights (DESIGN §12), now more critical because the release does attestation/signing.
  • Recommendation. Pin each uses: by @<sha40> and use Dependabot/Renovate for controlled bumps. Consistent with Quorum's own chain-security stance.
  • Effort: S. Impact: Medium–High.

D2. SBOM of the binaries in the release (parity with the image)

  • Status. [IMPLEMENTED v0.8.3]. See S3 — sboms: in .goreleaser.yaml + per-binary attest-sbom close parity with the image.
  • Follow-up. Keep parity when adding new release artifacts.
  • Effort: — (done). Impact: Medium.

D3. Reproducibility verification / complete make targets

  • Status. [OPEN].
  • Problem. The Makefile covers test/vet/build/docker-full. A single pre-release target (lint + vuln + local sbom + :full smoke) is missing to reduce CI surprises.
  • Recommendation. A make ci target that reproduces the pipeline locally, including cosign verify of the freshly-built image (the Action already cosign-verifies the image before running).
  • Effort: S. Impact: Low–Medium.

D4. Action's moving v0 tag: automate and protect

  • Status. [IMPLEMENTED v0.8.3].
  • What was done. The dedicated tag-major.yml workflow advances the moving v0 tag (major/minor) to the freshly released commit on every semver release, separate from the image build. The README recommends uses: Martinez1991/quorum-sec-scan@v0 (pin by @<sha> in production).
  • Follow-up. Document the pin policy in the README/release docs.
  • Effort: — (done). Impact: Medium.

10. Automation

AU1. --metrics (execution telemetry in a consumable format)

  • Status. [IMPLEMENTED v0.8.3].
  • What was done. --metrics <file> (scan.go, writeMetricsFilereport.WriteMetrics, internal/report/metrics.go) emits metrics in Prometheus text-format (for the node_exporter textfile collector or Pushgateway): per-scanner (status, durationMs, findings, version — data from ScannerRun), consensus aggregates (merged, multiDetected, bySeverity) and pipeline ones. Complemented by --log-format text|json for the progress log. Under --advice the reporter also emits the advisory series quorum_advice_enriched{kind=remediation|references| recommendation}, quorum_advice_provider{provider} and quorum_advice_fix{stage=proposed| verified} (the verified/proposed ratio is the verify-the-fix rate) — off by default and absent without --advice.
  • Follow-up. Extend with alias-cache counters (hits vs OSV) and unmapped (see A3/E2).
  • Effort: — (done). Impact: Medium (CI observability without inventing a daemon).

AU2. Assisted baseline generation (--write-baseline)

  • Status. [OPEN].
  • Problem. Adopting --fail-on requires a baseline (.quorumignore) built by hand from the report's fingerprints (README "Baseline"). Adoption friction.
  • Recommendation. A --write-baseline <file> flag that writes the current fingerprints (with comments: title, severity, scanners) for the user to triage. Never silently suppresses; it only materializes what is already in the report.
  • Effort: S–M. Impact: Medium (adoption of the CI gate).

AU3. PR decoration / automatic comment

  • Status. [OPEN].
  • Problem. The Action uploads SARIF (GitHub code scanning), but there is no readable summary on the PR.
  • Recommendation. An optional Action step that posts a comment with the consensus summary (reusing --format table/markdown from U2). Keeps the product CLI-only; the automation lives in the Action.
  • Effort: M. Impact: Medium.

AU4. Scheduled vulnerability-DB update / :full rebuild

  • Status. [OPEN] (urgency reduced by GRYPE_DB_VALIDATE_AGE=false).
  • Problem. The Grype DB in :full is "frozen as of build time"; without a rebuild it ages. v0.8.3 advance: GRYPE_DB_VALIDATE_AGE=false keeps the frozen DB from breaking offline scans by expiry — but it does keep aging in fact.
  • Recommendation. A scheduled (cron) workflow that rebuilds and re-publishes :full periodically (without a new semver), updating only the DB; signing/attestation follow the same flow (SLSA + attested SBOM + cosign).
  • Effort: M. Impact: Medium.

AU5. Per-scanner passthrough and socket auto-mount (delivered)

  • Status. [IMPLEMENTED v0.8.3].
  • What was done. (a) QUORUM_<SCANNER>_ARGS (internal/adapter/adapter.go, extraArgs) lets you widen coverage or pass a platform key without touching the adapter — e.g.: QUORUM_CHECKOV_ARGS="--bc-api-key <key>" unlocks Prisma Cloud/Bridgecrew policies through the embedded Checkov OSS; the Action exposes trivy-args/checkov-args/etc. (b) The Action (action.yml) auto-mounts /var/run/docker.sock on type: image (input docker-socket to force/disable), eliminating the false-zero when scanning a locally built image, and cosign-verifies the image before running. The Action now also exposes all advisory inputs (advice, advice-provider, advice-endpoint, advice-model, advice-embed-model, advice-max, advice-cache, advice-allow-egress, advice-api-key, fix), auto-adding host-gateway for local and forwarding the API key via env for remote.
  • Follow-up. None immediate.
  • Effort: — (done). Impact: Medium–High (coverage and false-zero fix).

AU6. quorum advise-index — semantic embedding of the OWASP corpus (delivered)

  • Status. [IMPLEMENTED v0.8.3].
  • What was done. The new subcommand quorum advise-index embeds the digest-pinned OWASP corpus (knowledge/owasp/corpus.yaml, internal/rag), preserving the pin, so Phase 2 retrieval can run semantic (embeddings) instead of the default lexical search — scan auto-picks semantic once the corpus carries vectors. Retrieval stays deterministic and RAG-as-artifact (no model at scan time for lexical), and the corpus attestation lets consumers verify the pack with gh attestation verify knowledge/owasp/corpus.yaml.
  • Follow-up. Publish an embedded corpus artifact per release so users get semantic retrieval without running advise-index locally.
  • Effort: — (done). Impact: Medium.

11. Items marked N/A (with justification)

This architectural boundary stays largely unchanged from v0.2.3 to v0.8.3: the product grew in depth (more scanners, multi-cloud/k8s consensus, a hardened supply chain, an opt-in advisory layer), not in surface.

Template theme Status Justification
Web frontend / SPA N/A Quorum is CLI/Docker only ("no panel, no daemon", README). There is no UI.
Relational database N/A Persistence is only the JSON alias cache (internal/cache, schemaVersion+0600) and the on-disk advice cache (keyed by fingerprint+provider+model). No RDBMS by design (DESIGN §7 avoids CGO/DB).
Runtime HTTP REST API N/A Integration is by exit code (0 ok / 1 gate / 2 error) + SARIF/JSON/XML in CI. Outbound network is OSV.dev (validated id + PathEscape) and, only under --advice-provider, an OpenAI-compatible endpoint (local by default; remote gated by --advice-allow-egress).
Authentication / accounts N/A No multi-user or session. Trust comes from cosign/SLSA/attested SBOM (and the attested knowledge pack) in distribution.
AI / LLM in the core N/A (opt-in advisory layer exists, off by default) The consensus core is deterministic (correlationKey, the confidence formula, YAML crosswalk) — no ML. The AI parts are the presentation-only advisory layer (--advice, Phases 1/3), off by default and never touching the score or the gate; the OWASP LLM Top 10 applies only when that layer is enabled. See 13-ia.md and 21-proposta-ia.md.
Runtime K8s orchestration (Falco/Tetragon) N/A (separate future) A stream model, not a static scan — a separate product (DESIGN §2, §13).

Future proposal (clearly separated): if there is ever demand for centralized aggregation of results across many pipelines, the design-coherent path would be an external consumer of the SARIF/JSON (e.g. DefectDojo), not embedding a database/API in Quorum. The portable fingerprint (quorum/v1) was designed for exactly that (DESIGN §11).


12. Prioritized table (impact × effort)

Ordered by suggested priority. Items completed in v0.8.3 stay at the end, for traceability.

# Axis Recommendation Effort Impact Status Priority
A1 Architecture Limit scanner concurrency (semaphore/errgroup) S–M High Open P0
U4 UX Warning/flag when no scanner ran (--require-scanners) S High Open P0
A2 Architecture Validate output (empty JSON) before parsing M High Partial P0
S1 Security Pin the Checkov pip with hashes (--require-hashes) S–M High Partial P0
D1 DevOps Pin third-party actions by SHA S Medium–High Open P1
Q2 Quality golangci-lint + govulncheck in CI S Medium Partial P1
S5 Security Self-scan (dogfooding: trivy/govulncheck) in CI S Medium Partial P1
P1 Performance Alias cache with batched flush S Medium Open P1
U1 UX NO_COLOR/TTY before any color S Medium Partial P1
P3 Performance Concurrent alias resolution + dedup M Medium–High Open P1
U2 UX --format table/markdown M Medium Open P2
U3 UX Actionable error messages (empty crosswalk, baseline) S Medium Partial P2
AU2 Automation --write-baseline S–M Medium Open P2
P2 Performance Reuse the probe's version (do not call Version 2x) S Low–Medium Open P2
S2 Security Cosign-verify upstream binaries at build M Medium–High Partial P2
S4 Security Non-root image + runtime hardening M Medium Open P2
S6 Security RBAC consensus (2nd engine / cluster context) M–L Medium Open P2
E3 Scalability Consensus peer for Dockle (image posture) M Medium Open P2
A4/C1 Architecture/Costs Image profiles (:sca/:iac/:k8s) + size reduction M Medium Open P2
E2 Scalability Crosswalk validator + coverage in CI M Medium Partial P2
Q1 Quality Fuzz/golden of malformed output in the parsers M Medium Open P2
Q3 Quality Table tests of the consensus formula S–M Medium Open P2
Q4 Quality Versioned JSON Schema of the output M Medium Open P2
AU4 Automation Scheduled :full rebuild (fresh DB) M Medium Open P2
AU6+ Automation Publish an embedded OWASP corpus per release S Medium Open P3
P4 Performance Documented Grype/Trivy DB reuse in CI (:slim) S Medium Partial P3
AU3 Automation PR decoration via the Action M Medium Open P3
E1 Scalability Weights per scanner class (heavy/light) M Medium Open P3
A3 Architecture Split/observe enrichment M Medium Partial P3
E4 Scalability Streaming writer for huge reports M Low Open P3
C2 Costs Grype DB layer cache in the build S Low–Medium Partial P3
D3 DevOps Pre-release make ci target S Low–Medium Open P3
Completed in v0.8.3
AU1 Automation --metrics (Prometheus textfile) + --log-format + advice series M Medium Done
AU5 Automation Passthrough QUORUM_<SCANNER>_ARGS + auto-socket + advisory inputs M Medium–High Done
AU6 Automation quorum advise-index (semantic OWASP corpus) M Medium Done
Q5 Quality Advisory-layer evals in CI (internal/evals) M Medium Done
S3/D2 Security/DevOps SLSA + attested SPDX SBOM (image + binaries + knowledge pack) S–M Medium Done
D4 DevOps Automate the moving v0 tag (tag-major.yml) S Medium Done
Product Opt-in advisory layer (--advice, Phases 0–3) L High Done
Security DoS caps (MAX_OUTPUT_BYTES/MAX_TARGET_BYTES) S Medium Done
Security Multi-cloud/k8s consensus (crosswalk aws/azure/gcp/k8s) L High Done

13. Suggested roadmap by release

The delivered block gathers what is already in v0.8.3; the next blocks are backlog, not current behavior.

flowchart LR
    subgraph delivered["Delivered through v0.8.3"]
        m1["AU1 --metrics + --log-format"]
        m2["AU5 args passthrough + auto-socket"]
        m3["S3/D2 SLSA + attested SBOM"]
        m4["DoS caps (bytes)"]
        m5["Multi-cloud/k8s consensus"]
        m6["D4 moving v0 tag"]
        m7["Advisory layer --advice (0-3)"]
        m8["AU6 advise-index + Q5 evals"]
    end
    subgraph v0_9["v0.9 (stability + gate)"]
        a1["A1 limit concurrency"]
        u4["U4 require-scanners"]
        a2["A2 validate empty output"]
        s1["S1 pin Checkov pip (hashes)"]
        d1["D1 pin actions SHA"]
    end
    subgraph v0_10["v0.10 (supply chain++ / consensus)"]
        s2["S2 cosign upstream"]
        s4["S4 non-root image"]
        s6["S6 RBAC consensus"]
        e3["E3 Dockle peer"]
        e2["E2 crosswalk coverage in CI"]
    end
    subgraph v1_0["v1.0 (stable contract)"]
        a4["A4 image profiles"]
        q4["Q4 versioned schema"]
        u2["U2 --format table"]
        au3["AU3 PR decoration"]
    end
    delivered --> v0_9 --> v0_10 --> v1_0

Assumptions

  • The current version analyzed is v0.8.3 (revision 2026-07-04); statements reflect the code in the repository's current state (branch main), in particular: internal/orchestrator/orchestrator.go, cmd/quorum/scan.go, cmd/quorum/root.go, cmd/quorum/advise_index.go, internal/adapter/{adapter.go,trivy.go,conftest.go,…} (12 adapters), internal/cache/store.go, internal/alias/{osv.go,resolver.go}, internal/correlate/correlate.go, internal/report/metrics.go, internal/{enrich,rag,advisor,evals}, knowledge/{aws,azure,gcp,k8s,image,categories}.yaml, knowledge/owasp/corpus.yaml, crosswalk/{aws,azure,gcp,k8s}.yaml, Dockerfile.full, action.yml, .github/workflows/{ci,release,tag-major}.yml, .goreleaser.yaml.
  • The markers [IMPLEMENTED v0.8.3] / [PARTIAL] / [OPEN] were assigned by reading the code as-is: for example, --metrics/--log-format exist in scan.go; the DoS caps exist in adapter.go/scan.go; the advisory layer (--advice, Phases 0–3) exists in internal/{enrich,rag,advisor} and is off by default; the fan-out in orchestrator.Run still has no concurrency cap; report.ParseFormat still accepts only sarif|json|xml; ci.yml runs coverage but not govulncheck/golangci-lint; Dockerfile.full does not declare USER; Checkov is installed by pip install without --require-hashes.
  • "Effort" is a relative engineering estimate, not accounting for external review/QA; it serves for prioritization, not for precise sprint planning.
  • I assumed the product goal stays CLI/Docker only (no panel, daemon, REST API or relational database), and that the deterministic core stays AI-free — the advisory layer is opt-in and off by default (--advice), presentation-only, and never affects the score or the gate. Per README/DESIGN, recommendations toward a panel/daemon/RDBMS were treated as N/A or external future proposals. That boundary stays unchanged from v0.2.3 to v0.8.3.
  • I assumed the primary audience is CI/CD (gating by exit code, SARIF consumption), which weighs on the prioritization of stability on small runners, chain security and log UX.
  • I did not run the scanners or the full pipeline; the performance conclusions (P1–P4) derive from code reading (e.g. cache.Store.Put rewriting the full snapshot on every call; Version called in the probe and in Run; sequential alias resolution in Enrich), not from empirical profiling.
  • The absence of colors today (U1) is a code observation (printSummary/logf use plain text, now with --log-format json available); the color/TTY item stays preventive for legacy terminals.
  • The proposed flag names (--max-concurrency, --require-scanners, --format table, --write-baseline, --no-color) are suggestions; the definitive ones should follow the existing cobra convention in scan.go (where --metrics, --log-format, --min-severity, --advice, etc. already live).

See also

  • DESIGN.md — data model, correlation matrix (§6), consensus math (§9), supply chain (§12), scanner status (§14).
  • README.md — usage, exit codes, baseline, signed distribution.
  • THIRD_PARTY_NOTICES.md — license inventory of the embedded tools.