Imported from dmesser/openshift-cve-diff (
AGENTS.md). Install upstream withnpx skills add dmesser/openshift-cve-diff. Copyright stays with the author.
AGENTS.md — openshift-cve-diff
Guidance for coding agents working in this repository. Treat this as the project orientation; README.md remains the human operator guide.
Purpose
Feasibility demo for RFE-9552: compare the CVE surface between OpenShift Container Platform (OCP) z-stream releases by scanning core payload images with ACS (roxctl), caching results, and emitting a PatternFly v6 static SPA.
This intentionally does not depend on z-stream SPDX product SBOMs or CSAF/VEX remediation feeds. Source of vulnerability data is ACS image scan (+ SBOM inventory for component presence/type/version).
Repo: https://github.com/dmesser/openshift-cve-diff
Live site (after CI deploy): https://dmesser.github.io/openshift-cve-diff/
Hard constraints for agents
- Never commit secrets or large generated trees. Gitignored:
cache/,dist/*(exceptdist/README.md),rox-central-address.txt,rox-central-token.txt, tokens/keys. - Do not start a long ACS rescan unless the user explicitly asks. Prefer
--skip-scanto rebuilddist/from./cache/. Scans of multiple minors can take many hours. - Preview the SPA over HTTP, not
file://— the shell fetches./data/*.json. Example:python3 -m http.server -d dist 8080. - GitHub Pages size ceiling applies to
dist/, not./cache/. Published site size in report meta is measured from the emitteddist/tree. - Human in the loop for RFE claims and production messaging; this is a feasibility tool.
Architecture (baseline, not a migration story)
Two artifact layers:
| Layer | Path | Role |
|---|---|---|
| Scan cache | ./cache/releases/{version}/ |
Full ACS/SBOM detail (meta.json, summary.json, images/*.json). TTL-based reuse; failed image scans are always retried next run (successes kept). Within a z-stream, remaining failures get up to 3 serial end-of-release retries (concurrency=1). roxctl image scan also has its own --retries (default 3). |
| Published site | dist/index.html + dist/data/ |
Compact SPA for Pages. Lazy-loads per-release JSON. |
Cincinnati (z-streams)
→ oc adm release info (payload images)
→ roxctl image scan + sbom (ACS Central)
→ cache/releases/{version}/
→ render → dist/ (shell + manifest + releases/*.json)
Published site layout
dist/index.html— Jinja-rendered SPA shell (templates/app.html.j2)dist/assets/— logo + favicons copied from packagestatic/at render time (relative./assets/URLs for GitHub Pages)dist/data/manifest.json— versions, chart summaries, reportmetadist/data/releases/{version}.json— findings for one z-stream (fetched on compare)dist/data/history.json— chart Time Machine snapshots (unique severity totals by UTC day)
Release JSON keeps compact findings (cveId, severity, componentName, componentVersion, componentFixedVersion) plus per-image components inventory: { name: { type, version } } from the ACS SBOM. Full graphs stay in cache.
Chart Time Machine history
- Durable store:
cache/releases/{version}/history.json(survivessave_release; never wipe per-version dirs wholesale). - Each snapshot stores
unique_severity_totals(CVE ID once) andtotal_severity_totals(image×CVE once), plusfailed_image_count/image_countfor incomplete-scan greyscale and!markers. - New snapshots also store optional
image_cves:{ imageName: { cveId: severity } }(best severity per image×CVE) so the SPA can recompute Unique/Total after client-side image exclusions. Legacy snapshots withoutimage_cvesstay valid; filtered history falls back to unfiltered + banner for those days. - Append on fresh scan only when UTC calendar day is new and unique totals, total totals,
image_cvespresence/content, orfailed_image_countdiffer from the latest snapshot. - Publish aggregates into
dist/data/history.json(days+ sparsereleaseswith nested unique/total maps and optionalimage_cves); versions with no history file are omitted (ghost-only bars). SPA toggles Unique CVEs vs Total CVEs for the chart + Time Machine. - Manifest summaries include
image_names(sorted payload short names) for exclusion-modal validation without loading every release JSON. - Local seed for UI testing:
python scripts/seed_chart_history.py --cache-dir ./cache(default minors 4.18–4.22, 20 weeks), then--skip-scan. Do not commitcache/or generateddist/data/history.json.
| Invocation | Appends cache/.../history.json? |
Updates dist/data/history.json? |
|---|---|---|
| Default (scan + write site) | Yes, per version on fresh scan when totals changed and UTC day is new | Yes, at end of write_report |
--scan-only |
Same as above | No |
--skip-scan |
Never | Yes, from whatever is already in ./cache |
| Cache hit (TTL fresh, complete, no failed images) | No | Yes if writing the site |
| Fresh cache with failed images | Retries failures only (not a no-op hit); may append if totals/failures change | Yes if writing the site |
--force-rescan |
Treated as fresh scan → append if day/totals gates pass | Yes if writing the site |
CI accumulation: Durability is the per-minor GitHub Actions cache (cve-cache-…-minor-DAY + restore-keys prefix), not the 7-day scan artifacts (same-run assemble only). Workflow concurrency.group: generate-report with cancel-in-progress: false and scan max-parallel: 1 avoid overlapping writers on the same minor cache.
Failure modes to remember:
- Actions cache eviction/miss → that minor’s timeline resets; next fresh scans start a new history.
- Artifact retention does not protect multi-week history.
- With
--cache-ttl-hours 168, weekly runs tend to rescan and can append ~weekly; frequent cache-hit runs do not grow history until a fresh scan on a new UTC day with changed totals. - Concurrent writers on the same minor cache can drop days (unsupported; mitigated by concurrency).
Report meta (manifest)
Collected at render time in environment.py / write_report():
- Counts:
z_streams_scanned,z_streams_scanned_display(e.g.118 (4.18–4.22)or10 (4.18–4.19) [10]when--max-releaseswas set),minors_scanned, optionalmax_releases,component_images_scanned,generated_at(+ display) - Toolchain:
roxctl_version,oc_version,central_version(ACS/v1/metadata),cincinnati_endpoint,cincinnati_channel(CLI/workflow--channel, defaultfast) scan_duration_seconds/scan_duration_display— wall-clock of a scan session, or sum of per-releasescan_duration_secondsfrom cache meta when rebuilding with--skip-scanpublished_site_bytes/published_site_size_display— size ofdist/after emit
UI shows these in a collapsed-by-default expandable (“Show scan environment”). Missing values render as Unavailable.
Python package map (src/openshift_cve_diff/)
| Module | Responsibility |
|---|---|
cli.py |
Entrypoint: inventory / scan / --scan-only / --skip-scan / write site |
cincinnati.py |
Public upgrade-graph API; resolve {channel}-{minor} → z-streams (default channel fast) |
release_images.py |
oc adm release info → payload image list |
scanner.py |
roxctl image scan + SBOM; normalize findings; parallel per release |
cache.py |
Filesystem TTL cache; history.json append; reject incomplete/filtered caches |
history.py |
Publish history.json; synthetic weekly seed helper |
aggregate.py |
Severity totals |
diff.py |
Compare two releases (fixed / introduced semantics) |
render.py |
Compact payloads, manifest, history publish, Jinja HTML; measure published size |
environment.py |
Toolchain versions, duration/size formatting |
semver_util.py |
OCP X.Y.Z helpers |
templates/app.html.j2 |
Entire SPA (HTML/CSS/JS); PatternFly 6 from CDN |
Entry point: openshift-cve-diff = openshift_cve_diff.cli:main (pyproject.toml).
CLI modes agents should know
# Full scan + site (long-running)
openshift-cve-diff --minors 4.18 4.19 … --output dist/index.html
# Cache only (CI scan jobs)
openshift-cve-diff --scan-only --minors 4.18 …
# Site only from cache (local UI iteration / CI assemble)
openshift-cve-diff --skip-scan --minors 4.18 … --output dist/index.html
# No ACS
openshift-cve-diff --dry-run inventory --minors 4.22
--scan-only and --skip-scan are mutually exclusive.
Useful knobs: --cache-dir, --cache-ttl-hours, --concurrency, --max-releases, --image-filter, --force-rescan, --rox-endpoint-file, --rox-token-file.
SPA behavior (important product rules)
Implemented in templates/app.html.j2:
- Compare start/target z-streams; history chart for the inclusive range.
- Excluded images (browser-local): Compare-releases modal (PatternFly editable labels) persists names under
localStoragekeyocp-cve-diff-excluded-images. Validated against manifestimage_names/ loaded release keys. Active filter badges on Compare / CVE history / Release comparison. Unique and Total counts omit excluded images; accordion keeps excluded rows visible in greyscale. History/Time Machine useimage_cveswhen present; otherwise unfiltered + banner. - Time Machine (chart only): vertical scrubber walks
history.jsondays (latest at top). Light-grey ghost stacks = latest report; colored stacks = selected day (carry-forward). On the latest day, current summary severity colors always show (even with nohistory.json). Earlier days with no snapshot → ghost only, never fake zeros. - Unique CVE vs image×CVE totals are distinct metrics; labels/copy should not conflate them.
- Absent (grey): image missing on one side, or component not in that image’s SBOM inventory. Not the same as “0 open CVEs” / CVE-free.
- CVE-free: component still in SBOM inventory with no findings; show inventory version when findings lack one.
- Scan failures: failed image scans contribute no findings → underreported totals. Surface in summary cards, image comparison, and chart (greyscale stack + incomplete marker/tooltips). Tooltips must wrap (
white-space: normal). - Image comparison: expensive; use loading overlay, chunked row mount, lazy panel bodies on expand.
- Delta chips: set
titleto the full formatted Δ (large numbers truncate visually). - Masthead: theme toggle + official GitHub Mark link (
GITHUB_REPO_URLinrender.py).
Prefer PatternFly v6 components/tokens already used in the template; keep the shell static (no bundler).
CI / Pages
| Workflow | When to use |
|---|---|
.github/workflows/generate-report.yml |
Scan (or cache-hit scan) + assemble + deploy |
.github/workflows/assemble-deploy.yml |
SPA/template/publish-format changes only: restore Actions caches → --skip-scan → deploy |
Generate:
prepare— minor matrix from workflow input (default4.18–4.22).scan— one job per minor,max-parallel: 1,fail-fast: false, 6h timeout,--scan-only, per-minor Actions cache (app TTL 168h), uploadcache-*artifacts. ACS load is the workflowconcurrencyinput (default4), not 5×.assemble— merge caches,--skip-scan, deploy Pages once.
Assemble and deploy: restore the same cve-cache-…-minor-… keys (no ACS image scans), merge, --skip-scan, deploy. Fails if a minor has never been cached by Generate.
Do not redeploy Pages per minor. dist/ is gitignored — there is no checked-in-dist/ deploy path. Secrets: ROX_CENTRAL_ADDRESS, ROX_CENTRAL_TOKEN (assemble still needs them for report meta).
Local development pattern
./scripts/bootstrap-venv.sh
source .venv/bin/activate
# Iterate on UI/render without ACS:
openshift-cve-diff --skip-scan --minors 4.18 4.19 4.20 4.21 4.22 \
--cache-dir ./cache --output ./dist/index.html \
--rox-endpoint-file ./rox-central-address.txt \
--rox-token-file ./rox-central-token.txt
python3 -m http.server -d dist 8080
Template-only tweaks: regenerate dist/index.html via render_html(manifest) or --skip-scan (do not rewrite release JSON unless payload shape changed).
Tests: pytest under tests/ (dev extra). Prefer unit tests for render/CLI/environment; do not require live ACS in CI unit tests.
Change etiquette
- Match existing module boundaries; keep published JSON compact (watch
RELEASE_JSON_WARN_BYTES~10 MiB soft limit per release file). - When changing published payload shape, update
render.py, SPA consumers, andtests/test_render.pytogether. - Commit messages follow conventional style already in history (
feat:,fix:,ci:,docs:). - Do not commit regenerated
dist/orcache/as part of normal feature work. - Ask before destructive git operations or force-push to
main.