Non-Functional Requirements¶
This document specifies the Non-Functional Requirements (NFRs) of Quorum
(quorum-sec-scan), version v0.8.3 (revision 2026-07-04). Quorum is a
consensus security scanning tool distributed as a CLI and Docker image —
no web frontend, no database, no REST API, and no user authentication. It
orchestrates a pool of 12 OSS scanners (Trivy, Grype, Checkov, KICS, Dockle,
Kubescape, Polaris, kube-score, Terrascan, tfsec, Regula and Conftest),
normalizes everything to a canonical model (model.Finding), resolves
vulnerability aliases, correlates equivalent findings, computes a (consensus)
confidence score, and emits SARIF/JSON/XML.
The deterministic core has no AI. As of v0.8.3 there is an opt-in advisory
layer (enabled with --advice) that is presentation-only: it never touches
correlationKey, Fingerprint, confidence, aggregated severity, or the
--fail-on gate. Without --advice the output is byte-identical to earlier
releases, and every AI attachment is labeled "AI-generated, advisory only". See
13-ia.md and 21-proposta-ia.md for the full
framing; its impact on the NFRs (determinism, egress, supply chain) is called out
inline below.
Being an ephemeral binary/image (not a long-running service), several classic SaaS NFRs do not apply directly. Where that is the case, the requirement is explicitly declared N/A with a technical justification and — when it adds value — accompanied by a clearly separated Future proposal. The product's architectural nature is described in 01-visao-geral.md and 04-arquitetura.md; the data model, the correlation matrix and the consensus math are in DESIGN.md.
NFR criticality convention: P0 = essential (release blocks if violated), P1 = important, P2 = desirable.
1. NFR Overview¶
mindmap
root((Quorum NFRs))
Performance
Parallel fan-out 12 scanners
Per-scanner timeout
Version probe 60s
Pre-cached Grype DB
DoS caps (output/target)
Scalability
Stateless CLI
Horizontal scale on runners
Availability
N/A service
GHCR/Releases distribution
Security
Supply chain
cosign-signed images
SLSA provenance
Attested SPDX SBOM
Knowledge-pack attestation
sha256 pinning
No secrets
Observability
stderr logs text/json
Prometheus metrics
Per-scanner status
Auditability
Report as evidence
Fingerprints
Compliance
LGPD
PCI DSS
ISO 27001
OWASP ASVS / Top 10
Resilience
Graceful degradation
cosign retry
Grype DB never expires
Continuity
Stateless
Reproducible artifacts
| # | Category | Applicability | Priority | Summary |
|---|---|---|---|---|
| 2 | Performance | Applicable | P0 | Parallelism, timeouts, pre-cached DB, DoS caps |
| 3 | Scalability | Applicable | P1 | Stateless CLI, horizontal scale in CI |
| 4 | Availability | Reinterpreted | P1 | Not a service; distribution matters |
| 5 | Security | Applicable | P0 | Supply chain, signatures, SBOM, no secrets |
| 6 | Observability | Applicable | P1 | stderr logs (text/json), Prometheus metrics, per-scanner status |
| 7 | Auditability | Applicable | P1 | Report as evidence, fingerprints |
| 8 | LGPD | Applicable (by exclusion) | P1 | No PII processed |
| 9 | PCI DSS | N/A as a product | P2 | Can support compliance |
| 10 | ISO 27001 | Partial mapping | P2 | Controls applicable to the binary/pipeline |
| 11 | OWASP ASVS | CLI subset | P1 | Controls relevant to a CLI |
| 12 | OWASP Top 10 | Applicable to the artifact itself | P0 | Risk of the binary/image, not of the target |
| 13 | Resilience | Applicable | P0 | Graceful degradation |
| 14 | Backup/DR/RTO/RPO | N/A (stateless) | P2 | Reproducible artifacts |
| 15 | SLA/SLO/SLI | Defined | P1 | Use in CI and release pipeline |
2. Performance¶
Quorum is dominated by I/O and by subprocesses (each scanner is an external
binary invoked via exec). The wall-clock time of a scan is, in practice,
max(time of each scanner) + correlation/consensus overhead, because the
adapters run in parallel. The opt-in advisory layer (--advice) runs after
the deterministic pipeline and is off by default, so it never enters this
critical path unless explicitly requested.
2.1 Parallelism (fan-out)¶
The orchestrator fires one goroutine per adapter and aggregates the results
under a mutex (internal/orchestrator/orchestrator.go, Run). There are 12
adapters registered in internal/adapter (trivy, grype, checkov, kics, dockle,
kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest); only those
that support the target type enter the fan-out:
flowchart LR
T[target] --> O[Orchestrator.Run]
O -->|goroutine| A1[trivy.Run]
O -->|goroutine| A2[grype.Run]
O -->|goroutine| A3[checkov.Run]
O -->|goroutine| A4[kics.Run]
O -->|goroutine| A5[dockle.Run]
O -->|goroutine| A6[kubescape.Run]
O -->|goroutine| A7[polaris.Run]
O -->|goroutine| A8[kube-score.Run]
O -->|goroutine| A9[terrascan.Run]
O -->|goroutine| A10[tfsec.Run]
O -->|goroutine| A11[regula.Run]
O -->|goroutine| A12[conftest.Run]
A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 & A9 & A10 & A11 & A12 --> W[wg.Wait + mutex aggregates]
W --> C[Correlator.Enrich]
C --> M[consensus.Merge]
M --> R[Reporter SARIF/JSON/XML]
Performance implications:
- Scaling is bounded by the slowest scanner, not by the sum. Adding a fast scanner to a set that already contains a slow one does not change the total time much. With 12 scanners, the typical bottleneck is still the heavy Python/heavy processes (Checkov, KICS) and not the number of adapters itself.
- The correlation/consensus phase is in-process and CPU-light (grouping by
correlationKeyin maps,sha256per key) — negligible compared to the scanners. - Alias resolution can add network latency (OSV.dev). It is mitigated by a
local cache (
~/.cache/quorum/aliases.json) and can be disabled with--offline.
2.2 Per-scanner timeout¶
Two distinct time budgets protect the pipeline against hangs:
| Parameter | Default | Where | Effect on expiry |
|---|---|---|---|
--timeout (PerScannerTime) |
5m | cmd/quorum/scan.go, Options.PerScannerTime |
Scanner marked timeout; the others continue |
ProbeTime (version probe) |
60s | orchestrator.defaultProbeTime |
Scanner marked unavailable with diagnostics |
The 60s version probe is deliberately generous: heavy tools (e.g. Checkov, a Python process) are slow to start cold, especially when all 12 scanners start at the same time on a memory-starved runner. The orchestrator distinguishes three probe failures and emits an actionable message:
- probe timeout → "tool too slow to start or resource-starved (give the
container more memory, or scope
--scanners)"; - killed (OOM) —
signal: killed→ "likely out of memory; raise the container's memory limit"; - not installed → "not installed/available".
An execution timeout never brings down the whole scan: the scanner's status
becomes timeout/error and the report records that explicitly — "0 findings is
not proof of safety".
2.3 Pre-cached Grype DB¶
The :full image (Dockerfile.full) freezes Grype's vulnerability base at build
time:
ENV GRYPE_DB_CACHE_DIR=/opt/grype/db \
GRYPE_DB_AUTO_UPDATE=false \
GRYPE_DB_VALIDATE_AGE=false
RUN grype db update && grype db status
Direct performance and resilience benefits:
- The first scan works offline and does not fail with "failed to load vulnerability db: database does not exist".
- It removes the DB download from the critical path of every CI job.
GRYPE_DB_VALIDATE_AGE=falseis mandatory here: without it Grype would refuse a DB older than 5 days (db.max-allowed-built-age) and fail every scan a few days after the build. The baked DB is accepted by design (it never expires); updating it means a rebuild orGRYPE_DB_AUTO_UPDATE=trueat runtime (requires network).- Documented trade-off: the DB is frozen at build date — freshness is a supply-chain choice, not a crash.
2.4 DoS caps (resource limits)¶
Two limits protect the Quorum process against pathological targets/outputs (R2/R5), independently of scanner reliability:
| Cap | Default | Override env | Behavior on expiry | Where |
|---|---|---|---|---|
| Stdout per scanner | 512 MiB | QUORUM_MAX_OUTPUT_BYTES (bytes) |
aborts the adapter with an error (avoids OOM while buffering) | internal/adapter/adapter.go, runCmd/capWriter |
| Target size (repo/k8s) | 20 GiB | QUORUM_MAX_TARGET_BYTES (bytes; 0 disables) |
refuses the scan early with an actionable message | cmd/quorum/scan.go, checkTargetSize |
- The output cap uses a
capWriterthat cuts the read as soon as the ceiling is crossed — a scanner that dumps gigabytes of JSON does not bring the process down. - The target cap does a
filepath.WalkDirwith early-abort: as soon as the sum of regular files exceeds the ceiling, the walk stops (normal repos pay only a lightstatpass). Image-type targets (no local tree) are skipped.
2.5 Performance goals (product SLO)¶
Target goals; not yet validated by a formal benchmark (see Gaps).
| Metric | Goal | Condition | Priority |
|---|---|---|---|
| Orchestrator overhead (correlation+consensus+report) | ≤ 2s | ≤ 50k raw findings | P1 |
| Probe time for all scanners | ≤ 60s | :full image warm |
P0 |
| Alias latency (cache hit) | ~0 (no network) | idempotent re-scan | P1 |
Total SCA scan time (:full, average image) |
≤ slowest scanner time + 2s | 2 vCPU / 4 GB runner | P1 |
| Orchestrator peak memory (excluding scanners) | ≤ 256 MB | ≤ 50k findings + DoS caps active | P2 |
Performance checklist:
- [ ] Run with
--offlinein CI when the OSV network is not desired on the critical path. - [ ] Restrict
--scannersto what is needed on small runners (12 scanners on:full). - [ ] Persist
~/.cache/quorum/aliases.jsonacross CI jobs. - [ ] Give the
:fullcontainer enough memory (Checkov/KICS are heavy). - [ ] Tune
QUORUM_MAX_OUTPUT_BYTES/QUORUM_MAX_TARGET_BYTESonly if a legitimate target exceeds the defaults. - [ ] Keep
--adviceoff (default) on the critical path; the advisory layer is post-processing and optional.
3. Scalability¶
3.1 Stateless CLI¶
Each Quorum execution is self-contained and without shared state:
- No state is persisted between runs beyond optional caches (on-disk aliases,
and — only under
--advice— the advice cache keyed byfingerprint+provider+model) that are idempotent and rebuildable. - There is no server, session, queue or cross-process coordination.
- The output (SARIF/JSON/XML) is a deterministic function of the inputs
(target + scanners + crosswalk + baseline), with pure
correlationKey/Fingerprint. The advisory layer does not change this: with--adviceoff the output is byte-identical, and even with it on the correlation and consensus fields are untouched.
3.2 Horizontal scale on CI runners¶
flowchart TB
subgraph CI[Pipeline / CI fleet]
R1[Runner 1\nquorum scan repoA]
R2[Runner 2\nquorum scan repoB]
R3[Runner N\nquorum scan imageX]
end
R1 --> S1[(SARIF/JSON\nartifact)]
R2 --> S2[(SARIF/JSON\nartifact)]
R3 --> S3[(SARIF/JSON\nartifact)]
- Scaling is embarrassingly parallel: N independent executions on N runners, with no central contention point (except the registry/Releases when obtaining the artifact).
- The scaling bottleneck is the runner's resources (CPU/memory for the 12 scanners), not Quorum itself.
- Single external dependency at execution time: OSV.dev for aliases —
mitigable with
--offlineor a shared cache.
| Scale vector | Mechanism | Limit |
|---|---|---|
| More repositories/images | more parallel jobs/runners | CI fleet capacity |
| More scanners per scan | goroutine fan-out (up to 12) | runner memory |
| Frequent re-scans | alias cache | cache size (negligible) |
N/A: vertical scalability of a service (pod auto-scaling, database sharding, load balancing) — there is no long-running service. See §4.
4. Availability¶
N/A as a service. Quorum exposes no endpoint, daemon or long-running process; therefore there is no service uptime to measure. The concept is reinterpreted as distribution availability — the ability to obtain and run the artifact.
flowchart LR
Dev[Developer / CI] -->|docker pull| GHCR[(GHCR\nghcr.io)]
Dev -->|download| REL[(GitHub Releases\nGoReleaser)]
Dev -->|api.osv.dev| OSV[(OSV.dev\noptional)]
GHCR --> Run[quorum scan ...]
REL --> Run
OSV -. graceful degradation .-> Run
| Dependency | Role | Availability | Mitigation |
|---|---|---|---|
GHCR (ghcr.io) |
distribution of the :full/:slim images |
GitHub SLA | pin by @sha256, internal mirror, runner cache |
| GitHub Releases | native binaries + checksums + signatures | GitHub SLA | internal mirror, versioned copy |
| OSV.dev | alias resolution (VULN only) | best-effort | --offline, local cache, graceful degradation |
| Scanner binaries | actual execution | local (:full image, 12 scanners) or PATH (:slim) |
:full already bundles everything |
Distribution availability goals:
- [ ] Images published reproducibly on every semver tag (
release.yml). - [ ] Able to operate 100% offline with
:full(pre-cached Grype DB + bundled crosswalk +--offline). - [ ] Enterprise recommendation: mirror the images in an internal registry and pin by digest.
Future proposal (clearly separated): there is no plan to turn Quorum into a hosted service; should that arise, this section would define real service uptime/SLA.
5. Security¶
Quorum's security surface has two fronts: (a) security of the artifact itself and of its supply chain, and (b) execution hygiene (not handling secrets). The scanned target is input data — Quorum only reads it.
5.1 Supply chain¶
flowchart LR
src[Source code\nGo 1.26] --> rel[release.yml]
rel --> img[Images :full / :slim]
rel --> bin[GoReleaser binaries]
rel --> kn[Knowledge pack + crosswalk]
img --> cosign[cosign sign --yes\nkeyless OIDC + retry]
bin --> cblob[cosign verify-blob\nchecksums.txt.sig]
img --> slsa1[attest-build-provenance\nSLSA v1]
img --> sbom1[attest-sbom\nattested SPDX]
bin --> slsa2[attest-build-provenance\nsubject-checksums]
bin --> sbom2[GoReleaser + syft\nSBOM per binary]
kn --> slsa3[attest-build-provenance\nknowledge.sha256]
cosign --> ver[Verify on release + retry]
slsa1 --> ver
sbom1 --> ver
slsa2 --> ver
slsa3 --> ver
| Control | Implementation | Evidence |
|---|---|---|
| Keyless image signing | cosign sign via GitHub OIDC (no keys), with retry (4 attempts) |
release.yml ("Sign image" step) |
| SLSA build-provenance attestation | actions/attest-build-provenance@v2 (image by digest and binaries by subject-checksums) |
release.yml |
| Attested SPDX SBOM | actions/attest-sbom@v2 for the image (syft SPDX-JSON) in addition to BuildKit sbom: true; GoReleaser+syft per binary |
release.yml, .goreleaser.yaml |
| Knowledge-pack + crosswalk attestation | actions/attest-build-provenance@v2 over knowledge.sha256 (every advisory pack + crosswalk file); verified in the same release |
release.yml (knowledge job) |
| Verification on the release itself | gh attestation verify oci://…@DIGEST (with retry) fails the build if invalid |
release.yml ("Verify provenance" step) |
| Binary signing | checksums.txt + cosign verify-blob (.sig/.pem) |
README §Native binary |
| Scanner pinning by digest | FROM …@sha256: (Trivy, KICS, golang, alpine); Kubescape/tfsec/Terrascan/Regula/Conftest verified by SHA256 checksum |
Dockerfile.full |
Bases pinned by @sha256 |
build/runtime images (golang:1.26-alpine, alpine:3.20) pinned by digest |
Dockerfile.full |
| GitHub Action verifies before running | action.yml (composite) cosign-verifies :full by default (verify: true) |
action.yml |
| Restricted release trigger | semver tags v[0-9]+.[0-9]+.[0-9]+ only |
release.yml |
Moving v0/v0.x tag for action pin |
advanced automatically on each semver release; does not trigger a release | tag-major.yml, README §CI/CD |
THIRD_PARTY_NOTICES.md |
license inventory of dependencies/scanners | repository |
Expected consumer verification (recommended in production):
cosign verify ghcr.io/martinez1991/quorum-sec-scan:full \
--certificate-identity-regexp \
"https://github.com/Martinez1991/quorum-sec-scan/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/martinez1991/quorum-sec-scan:full \
--repo Martinez1991/quorum-sec-scan
# Advisory knowledge pack (digest-pinned OWASP corpus + remediation templates):
gh attestation verify knowledge/owasp/corpus.yaml \
--repo Martinez1991/quorum-sec-scan
Honest risk note:
Dockerfile.fullpins Trivy, KICS and the bases (golang/alpine) by@sha256, and verifies the remaining scanners (Grype/Syft/Dockle/Kubescape/Polaris/kube-score/tfsec/Terrascan/Regula/Conftest) by SHA256 checksum of the official release artifacts. Converting all of them to immutable image digests is an incremental improvement (DESIGN §12). See Gaps.
5.2 Absence of secrets¶
| Aspect | State | Justification |
|---|---|---|
| User credentials | none | no accounts/authentication |
| Own API tokens | none | OSV.dev is a public keyless endpoint |
| Scanner credential passthrough | opt-in via QUORUM_<SCANNER>_ARGS (e.g. QUORUM_CHECKOV_ARGS --bc-api-key …) |
supplied by the operator, never echoed in logs (action.yml) |
| Advisory provider key | only with --advice-provider=remote, via QUORUM_ADVICE_API_KEY (opt-in) |
forwarded as env by action.yml; remote is gated on --advice-allow-egress, blocked by --offline, and refuses --fix (would upload source) |
| Secrets in build/release | only the runner's ephemeral GITHUB_TOKEN |
minimal scope (contents, packages, id-token, attestations) |
| Secret persistence | none | nothing is written beyond the alias cache (public) and the opt-in advice cache (normalized findings + advice text, no source code) |
| Secret detection in the target | via the trivy adapter (TypeSecret) |
findings reported, with Trivy's Match redacted (redactSecretText), never persisted outside the report |
Secure-execution principles:
- [ ] Quorum does not request or store credentials.
- [ ] Logs go to stderr and must not contain secrets (detected secret content is redacted before it leaves in the report).
- [ ] Release workflow permissions follow least privilege.
- [ ] Images must be verified before use (cosign + provenance + SBOM).
- [ ] The report is written with restricted
0600permission and the path is normalized withfilepath.Clean(emit,scan.go). - [ ] The remote advisory provider sends only the normalized finding, never source code; it is off by default and requires explicit egress consent.
5.3 Threat model summary¶
| Threat | Vector | Mitigation |
|---|---|---|
| Tampered image | compromised registry / mutable tag | cosign signature + SLSA + attested SBOM + digest pinning |
| Trojanized scanner binary | compromised upstream | digest/checksum pinning at build |
| Silent false negative | scanner does not run but reports 0 | explicit per-scanner status (§6) |
| Malformed bind mount | wrong -v scans empty /work; local image invisible |
documented warning; action.yml auto-mounts docker.sock for type: image |
| Argument injection | target starting with - interpreted as a scanner flag |
validateTargetRef refuses targets starting with - |
| DoS by giant output/target | scanner dumps GBs / pathological tree | caps QUORUM_MAX_OUTPUT_BYTES (512 MiB) and QUORUM_MAX_TARGET_BYTES (20 GiB) (§2.4) |
| Target-data exfiltration | runtime network | --offline; the only external call is OSV (vuln IDs only). The remote advisory provider is off by default, requires --advice-allow-egress, and is blocked by --offline |
6. Observability¶
Being an ephemeral CLI, observability is local to the process: logs for humans or machines on stderr, machine status in the report itself, and exportable metrics for a Prometheus collector.
6.1 Logs (stderr) — text or JSON¶
- All progress telemetry goes to stderr (
scan.go,logf), keeping stdout clean for the report when there is no--output. - The
--log-format text|jsonflag (defaulttext) chooses the format: text: readable lines prefixed with[quorum];json: one object per line with{ts, level, msg}(RFC3339 UTC), ready for ingestion by a structured log collector.--quiet/-qsuppresses the progress logs and the summary.- Logged events: target/type/crosswalk rule count, per-scanner start/end with duration, baseline/min-severity suppressions, metrics writes and the gate firing.
[quorum] target=. type=repo crosswalk=142 rules (./crosswalk) offline=false
[quorum] run trivy (0.71.2) ...
[quorum] done trivy: 17 findings in 4.2s
[quorum] skip grype: does not support target repo
[quorum] filtered: 3 suppressed by baseline (5 entries), 2 below min-severity medium
[quorum] metrics written to quorum.prom
[quorum] gate: found HIGH finding >= --fail-on high → exit 1
The same event in --log-format json:
6.2 Prometheus metrics (--metrics)¶
The --metrics <file> flag writes the scan's metrics in Prometheus text
format (internal/report/metrics.go, WriteMetrics), suitable for a
node_exporter textfile collector or a Pushgateway — the canonical way to expose
telemetry from a CLI without a long-running process. The file is written with
0644 permission (non-sensitive counts, meant for scraping).
Emitted metrics:
| Metric | Type | Labels | Meaning |
|---|---|---|---|
quorum_scan_duration_seconds |
gauge | — | total wall-clock scan time |
quorum_scanner_up |
gauge | scanner, status |
1 if it ran, 0 otherwise |
quorum_scanner_findings |
gauge | scanner |
raw findings per scanner (pre-consensus) |
quorum_scanner_duration_seconds |
gauge | scanner |
duration per scanner |
quorum_findings_after_consensus |
gauge | — | findings after consensus merge |
quorum_findings_total |
gauge | severity |
consensus findings per severity |
quorum_multi_detected |
gauge | — | findings corroborated by more than one scanner |
Advisory metrics (emitted only under --advice, and only describing the
advisory layer — they never alter the consensus metrics above):
| Metric | Type | Labels | Meaning |
|---|---|---|---|
quorum_advice_enriched |
gauge | kind (remediation|references|recommendation) |
findings enriched by each advisory phase |
quorum_advice_provider |
gauge | provider |
advisory provider in use (none|local|remote) |
quorum_advice_fix |
gauge | stage (proposed|verified) |
verify-the-fix rate (verified/proposed) |
6.3 Per-scanner status¶
Each run produces a ScannerRun (orchestrator.go) with machine status,
exposed in the summary (stderr), in the report and in the metrics
(quorum_scanner_up):
| Status | Meaning |
|---|---|
ran |
ran and produced results (even if 0) |
skipped |
scanner does not support the target type |
unavailable |
binary missing / probe failed (timeout/OOM/not installed) |
error |
scanner execution error |
timeout |
execution exceeded --timeout |
The final summary prints status, count per severity, multi-detected findings and the anchor phrase "0 findings is not proof of safety — see scanner statuses above." This turns status into an observable signal, not noise.
6.4 State and proposals¶
| Capability | State | Note |
|---|---|---|
| stderr logs | ✅ Implemented | prefixed, with --quiet |
| Structured JSON log on stderr | ✅ Implemented | --log-format json ({ts,level,msg}) |
| Per-scanner status | ✅ Implemented | in the summary, report and metrics |
| Per-scanner duration | ✅ Implemented | ScannerRun.Duration |
| Prometheus metrics (textfile/Pushgateway) | ✅ Implemented | --metrics <file> |
| Advisory metrics | ✅ Implemented | only under --advice (quorum_advice_*) |
| OpenTelemetry traces | ⚠️ Future proposal | no long-running process; not prioritized |
7. Auditability¶
The report is the audit evidence. It is reproducible, stamps who detected what and provides a stable identity per finding.
| Audit artifact | Content | Where |
|---|---|---|
| Per-scanner status | ran/skipped/unavailable/error/timeout + version + duration |
Result.Runs, report, metrics |
| Detection provenance | detectedBy[], detectionCount, confidence |
MergedFinding, SARIF properties |
| Stable identity | Fingerprint = sha256(correlationKey) |
SARIF partialFingerprints["quorum/v1"], JSON fingerprint |
| Suppressions | always logged and counted, never silently dropped | baseline .quorumignore, filter.Apply |
| Scanner versions | ScannerVersion per finding / ScannerRun.Version |
report |
| Scan metrics | counts/durations in Prometheus format | --metrics file |
Audit properties:
- Reproducibility: same input → same
correlationKey/Fingerprint(pure function). This enables temporal diffs of reports. The opt-in advisory attachments (Remediation/References/Advice) are additive and clearly labeled "AI-generated, advisory only"; they do not alter identity. - Suppression traceability: a finding suppressed by the baseline is reported as suppressed (with the entry that suppressed it), it does not vanish.
- Artifact chain of custody: SARIF/JSON can be archived as CI-gate evidence; signatures/provenance/SBOM (§5) attest the origin of the binary that generated it.
CI audit checklist:
- [ ] Archive the report (SARIF/JSON) as a job artifact.
- [ ] Archive the
--metricsfile alongside the report (historical telemetry). - [ ] Version-control the
.quorumignore(each suppression reviewed and dated). - [ ] Retain the summary output (per-scanner status) alongside the artifact.
- [ ] Record the version/digest of the Quorum image used.
8. LGPD (Brazilian General Data Protection Law)¶
Applicable by exclusion — Quorum does not process personal data. The justification is technical: the data processed is software and infrastructure metadata, not data about natural persons.
| Data category | Processed? | Nature |
|---|---|---|
| Vulnerability identifiers (CVE/GHSA/AVD) | Yes | public technical data |
| PURLs / package versions | Yes | technical data |
| File paths, IaC/K8s resources | Yes | technical data of the target |
| Detected secret content (Trivy) | Possible | not PII by definition; redacted and kept only in the user's report |
| Personal data (name, CPF, e-mail, etc.) | No | out of functional scope |
Consequences:
- No PII ⇒ no need for a legal basis, consent, DPO or DPIA attributable to Quorum as controller/processor.
- The only external call (OSV.dev) sends only vulnerability IDs (e.g.
CVE-2021-…) — there is no personal data in the traffic. Disableable with--offline. - User responsibility: if the scanned target contains PII (e.g. a secret that is an e-mail), the generated report may contain it even with secret redaction. The handling and retention of that report is the responsibility of whoever operates Quorum, not of the product.
- The remote advisory provider (opt-in) sends only the normalized finding, not
source code; it is off by default, requires explicit egress consent, and is
blocked by
--offline.
N/A: data-subject rights, portability, internal anonymization — there is no personal-data store under Quorum's control.
9. PCI DSS¶
N/A as a product. Quorum does not store, process or transmit cardholder data (CHD/SAD) and is not part of a Cardholder Data Environment (CDE) by construction. Therefore it is not in scope of PCI DSS as a system.
However, Quorum can support the compliance of whoever operates a CDE, as a supporting tool for the requirements below:
| PCI DSS v4.0 requirement | How Quorum helps |
|---|---|
| 6.2 / 6.3 — develop secure software and manage vulnerabilities | SCA consensus (Trivy+Grype) with confidence for prioritization |
| 6.3.1 — identify vulnerabilities from recognized sources | CVE IDs via OSV; authoritative confirmation weighs into the score |
| 6.3.2 — inventory of components (bespoke/3rd-party) | PURLs per finding; attested SPDX SBOM of the :full image |
| 6.4 / 6.5 — protect applications and manage changes | CI gate via --fail-on; auditable baseline; policy-as-code (Conftest) |
| 11.3 — vulnerability scans | pipeline-automatable scanning, SARIF evidence |
Quorum does not replace an external ASV scan or a pentest. It is a compensating/preventive control in the SDLC, not a compliance attestation.
10. ISO/IEC 27001 (Annex A)¶
Mapping of the controls applicable to Quorum (as a tool and as a build chain). Broad organizational controls are the responsibility of the adopter.
| Control (ISO 27001:2022) | Applicability | How Quorum addresses it |
|---|---|---|
| A.8.8 — Technical vulnerability management | ✅ Direct | product core: detection + consensus + gate |
| A.8.25 — Secure development lifecycle | ✅ Supports | integrates into the SDLC via CI |
| A.8.28 — Secure coding | ✅ Supports | actionable IaC/MISCONFIG/SECRET/policy-as-code findings |
| A.8.29 — Security testing in dev/acceptance | ✅ Supports | gate via exit code |
| A.8.30 — Outsourced development | ⚙️ Partial | scan of 3rd-party components (SCA) |
| A.8.31 — Separation of environments | ➖ Adopter's | isolated execution per ephemeral job |
| A.8.15 — Logging | ⚙️ Partial | stderr logs (text/json) + per-scanner status + metrics |
| A.8.16 — Monitoring | ⚙️ Partial | Prometheus metrics (--metrics) |
| A.5.23 — Security in cloud services | ➖ Adopter's | N/A (no own cloud service) |
| A.5.7 — Threat intelligence | ⚙️ Partial | OSV/CVE as vulnerability source |
| Supply chain (A.5.19–A.5.23) | ✅ Own | cosign + SLSA provenance + attested SBOM + knowledge-pack attestation + sha256/checksum pinning (§5) |
Legend: ✅ meets/supports directly · ⚙️ partial · ➖ adopter's responsibility.
11. OWASP ASVS (controls relevant to a CLI)¶
Most ASVS chapters assume a web application with session and authentication — N/A here. What remains are the controls applicable to a binary/CLI:
| ASVS domain | Applicability | State in Quorum |
|---|---|---|
| V1 Architecture & threat modeling | ✅ | DESIGN.md + §5.3 of this doc |
| V5 Input validation | ✅ | flag parsing (cobra); validateTargetRef refuses -…; adapter parsers with contract tests |
| V7 Error handling & logging | ✅ | actionable errors, stderr logs (text/json), secret redaction |
| V10 Malicious code / integrity | ✅ | signing, provenance, SBOM, dependency pinning |
| V12 Files & resources | ⚙️ | reads target/crosswalk/baseline; report written with 0600 and filepath.Clean; output/target DoS caps |
| V14 Configuration | ✅ | secure defaults (--offline optional, crosswalk with fallback, advisory off by default) |
| V2 Authentication | ➖ N/A | no authentication |
| V3 Session | ➖ N/A | no session |
| V4 Access control | ➖ N/A | no multi-user |
| V6 Stored cryptography | ➖ N/A | stores no sensitive data |
| V8/V9 Data/Communication | ⚙️ | OSV via HTTPS; remote advisory (opt-in) via HTTPS with explicit egress consent; no sensitive data in transit |
| V13 APIs | ➖ N/A | exposes no API |
Practical ASVS checklist:
- [ ] Validate flag format/severity and fail with exit code
2on invalid usage. - [ ] Ensure adapter parsers have a versioned fixture/contract test (coverage in CI).
- [ ] Do not emit secret content in progress logs (redaction active).
- [ ] Verify binary/image integrity before executing.
12. OWASP Top 10 (of the binary/image itself)¶
Scope: risks of the Quorum artifact itself, not of the target it scans (that is the scanners' job). Mapping to OWASP Top 10:2021.
| Risk | Relevance to Quorum | Control |
|---|---|---|
| A01 Broken Access Control | Low | no access control (local CLI) |
| A02 Cryptographic Failures | Low | keeps no secrets; signatures via Sigstore; report 0600 |
| A03 Injection | Medium | invokes scanners via exec (args, not shell); validateTargetRef blocks argument injection via - |
| A04 Insecure Design | Medium | "false split > false merge" principle; threat model in DESIGN |
| A05 Security Misconfiguration | Medium | secure defaults; docker.sock auto-mount for type: image; crosswalk with fallback |
| A06 Vulnerable & Outdated Components | High | Go deps + 12 pinned scanner binaries; SPDX SBOM; Quorum itself should be scanned |
| A07 Identification & Auth Failures | N/A | no authentication |
| A08 Software & Data Integrity Failures | High | cosign (with retry) + SLSA provenance + attested SBOM + knowledge-pack attestation + digest/checksum pinning |
| A09 Logging & Monitoring Failures | Medium | per-scanner status + metrics avoid silent false negatives |
| A10 SSRF | Low | the only network egress is OSV.dev (fixed host); --offline disables it. The opt-in remote advisory endpoint is user-configured and gated on explicit egress consent |
Prioritized actions:
- [ ] A06/A08: periodic rebuild to refresh the Grype DB and scanner version bumps.
- [ ] A08: convert the remaining checksums to image
@sha256inDockerfile.full. - [ ] A03: keep scanner invocation via args (no shell), reviewed in code review.
Note on the AI advisory layer: the OWASP LLM Top 10 only applies when the AI layer is enabled (
--advice-provider=local|remote). The deterministic core has no AI; see 13-ia.md.
13. Resilience (graceful degradation)¶
Resilience is a central design principle: a partial failure degrades, it does not crash, and the degradation is always visible.
flowchart TD
A[Scanner missing] --> A1[status unavailable\nscan continues]
B[Slow probe / OOM] --> B1[status unavailable\nactionable message]
C[Scanner exceeds --timeout] --> C1[status timeout\nothers continue]
D[OSV.dev unavailable] --> D1[uses original id\nlocal cache; no failure]
E[Crosswalk missing] --> E1[fallback /opt/quorum/crosswalk\nor unmapped finding]
F[Unmapped control] --> F1[isolated finding\nunmapped flag]
G[Giant output/target] --> G1[DoS cap aborts\nwith actionable message]
H[cosign/OIDC unstable on release] --> H1[retry 4x\nwith backoff]
I[Advice model unreachable] --> I1[report ships without AI advice\nscan never fails]
| Failure | Behavior | Source |
|---|---|---|
| Scanner not installed | unavailable, scan proceeds |
orchestrator.runOne |
| Slow probe / OOM | unavailable with diagnostics (memory/scope) |
orchestrator.runOne |
| Execution timeout | timeout, other scanners continue |
orchestrator + --timeout |
| OSV network unavailable | aliases degrade to original id; never fails the scan | alias/DESIGN §7 |
:full without network |
pre-cached Grype DB + bundled crosswalk | Dockerfile.full |
| Aged Grype DB | GRYPE_DB_VALIDATE_AGE=false — DB never expires, scan does not hang |
Dockerfile.full |
| Giant scanner output | aborted at the 512 MiB cap (QUORUM_MAX_OUTPUT_BYTES) |
adapter.runCmd |
| Giant repo/k8s target | refused at the 20 GiB cap (QUORUM_MAX_TARGET_BYTES) |
scan.checkTargetSize |
| Missing default crosswalk | automatic fallback to /opt/quorum/crosswalk |
resolveCrosswalkDir |
| Unmappable control | finding stays isolated, unmapped flag (does not guess a merge) |
DESIGN §6 |
| Unstable signing/verification on release | retry (4 attempts, growing backoff) on cosign and gh attestation verify |
release.yml |
| Advice model/endpoint unreachable | graceful degradation: report ships without AI advice, scan never fails | internal/advisor |
Resilience invariant: "0 findings is not proof of safety" — every degradation shows up as status, preventing a failure from turning into a false "all clear".
14. Backup, DR, RTO and RPO¶
N/A — Quorum is stateless. There is no production data under its custody that could be lost. Continuity depends on reproducible artifacts, not on backup/restore.
| Concept | Applicability | Justification |
|---|---|---|
| Data backup | N/A | no persistent storage of business state |
| Disaster Recovery (DR) | N/A (service) | there is no service to recover |
| RTO | ~minutes, reinterpreted | "recover" = re-run the scan on another runner |
| RPO | 0, reinterpreted | nothing to lose; each scan is recomputable from source |
What replaces backup/DR:
- Reproducibility: digest-pinned images + signed binaries + provenance + attested SBOM allow rebuilding/obtaining exactly the same artifact.
- Distribution mirror: mirroring images/binaries in an internal registry protects against GHCR/Releases unavailability.
- Alias cache: disposable; rebuilt from OSV/scanner. Written with
0600permission and aschemaVersionfor safe invalidation. (The opt-in advice cache is equally disposable and rebuildable, keyed byfingerprint+provider+model.)
The only truly recoverable item: the archived report as evidence (§7) — its retention/backup is the responsibility of the pipeline that generates it, not of Quorum.
Future proposal (separated): if a mode with a shared persistent cache is adopted at scale, define a backup/retention policy for that cache.
15. SLA / SLO / SLI¶
There is no contractual service SLA (it is not a service). We define SLO/SLI for two real operational contexts: use in CI and release pipeline.
15.1 Use in CI (the binary/image as a pipeline dependency)¶
| SLI | Definition | Target SLO | How to measure |
|---|---|---|---|
| Scan reliability | % of jobs that finish with exit 0/1 (no 2) |
≥ 99% | job exit codes |
| Determinism | % of re-scans of the same input with the same fingerprint set | 100% | report diff |
| Overhead latency | orchestrator overhead (excluding scanners) | ≤ 2s p95 | summary/Duration/metrics |
| Network-failure resilience | scans completed with OSV unavailable | 100% (degrades) | --offline execution |
| Execution coverage | % of supported scanners with ran status |
≥ 95% on :full |
per-scanner status / quorum_scanner_up |
| Advisory neutrality | % of scans where --advice leaves the core output (fingerprints/consensus) unchanged |
100% | byte-diff with/without --advice |
15.2 Release pipeline (artifact publishing)¶
| SLI | Definition | Target SLO | How to measure |
|---|---|---|---|
| Publishing success | % of semver tags that publish images+binaries | 100% | release.yml |
| Verified integrity | % of releases with cosign+provenance+SBOM+knowledge-pack verified in the build itself | 100% | "Verify provenance" / attest-sbom / knowledge job steps |
| Build reproducibility | deterministic build (-trimpath, pinned deps, bases by sha256) |
100% | Dockerfile.full/GoReleaser |
| Test coverage | tests with -race -covermode=atomic on every push/PR |
blocking green | ci.yml ("Test (race + coverage)" step) |
| Advisory eval coverage | remediation coverage, OWASP reference relevance and verify-the-fix rate measured in CI | tracked (no heavy model) | internal/evals harness |
| Release time | tag → images+binaries published | ≤ 30 min | workflow duration |
Important distinction between product SLA vs. integrity: publishing availability depends on GitHub/GHCR (not under our control), but integrity (signature + provenance + SBOM + knowledge-pack verified) is blocking in the release itself — an artifact with broken attestation fails the build. Transient Sigstore/OIDC instability is absorbed by retry (§13), not by relaxing the verification.
Assumptions¶
- The numbers/goals in §2.5 and §15 are engineering targets, not yet validated by a formal benchmark in this repository.
- The production consumer is assumed to verify the image/binary (cosign +
gh attestation verify) and, ideally, pin by digest — the product provides the means (signature, provenance, attested SBOM, knowledge-pack attestation), the application is the adopter's. --offlineis the recommended mode when CI policy forbids network egress; with it, the only external runtime dependency (OSV.dev) is eliminated, and the remote advisory provider is blocked entirely.- "No PII" assumes the product is not configured to process personal data; PII that happens to be present in the target is the responsibility of the report's operator (even with secret redaction applied).
- The advisory layer (
--advice) is opt-in and off by default; when off, the output is byte-identical and determinism is preserved. Only the remote provider causes data to leave the host, and only under explicit consent (--advice-allow-egress), sending the normalized finding, never source code. - The PCI DSS, ISO 27001, ASVS and OWASP Top 10 mappings are interpretive, based on the as-is behavior of the code (v0.8.3), and do not constitute a compliance attestation.
- Distribution availability depends on third-party services (GitHub GHCR/Releases, OSV.dev), whose SLAs are not controlled by this project.
- The
:fullimage islinux/amd64(the 12 bundled scanners are amd64); arm64 scenarios use:slim(amd64+arm64) with scanners on the PATH, which changes the local performance/availability profile. - The DoS cap defaults (512 MiB of output per scanner, 20 GiB target) are sized far above any real case; legitimate targets that exceed them require an explicit env override.
Known gaps¶
- There is no versioned performance benchmark to back the target SLOs.
- Not all scanners in
Dockerfile.fullare pinned by an immutable image digest (Trivy/KICS/bases are; the rest by version+SHA256 checksum); hardening all of them to@sha256is a recommended incremental improvement. - Observability covers logs (text/json), per-scanner status and Prometheus metrics
(
--metrics, plus the opt-inquorum_advice_*); OpenTelemetry traces remain a future proposal. - There is no native mirror/retention mechanism for artifacts; it is the adopter's responsibility.