Interfaces (CLI) and Output Formats¶
Documented version: v0.8.3 · Revision: 2026-07-04
Quorum (quorum-sec-scan, v0.8.3) is a CLI/Docker-only consensus security
scanning tool. There is no web frontend, relational database, REST/HTTP API or
authentication layer. Therefore, this product's equivalent of "APIs" is the set of
interface contracts it exposes to the outside world:
- The command-line interface (
quorum scan,quorum list-scanners) — inputs (args/flags/env), outputs (stdout/stderr/file) and exit codes; - The output formats (primary SARIF, JSON, XML) — each a stable serialization
contract — plus Prometheus telemetry (
--metrics) as an auxiliary format; - The composite GitHub Action (
action.yml) — declarative inputs/outputs that wrap the signed:fullimage.
This document treats each command, flag and format as a versioned contract: invocation
method, inputs, outputs, validation, errors and examples. Where the "API" template asks for
OpenAPI/HTTP, we record N/A with technical justification and deliver instead a
JSON Schema of the JSON output and the declarative interface of action.yml.
Code references (source of truth for this page):
cmd/quorum/scan.go, cmd/quorum/root.go,
internal/report/, internal/orchestrator/orchestrator.go,
internal/adapter/adapter.go, internal/model/model.go, action.yml.
1. Why "HTTP API / OpenAPI = N/A"¶
| "API" template item | Applicability in Quorum | Technical justification |
|---|---|---|
| HTTP / REST endpoint | N/A | There is no server, daemon or network listener. The binary runs, produces the report and exits. root.go explicitly states "No panel, no daemon". |
| OpenAPI/Swagger specification | N/A | There is no HTTP surface to describe. The equivalent contract is the CLI (this page) + the JSON Schema of the output (§7) + the action.yml interface (§8). |
| Authentication / OAuth / API keys | N/A | No accounts, no session, no multi-tenancy. The only relevant credential is the keyless cosign (OIDC) verification of the image in the Action — it is not user authentication. Passthrough of platform keys (e.g. Checkov's --bc-api-key) is done via env, not login. See 10-infraestrutura.md. |
| API rate limiting | N/A for the CLI; applies indirectly to OSV.dev | The CLI neither imposes nor suffers its own rate limit. The only network access is alias resolution via OSV.dev, with graceful degradation on failure/limit and full shutdown via --offline. See §5 and 07-persistencia-e-artefatos.md. |
| Quotas / resource limits | Applies as DoS caps via env | Two ceilings protect the process: QUORUM_MAX_OUTPUT_BYTES (512 MiB of buffered stdout per scanner) and QUORUM_MAX_TARGET_BYTES (20 GiB of on-disk tree). See §4.3. |
| API versioning | Applies as release/semver versioning + the quorum/v1 schema |
See §9. |
The network is only touched for alias enrichment (OSV.dev) and, in the Action's context, to pull/verify the image. The scan flow itself is local to the host/container.
2. Interface map¶
flowchart LR
subgraph Invocation
A[Native binary<br/>quorum]
B[Docker<br/>ghcr.io/.../quorum-sec-scan:full|:slim]
C[GitHub Action<br/>Martinez1991/quorum-sec-scan@v0]
end
A --> CLI[CLI cobra]
B --> CLI
C -->|docker run| B
CLI -->|scan <target>| ORCH[Orchestrator<br/>12 scanners]
CLI -->|list-scanners| REG[Adapter registry]
ORCH --> REP[report.Write]
REP -->|sarif / json / xml| OUT{--output?}
OUT -->|empty| STDOUT[stdout]
OUT -->|file| FILE[0600 file on disk]
ORCH -.->|--metrics| MET[report.WriteMetrics<br/>Prometheus textfile 0644]
CLI -->|progress/summary| STDERR[stderr text|json]
CLI -->|0 / 1 / 2| EXIT[exit code]
3. Global CLI contract¶
| Aspect | Contract |
|---|---|
| Binary | quorum |
| Commands | scan <target>, list-scanners, advise-index |
| Global flags | --version / -v (prints the version), --help / -h |
| Version | Injected at build via -ldflags "-X main.version=..."; fallback default 0.1.0 (root.go). The version is also stamped into the SARIF driver and the fingerprint namespace (report.Version). |
stdout |
Only the report when --output is empty. Nothing else is written to stdout. |
stderr |
Progress logs and the human summary block. Log format selectable via --log-format text\|json. Silenceable with --quiet/-q. |
| Error behavior | SilenceUsage: true and SilenceErrors: true on the root — errors are handled by main (no noisy usage dump). |
3.1 Exit-code contract (shared by all commands)¶
| Exit code | Meaning | Origin in code |
|---|---|---|
0 |
OK — execution completed and no finding reached --fail-on (or --fail-on absent). |
Normal return of runScan. |
1 |
Gate tripped — at least one finding has severity >= --fail-on. |
os.Exit(1) in runScan after severity.AtLeast(worst, failThreshold). |
2 |
Usage or runtime error — invalid flag, missing baseline, unknown format, invalid --log-format, target starting with -, target over the size cap, crosswalk load failure, fatal pipeline error. |
error return from RunE, converted to exit 2 by main. |
Operational principle: the exit code is the gating mechanism in CI.
0does not mean "safe" — it means "nothing crossed the threshold". The summary itself reinforces: "0 findings is not proof of safety". See 09-backend.md.
4. scan <target> command — detailed contract¶
4.1 Invocation¶
<target>is required and exactly 1 argument (cobra.ExactArgs(1)). Zero or more than one argument → usage error (exit 2).<target>is an image reference (alpine:3.19), a repository/IaC directory (.,/work) or a k8s manifests directory.- Hardening (anti argument-injection): a
<target>starting with-is rejected (validateTargetRef), since a downstream scanner could interpret it as a flag. For a literal path, use./-name(exit 2 with a message suggesting the form).
4.2 Inputs — flags¶
Defined in cmd/quorum/scan.go (newScanCmd):
| Flag | Short | Type | Default | Description / validation |
|---|---|---|---|---|
--type |
— | string | "" (inferred) |
image \| repo \| k8s. Accepts aliases: repo = fs/dir, k8s = kubernetes/manifests. Invalid value → error (exit 2). If omitted, infers: a path existing on disk ⇒ repo; otherwise ⇒ image. |
--scanners |
— | string (CSV) | "" (all) |
Comma-separated list. Normalized to lowercase, spaces and empty items dropped. Unknown names do not abort: they emit warning: unknown scanner ... and are ignored (orchestrator). |
--format |
-f |
string | sarif |
sarif \| json \| xml (case-insensitive, trimmed). Invalid → unknown format ... (exit 2). |
--output |
-o |
string | "" (stdout) |
Output file path. The path is normalized with filepath.Clean; parent directories are created (MkdirAll 0755); the file is written with mode 0600 (owner-only — the report may carry sensitive finding detail). Empty ⇒ stdout. |
--fail-on |
— | string | "" |
critical \| high \| medium \| low. Enables gating. Invalid → error (exit 2). |
--min-severity |
— | string | "" |
Removes findings below this level from the report and from gating (filter applied before emitting/gating). Invalid → error (exit 2). |
--baseline |
— | string | .quorumignore |
File of fingerprints/correlationKeys to suppress. If the user explicitly passed the flag and the file does not exist ⇒ error (exit 2). If it is the default and does not exist ⇒ proceeds without a baseline. |
--crosswalk |
— | string | ./crosswalk |
Mappings directory. If default and ./crosswalk is absent, automatically falls back to /opt/quorum/crosswalk (image bundle). If passed explicitly, it is honored verbatim. |
--advice |
— | bool | false |
Attaches deterministic template-based remediation + OWASP references to findings, including retrieval (RAG) from a versioned, digest-pinned OWASP corpus (knowledge/owasp/, deterministic lexical retrieval, no model). Advisory layer, no AI by default; presentation-only: it does not change correlationKey/fingerprint/confidence/severity nor the gating. Without the flag, the output is byte-identical. See 21-proposta-ia (Phases 0 and 2). |
--knowledge |
— | string | ./knowledge |
Directory of the knowledge pack (templates + OWASP refs) used by --advice. Same fallback as the crosswalk: if default and absent, falls back to /opt/quorum/knowledge (image bundle). |
--advice-provider |
— | string | none |
none \| local \| remote. With local, it queries an on-host LLM (OpenAI-compatible endpoint) for a per-finding recommendation. With remote, it queries an external API (Phase 3) — it sends findings off-host, so it requires --advice-allow-egress + QUORUM_ADVICE_API_KEY, is blocked by --offline and refuses --fix (does not upload code). Presentation-only, labeled "AI-generated, advisory only"; does not affect gating. Graceful degradation: model unreachable ⇒ no advice, the scan does not fail. local is on-host (not disabled by --offline). |
--advice-allow-egress |
— | bool | false |
Explicit consent to send findings (titles, paths, controls) to an external service. Required for --advice-provider=remote; without it, remote is refused. |
--advice-endpoint |
— | string | http://localhost:11434/v1 |
OpenAI-compatible base URL (e.g. Ollama) for --advice-provider=local. |
--advice-model |
— | string | qwen2.5-coder:7b |
Local model id. Composes the cache key (reproducibility). |
--advice-embed-model |
— | string | nomic-embed-text |
Embedding model for semantic retrieval of the OWASP corpus. Only used when the corpus ships vectors (embedding:) and --advice-provider=local; otherwise retrieval is lexical (no model). |
--advice-cache |
— | string | ~/.cache/quorum/advice.json |
Cache of advice keyed by fingerprint+model. Same finding+model ⇒ same recommendation (served from cache). |
--advice-max |
— | int | 50 |
Max findings sent to the model per run (0 = no cap). Cost/latency bound. |
--fix |
— | string | off |
off \| suggest. With suggest, the model proposes a patch that is only attached if it passes the verify-the-fix (re-scan of the fixed file: the finding is gone and the file parses). It never applies automatically. Scope: IaC/K8s (MISCONFIG/K8S_POSTURE); SCA/CVE and image are left out. |
--cache |
— | string | ~/.cache/quorum/aliases.json (via os.UserCacheDir) |
Cache file for the alias resolver. Falls back to .quorum-cache.json if the OS cache dir does not resolve. |
--metrics |
— | string | "" (off) |
Writes Prometheus text-format metrics to this file, alongside the normal report (telemetry). Path normalized with filepath.Clean; parent dirs created; file written with mode 0644 (non-sensitive counts, meant for scraping). See §7.5. |
--log-format |
— | string | text |
text \| json. Controls the shape of progress logs on stderr. text ⇒ [quorum] ...; json ⇒ one JSON line per event ({"ts","level","msg"}). Invalid → invalid --log-format "x" (want text\|json) (exit 2). |
--timeout |
— | duration | 5m |
Per-scanner timeout (not global). Go time.Duration format (30s, 2m, 1h). |
--offline |
— | bool | false |
Turns off OSV.dev lookups (uses the scanner's local aliases + cache). |
--quiet |
-q |
bool | false |
Suppresses progress logs and the summary on stderr (independent of --log-format). |
Note on the version probe: the probe timeout (
Options.ProbeTime, internal default) is an orchestrator constant and is not exposed as a flag in this version. It distinguishes timeout / killed(OOM) / not-installed. See 09-backend.md.
4.3 Inputs — environment variables¶
Unlike earlier versions, the CLI now reads its own environment variables (there is no env→flag binding for the flags above, but there is env for passthrough and for the DoS caps). Inherited from the environment / OS:
HOME/ equivalents — used byos.UserCacheDir()to resolve the default of--cache.
Per-scanner passthrough (internal/adapter/adapter.go, extraArgs):
QUORUM_<SCANNER>_ARGS— extra CLI arguments appended to that scanner's invocation, without changing the adapter.<SCANNER>is the adapter name in UPPERCASE (e.g.QUORUM_TRIVY_ARGS,QUORUM_GRYPE_ARGS,QUORUM_CHECKOV_ARGS,QUORUM_KICS_ARGS,QUORUM_DOCKLE_ARGS,QUORUM_KUBESCAPE_ARGS). The value is split shell-style (respects single/double quotes; no variable expansion). Typical use:QUORUM_CHECKOV_ARGS="--bc-api-key <key> --repo-id org/repo"unlocks Prisma Cloud/Bridgecrew policies through the embedded Checkov OSS. It is an operator control (same trust level as the flags); values may carry secrets and for that reason are not echoed.
Protection caps (DoS):
QUORUM_MAX_OUTPUT_BYTES— ceiling of buffered stdout per scanner (default 512 MiB). If a scanner (or a zip/xml bomb) exceeds it, the run is aborted with a clear error instead of causing OOM.internal/adapter/adapter.go.QUORUM_MAX_TARGET_BYTES— ceiling for the on-disk size of arepo/k8starget (default 20 GiB;0disables). The size walk stops as soon as the cap is crossed (normal repos pay only a light stat).imagetargets (no local tree) are ignored. Invalid value → error (exit 2).cmd/quorum/scan.go(checkTargetSize).
The Action (action.yml) injects the inputs as env inside its own shell script,
translating them to CLI flags and to QUORUM_<SCANNER>_ARGS; that is an Action detail (§8),
not a CLI one.
4.4 Outputs¶
flowchart TD
R[runScan] --> EMIT[emit]
EMIT -->|output == ""| SO[cmd.OutOrStdout → stdout]
EMIT -->|output != ""| WF[filepath.Clean<br/>os.WriteFile 0600<br/>creates parent dirs]
R --> PS[printSummary → stderr]
PS -. --quiet .-> NONE[suppressed]
R -->|--metrics != ""| MW[writeMetricsFile 0644<br/>Prometheus textfile]
R --> GATE{gating?}
GATE -->|worst >= fail-on| EX1[os.Exit 1]
GATE -->|otherwise| EX0[return nil → 0]
- stdout: the serialized report (SARIF/JSON/XML) when
--outputis empty. - file (
--output): the same content, with mode0600. - file (
--metrics): Prometheus metrics, with mode0644(§7.5). - stderr: progress logs (
target=... type=... crosswalk=N rules ..., per-scanner status, a filtering line when there are suppressions) and the── quorum summary ──block (counts by severity, multi-detected, elapsed, and the note "0 findings is not proof of safety"). In--log-format jsonmode, progress logs come out as one JSON line per event; the human summary block stays as text. Everything is suppressed by--quiet.
4.5 Validation and errors (summary)¶
| Condition | Message (form) | Exit |
|---|---|---|
| # of args ≠ 1 | cobra args error | 2 |
<target> starts with - |
invalid target "-x": must not start with '-' (use "./-x" for a path) |
2 |
Invalid --type |
invalid --type "x" (want image\|repo\|k8s) |
2 |
Invalid --log-format |
invalid --log-format "x" (want text\|json) |
2 |
| Target over the cap | target "x" exceeds the N-byte size cap (...QUORUM_MAX_TARGET_BYTES) |
2 |
Non-numeric QUORUM_MAX_TARGET_BYTES |
invalid QUORUM_MAX_TARGET_BYTES "x" |
2 |
Invalid --fail-on |
invalid --fail-on "x" (want critical\|high\|medium\|low) |
2 |
Invalid --min-severity |
invalid --min-severity "x" (...) |
2 |
Invalid --advice-provider |
invalid --advice-provider "x" (want none\|local\|remote) |
2 |
remote without --advice-allow-egress |
--advice-provider=remote sends your findings ...; re-run with --advice-allow-egress to consent ... |
2 |
remote under --offline |
--advice-provider=remote is disabled by --offline ... |
2 |
remote without API key |
--advice-provider=remote needs an API key in QUORUM_ADVICE_API_KEY |
2 |
--fix with remote |
--fix is not allowed with --advice-provider=remote ... |
2 |
Explicit --baseline missing |
baseline file not found: <path> |
2 |
Invalid --format |
unknown format "x" (want sarif\|json\|xml) |
2 |
| Crosswalk load failure | loading crosswalk: ... |
2 |
| Metrics write failure | writing metrics: ... |
2 |
| Pipeline error (orchestrator) | propagated error | 2 |
Finding >= --fail-on |
(not an error) gate logged, exit 1 | 1 |
4.6 Examples¶
# 1) Repository scan, gate at HIGH, SARIF to file (typical CI case)
quorum scan . --type repo --fail-on high -o quorum.sarif
# 2) Image scan, only two scanners, JSON output to stdout
quorum scan alpine:3.19 --type image --scanners trivy,grype --format json
# 3) k8s manifests, offline, suppressing findings below MEDIUM
quorum scan ./k8s --type k8s --offline --min-severity medium
# 4) With baseline, larger per-scanner timeout and Prometheus metrics to a textfile
quorum scan . --baseline .quorumignore --timeout 10m \
-o report.xml -f xml --metrics /var/lib/node_exporter/quorum.prom
# 5) JSON logs (for aggregators) and passthrough of args to Checkov
QUORUM_CHECKOV_ARGS="--bc-api-key $BC_KEY --repo-id org/repo" \
quorum scan . --type repo --log-format json --fail-on high -o quorum.sarif
# 6) Advisory layer with a local model + verify-the-fix suggestions
quorum scan . --type repo --advice --advice-provider local \
--advice-model qwen2.5-coder:7b --fix suggest -o quorum.sarif
# 7) Via Docker (self-contained :full image)
docker run --rm -v "$PWD:/work" -w /work \
ghcr.io/martinez1991/quorum-sec-scan:full \
scan . --type repo --fail-on critical -o quorum.sarif
5. --offline, OSV.dev and rate limiting¶
- Without
--offline, the alias resolver may query OSV.dev (preferring CVE IDs), with a local cache at~/.cache/quorum/aliases.json(a file with mode0600and aschemaVersion— see 07-persistencia-e-artefatos.md). - The CLI does not implement its own rate limiting and exposes no throttling controls. On a network failure or OSV unavailability, there is graceful degradation: the pipeline continues with the scanner's local aliases + cache, without aborting.
--offlinefully turns off the resolver's network access (osvbecomesnilinrunScan). In air-gapped CI environments, it is the flag to use.
Details in 07-persistencia-e-artefatos.md.
6. list-scanners command — contract¶
6.1 Invocation¶
- No arguments, no specific flags.
- Lists the registered adapters (sorted by name) and their supported finding types
(
Capabilities()).
6.2 Output¶
- stdout, one line per scanner, format
"%-12s %v"(aligned name + slice of types). In v0.8.3 there are 12 registered adapters. The canonical output (derived from theCapabilities()ininternal/adapter/):
checkov [MISCONFIG]
conftest [MISCONFIG]
dockle [IMG_HARDENING]
grype [VULN]
kics [MISCONFIG]
kube-score [K8S_POSTURE]
kubescape [K8S_POSTURE]
polaris [K8S_POSTURE]
regula [MISCONFIG]
terrascan [MISCONFIG]
tfsec [MISCONFIG]
trivy [VULN MISCONFIG SECRET]
The 12 scanners cover four families: SCA/VULN (trivy, grype), MISCONFIG/IaC (checkov, kics, terrascan, tfsec, regula, conftest), K8S_POSTURE (kubescape, polaris, kube-score) and IMG_HARDENING (dockle).
conftestruns policy-as-code with its own Rego from./policy. See 09-backend.md and the adapters ininternal/adapter/.
6b. advise-index command — enabling semantic RAG¶
The OWASP corpus (21-proposta-ia, Phase 2) runs lexical by default (no vectors). To enable semantic retrieval, embed the corpus once against a local embeddings endpoint (e.g. Ollama):
quorum advise-index \
--corpus knowledge/owasp/corpus.yaml \
--advice-endpoint http://localhost:11434/v1 \
--advice-embed-model nomic-embed-text
- It adds
embedding:to each chunk and records theembedModelin the file. The content digest is preserved (embeddings are excluded from the hash), so the pin stays valid. - From then on,
scan --advice --advice-provider localpicks the semantic retriever automatically (thescanembeds the query with the same model registered in the corpus). Without embeddings, it stays lexical and deterministic.
6.3 Exit codes¶
0on success;2only on an unexpected runtime error.
7. Output-format contract¶
Selected by --format/-f. The three report formats serialize the same
orchestrator.Result (internal/report/report.go → Write). They differ in shape and
target consumer. There is also an auxiliary telemetry format (Prometheus), emitted in
parallel by --metrics (§7.5).
flowchart LR
RES[orchestrator.Result] --> W{Format}
W -->|sarif| S[writeSARIF<br/>SARIF 2.1.0]
W -->|json| J[writeJSON<br/>quorum JSON]
W -->|xml| X[writeXML<br/>quorumReport]
RES -.->|--metrics| M[WriteMetrics<br/>Prometheus text-format]
7.1 SARIF (primary) — --format sarif¶
Source: internal/report/sarif.go.
$schema:https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.jsonversion:2.1.0- A single
runwith: tool.driver:name: "quorum",informationUri: "https://github.com/quorum-sec/quorum",version(=report.Version), and theruleslist (deduplicated byruleId, sorted by id).results[]: one perMergedFinding.- run
properties:target(ref) andscanners(array of{name, status, version}).
Contract of each result:
| Field | Origin | Note |
|---|---|---|
ruleId |
sarifRuleID(m) |
For VULN: VulnID of the 1st member (CVE/GHSA). For the other types: CanonicalControl (AVD/CIS) or, failing that, the scanner's RuleID; final fallback: CorrelationKey. |
level |
sarifLevel(severity) |
CRITICAL/HIGH ⇒ error; MEDIUM ⇒ warning; others ⇒ note. |
message.text |
m.Title |
|
locations[] |
members with Location.File |
Deduplicated by file; region.startLine/endLine when StartLine > 0. |
partialFingerprints |
{ "quorum/v1": m.Fingerprint } |
Stable correlation key across runs (= sha256(correlationKey)). This is what avoids duplication/re-alert in GitHub Code Scanning. |
properties |
Quorum object | detectedBy (list of scanners), detectionCount, confidence (rounded to 2 places), severity, correlationKey, unmapped. |
Example (excerpt):
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "quorum",
"informationUri": "https://github.com/quorum-sec/quorum",
"version": "0.8.3",
"rules": [
{ "id": "CVE-2023-1234", "name": "VULN",
"shortDescription": { "text": "openssl: heap overflow" },
"properties": { "type": "VULN" } }
]
}
},
"results": [
{
"ruleId": "CVE-2023-1234",
"level": "error",
"message": { "text": "openssl: heap overflow" },
"locations": [],
"partialFingerprints": { "quorum/v1": "9f2b...c0" },
"properties": {
"detectedBy": ["trivy", "grype"],
"detectionCount": 2,
"confidence": 0.88,
"severity": "HIGH",
"correlationKey": "VULN|CVE-2023-1234|pkg:apk/alpine/openssl",
"unmapped": false
}
}
],
"properties": {
"target": "alpine:3.19",
"scanners": [
{ "name": "grype", "status": "ran", "version": "0.74.0" },
{ "name": "trivy", "status": "ran", "version": "0.50.0" }
]
}
}
]
}
The
partialFingerprints["quorum/v1"]is the most important integration point with platforms that consume SARIF (e.g. GitHub Advanced Security): it guarantees deterministic deduplication based on consensus, not on the individual scanner.Secret redaction:
SECRETfindings have the matching snippet (trivy'sMatch) redacted in the adapter (redactSecretText) — only the first 4 characters of each long token survive, followed by…REDACTED…. The report carries the context of the secret without leaking its value.
7.2 JSON — --format json¶
Source: internal/report/json.go. Encoder with 2-space indentation
and SetEscapeHTML(false).
Stable shape (jsonReport):
{
"tool": "quorum",
"version": "0.8.3",
"target": { "type": "image", "ref": "alpine:3.19" },
"scanners": [ /* []orchestrator.ScannerRun */ ],
"summary": {
"totalFindings": 12,
"durationMs": 8421,
"bySeverity": { "CRITICAL": 1, "HIGH": 4, "MEDIUM": 5, "LOW": 2 },
"multiDetected": 6
},
"findings": [ /* []model.MergedFinding */ ]
}
7.2.1 JSON Schema (Draft 2020-12) — OpenAPI substitute¶
This is the formal contract of the JSON output. It reflects jsonReport, ScannerRun and
MergedFinding (with members = Finding). Fields with the omitempty Go tag are optional here.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/quorum-sec/quorum/schema/quorum-report-v1.json",
"title": "Quorum JSON Report (quorum/v1)",
"type": "object",
"required": ["tool", "version", "target", "scanners", "summary", "findings"],
"additionalProperties": false,
"properties": {
"tool": { "const": "quorum" },
"version": { "type": "string", "description": "binary/report version" },
"target": {
"type": "object",
"required": ["type", "ref"],
"properties": {
"type": { "type": "string", "enum": ["image", "repo", "k8s"] },
"ref": { "type": "string" }
},
"additionalProperties": false
},
"scanners": {
"type": "array",
"items": { "$ref": "#/$defs/scannerRun" }
},
"summary": {
"type": "object",
"required": ["totalFindings", "durationMs", "bySeverity", "multiDetected"],
"properties": {
"totalFindings": { "type": "integer", "minimum": 0 },
"durationMs": { "type": "integer", "minimum": 0 },
"bySeverity": {
"type": "object",
"additionalProperties": { "type": "integer", "minimum": 0 }
},
"multiDetected": { "type": "integer", "minimum": 0,
"description": "findings with detectionCount > 1" }
},
"additionalProperties": false
},
"findings": {
"type": "array",
"items": { "$ref": "#/$defs/mergedFinding" }
}
},
"$defs": {
"severity": {
"type": "string",
"enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO", "UNKNOWN"]
},
"findingType": {
"type": "string",
"enum": ["VULN", "MISCONFIG", "SECRET", "K8S_POSTURE", "IMG_HARDENING"]
},
"scannerRun": {
"type": "object",
"required": ["name", "status", "findings", "durationMs"],
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"status": { "type": "string",
"enum": ["ran", "skipped", "unavailable", "error", "timeout"] },
"findings": { "type": "integer", "minimum": 0 },
"durationMs":{ "type": "integer" },
"error": { "type": "string" }
},
"additionalProperties": false
},
"mergedFinding": {
"type": "object",
"required": ["correlationKey", "type", "title", "severity",
"detectedBy", "detectionCount", "confidence",
"members", "fingerprint"],
"properties": {
"correlationKey": { "type": "string" },
"type": { "$ref": "#/$defs/findingType" },
"title": { "type": "string" },
"severity": { "$ref": "#/$defs/severity" },
"detectedBy": { "type": "array", "items": { "type": "string" } },
"detectionCount": { "type": "integer", "minimum": 1 },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"unmapped": { "type": "boolean" },
"members": { "type": "array", "items": { "$ref": "#/$defs/finding" } },
"fingerprint": { "type": "string",
"description": "sha256(correlationKey)" }
},
"additionalProperties": false
},
"finding": {
"type": "object",
"required": ["type", "scanner", "severity", "title"],
"properties": {
"type": { "$ref": "#/$defs/findingType" },
"scanner": { "type": "string" },
"scannerVersion": { "type": "string" },
"vulnId": { "type": "string" },
"aliases": { "type": "array", "items": { "type": "string" } },
"purl": { "type": "string",
"description": "pkg:type/ns/name@version" },
"ruleId": { "type": "string" },
"canonicalControl": { "type": "string" },
"category": { "type": "string" },
"unmapped": { "type": "boolean" },
"resource": {
"type": "object",
"properties": {
"kind": { "type": "string" },
"name": { "type": "string" },
"namespace": { "type": "string" },
"address": { "type": "string" }
},
"additionalProperties": false
},
"location": {
"type": "object",
"properties": {
"file": { "type": "string" },
"startLine": { "type": "integer" },
"endLine": { "type": "integer" },
"imageLayer": { "type": "string" }
},
"additionalProperties": false
},
"severity": { "$ref": "#/$defs/severity" },
"cvss": { "type": "number", "description": "0 = absent" },
"correlationKey": { "type": "string" },
"fingerprint": { "type": "string" },
"title": { "type": "string" },
"description": { "type": "string" },
"confirmed": { "type": "boolean",
"description": "confirmed by an authoritative source (NVD/OSV)" }
},
"additionalProperties": false
}
}
}
Fidelity note: the
Rawfield ofFindinghas thejson:"-"tag and is never serialized. That is why it does not appear in the schema above. Likewise,Result.Findings(raw canonical) has thejson:"-"tag at theResultlevel, but the JSON reporter publishes the merged findings viaMergedFinding.Members, so theFindingobjects appear nested.ScannerRunuses a customMarshalJSONthat emitsdurationMsin milliseconds (consistent withsummary.durationMs).
7.3 XML — --format xml¶
Source: internal/report/xml.go. Mirrors the JSON structure for
legacy/JUnit-like pipelines. xml.Header header, 2-space indentation.
Shape (quorumReport):
<?xml version="1.0" encoding="UTF-8"?>
<quorumReport tool="quorum" version="0.8.3">
<target type="image">alpine:3.19</target>
<scanners>
<scanner name="trivy" status="ran" version="0.50.0" findings="9"></scanner>
<scanner name="grype" status="ran" version="0.74.0" findings="7"></scanner>
</scanners>
<findings>
<finding type="VULN" severity="HIGH" detectionCount="2" confidence="0.88"
fingerprint="9f2b...c0">
<correlationKey>VULN|CVE-2023-1234|pkg:apk/alpine/openssl</correlationKey>
<title>openssl: heap overflow</title>
<detectedBy>
<scanner>trivy</scanner>
<scanner>grype</scanner>
</detectedBy>
<locations>
<location file="Dockerfile" startLine="3" endLine="3"></location>
</locations>
</finding>
</findings>
</quorumReport>
Relevant attributes/elements: unmapped only appears when true (omitempty); error
on <scanner> only when present; locations deduplicated by file.
7.4 Report-format comparison¶
| Characteristic | SARIF | JSON | XML |
|---|---|---|---|
| Default | Yes | No | No |
| Target consumer | GitHub Code Scanning, IDEs, SARIF dashboards | automation/scripts, programmatic diff | legacy/JUnit-like pipelines |
| Stable fingerprint | partialFingerprints["quorum/v1"] |
findings[].fingerprint |
fingerprint attribute |
| Aggregated summary | via properties + rules |
dedicated summary block |
attributes on <scanner> |
| Per-scanner status | run.properties.scanners |
full scanners[] |
<scanners> |
Raw members (Finding) |
no (consensus only) | yes (members[]) |
partial (locations/detectedBy) |
7.5 Prometheus (telemetry) — --metrics <file>¶
Source: internal/report/metrics.go (WriteMetrics). It is not selected by
--format: it is emitted in parallel with the report, only when --metrics points at a
file. Prometheus text-format, meant for a node_exporter textfile collector or a
Pushgateway — exportable telemetry for a CLI that has no long-lived process to be
scraped. File written with mode 0644.
Emitted series (all gauge):
| Metric | Labels | Meaning |
|---|---|---|
quorum_scan_duration_seconds |
— | total wall-clock time of the scan (s). |
quorum_scanner_up |
scanner, status |
1 if the scanner ran; 0 for skipped/unavailable/error/timeout. |
quorum_scanner_findings |
scanner |
raw findings per scanner (pre-consensus). |
quorum_scanner_duration_seconds |
scanner |
per-scanner duration (s). |
quorum_findings_after_consensus |
— | findings remaining after the consensus merge. |
quorum_findings_total |
severity |
consensus findings by severity (CRITICAL/HIGH/MEDIUM/LOW/INFO). |
quorum_multi_detected |
— | findings corroborated by more than one scanner. |
quorum_advice_enriched |
kind |
(only with --advice) findings enriched by kind: remediation, references, recommendation. |
quorum_advice_provider |
provider |
(only with AI) active recommendation provider (local/remote), value 1. |
quorum_advice_fix |
stage |
(only with AI) fixes by verify-the-fix stage: proposed vs verified (the ratio is the success rate). |
The
quorum_advice_*series only appear when--adviceis on; without it, the metrics output is unchanged.provider/fixonly surface with an AI provider (local/remote); with pure Phase 0/2 there is onlyquorum_advice_enriched.
Example (excerpt):
# HELP quorum_scan_duration_seconds Total scan wall-clock time in seconds.
# TYPE quorum_scan_duration_seconds gauge
quorum_scan_duration_seconds 8.421
# TYPE quorum_scanner_up gauge
quorum_scanner_up{scanner="trivy",status="ran"} 1
quorum_scanner_up{scanner="grype",status="ran"} 1
# TYPE quorum_findings_total gauge
quorum_findings_total{severity="CRITICAL"} 1
quorum_findings_total{severity="HIGH"} 4
# TYPE quorum_multi_detected gauge
quorum_multi_detected 6
8. GitHub Action interface (action.yml) — the "API contract" substitute¶
Source: action.yml. Type composite. Available from v0.2.1+;
pinned via the moving tag v0 (auto-advanced on each semver release by tag-major.yml).
The Action wraps the :full (self-contained) image and, by default, verifies the cosign
signature before running.
flowchart TD
U[uses: Martinez1991/quorum-sec-scan@v0] --> V{verify == true?}
V -->|yes| C[cosign verify IMAGE<br/>OIDC issuer + identity regexp]
V -->|no| MNT
C --> MNT[mounts -v WORKDIR:/work<br/>+ docker.sock if type=image<br/>+ host-gateway if advice-provider=local]
MNT --> R[docker run --rm ... IMAGE scan ...<br/>+ QUORUM_*_ARGS envs]
R --> O1[output-file]
R --> O2[exit-code]
R --> EX[exit code propagated to the step]
8.1 Inputs¶
| Input | Required | Default | Maps to |
|---|---|---|---|
target |
no | . |
arg <target> |
type |
no | "" |
--type (if non-empty) |
scanners |
no | "" |
--scanners (if non-empty) |
format |
no | sarif |
--format |
output |
no | quorum.sarif |
--output (relative to the working dir) |
fail-on |
no | "" |
--fail-on (if non-empty) |
min-severity |
no | "" |
--min-severity (if non-empty) |
baseline |
no | "" |
--baseline (if non-empty) |
crosswalk |
no | /opt/quorum/crosswalk |
--crosswalk |
timeout |
no | "" |
--timeout (if non-empty) |
offline |
no | "false" |
--offline (when "true") |
quiet |
no | "false" |
--quiet (when "true") |
image |
no | ghcr.io/martinez1991/quorum-sec-scan:full |
image to run (pinning by @sha256:... is recommended in production) |
verify |
no | "true" |
enables cosign verify before the run |
working-directory |
no | ${{ github.workspace }} |
mounted as /work in the container |
docker-socket |
no | "" |
Mounts /var/run/docker.sock in the container. Auto-mounted for type: image (the scanner needs the runner's daemon to see a freshly-built local image). "true" forces it for other types; "off" disables it even in an image scan. |
advice |
no | "false" |
--advice (when "true") — attaches the advisory layer (Phase 0 templates + Phase 2 lexical RAG). |
advice-provider |
no | none |
--advice-provider (none\|local\|remote). For local, the Action auto-adds the host-gateway mapping so the container can reach a model on the runner host. |
advice-endpoint |
no | http://host.docker.internal:11434/v1 |
--advice-endpoint (OpenAI-compatible base URL; the default points at the runner host via host.docker.internal). |
advice-model |
no | "" |
--advice-model (if non-empty) |
advice-embed-model |
no | "" |
--advice-embed-model (if non-empty) — semantic OWASP retrieval, only when the corpus ships vectors. |
advice-max |
no | "" |
--advice-max (if non-empty) |
advice-cache |
no | "" |
--advice-cache (relative to the working dir; persist with actions/cache for reproducibility). |
advice-allow-egress |
no | "false" |
--advice-allow-egress (when "true") — required for advice-provider=remote. |
advice-api-key |
no | "" |
forwarded as env QUORUM_ADVICE_API_KEY for advice-provider=remote; never logged, blocked under offline. |
fix |
no | "" |
--fix (off\|suggest) — requires advice-provider=local (remote refuses --fix). |
trivy-args |
no | "" |
env QUORUM_TRIVY_ARGS |
grype-args |
no | "" |
env QUORUM_GRYPE_ARGS |
checkov-args |
no | "" |
env QUORUM_CHECKOV_ARGS (e.g. --bc-api-key <key> unlocks Prisma/Bridgecrew policies) |
kics-args |
no | "" |
env QUORUM_KICS_ARGS |
dockle-args |
no | "" |
env QUORUM_DOCKLE_ARGS |
kubescape-args |
no | "" |
env QUORUM_KUBESCAPE_ARGS |
The advisory inputs are presentation-only and never touch gating: without
advice: truethe output is byte-identical. The six*-argsinputs are the passthrough-envs explicitly exposed by the Action. At the CLI/Docker level, theQUORUM_<SCANNER>_ARGSmechanism works for any scanner (§4.3); for the others, inject the env directly viadocker run -e.
8.2 Outputs¶
| Output | Description | Origin |
|---|---|---|
output-file |
Absolute path of the written report (${WORKDIR}/${OUTPUT}); empty if output went to stdout. |
steps.run.outputs.output-file |
exit-code |
Quorum exit code: 0 ok, 1 gate, 2 error. |
steps.run.outputs.exit-code |
The step propagates the exit code of
docker run(exit "${code}"), so the--fail-ongating fails the job naturally. To capture the report without failing the job, combine withcontinue-on-errorand read theexit-codeoutput.
8.3 Signature verification (cosign)¶
When verify: true, the step installs cosign (if absent) and runs:
cosign verify "${IMAGE}" \
--certificate-identity-regexp \
"https://github.com/Martinez1991/quorum-sec-scan/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
This validates the keyless OIDC signature issued by the release workflow. The supply
chain is complemented at release time by a SLSA build-provenance attestation and an
attested SPDX SBOM (via actions/attest-sbom) for the image and per-binary, plus
BuildKit's sbom: true. The knowledge pack + crosswalk also get their own SLSA
build-provenance attestation each release (the knowledge job in release.yml); verify it
with gh attestation verify knowledge/owasp/corpus.yaml. See 10-infraestrutura.md.
8.4 Docker socket mounting (avoiding false-zero)¶
For type: image, a scan of an image built locally on the runner is invisible from
inside the container without access to the host's Docker daemon. The Action, by default,
auto-mounts /var/run/docker.sock in that case, avoiding the dangerous silent
"0 findings". A scan of a registry image simply ignores the socket. Behavior:
docker-socket: ""(default): mounts fortype: image, does not mount for the others.docker-socket: "true": forces the mount for any type.docker-socket: "off": disables it even in an image scan.- If the socket does not exist on the runner, the Action emits a warning explaining the risk.
8.5 Usage example¶
name: security
on: [pull_request]
permissions:
contents: read
security-events: write # for upload-sarif
jobs:
quorum:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: scan
uses: Martinez1991/quorum-sec-scan@v0
with:
target: .
type: repo
fail-on: high
output: quorum.sarif
advice: "true" # deterministic remediation + OWASP refs (presentation-only)
checkov-args: "--bc-api-key ${{ secrets.PRISMA_KEY }} --repo-id org/repo"
continue-on-error: true # capture SARIF even if the gate fails
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ steps.scan.outputs.output-file }}
- name: Enforce gate
if: steps.scan.outputs.exit-code == '1'
run: exit 1
8.6 CI adoption checklist¶
- [ ] Pin the Action by tag (
@v0) or commit SHA. - [ ] In production, pin the image by digest (
image: ...@sha256:...). - [ ]
verify: true(default) enabled. - [ ]
permissions: security-events: writeif you will upload SARIF. - [ ]
fail-onset according to the team's gate policy. - [ ]
offline: trueon air-gapped runners (also blocksadvice-provider=remote). - [ ]
docker-socketleft at the default (auto) for local image scans;"off"if the image comes only from a registry and the socket must not be exposed. - [ ] Platform secrets passed via
*-args(andadvice-api-key) fromsecrets.*(never hardcoded). - [ ] Capture of
output-filefor downstream artifact/integration.
9. Contract versioning and stability¶
| Contract | How it is versioned | Stability |
|---|---|---|
| Product version | SemVer per release (v[0-9]+.[0-9]+.[0-9]+ tags). Stamped in version (build via ldflags). |
v0.x — pre-1.0; incompatible changes may occur in a minor, announced in the release. |
| Output schema (all formats) | quorum/v1 namespace — visible as partialFingerprints["quorum/v1"] in SARIF and as $id .../quorum-report-v1.json in the JSON Schema. |
The v1 shape is the stable findings contract; an incompatible break would introduce quorum/v2. |
| CLI (flags/exit codes) | Follows the product SemVer. Exit codes 0/1/2 are a long-term contract. |
Stable; new flags are additive (e.g. --metrics, --log-format, the --advice*/--fix family). |
| Passthrough / caps (env) | QUORUM_<SCANNER>_ARGS, QUORUM_MAX_OUTPUT_BYTES, QUORUM_MAX_TARGET_BYTES, QUORUM_ADVICE_API_KEY. |
Additive; stable names. Values are operator controls. |
| Prometheus metrics | quorum_* series names. |
Additive; names treated as a telemetry contract. |
| Action (inputs/outputs) | Moving tag v0 for pinning (advanced by tag-major.yml); additive inputs. |
Stable within v0. |
| Scanner version | Reported per scanner in scanners[].version; the probe distinguishes unavailability. |
Informational. |
Fingerprint compatibility: since
Fingerprint = sha256(correlationKey)and thecorrelationKeyis deterministic per type, any change in the key's construction changes the fingerprints and is therefore treated as a break of thequorum/v1contract. See DESIGN.md §6 (correlation matrix).
10. Traceability — flag → code → output¶
flowchart LR
F1[--fail-on] --> RS[runScan: gating]
RS --> EC[exit 1]
F2[--min-severity] --> FA[filter.Apply]
F3[--baseline] --> LB[filter.LoadBaseline]
LB --> FA
F4[--format] --> PF[report.ParseFormat] --> WR[report.Write]
F5[--output] --> EM[emit → stdout/file 0600]
F6[--scanners] --> SS[splitScanners] --> ORCH[orchestrator.Run]
F7[--timeout] --> ORCH
F8[--offline] --> AL[alias resolver / OSV]
F9[--crosswalk] --> CW[crosswalk.Load + fallback]
F10[--metrics] --> MW[writeMetricsFile 0644]
F11[--log-format] --> LG[logf text|json]
F12[--advice / --advice-provider / --fix] --> ADV[enrich / rag / advisor]
ADV --> WR
ENV[QUORUM_*_ARGS] --> ADP[adapter.extraArgs] --> ORCH
ORCH --> MG[consensus.Merge] --> WR
WR --> EM
ORCH --> MW
Assumptions¶
- Product version (v0.8.3): the default of
versionin the code is0.1.0(build fallback); we assume the v0.8.3 release injects0.8.3via-ldflags. The examples use"version": "0.8.3"to reflect the documented release, not the source default. list-scannersoutput: the list of 12 scanners and their types is enumerated from the code (theCapabilities()of each adapter ininternal/adapter/), with the format"%-12s %v". Ordering is alphabetical by name (sort.Strings). If an adapter changes its capabilities, the corresponding line changes with it.- JSON Schema: written as a faithful representation of
jsonReport/ScannerRun/MergedFinding/Finding(jsontags), withadditionalProperties: falseas an editorial choice of rigor; the product does not publish this schema as a file in the repo in this version — it is a documentation artifact derived from the code. durationMsin JSON: in v0.8.3 it is consistent.ScannerRun.MarshalJSONemitsdurationMsin milliseconds (s.Duration.Milliseconds()), consistent withsummary.durationMs. This was fixed on the v0.2.4 line (#18); until v0.2.3,scanners[].durationMsserialized in nanoseconds. ⚠️ Very old consumers (pre-v0.2.4) that read the value in ns must adjust — the current contract is ms.- Environment variables: unlike the v0.2.3 assumption, the CLI reads its own env in
v0.8.3 —
QUORUM_<SCANNER>_ARGS(passthrough),QUORUM_MAX_OUTPUT_BYTES,QUORUM_MAX_TARGET_BYTES(DoS caps) andQUORUM_ADVICE_API_KEY(remote provider key, forwarded by the Action). There is, however, no env→flag binding for thescanflags (the flags are only set on the command line or by the Action's shell). The use ofos.UserCacheDir(indirect, viaHOME) remains. - Passthrough for all scanners:
extraArgsderives the env name fromQUORUM_<NAME in UPPERCASE>_ARGS. For adapters whose name does not form a valid env identifier (e.g.kube-score→ hyphen), the env must be injected equivalently by the environment/host; the Action exposes only 6*-argsinputs. We do not enumerate here the env of each of the 12 scanners — the mechanism is uniform. - Advisory layer is opt-in and presentation-only: all
--advice*/--fixflags and their Action inputs default to off; without them the output (and the metrics) are byte-identical to the deterministic core, which has no AI. See 21-proposta-ia and 13-ia.md. - Cross-links: the files
07-persistencia-e-artefatos.md,09-backend.mdand10-infraestrutura.mdare referenced by numbering convention; they may not yet exist at the time of this writing.