Skip to content

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:

  1. The command-line interface (quorum scan, quorum list-scanners) — inputs (args/flags/env), outputs (stdout/stderr/file) and exit codes;
  2. The output formats (primary SARIF, JSON, XML) — each a stable serialization contract — plus Prometheus telemetry (--metrics) as an auxiliary format;
  3. The composite GitHub Action (action.yml) — declarative inputs/outputs that wrap the signed :full image.

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. 0 does 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

quorum scan <target> [flags]
  • <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 by os.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 a repo/k8s target (default 20 GiB; 0 disables). The size walk stops as soon as the cap is crossed (normal repos pay only a light stat). image targets (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 --output is empty.
  • file (--output): the same content, with mode 0600.
  • file (--metrics): Prometheus metrics, with mode 0644 (§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 json mode, 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 mode 0600 and a schemaVersion — 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.
  • --offline fully turns off the resolver's network access (osv becomes nil in runScan). 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

quorum list-scanners
  • 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 the Capabilities() in internal/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). conftest runs policy-as-code with its own Rego from ./policy. See 09-backend.md and the adapters in internal/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 the embedModel in 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 local picks the semantic retriever automatically (the scan embeds the query with the same model registered in the corpus). Without embeddings, it stays lexical and deterministic.

6.3 Exit codes

  • 0 on success; 2 only 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.goWrite). 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.json
  • version: 2.1.0
  • A single run with:
  • tool.driver: name: "quorum", informationUri: "https://github.com/quorum-sec/quorum", version (= report.Version), and the rules list (deduplicated by ruleId, sorted by id).
  • results[]: one per MergedFinding.
  • run properties: target (ref) and scanners (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/HIGHerror; MEDIUMwarning; 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: SECRET findings have the matching snippet (trivy's Match) 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 Raw field of Finding has the json:"-" tag and is never serialized. That is why it does not appear in the schema above. Likewise, Result.Findings (raw canonical) has the json:"-" tag at the Result level, but the JSON reporter publishes the merged findings via MergedFinding.Members, so the Finding objects appear nested. ScannerRun uses a custom MarshalJSON that emits durationMs in milliseconds (consistent with summary.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 --advice is on; without it, the metrics output is unchanged. provider/fix only surface with an AI provider (local/remote); with pure Phase 0/2 there is only quorum_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: true the output is byte-identical. The six *-args inputs are the passthrough-envs explicitly exposed by the Action. At the CLI/Docker level, the QUORUM_<SCANNER>_ARGS mechanism works for any scanner (§4.3); for the others, inject the env directly via docker 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-on gating fails the job naturally. To capture the report without failing the job, combine with continue-on-error and read the exit-code output.

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 for type: 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: write if you will upload SARIF.
  • [ ] fail-on set according to the team's gate policy.
  • [ ] offline: true on air-gapped runners (also blocks advice-provider=remote).
  • [ ] docker-socket left 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 (and advice-api-key) from secrets.* (never hardcoded).
  • [ ] Capture of output-file for 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 the correlationKey is deterministic per type, any change in the key's construction changes the fingerprints and is therefore treated as a break of the quorum/v1 contract. 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

  1. Product version (v0.8.3): the default of version in the code is 0.1.0 (build fallback); we assume the v0.8.3 release injects 0.8.3 via -ldflags. The examples use "version": "0.8.3" to reflect the documented release, not the source default.
  2. list-scanners output: the list of 12 scanners and their types is enumerated from the code (the Capabilities() of each adapter in internal/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.
  3. JSON Schema: written as a faithful representation of jsonReport/ScannerRun/ MergedFinding/Finding (json tags), with additionalProperties: false as 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.
  4. durationMs in JSON: in v0.8.3 it is consistent. ScannerRun.MarshalJSON emits durationMs in milliseconds (s.Duration.Milliseconds()), consistent with summary.durationMs. This was fixed on the v0.2.4 line (#18); until v0.2.3, scanners[].durationMs serialized in nanoseconds. ⚠️ Very old consumers (pre-v0.2.4) that read the value in ns must adjust — the current contract is ms.
  5. 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) and QUORUM_ADVICE_API_KEY (remote provider key, forwarded by the Action). There is, however, no env→flag binding for the scan flags (the flags are only set on the command line or by the Action's shell). The use of os.UserCacheDir (indirect, via HOME) remains.
  6. Passthrough for all scanners: extraArgs derives the env name from QUORUM_<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 *-args inputs. We do not enumerate here the env of each of the 12 scanners — the mechanism is uniform.
  7. Advisory layer is opt-in and presentation-only: all --advice*/--fix flags 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.
  8. Cross-links: the files 07-persistencia-e-artefatos.md, 09-backend.md and 10-infraestrutura.md are referenced by numbering convention; they may not yet exist at the time of this writing.