Skip to content

11 - DevOps

This section describes, faithfully to the code, how Quorum (quorum-sec-scan, v0.8.3 — revision 2026-07-04) is built, tested, versioned and distributed. Quorum is a CLI/Docker consensus security scanning tool written in Go 1.26; there is no long-running service, web frontend, database or REST API. Because of that, "DevOps" here essentially means release engineering: a Continuous Integration (CI) pipeline that proves consensus between scanners actually happens and measures test coverage, and a Continuous Delivery (CD) pipeline that publishes signed Docker images and signed native binaries, both with SLSA provenance attestation and attested SPDX SBOM, verified in the release itself. The artifact is immutable and versioned; "deploy" is publish; "rollback" is re-pointing a moving tag to an earlier digest.

Every fact below derives directly from:


1. Overview

flowchart LR
  dev[Developer] -->|feature branch| pr[Pull Request]
  pr -->|on: pull_request| ci[ci.yml<br/>build-test + coverage]
  pr -->|on: pull_request| e2e[e2e.yml<br/>consensus]
  ci -->|green| merge{merge into main}
  e2e -->|green| merge
  merge -->|push: main| ci2[ci.yml + e2e.yml<br/>re-run on main]
  merge -->|push: main + docs/**| docs[docs.yml<br/>GitHub Pages]
  merge -->|semver tag vX.Y.Z| rel[release.yml]
  merge -->|semver tag vX.Y.Z| tm[tag-major.yml<br/>advance v0 / v0.2]
  rel --> ghcr[(GHCR<br/>:full/:slim images)]
  rel --> ghrel[(GitHub Release<br/>binaries + checksums)]
  rel --> know[(knowledge pack<br/>SLSA attestation)]
  rel --> sig[cosign keyless + SLSA provenance + SPDX SBOM]
Stage Workflow Trigger What it proves / does
Build, test & coverage ci.yml push to main, any pull_request Compiles, go vet, go test -race with atomic coverage, publishes the coverage summary, uploads coverage.out, builds the binary, smoke (list-scanners)
Consensus proof e2e.yml push to main, any pull_request, workflow_dispatch Runs real scanners over known targets and fails if no finding is corroborated by two engines
Documentation docs.yml push to main touching docs/**, mkdocs.yml or the workflow itself; workflow_dispatch Builds the bilingual MkDocs Material site (mkdocs-static-i18n) and publishes it to GitHub Pages
Release release.yml push of tag v[0-9]+.[0-9]+.[0-9]+, workflow_dispatch Publishes images and binaries, attests the knowledge pack, signs (cosign keyless with retry), attests and verifies SLSA provenance + SPDX SBOM
Moving-tag advance tag-major.yml push of tag v[0-9]+.[0-9]+.[0-9]+ Repositions v0 and v0.2 to the release commit (Action pin)

2. Continuous Integration (CI)

2.1 ci.yml — job build-test

Trigger: push to main and any pull_request. Runner: ubuntu-latest. Go 1.26 with cache enabled.

Exact steps:

# Step Command Fails if
1 Checkout actions/checkout@v4
2 Setup Go actions/setup-go@v5 (go-version: "1.26", cache: true)
3 Vet go vet ./... Static/suspicious error reported by vet
4 Test (race + coverage) go test -race -covermode=atomic -coverprofile=coverage.out ./... Any test fails or a data race is detected
5 Coverage summary go tool cover -func=coverage.out \| tail -1 (also written to GITHUB_STEP_SUMMARY) — (informational)
6 Upload coverage actions/upload-artifact@v4 (name: coverage, path: coverage.out, if-no-files-found: warn)
7 Build go build -trimpath -o dist/quorum ./cmd/quorum Compilation failure
8 Smoke ./dist/quorum list-scanners Binary does not run the basic command

Relevant points:

  • -race is mandatory. The orchestrator does parallel fan-out with goroutines (one scanner per goroutine), so the race detector is the first line of defense against concurrency regressions.
  • Test coverage in CI. go test runs with -covermode=atomic -coverprofile=coverage.out; the Coverage summary step prints the total line (go tool cover -func) into the log and the step summary, and coverage.out is published as an artifact for later inspection. Coverage is observed on every push/PR (there is no numeric gate that fails the build).
  • Each adapter's contract tests run against fixtures in internal/adapter/testdata inside go test ./... — they do not require the scanners installed, being deterministic and fast. Current coverage spans the 12 adapters (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest) plus the real-data tests (realdata_test.go).
  • The Smoke step guarantees the binary boots and the scanner registry (list-scanners) responds, without depending on the network or external scanners.

Advisory layer under test. The advisory layer added in v0.8.3 (opt-in via --advice, packages internal/enrich, internal/rag, internal/advisor) is covered by the same deterministic go test ./... — including the internal/evals harness that measures deterministic remediation coverage, OWASP reference relevance and the verify-the-fix rate. These evals run in CI with no heavy model, so they stay fast and deterministic. Without --advice, output is byte-identical to the deterministic core.

2.2 e2e.yml — job consensus

This workflow is the living proof of the product's principle: "0 findings is not proof of safety", and consensus only counts with real scanners, not fixtures. Trigger: push to main, any pull_request and workflow_dispatch (manual run).

Sequence:

  1. Checkout + Setup Go 1.26 (with cache).
  2. Build quorum to dist/quorum and add dist/ and ~/.local/bin to GITHUB_PATH.
  3. Install scanners by direct download of releases (pinned versions):
  4. Trivy 0.71.2, Grype 0.114.0 (a comment in the workflow warns that old vulnerability-DB schemas of Grype are retired by Anchore), Checkov via pipx install checkov.
  5. Verify scanners and pre-fetch DB: trivy --version, grype version, checkov --version, grype db update (fails early if the DB cannot be downloaded), docker pull alpine:3.10 (ensures both engines resolve the same local image) and quorum list-scanners.
  6. IaC consensus (Trivy + Checkov over examples/terraform, --type repo, --offline): requires summary.multiDetected >= 1, otherwise it emits ::error::no cross-engine IaC consensus reached and fails.
  7. SCA consensus (Trivy + Grype over alpine:3.10, --type image, --offline): same multiDetected >= 1 rule.
  8. Upload reports (iac.json, sca.json) as an artifact, with if: always().

--offline disables alias lookups on OSV.dev, making the e2e deterministic and independent of network variation with respect to alias correlation.

Note on e2e scope. consensus proves cross-engine for SCA (Trivy + Grype) and IaC (Trivy + Checkov). Consensus for MISCONFIG/IaC and K8S_POSTURE across the other engines (kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest) is derived at runtime via the crosswalk (crosswalk/*.yaml); the e2e keeps the minimal deterministic pair that runs fast in CI. The advisory layer is not exercised here — it is presentation-only and never touches consensus.

flowchart TD
  a[Build quorum] --> b[Install trivy/grype/checkov<br/>pinned versions]
  b --> c[grype db update + docker pull alpine:3.10]
  c --> d[scan IaC: trivy+checkov]
  c --> e[scan SCA: trivy+grype]
  d --> f{multiDetected >= 1?}
  e --> g{multiDetected >= 1?}
  f -->|no| x1[::error:: no IaC consensus -> FAIL]
  g -->|no| x2[::error:: no SCA consensus -> FAIL]
  f -->|yes| h[upload iac.json/sca.json]
  g -->|yes| h

2.3 docs.yml — bilingual MkDocs Material site on GitHub Pages

The documentation (this and the other sections in docs/) is published as a static site on GitHub Pages via MkDocs Material. Since v0.8.3 the site is bilingual — English (default) and Brazilian Portuguese — built with the mkdocs-static-i18n plugin.

  • Trigger: push to main touching docs/**, mkdocs.yml or .github/workflows/docs.yml; or workflow_dispatch. Changes that do not touch the documentation do not rebuild the site.
  • Permissions: contents: read, pages: write, id-token: write (Pages OIDC deploy).
  • Concurrency: group pages with cancel-in-progress: false — allows one publish at a time, without cancelling an in-progress deploy.
  • Job build: actions/checkout@v4actions/setup-python@v5 (python-version: "3.x") → pip install "mkdocs-material==9.*" "mkdocs-static-i18n==1.*"mkdocs build --site-dir siteactions/configure-pages@v5actions/upload-pages-artifact@v3 (path: site).
  • Job deploy (needs: build): actions/deploy-pages@v4, on the github-pages environment, exposes the published URL (page_url).

The site is a read-only projection of docs/. There is no application build or runtime state — just static HTML generated from Markdown. Each page has an English (<file>.md) and a Portuguese (<file>.pt.md) source; the i18n plugin builds the language switcher.


3. Git flow and branching strategy

The flow is classic GitHub Flow, PR-based to main, with semver tag versioning and automatically advanced moving tags.

gitGraph
  commit id: "main"
  branch feature/xyz
  commit id: "work"
  commit id: "more work"
  checkout main
  merge feature/xyz tag: "PR + CI green"
  commit id: "v0.8.3" tag: "v0.8.3"

Practical rules:

  1. main is the release line. All work happens on feature/fix branches (e.g. ci/auto-advance-major-tag, docs/fix-site-links, as seen in recent history).
  2. Every change enters via Pull Request. The PR triggers ci.yml (build-test + coverage) and e2e.yml (consensus). Both must be green before merge.
  3. Merge into main re-runs ci.yml and e2e.yml on the push to main (defense in depth against merge races) and, if docs/** changed, triggers docs.yml to republish the site.
  4. Release by semver tag. Only tags of the form v[0-9]+.[0-9]+.[0-9]+ (e.g. v0.8.3) trigger release.yml. There is no automatic release on merge — the tag is the explicit gesture of publication.
  5. Moving tags v0 (major) and v0.2 (minor) exist to pin the GitHub Action: consumers use uses: Martinez1991/quorum-sec-scan@v0. These tags are advanced automatically by tag-major.yml on every semver release — no longer a manual step. Both release.yml and tag-major.yml are restricted to full semver tags (X.Y.Z), so moving the Action pointer does not re-trigger a build or republish images.

3.1 tag-major.yml — automatic moving-tag advance

flowchart LR
  t[push tag vX.Y.Z] --> j[job move]
  j --> chk[checkout fetch-depth: 0]
  chk --> calc["derive major=vX and minor=vX.Y<br/>(strip the suffix)"]
  calc --> mv["git tag -f -a vX / vX.Y -> same commit"]
  mv --> push["git push -f origin vX and vX.Y"]
  push --> stop["does not re-trigger release.yml/tag-major.yml<br/>(semver scope + push via GITHUB_TOKEN)"]
  • Trigger: push of tag v[0-9]+.[0-9]+.[0-9]+. Permission: contents: write.
  • Logic: from github.ref_name (e.g. v0.8.3) it derives major (v0) and minor (v0.8) and force-updates both annotated tags to github.sha (git tag -f -a + git push -f), as github-actions[bot].
  • No loop: the trigger only accepts full semver tags, and pushes made with GITHUB_TOKEN do not trigger new workflows — that is why moving v0/v0.2 neither rebuilds images nor re-invokes itself.
  • [ ] Branch created from an up-to-date main.
  • [ ] go vet ./... and go test -race ./... pass locally.
  • [ ] New adapter parsers have a contract test against a fixture in internal/adapter/testdata.
  • [ ] New crosswalk correlations (AWS/Azure/GCP/K8s) are backed by real scanner output (false split > false merge).
  • [ ] CI (build-test) green; coverage observed in the step summary.
  • [ ] E2E (consensus) green — real consensus still happens.
  • [ ] If the advisory layer changed, internal/evals still passes (remediation coverage, OWASP reference relevance, verify-the-fix rate) and non---advice output stays byte-identical.
  • [ ] If you touched docs/**/mkdocs.yml, confirm the site build (docs.yml) is green in both languages.
  • [ ] Commit messages follow conventional prefixes (fix:, feat:, ci:, etc.); docs:/test:/chore: are filtered from the changelog (see .goreleaser.yaml).

