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|jsonstructures the progress log on stderr (scan.go,logf). (AU1, U1-partial) - [x] DoS caps.
QUORUM_MAX_OUTPUT_BYTES(default 512 MiB,internal/adapter/adapter.gocapWriter/maxOutputBytes) aborts scanners that dump a giant output;QUORUM_MAX_TARGET_BYTES(default 20 GiB,scan.gocheckTargetSize) 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'ssbom: 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.ymlknowledgejob);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'sC-####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.sockautomatically ontype: image, avoiding the false-zero when scanning a local image. (closes a U4 class) - [x] Automated moving
v0tag.tag-major.ymladvancesv0on every semver release. (D4) - [x] I/O hardening.
--outputwithfilepath.Cleanand perm0600;aliases.jsoncache0600+schemaVersion(internal/cache/store.go); OSV id validated +url.PathEscape(internal/alias/osv.go); target starting with-refused (validateTargetRef); secret redaction (redactSecretText); Grype DB withGRYPE_DB_VALIDATE_AGE=false(does not expire). - [x] Opt-in advisory layer (
--advice). A presentation-only layer that never touchescorrelationKey/fingerprint/confidence/aggregated severity or the--fail-ongate (without--advicethe 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 viaquorum advise-index; Phase 1 — opt-in local LLM (--advice-provider=local) plus--fix=suggestgated 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 noMaxConcurrency/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. ThedefaultProbeTime = 60scomment still acknowledges "while every scanner launches at once on a memory-constrained runner", andrunOnestill diagnoses OOM post-mortem (killedSignal, "version probe killed — likely out of memory"). What changed: theQUORUM_MAX_OUTPUT_BYTES/QUORUM_MAX_TARGET_BYTEScaps 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-concurrencyor envQUORUM_MAX_CONCURRENCY) and use a buffered semaphore (chan struct{}) orgolang.org/x/sync/errgroupwithSetLimit. Treat OOM mitigation as admission control, not only as a post-mortem diagnosis. - Effort: S–M. Impact: High (stability of
:fullwith 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:capWriterflagsoverwhen output exceeds the cap andrunCmdfails 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 statusran, 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.
Resultspresent in Trivy); (b) when exit was != 0 and the JSON lacks the expected shape, mark the run aserrorwith a clear message instead ofran/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, forVULN, callsAlias.Canonicalone id at a time, each possibly hitting the network (OSV). Alias resolution, crosswalk and keying happen together. What changed: the--metricsreporter 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
--metricswith resolution counters (cache vs OSV vs local) andunmapped. - Effort: M. Impact: Medium.
A4. Image profiles (:sca, :iac, :k8s)¶
- Status.
[OPEN]. Only:fulland:slimexist. - Problem. Today there is
:full(all 12 scanners,linux/amd64) and:slim(orchestrator,amd64+arm64), both on GHCR.:fullembeds 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 sameDockerfile.fullparameterized 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.fullbases 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 viapip 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 | shwith 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.txtgenerated bypip-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, runcosign verify-blob/cosign verifywith 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.ymlnow 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'ssbom: true/provenance: true) and per-binary on the GoReleaser channel (anchore/sbom-action/download-syft+sboms:in.goreleaser.yaml), re-verified end-to-end withgh attestation verify. In addition, the knowledge pack + crosswalk now get their own SLSA build-provenance attestation each release (theknowledgejob inrelease.yml), verifiable withgh 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.fullstill does not declareUSER(runs asroot). - Problem.
Dockerfile.fullends asroot, withdocker-cliinstalled (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 ALLand 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.ymlnow runsgo test -race -covermode=atomic -coverprofile=coverage.out ./...and publishes coverage in the summary. Still open: CI does not rungovulncheck,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-enginemark 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 (logfprefix[quorum],printSummary) uses plain text with no color policy, andprintSummarydraws 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 orNO_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.ParseFormatstill accepts onlysarif|json|xml. - Problem. The only human summary is
printSummaryon stderr (count per severity + status per scanner). For interactive/local use there is no readable findings table on stdout. - Recommendation. Add
--format table(and maybemarkdown) that lists theMergedFindings withseverity,detectedBy,detectionCount,confidenceand 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/crosswalkwhen./crosswalkis missing (resolveCrosswalkDir), avoiding the silent "0 rules loaded" when runningdocker run … scan .. Still open: a failingcrosswalk.Loadstill returns raw"loading crosswalk: %w";LoadBaselinereturns 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.
printSummaryalready prints "0 findings is not proof of safety". But if all scanners endedunavailable/skipped/timeout, the report ships with 0 findings and exit 0. v0.8.3 advance: the Action (action.yml) auto-mounts/var/run/docker.sockontype: imageand warns when the socket is missing, closing the most common cause of the local-image false-zero. Still open: there is no CLI--require-scannersthat 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 gainedschemaVersionand perm0600(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 inrunScanafterEnrich), keeping flush-per-Putas a fallback. Preserves rename atomicity and theschemaVersion. - Effort: S. Impact: Medium.
P2. Reuse the version already obtained in the probe (avoid Version() twice)¶
- Status.
[OPEN]. - Problem.
runOnecallsa.Version(verCtx)in the probe; then several adapters callVersionagain insideRunto stampScannerVersion. Twoexecof the binary per scan — now multiplied by 12 scanners in:full. - Recommendation. Pass the already-resolved version into
Run(viaTarget/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.
Enrichresolves 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--offlinethe 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:
:fullpre-caches the Grype DB in/opt/grype/dbwithGRYPE_DB_AUTO_UPDATE=falseandGRYPE_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_DIRand 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.
:fullcarries 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.ymlalready usescache-from/cache-to: type=gha. The remaining cost is thegrype dbon 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: aheavy|lightclass 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) andcrosswalk/k8s.yaml(kubescape'sC-####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) anunmappedmetric 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-enginein 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.Writebuilds everything in abytes.Bufferbefore writing (emitinscan.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 -fuzzin 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.ymlrunsgo test -race -coverprofileand publishes coverage. Still open: there is nogolangci-lintnorgovulncheckof 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.scannerCategorycrossing 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 beyondpartialFingerprints["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/evalsharness 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-binaryattest-sbomclose 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
Makefilecoverstest/vet/build/docker-full. A single pre-release target (lint + vuln + local sbom +:fullsmoke) is missing to reduce CI surprises. - Recommendation. A
make citarget that reproduces the pipeline locally, includingcosign verifyof 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.ymlworkflow advances the movingv0tag (major/minor) to the freshly released commit on every semver release, separate from the image build. The README recommendsuses: 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,writeMetricsFile→report.WriteMetrics,internal/report/metrics.go) emits metrics in Prometheus text-format (for thenode_exportertextfile collector or Pushgateway): per-scanner (status,durationMs,findings,version— data fromScannerRun), consensus aggregates (merged,multiDetected,bySeverity) and pipeline ones. Complemented by--log-format text|jsonfor the progress log. Under--advicethe reporter also emits the advisory seriesquorum_advice_enriched{kind=remediation|references| recommendation},quorum_advice_provider{provider}andquorum_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-onrequires 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/markdownfrom 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 byGRYPE_DB_VALIDATE_AGE=false). - Problem. The Grype DB in
:fullis "frozen as of build time"; without a rebuild it ages. v0.8.3 advance:GRYPE_DB_VALIDATE_AGE=falsekeeps 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
:fullperiodically (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 exposestrivy-args/checkov-args/etc. (b) The Action (action.yml) auto-mounts/var/run/docker.sockontype: image(inputdocker-socketto 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-addinghost-gatewayforlocaland forwarding the API key via env forremote. - 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-indexembeds 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 —scanauto-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 withgh attestation verify knowledge/owasp/corpus.yaml. - Follow-up. Publish an embedded corpus artifact per release so users get semantic retrieval
without running
advise-indexlocally. - 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-formatexist inscan.go; the DoS caps exist inadapter.go/scan.go; the advisory layer (--advice, Phases 0–3) exists ininternal/{enrich,rag,advisor}and is off by default; the fan-out inorchestrator.Runstill has no concurrency cap;report.ParseFormatstill accepts onlysarif|json|xml;ci.ymlruns coverage but notgovulncheck/golangci-lint;Dockerfile.fulldoes not declareUSER; Checkov is installed bypip installwithout--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.Putrewriting the full snapshot on every call;Versioncalled in the probe and inRun; sequential alias resolution inEnrich), not from empirical profiling. - The absence of colors today (U1) is a code observation (
printSummary/logfuse plain text, now with--log-format jsonavailable); 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 existingcobraconvention inscan.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.