--- name: container-cve-validator description: Validate a CVE against a Red Hat container image using official SBOM attestations, Red Hat VEX data, and CVE metadata from MITRE/OSV.dev. license: Apache-2.0 user_invocable: true model: inherit color: red --- # Red Hat Container Security Validator ## When to Use This Skill Use this skill when the user asks you to check, validate, or analyze one or more CVEs against Red Hat container images. Input may be a single CVE ID + image reference, or a CSV file path containing multiple pairs. ## Optional Output Flags The user may specify these optional flags. Parse them before extracting the required inputs. - `--format markdown` — (default) Markdown report; verbosity controlled by `--output` - `--format json` — machine-readable JSON with 6 structured fields - `--format csv` — single-row CSV with a header row (one row per scan in batch mode) - `--output full` — (default) complete Markdown report with all sections; only applies to `--format markdown` - `--output summary` — condensed Markdown: executive summary + recommended actions only; only applies to `--format markdown` - `--file PATH` — path to a CSV input file for batch scanning (mutually exclusive with positional CVE ID and image reference) Record the resolved values as `OUTPUT_FORMAT` (default: `markdown`), `OUTPUT_MODE` (default: `full`), and `INPUT_FILE` (default: empty). Strip all flag tokens before extracting remaining positional arguments. ## Input Validation Use the `validate_input.py` script for all input validation — both single scan and batch mode. **Single scan:** ```bash python $SCRIPTS_DIR/validate_input.py --cve [CVE-ID] --image [IMAGE_REFERENCE] ``` **Batch mode (when `--file` was provided):** ```bash python $SCRIPTS_DIR/validate_input.py --file [PATH] ``` The script returns JSON with `valid` (boolean), `mode` ("single" or "batch"), `entries` (array of validated CVE/image pairs with registry classification), and `errors` (array of error messages with line numbers for batch mode). - If `valid` is `false`: print the errors and stop. Do not proceed to any execution step. - If `valid` is `true`: use the `entries` array to drive the pipeline. Each entry contains `image_ref`, `ref_format` ("tag" or "digest"), `registry` info, and `input_type`. - If `input_type` is `"rhsa"`: the user provided an RHSA advisory ID. Resolve it to individual CVEs using (**do NOT use WebFetch or curl to access access.redhat.com**): ```bash python $SCRIPTS_DIR/fetch_rhsa_advisory.py [RHSA-ID] ``` Run the full validation pipeline for each CVE in the returned `cve_ids[]` list against the same image. - If `input_type` is `"cve"`: proceed directly with the single CVE ID. - For batch mode, run the full validation pipeline for each entry. Print a progress indicator: `[1/N] Scanning CVE-YYYY-NNNNN — registry.../image:tag` **Batch output aggregation:** - **`--format markdown`:** print each report separated by a `====` divider line - **`--format json`:** output a single JSON array `[{...}, {...}]` — one object per scan - **`--format csv`:** one header row followed by one data row per scan (no repeated headers) ### Image Inspection Use the `inspect_image.py` script to fetch image metadata: ```bash python $SCRIPTS_DIR/inspect_image.py [IMAGE_REFERENCE] ``` The script returns JSON with `labels` (cpe, name, com.redhat.component, vendor, maintainer, org.opencontainers.image.created), `digest`, `architecture`, and `errors`. If the script fails with an authentication error, log in first: ``` regctl registry login registry.redhat.io ``` Then re-run. If login fails, stop and inform the user. From the output, record: - `labels.cpe` → `product_cpe` — primary anchor for VEX product_tree matching - `labels.name` → public component name for VEX matching - `labels["com.redhat.component"]` → internal build name (secondary fallback only) - `labels.vendor` and `labels.maintainer` — for registry ownership validation - `labels["org.opencontainers.image.created"]` → image build timestamp **Registry ownership check:** The `validate_input.py` output already classifies the registry (`is_redhat`, `type`). For non-Red Hat registries: - If `vendor` or `maintainer` from inspect output is `"Red Hat, Inc."`: the image is a Red Hat image mirrored to an alternate registry. Record the canonical `registry.redhat.io` form for VEX matching. - Otherwise: stop and report that this skill only validates Red Hat-maintained images. **Input format classification:** The `validate_input.py` output provides `ref_format` ("tag" or "digest"). For digest-based references, the human-readable tag will be extracted from the SBOM in Step 2. Do NOT use `version` or `release` labels to reconstruct the image tag. ## Prerequisites **Step 0 — Resolve scripts directory.** Before anything else, locate the helper scripts. They are inside the skill's `scripts/` subfolder. Run: ```bash SCRIPTS_DIR="scripts" test -f "$SCRIPTS_DIR/validate_input.py" || { echo "Error: Scripts directory not found at $SCRIPTS_DIR"; exit 1; } ``` Use `$SCRIPTS_DIR` in all subsequent script calls. The scripts handle tool checks internally (regctl, cosign, syft) and return clear errors if tools are missing. ## Workflow **MANDATORY EXECUTION CONTRACT — read before starting:** This skill requires completing ALL of the following steps in order. No step may be skipped, condensed, or replaced with an alternative approach — regardless of which model is executing. | # | Step | Required | May skip only when | |---|---|---|---| | 0 | Prerequisites check | Always | Never | | 0 | Input validation + image inspection | Always | Never | | 1 | CVE Reconnaissance (MITRE → OSV.dev → Go vuln DB) | Always | Never | | 2 | SBOM Extraction + package verification | Always | Never | | 3 | Red Hat VEX Validation | Always | Package NOT found in SBOM (Step 2) | | 4 | Newer Image Availability Scan | Conditional | Trigger conditions not met (see Step 4 header) | | 5 | Final Report | Always | Never | **Strict rules:** 1. Execute each step using the helper scripts below. After each step, print: `✓ Step N complete — [key finding]` 2. **FORBIDDEN COMMANDS — never use these, even inside loops or pipelines:** - `grep` — do not grep through JSON data files - `jq` — do not use jq filters on JSON data - `curl` — do not fetch URLs directly, use the helper scripts - `cosign` — do not call cosign directly, use `download_sbom.py` - `for ... do ... done` loops that process tool-result files - Any ad-hoc bash script, Python one-liner, or shell pipeline that parses or filters JSON data 3. **The ONLY allowed bash commands are:** running the helper scripts (`python $SCRIPTS_DIR/...`), `SCRIPTS_DIR=...` resolution, `test -f`, and `cat` to read a file. 4. **How to handle large VEX/SBOM data:** Read the raw JSON output directly in your context window. Analyze it in your reasoning, not with shell tools. Do NOT attempt to filter it with grep/jq. 5. Do not combine, reorder, or parallelise steps. Each step depends on outputs from previous steps. 6. If a step produces unexpected results, report them and continue — do not stop unless explicitly instructed. 7. Every field in the final report must come from actual script outputs. Use `N/A — [reason]` for undetermined fields. Never fabricate values. 8. The `fetch_redhat_vex.py` script takes ONLY a CVE-ID argument — no flags. It returns the full raw VEX document. --- ### Step 1: CVE Reconnaissance Run the `fetch_cve_metadata.py` script to query all CVE data sources in a single call: ```bash python $SCRIPTS_DIR/fetch_cve_metadata.py [CVE-ID] ``` The script automatically queries MITRE CVE API, OSV.dev, and (if a GO-* alias is found) the Go vulnerability database. It returns merged JSON with: - `description` — CVE description - `affected[]` — all affected packages with `ecosystem`, `package`, `versions` (introduced/fixed), and `source` (mitre/osv/go_vuln_db) - `aliases[]` — cross-references (GO-*, GHSA-*, etc.) - `errors[]` — any API failures (non-fatal; the script continues with available data) From the output, identify: - The target package name and ecosystem for SBOM matching - For Go CVEs, the Go vuln DB module path is authoritative for SBOM matching - Vulnerable version ranges (use the union across all sources) - If `affected` is empty and errors indicate the CVE was not found: stop and report "CVE not found" Print: `✓ Step 1 complete — [package name], ecosystem: [ecosystem], sources: [list from affected[].source]` --- ### Step 2: SBOM Extraction Run the `download_sbom.py` script to handle all SBOM extraction logic in a single call: ```bash python $SCRIPTS_DIR/download_sbom.py [IMAGE_REFERENCE] ``` The script handles attestation/build-time fallback and image index detection automatically. It returns JSON with: - `sbom_source` — "attestation" or "build_time" - `spdx` — the **full raw SPDX JSON document** with all packages, relationships, PURLs, and version info - `errors[]` — any issues encountered **Read and interpret the full raw SPDX document from the `spdx` field.** Analyze the SPDX data directly — packages (with `externalRefs` PURLs and `versionInfo`), relationships (for parent RPM linkage), checksums. Do NOT use grep or jq to search the SBOM. If `spdx` is null (no SBOM found), fall back to syft: ```bash python $SCRIPTS_DIR/generate_sbom_syft.py [IMAGE_REFERENCE] ``` This generates an analyzed SBOM using syft. The output has the same JSON schema but `sbom_source` will be `"syft_analyzed"` — note this in the report as a generated SBOM, not an official one. If neither method produces an SBOM: stop and report "Unable to extract SBOM. Cannot determine package presence." **Package presence check and version confirmation:** Red Hat container image SBOMs contain both RPM packages and non-RPM content (Go modules, Python packages, Node.js packages, etc.), each identified by ecosystem-specific PURLs. A non-RPM package may exist as a standalone SBOM entry AND be linked via an SPDX relationship to the RPM package that delivers it. Use the following matching logic: 1. **Identify the target package name and ecosystem from Step 1.** For Go, use the module path from the Go vuln DB as the authoritative name. 2. **Search SBOM packages by PURL and name:** - For each package in the SBOM, check its `externalRefs[]` for entries with `referenceType: purl`. Parse the PURL to extract ecosystem, name, and version. - Match the ecosystem prefix against the CVE ecosystem (`pkg:rpm`, `pkg:golang`, `pkg:pypi`, `pkg:npm`, etc.). - Match the package name case-insensitively. For Go, match against the full module path (e.g., `golang.org/x/net`). - If found as a non-RPM package: also check its SPDX relationships — look for a `DYNAMIC_LINK`, `STATIC_LINK`, or `CONTAINED_BY` relationship to a parent RPM package entry. Record both the non-RPM package entry and its parent RPM if present. - If found only as an RPM package: check `name` and `sourceName` fields (case-insensitive). 3. **If the package is NOT found by any method:** skip Step 3 and go to Step 4 (Early Exit — package not in image). 4. **If the package IS found:** record the installed version from the SPDX `versionInfo` field and proceed to version range confirmation. 5. **Confirm the installed version falls within the vulnerable range:** - Compare the installed version against the vulnerable version ranges from Step 1 (MITRE and, for Go, Go vuln DB). - Version comparison rules by ecosystem: - **RPM:** use RPM EVR ordering (`Epoch:Version-Release`). A higher EVR is a newer, potentially fixed version. If the installed version is within the `introduced`–`fixed` range from MITRE, it is vulnerable. - **Go:** use semver ordering. Compare using the `introduced` and `fixed` boundaries from the Go vuln DB `ranges[].events[]`. - **PyPI / npm:** use the version ordering rules of the respective ecosystem as documented in the CVE `versions[]` array. - If the installed version is **within** the vulnerable range: record as **vulnerable — version confirmed**. - If the installed version is **at or above** the fixed version: record as **not vulnerable — patched version installed**, skip Step 3, and go to Step 4. - If version comparison is inconclusive (e.g., non-standard version string): record as **version comparison inconclusive** and proceed to Step 3 noting this uncertainty. Print: `✓ Step 2 complete — package [found|not found], version: [version], verdict: [vulnerable|patched|inconclusive], SBOM method: [attestation|build-time]` --- ### Step 3: Red Hat VEX Validation Run the `fetch_redhat_vex.py` script to fetch the full raw VEX document: ```bash python $SCRIPTS_DIR/fetch_redhat_vex.py [CVE-ID] ``` The script returns JSON with: - `http_status` — 200, 404, or error code - `vex` — the **full raw CSAF VEX document** with `product_tree`, `vulnerabilities`, `remediations`, `flags`, `threats` - `errors[]` — any issues **Read and interpret the full VEX document from the `vex` field.** The LLM should analyze the `product_tree` (branches, relationships, CPEs, PURLs), `vulnerabilities` (product_status, remediations, flags, threats) directly to perform matching against the image CPE and component name from Step 0. This gives the LLM full context to: - Match the specific container component under its CPE - Search across all CPE versions for name-only matches - Check parent RPM status across all product streams (especially RHEL) - Extract all RHSA advisory URLs, justification flags, severity **Interpret the results:** - If `http_status` is 404: record "No Red Hat VEX data available for this CVE" and proceed to Step 4 (check VEX data gap Condition B). - If `vex.product_tree` contains only a generic blanket statement (all products under `cpe:/a:redhat`): treat as no analysis performed (check VEX data gap Condition C). **Read [references/01-vex-validation-procedure.md](references/01-vex-validation-procedure.md)** for the complete VEX matching procedure (sub-steps 3–9: generic blanket detection, product_tree matching, status determination, remediation/flags/severity extraction, and parent RPM patch check). Print: `✓ Step 3 complete — VEX status: [status], severity: [severity], gap condition: [A|B|C|none]` --- ### Step 4: Newer Image Availability Scan **Trigger — perform this step only when ALL of the following are true:** - The vulnerable package is an RPM, OR a non-RPM package delivered via a parent RPM (identified in Step 2 via SPDX relationship) - A fixed RPM version is known — from either: - Red Hat VEX `product_status[].fixed` for the container (Step 3, sub-step 5), OR - The parent RPM patch status check (Step 3, sub-step 9) finding the RPM `fixed` in RHEL - The RHSA release date is known (from the `vendor_fix` remediation URL or advisory metadata) If trigger conditions are NOT met, print `✓ Step 4 skipped — trigger conditions not met ([reason])` and proceed to Step 5. **Procedure:** 1. Extract the image repository from the scanned image reference — strip tag or digest, keeping only registry + namespace + image name (e.g., `registry.redhat.io/multicluster-engine/cluster-proxy-addon-rhel9`). 2. Run the `scan_newer_images.py` script: ```bash python $SCRIPTS_DIR/scan_newer_images.py [IMAGE_REPOSITORY] --since [SCANNED_IMAGE_CREATED_LABEL] ``` The script returns JSON with `newer_images[]` — each entry has `tag`, `created`, `digest`, and `cpe`. Images are sorted by date (newest first), deduplicated by digest, capped at 10 results. If `newer_images` is empty: no newer images exist yet. Record this and proceed to Step 5. 3. For each newer image candidate, download its SBOM and search for the vulnerable RPM: ```bash python $SCRIPTS_DIR/download_sbom.py [IMAGE_REPOSITORY]:[TAG] ``` - Find the RPM package by name in the SPDX data - Compare its `versionInfo` against the known fixed version using RPM EVR ordering - If installed version **≥ fixed version**: this image contains the fix — record it 4. For each image found to contain the fix, compare its CPE against the scanned image's CPE: - **Same CPE** (same product stream): "Patched image available in the same product stream: `[IMAGE_REPOSITORY]:[TAG]` (`[DIGEST]`, built `[CREATED]`)" - **Different CPE** (different product stream): "Patched image available in a newer product stream (`[NEWER_CPE]`): `[IMAGE_REPOSITORY]:[TAG]` (`[DIGEST]`, built `[CREATED]`). Upgrading would change product stream from `[IMAGE_CPE]` to `[NEWER_CPE]`." 5. If no newer image contains the fixed RPM: record "No patched container image released yet. The RPM fix exists but the container has not yet been rebuilt with the updated RPM." Print: `✓ Step 4 complete — [patched image found: REPO:TAG | no patched image found | skipped]` --- ### Step 5: Final Report **Read [references/02-report-template.md](references/02-report-template.md)** for VEX data gap condition evaluation (Conditions A/B/C), executive summary rules, upstream patch note logic, RHSA advisory assessment, the full report template (markdown/JSON/CSV), and field semantics. Print: `✓ Step 5 complete — report generated` ## Dependencies ### Required MCP Servers - None — this skill uses bundled Python scripts, not MCP tools ### Required Helper Scripts - `validate_input` — validates CVE ID format and image reference - `inspect_image` — extracts container image metadata via regctl - `fetch_cve_metadata` — queries MITRE, OSV.dev, and Go vuln DB - `download_sbom` — fetches SBOM attestations and build-time SBOMs - `generate_sbom_syft` — fallback SBOM generation via syft (optional, used when attestations unavailable) - `fetch_redhat_vex` — retrieves Red Hat VEX security advisories - `fetch_rhsa_advisory` — resolves RHSA advisory IDs to CVE lists - `scan_newer_images` — lists and checks newer image tags for patched RPMs ### Related Skills - `cve-recon` — standalone CVE reconnaissance (Step 1 only) - `image-inspect` — standalone image inspection (Input Validation only) - `coreos-cve-validator` — CVE validation for CoreOS/RHCOS images ### Reference Documentation - [Red Hat CSAF VEX Data](https://security.access.redhat.com/data/csaf/v2/) - [MITRE CVE API](https://cveawg.mitre.org/api/cve/) - [OSV.dev API](https://api.osv.dev/v1/vulns/)