4. Continuous Delivery (CD) — release.yml

Release trigger: push of tag v[0-9]+.[0-9]+.[0-9]+ or workflow_dispatch (with input version, default dev).

Workflow permissions (least privilege needed):

Permission Why
contents: read (job images/knowledge) / contents: write (job binaries) Read the repo; create the release and upload binary assets
packages: write Push images to GHCR
id-token: write OIDC for keyless signing with cosign (Sigstore) and keyless attestation
attestations: write SLSA build-provenance attestation and SPDX SBOM attestation

4.1 Job images

Variant matrix (fail-fast: false):

Variant Dockerfile Platforms Contents
full Dockerfile.full linux/amd64 All scanners bundled (scanner binaries are amd64; grype DB pre-cached with GRYPE_DB_VALIDATE_AGE=false)
slim Dockerfile linux/amd64, linux/arm64 Orchestrator only

Tag resolution (step meta), with image=ghcr.io/<owner>/<repo> lowercased and version derived from GITHUB_REF_NAME without the v prefix:

Variant Published tags
full :full, :<version>, :<version>-full, :latest
slim :slim, :<version>-slim

Job pipeline:

  1. Checkout.
  2. Resolve version and image name (step meta, writes image, version, tags to GITHUB_OUTPUT).
  3. QEMU + Buildx (needed for the multi-arch slim build).
  4. Log in to GHCR with GITHUB_TOKEN.
  5. Install cosign (sigstore/cosign-installer@v3).
  6. Build & push via docker/build-push-action@v6 with provenance: true, sbom: true, scoped GHA cache (quorum-<variant>, mode=max) and build-args: VERSION=<version>.
  7. Sign image (keyless) with retry: cosign sign --yes "${IMAGE}@${DIGEST}" wrapped in a retry() helper (4 attempts, backoff 15s·i). Keyless signing depends on the GitHub OIDC endpoint + Sigstore (Fulcio/Rekor), which occasionally time out on long builds and can expire the OIDC token mid-sign; each attempt re-fetches a fresh token. It signs the (multi-arch) manifest digest once, covering every tag pointing at it.
  8. Attest SLSA build provenance: actions/attest-build-provenance@v2 generates an SLSA v1 attestation (subject-digest = image digest, push-to-registry: true), recording how the image was built (workflow, commit, runner).
  9. Verify provenance attestation (with retry): gh attestation verify "oci://${IMAGE}@${DIGEST}" --repo ... under the same retry() — re-verifies end-to-end (Sigstore transparency log + OIDC identity). A broken attestation fails the release.
  10. Generate image SBOM (syft): anchore/sbom-action@v0 generates sbom.spdx.json (format spdx-json) for the image by digest.
  11. Attest image SBOM: actions/attest-sbom@v2 issues a first-class SPDX attestation (verifiable by gh attestation verify/cosign), with push-to-registry: true, in addition to the BuildKit sbom: true attestation from the build step.
  12. Summary: writes variant, platforms, digest and tags to GITHUB_STEP_SUMMARY.

