CtrlK
BlogDocsLog inGet started
Tessl Logo

container-cve-validator

Validate a CVE against a Red Hat container image using official SBOM attestations, Red Hat VEX data, and CVE metadata from MITRE/OSV.dev.

65

Quality

79%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Fix and improve this skill with Tessl

tessl review fix ./ocp-admin/skills/container-cve-validator/SKILL.md
SKILL.md
Quality
Evals
Security

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:

python $SCRIPTS_DIR/validate_input.py --cve [CVE-ID] --image [IMAGE_REFERENCE]

Batch mode (when --file was provided):

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):
    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:

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:

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.

#StepRequiredMay skip only when
0Prerequisites checkAlwaysNever
0Input validation + image inspectionAlwaysNever
1CVE Reconnaissance (MITRE → OSV.dev → Go vuln DB)AlwaysNever
2SBOM Extraction + package verificationAlwaysNever
3Red Hat VEX ValidationAlwaysPackage NOT found in SBOM (Step 2)
4Newer Image Availability ScanConditionalTrigger conditions not met (see Step 4 header)
5Final ReportAlwaysNever

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:

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:

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:

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:

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 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:

    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:

    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 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
  • MITRE CVE API
  • OSV.dev API
Repository
RHEcosystemAppEng/agentic-plugins
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.