Story 1.2: Capture the pre-upgrade v1.3 CVE/SBOM baseline
Sprint Key: 1-2-capture-the-pre-upgrade-v1-3-cve-sbom-baseline
Epic: 1
PRD: opea-1.5-upgrade
baseline_commit: 735696c8
Story 1.2: Capture the pre-upgrade v1.3 CVE/SBOM baseline
Status: ready-for-dev
Story
As a security engineer, I want the v1.3 image CVE/SBOM baseline recorded before any bump, so that remediation after the OPEA 1.5 upgrade is demonstrable, not asserted.
Acceptance Criteria
-
Advisory baseline captured with CI-identical scanner. A per-image vulnerability advisory for the 16 GENIE.AI v1.3 images is captured using the same scanner version and severity taxonomy as the CI
scanstage (Trivy0.57.0, severityHIGH,CRITICAL,--ignore-unfixed,--ignorefile .trivyignore) — apples-to-apples for the FR-12 diff. -
SBOM baseline captured. A CycloneDX SBOM per image is captured and committed (NFR-S1). If the CI scan stage's syft output is SPDX (current reality), the driver must ALSO emit CycloneDX (Trivy
--format cyclonedx/ syftcyclonedx-json) so the NFR-S1 deliverable is not format-gapped. See Dev Notes §Decision D1. -
Committed, dated, locked artifact. The advisory + SBOM baseline is committed as
_bmad-output/implementation-artifacts/cve-sbom-baseline-v1.3.json, dated and locked as the reference point for the FR-12 baseline-diff. - Scanner/DB identity recorded. The artifact records Trivy version, Trivy DB ID/date (or sha), syft version, severity taxonomy, ignorefile hash, and the exact scan flags — so Story 3.2 can reproduce the same scan and prove the diff is meaningful.
-
Stack + image identity recorded. The artifact records the exact image tags + digests scanned (stack identity), capture date, and harness git identity — mirroring the RAG-baseline artifact (
rag-baseline-v1.3.json) schema. - Regenerable + deterministic. The capture is a committed, idempotent driver (not a hand-built report): re-running it against the same image digests reproduces the artifact. A unit test proves idempotency.
Tasks / Subtasks
-
T1: Write the capture driver tests/scan-baseline/capture_cve_baseline.py(AC: 1, 4, 5, 6)-
Reuse the CI .scan_templatecommands verbatim:trivy image --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore --format json(advisory) — do NOT invent new flags -
Emit CycloneDX SBOM per image (Trivy --format cyclonedxor syft-o cyclonedx-json); record format + decision in the artifact (AC: 2, D1) -
Pin scanner versions + DB identity: Trivy image tag, syft version, Trivy DB ID/date, severity taxonomy, .trivyignoresha256 (AC: 4) -
Accept --images-file PATH(16-image manifest) and--out PATH; record per-image tag + digest (AC: 5) -
Compute per-image + fleet summary (total HIGH/CRITICAL, fixable vs unfixed, ignored-by-ignorefile) — the numbers Story 3.2 diffs
-
-
T2: Author unit tests tests/scan-baseline/test_capture_cve_baseline.py(AC: 6)-
Idempotency: two runs with the same image digests + mocked trivy/syft output produce byte-identical advisory/sbom sections -
Summary math: count fixable/unfixed/ignored from a synthetic trivy JSON fixture -
CycloneDX-format assertion: the driver emits valid CycloneDX JSON (not just SPDX passthrough) -
Empty-image-list / missing-image guards raise, not silently commit an empty baseline
-
-
T3: Capture + commit the v1.3 baseline (AC: 3, 5) -
Run against the deployed v1.3 images (release/el-salvador registry tags — the same set the RAG baseline scanned, or a dedicated v1.3 tag) -
Verify 16/16 images scanned; record stack identity + capture date -
Commit cve-sbom-baseline-v1.3.json+ the driver + tests + any helper manifest
-
Review Findings
(No prior review — first implementation. Add findings here as code review surfaces them.)
Dev Notes
Non-negotiable constraints
-
Capture BEFORE any bump. This is pre-rebase milestone (a), parallel to Story 1.1. A baseline captured after
OPEA_VERSION=v1.5is worthless for FR-12 (PRD FR-12, architecture §6 pattern 6). - Scanner/taxonomy identity is the whole point. FR-12's gate is a diff — "same scanner version + severity taxonomy" — not a raw count. If Story 3.2 scans with a different Trivy version or a different severity set, the diff is noise. The driver must record what it used and Story 3.2 MUST be able to reproduce it.
-
Reuse the CI scan command, don't fork it. The CI
scanstage (.scan_templatein.gitlab-ci.yml) already defines the canonical advisory command. Copy that exact invocation into the driver so the baseline matches what CI produces for v1.5. -
.trivyignoreis currently EMPTY (header comments only, no governed exceptions). Record its sha256 so a later addition of an exception is a visible diff. -
Blocking mechanism is the MR approval policy, not
trivy --exit-code. Scan jobs areallow_failure: true; the gate is the security approval policy (.gitlab/security-policies/policy.yml,scan_finding/container_scanning, HIGH/CRITICAL → require maintainer approval). The baseline is an evidence artifact for FR-12/Story 3.2, not itself a pipeline gate. -
16 images, not 14 and not "the OPEA ones". The full set scanned by CI is:
genie-ai-frontend,genie-ai-backend,genie-ai-document-repository,genie-ai-db-migrations,genie-ai-chatqna-server,genie-ai-dataprep-arango,genie-ai-retriever-arango,genie-ai-reranker,genie-ai-embedding,genie-ai-textgen,genie-ai-nginx,genie-ai-kong-config,genie-ai-keycloak,genie-ai-keycloak-config,genie-ai-postgres-init,genie-ai-tempo-proxy. Confirm against.gitlab-ci.ymlbefore committing the manifest.
Decision D1 — SBOM format (must be resolved by the dev agent)
-
AC/NFR-S1 says CycloneDX. The current CI
scanstage emits SPDX (syft --scope squashed -o spdx-json > sbom.spdx.json). ADR-0001 §4 describes CycloneDX from the GitLab template (gl-sbom-*.cdx.json), but the actual pipeline overrode the template with a custom trivy+syft stage. -
Resolution: the baseline artifact must include a CycloneDX SBOM (Trivy
image --format cyclonedxis the cheapest — same binary already pinned, and it keeps scanner identity to one tool). Record in the artifact that CI's syft output is SPDX and that the CycloneDX baseline is produced via Trivycyclonedxso Story 3.2 can re-emit either format. If the team prefers syftcyclonedx-json, use that consistently — the point is one pinned path, documented. - If the AC's "CycloneDX" conflicts with what the pipeline will actually diff, surface this in the story's Review Findings rather than silently shipping SPDX-only.
Environment / deployment facts (substitute for your stack)
- v1.3 images are deployed as
registry.opensource.unicc.org/un/itu/genie-ai/<image>:release-el-salvador(the RAG baseline scanned exactly this set — seerag-baseline-v1.3.jsonstack.services[].image). Alternatively pin a dedicatedv1.3baseline tag if the release branch moves; the artifact must record whichever tag+digest was scanned. - Scanner binaries: Trivy
0.57.0(TRIVY_IMAGE_TAGin.gitlab-ci.ymlline 19), syftv1.26.0(line 20). Trivy DB: download withtrivy image --download-db-only(as CI does inbefore_script); recordtrivy db info/ the DB sha into the artifact so Story 3.2 can pin the same advisory DB. - Trivy needs network to fetch the DB (or a cached
/cache/.trivycache/). Run the capture on a node with registry + network access; a git-less node is fine (the RAG driver'sgit_identityreturnsnull— mirror that).
Files to create / touch
| File | Action |
|---|---|
tests/scan-baseline/capture_cve_baseline.py |
NEW — capture driver (advisory + CycloneDX SBOM + identity + summary) |
tests/scan-baseline/test_capture_cve_baseline.py |
NEW — idempotency + summary + format + guard tests |
tests/scan-baseline/images-v1.3.txt (or similar) |
NEW — the 16-image manifest (tags + digests) |
_bmad-output/implementation-artifacts/cve-sbom-baseline-v1.3.json |
NEW — the committed, dated, locked baseline artifact |
.gitlab-ci.yml |
READ-ONLY — source of the canonical scan invocation; do NOT modify in this story |
Testing standards
- Driver tests are pure (mock trivy/syft subprocesses + a fixture trivy JSON) — no live registry in CI.
- Existing scan/CI tests must stay green; this story adds a new test dir, does not modify
.gitlab-ci.yml. - Idempotency: two runs against the same input produce byte-identical advisory/sbom sections (assert in a unit test).
Project Structure Notes
- Baseline/evidence artifacts live in
_bmad-output/implementation-artifacts/(architecture verification boundary §4). The RAG baseline (rag-baseline-v1.3.json) is the schema precedent — mirror its top-level keys (artifact,version,baseline_stack,capture_date,git,harness_sha,stack, plus this story'sadvisory,sbom,scanner,summary). - Do not put committed artifacts under a gitignored output dir; keep the driver in
tests/scan-baseline/.
References
- PRD FR-12 (§FR-12: diff vs v1.3 baseline advisory, same scanner + taxonomy, accept-list for known-benign, no net-new high/critical, closures recorded) —
_bmad-output/planning-artifacts/prds/prd-genie-ai-2026-08-07/prd.md - NFR-S1 (CycloneDX SBOM + signed images, blocking scan), NFR-S2 (no net-new high/critical) — PRD, NFR section
- Architecture pattern 6 (v1.3 CVE/SBOM baseline captured before migration, locked, env recorded) + pattern 12 (evidence-ledger) + pattern 11 (canary exit criteria include CVE ≤ baseline) —
_bmad-output/planning-artifacts/architecture.md - Story 3.2 (consumes this baseline for the baseline-diff) —
_bmad-output/planning-artifacts/epics.md - CI scan stage
.scan_template(canonical trivy invocation) —.gitlab-ci.yml(stagescan, ~lines 629-667) - Scan/blocking architecture + SBOM + ADR-0001 —
docs/adr/0001-gitlab-registry-build-scan-pipeline.md - Security approval policy (the actual gate) —
.gitlab/security-policies/policy.yml - RAG baseline artifact schema precedent —
_bmad-output/implementation-artifacts/rag-baseline-v1.3.json - Trivy CycloneDX emission —
trivy image --format cyclonedx(Trivy docs; CLI reference)
Dev Agent Record
Agent Model Used
deepseek-v4-flash[1m] (Claude Code, bmad-create-story)
Debug Log References
- Story scope:
epics.mdStory 1.2 (CVE/SBOM baseline) + PRD FR-12 + NFR-S1/S2 + architecture patterns 6/11/12 - CI scan stage read:
.gitlab-ci.yml.scan_template(lines ~629-667),TRIVY_IMAGE_TAG=0.57.0,SYFT_VERSION=v1.26.0,.trivyignore(empty),docs/adr/0001(SBOM §4, scanning §6, addendum MR !259 (merged)) - Sibling precedent:
_bmad-output/implementation-artifacts/1-1-*.md(RAG-baseline driver pattern) +rag-baseline-v1.3.json(schema + stack identity)
Implementation Plan
(filled during dev-story)
Completion Notes List
- Story created from epics.md Story 1.2 + PRD FR-12/NFR-S1/S2 + architecture patterns 6/11/12. Previous story 1-1 (RAG-parity baseline) provides the driver/artifact precedent (mirror its
capture_baseline.pystructure +rag-baseline-v1.3.jsonschema). Story 1-1 review learnings (idempotency, guard rails,git_identitynull-safe, artifact regenerability) apply directly to this driver.
File List
-
_bmad-output/implementation-artifacts/1-2-capture-the-pre-upgrade-v1-3-cve-sbom-baseline.md(this file)
Change Log
- 2026-08-11: Story created (ready-for-dev) by bmad-create-story. Scope: v1.3 advisory (CI-identical scanner) + CycloneDX SBOM baseline, committed/dated/locked, consumed by Story 3.2.