4.2 Job binaries

Condition: if: github.ref_type == 'tag' (GoReleaser needs the tag). Additional permissions: contents: write, id-token: write, attestations: write.

Pipeline:

  1. Checkout with fetch-depth: 0 (GoReleaser needs full history for the changelog).
  2. Setup Go 1.26.
  3. Install cosign.
  4. Install syft (anchore/sbom-action/download-syft@v0) — used by GoReleaser to generate the file SBOMs.
  5. GoReleaser (goreleaser/goreleaser-action@v6, version: "~> v2", args: release --clean) — per .goreleaser.yaml:
  6. Builds CGO_ENABLED=0, -trimpath, ldflags -s -w -X main.version={{.Version}}.
  7. Matrix goos: [linux, darwin, windows] x goarch: [amd64, arm64].
  8. Archives quorum_<version>_<os>_<arch> (zip on Windows) including README.md, README.pt-BR.md, LICENSE and the crosswalk/** directory (bundles the default crosswalks — AWS/Azure/GCP/K8s).
  9. SPDX SBOM per archive (sboms, artifacts: archive, via syft) — each release ships a machine-readable bill of materials for the binaries.
  10. checksums.txt covering all artifacts.
  11. Keyless signature of the checksum file via cosign sign-blob (produces ${artifact}.sig and ${artifact}.pem). Since the checksum pins the hashes of every artifact, the signature over it covers the entire release.
  12. Creates the GitHub Release (draft: false, prerelease: auto) with GitHub changelog (excludes docs:/test:/chore:).
  13. Attest SLSA build provenance for binaries: subject-checksums: dist/checksums.txt — one attestation covering every listed artifact.
  14. Verify binary provenance attestation (with retry): gh attestation verify dist/quorum_*_linux_amd64.tar.gz --repo ... under the retry() helper (spot-check of one artifact).

4.3 Job knowledge — attesting the advisory knowledge pack

New in v0.8.3. The advisory layer's knowledge pack — the curated remediation templates plus the digest-pinned OWASP corpus (knowledge/*.yaml, knowledge/owasp/corpus.yaml) and the crosswalk/** mappings — ships inside the images, but is also a data artifact a consumer may want to verify independently. So it gets first-class SLSA build provenance, like the images and binaries.

  • Permissions: contents: read, id-token: write (keyless attestation via Sigstore OIDC), attestations: write.
  • Pipeline:
  • Checkout.
  • Compute knowledge checksums: a deterministic, sorted list of every pack file — find knowledge crosswalk -type f \( -name '*.yaml' -o -name '*.yml' \) -print0 | sort -z | xargs -0 sha256sum > knowledge.sha256.
  • Attest knowledge provenance: actions/attest-build-provenance@v2 with subject-checksums: knowledge.sha256 — one attestation covering every pack file.
  • Verify knowledge attestation (with retry): gh attestation verify knowledge/owasp/corpus.yaml --repo ... — spot-checks the OWASP corpus against its just-created attestation.

Because the corpus is digest-pinned and now carries a provenance attestation, the quorum advise-index step (which embeds the corpus for semantic retrieval) preserves the same pin, and a consumer can verify the pack with gh attestation verify knowledge/owasp/corpus.yaml before trusting the advice it feeds.

flowchart TD
  tag[push tag vX.Y.Z] --> imgs[job: images]
  tag --> bins[job: binaries]
  tag --> know[job: knowledge]

  subgraph images[job images — full/slim matrix]
    m1[meta: resolve tags] --> m2[buildx + qemu]
    m2 --> m3[build-push provenance+sbom]
    m3 --> m4[cosign sign digest + retry]
    m4 --> m5[attest SLSA provenance]
    m5 --> m6[gh attestation verify + retry]
    m6 --> m7[syft SBOM spdx-json]
    m7 --> m8[attest SBOM SPDX]
  end

  subgraph binaries[job binaries — GoReleaser]
    b1[goreleaser release --clean] --> b2[archives + SBOM + checksums.txt]
    b2 --> b3[cosign sign-blob checksums]
    b3 --> b4[GitHub Release]
    b4 --> b5[attest SLSA over checksums]
    b5 --> b6[gh attestation verify spot-check + retry]
  end

  subgraph knowledge[job knowledge — advisory pack]
    k1[sha256sum knowledge/ + crosswalk/] --> k2[attest SLSA over knowledge.sha256]
    k2 --> k3[gh attestation verify corpus.yaml + retry]
  end

  m8 --> ok1[(GHCR: signed + attested images + SBOM)]
  b6 --> ok2[(GitHub Release: signed + attested binaries + SBOM)]
  k3 --> ok3[(knowledge pack: SLSA-attested)]

5. "Deploy" for a CLI/Docker

There is no runtime environment managed by this project. Deploy = publish immutable, verifiable artifacts. The unit of deployment is the consuming user or pipeline that pulls the image or downloads the binary.

Traditional concept Quorum equivalent
Server/runtime environment N/A — no persistent service
Deploy Publish images on GHCR + binaries Release on GitHub
Immutable version Image digest (@sha256:...) and semver tag
Integrity/origin cosign keyless (OIDC) + SLSA provenance attestation + attested SPDX SBOM (+ SLSA-attested knowledge pack)
Runtime configuration CLI flags (--type, --scanners, --fail-on, --metrics, --log-format, the advisory flags --advice, --advice-provider, --fix, ...), per-scanner passthrough (QUORUM_<SCANNER>_ARGS) and Docker mounts

The advisory layer is opt-in configuration, not a runtime toggle. All AI behavior is off by default and selected per invocation: --advice turns on the deterministic Phase 0/2 enrichment (remediation templates + digest-pinned OWASP RAG); --advice-provider=local|remote and --fix=suggest add the opt-in LLM phases. Without --advice the output is byte-identical to the deterministic core, which still has no AI.

How the consumer verifies before using

Image:

cosign verify ghcr.io/martinez1991/quorum-sec-scan:slim \
  --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:slim \
  --repo Martinez1991/quorum-sec-scan

action.yml itself does this automatically: by default (verify: true) it cosign-verifies ghcr.io/martinez1991/quorum-sec-scan:full (installing cosign if needed) before running the image via docker run. On local image scans (type: image) the Action auto-mounts /var/run/docker.sock (input docker-socket), avoiding a false-zero from an unreachable image. Pinning the image by @sha256:... in production is recommended (input image). The Action also exposes all advisory inputs (advice, advice-provider, advice-endpoint, advice-model, advice-embed-model, advice-max, advice-cache, advice-allow-egress, advice-api-key, fix): for local it auto-adds host-gateway so the container can reach an on-host endpoint, and for remote it forwards the API key via env.

Knowledge pack (the advisory data artifact):

gh attestation verify knowledge/owasp/corpus.yaml \
  --repo Martinez1991/quorum-sec-scan

Binary (from a release):

cosign verify-blob checksums.txt \
  --signature checksums.txt.sig \
  --certificate checksums.txt.pem \
  --certificate-identity-regexp \
    "https://github.com/Martinez1991/quorum-sec-scan/.github/workflows/release.yml@.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c checksums.txt

6. Rollback

Since artifacts are immutable and versioned, rollback is a re-pointing/pin operation, not "undoing a deploy".

  • Pin to an exact version: swap :latest/:full for :<previous-version>-full (e.g. :0.8.2-full) or, ideally, for the digest @sha256:... of a known-good release.
  • Action: swap @v0 for an earlier fixed tag (@v0.8.2), or pin image: to the desired digest.
Scenario Rollback action
New image with a regression Re-pin to :<previous-version>-full or the previous digest
Buggy binary Download the asset from the previous (signed) release
Unstable Action @v0 Pin a specific @vX.Y.Z or pin image: by digest

For the maintainer

  • Re-tag the moving tag: move :full/:latest/:v0/:v0.2 back to the previous release digest. cosign signs the digest, so the previous release's signature/attestation stays valid when re-pointing the tag. Note that tag-major.yml only advances v0/v0.2 forward on the next semver release — a rollback of those tags is a manual maintainer intervention.
  • Forward-fix preferred: cutting a new semver tag (vX.Y.Z+1) with the fix is the cleanest path, since each release is fully rebuilt, signed and attested — and tag-major.yml re-aligns the moving tags automatically.

By design, an existing semver tag is not overwritten with different content. The semver tag is immutable; the moving tags (full, latest, v0, v0.2) are what reposition.

Rollback checklist

  • [ ] Identify the last known-good version (semver tag + digest).
  • [ ] Consumers: re-pin to the previous version/digest (or @vX.Y.Z of the Action).
  • [ ] Maintainer: if needed, re-point moving tags (v0/v0.2/full/latest) to the previous digest.
  • [ ] Confirm cosign verify + gh attestation verify on the target artifact.
  • [ ] Open a forward-fix and cut a new semver tag as soon as possible (tag-major.yml re-aligns v0/v0.2).

7. Feature flags, Blue-Green and Canary

N/A for the product. These techniques assume a long-running service with live traffic that can be routed, switched or gradually migrated. Quorum is an ephemeral CLI/Docker process: it runs, produces a report (SARIF/JSON/XML) and exits. There is no traffic, no parallel replicas, no runtime state to switch.

Technique Status Rationale
Feature flags N/A Behavior is controlled by CLI flags at invocation time (--scanners, --format, --fail-on, --offline, --metrics, --log-format, --advice, --fix, ...) and passthrough env (QUORUM_<SCANNER>_ARGS); there are no dynamic runtime toggles
Blue-Green N/A There is no "live" environment with a traffic pool to switch
Canary N/A (in the product) There is no fleet serving requests to roll out gradually

How the CONSUMER can canary versions

Although the product does not support canary internally, the consuming pipeline can canary Quorum versions with standard CI strategies:

  • Staggered pin: most repositories pin a stable version (digest/@vX.Y.Z); a pilot repository adopts the new version before broad adoption.
  • Non-blocking parallel run: run the new version in a job with continue-on-error: true alongside the stable version (gate), comparing the summary and the exit-code before promoting.
  • workflow_dispatch / version matrix: compare reports of two tags (stable vs. candidate) over the same target and only then update the pin.
  • Deterministic report comparison: use --offline to reduce network variance when comparing multiDetected/detectionCount between versions.
flowchart LR
  repoA[Production repos<br/>stable pin @vX.Y.Z] --> use[Stable Quorum]
  repoP[Pilot repo / canary job] --> cand[Candidate Quorum @vX.Y.Z+1]
  cand -->|compare summary/exit-code| dec{ok?}
  dec -->|yes| promote[promote pin to new version]
  dec -->|no| keep[keep previous pin]

A scan-behavior feature flag = simply choosing CLI flags per invocation. There is no — nor is there any desire for — a dynamic flag system for a single-run binary.


8. Summary of triggers and responsibilities

Workflow pull_request push: main tag vX.Y.Z workflow_dispatch
ci.yml (build-test + coverage) yes yes
e2e.yml (consensus) yes yes yes
docs.yml (GitHub Pages, i18n) yes (paths docs/**, mkdocs.yml, the workflow) yes
release.yml (images+binaries+knowledge) yes yes
tag-major.yml (advance v0/v0.2) yes
Artifact Where Signature Provenance SBOM Multi-arch
:full image GHCR cosign keyless (retry) SLSA (verified) SPDX attested (syft) + BuildKit linux/amd64
:slim image GHCR cosign keyless (retry) SLSA (verified) SPDX attested (syft) + BuildKit linux/amd64, linux/arm64
Binaries GitHub Release cosign sign-blob (over checksums) SLSA (verified) SPDX per archive (syft) linux/darwin/windows x amd64/arm64
Knowledge pack (templates + OWASP corpus + crosswalk) in-image / repo data — (checksum-listed) SLSA (verified) N/A

Assumptions

  1. Dockerfile contents were not detailed line by line. The :full/:slim variants and their contents (bundled scanners, grype DB pre-cached with GRYPE_DB_VALIDATE_AGE=false) are based on the release.yml comments, the .goreleaser.yaml and the product description; the build step itself (docker/build-push-action@v6) was documented from the workflow, not from an internal analysis of Dockerfile/Dockerfile.full.
  2. secrets.GITHUB_TOKEN is the default token provided by GitHub Actions; it is assumed that the packages: write, id-token: write, attestations: write and pages: write scopes are available in the repository (they are declared in the workflows).
  3. Branch protection / required checks: the document describes the PR-to-main flow as practice; there is no branch-protection configuration file verifiable in the repository here, so the "required checks" rules are recommendations aligned with the workflows' behavior. Likewise, coverage is observed (summary + artifact) but there is no numeric gate that fails the build.
  4. Consumer verification commands (cosign/gh attestation verify) were extrapolated from the comments and verification steps present in release.yml and action.yml; the identity regexp uses the owner Martinez1991, per action.yml.
  5. v0/v0.2 as moving tags of the Action: the repositioning is automated by the workflow .github/workflows/tag-major.yml, which runs on every semver release and force-updates v0/v0.2 to the same commit (git tag -f -a + git push -f as github-actions[bot]). These tags do not re-trigger release.yml/tag-major.yml because both are restricted to full semver tags and pushes via GITHUB_TOKEN do not chain new workflows.
  6. GitHub Pages: docs.yml publishes the MkDocs Material site; enabling Pages with "Build and deployment → Source: GitHub Actions" is a repository configuration prerequisite (documented in the workflow comment) that is not verifiable from the versioned files alone.

See also: README.md · README.pt-BR.md · DESIGN.md (supply chain §12, scanner status §14).