# VEX Checker

LLMS index: [llms.txt](/llms.txt) | Full content: [llms-full.txt](/llms-full.txt)

---

A CLI tool that works with the [Red Hat CSAF VEX feed] and the Hummingbird
catalog API to check CVE statuses and track coverage across Jira, VEX
advisories, and container image scan results.

## Subcommands

| Subcommand  | Purpose                                                        |
|-------------|----------------------------------------------------------------|
| `check`     | Look up a single CVE and show Hummingbird product statuses     |
| `reconcile` | Cross-reference Jira CVE tickets with the VEX feed             |
| `catalog`   | Cross-reference catalog API CVEs with Jira tickets             |

## Prerequisites

- **Python 3.11+**
- **Internet access** to `security.access.redhat.com` (no VPN required) for
  `check`/`reconcile`; to `api-hummingbird.hummingbird-project.io` (no VPN
  required) for `catalog`
- **`JIRA_EMAIL`** Atlassian account email — required for `reconcile` and `catalog`
- **`JIRA_TOKEN`** Jira API token — required for `reconcile` and `catalog`

## `check` — Single CVE lookup

Fetches the CSAF VEX document for a given CVE and prints the status of all
Hummingbird products, filtering out all other RHEL/OpenShift/etc. entries.

### check: usage

```bash
./check_vex.py check CVE-YYYY-NNNNN [--json]
```

### check: options

| Option       | Description            |
|--------------|------------------------|
| `CVE-ID`     | CVE to look up         |
| `--json, -j` | Output results as JSON |

### check: examples

```bash
$ ./check_vex.py check CVE-2014-8090

CVE-2014-8090 — CVE-2014-8090 ruby: REXML billion laughs attack (Moderate)
  hummingbird-1:ruby.src      known_affected  none_available
  hummingbird-1:ruby3.3.src   known_affected  none_available
  hummingbird-1:ruby3.4.src   known_affected  none_available
  hummingbird-1:ruby4.0.src   known_affected  none_available
```

```bash
$ ./check_vex.py check CVE-2014-8090 --json
{
  "cve": "CVE-2014-8090",
  "title": "...",
  "severity": "Moderate",
  "hummingbird": [
    { "product_id": "hummingbird-1:ruby.src", "status": "known_affected", "remediation": "none_available" },
    ...
  ]
}
```

### check: exit codes

| Code | Meaning                                                                               |
|------|---------------------------------------------------------------------------------------|
| `0`  | No VEX document found, no Hummingbird products listed, or no `known_affected` status  |
| `1`  | One or more Hummingbird products are `known_affected`                                 |
| `2`  | Invalid CVE ID format                                                                 |

## `reconcile` — Jira/VEX sync check

Queries the Jira HUM project (Security component) for all CVE tickets, then
checks each CVE in the VEX feed. Reports two types of mismatches:

- **Open in Jira but not `known_affected` in VEX** — VEX says the issue is
  resolved but the Jira ticket is still open.
- **Closed in Jira but `known_affected` in VEX** — the Jira ticket was closed
  but Red Hat's advisory still marks Hummingbird as affected.

Continuous closed-ticket VEX reconciliation (Done-Errata → `fixed`,
Not a Bug → `known_not_affected` or `package_not_listed`, scoped to the
ticket's package, with `awaiting-vex` work queue and dashboard persistence)
lives in `hummingbird-cve-analysis` / the dashboard Closed tab (HUM-5843).
`reconcile` remains the one-shot CLI audit.

Jira tickets are considered closed when their status is one of:
`Done`, `Closed`, `Won't Fix`, `Not a Bug`.

VEX documents are fetched in parallel (`--workers`, default 20) so the
command is fast even with hundreds of CVE tickets.

### reconcile: usage

```bash
export JIRA_EMAIL=you@redhat.com
export JIRA_TOKEN=<your-api-token>
./check_vex.py reconcile [--workers N] [--json]
```

Credentials are read exclusively from environment variables to avoid
exposing them in process listings.

### reconcile: options

| Option       | Description                                             |
|--------------|---------------------------------------------------------|
| `--jira-url` | Jira base URL (default: `https://redhat.atlassian.net`) |
| `--workers`  | Parallel VEX fetch workers (default: 20)                |
| `--json, -j` | Output results as JSON                                  |

### reconcile: example

```bash
$ ./check_vex.py reconcile

Fetching Jira tickets... 142 ticket(s)
Fetching VEX statuses for 89 unique CVE(s)...
  89/89
2 mismatch(es) found:

  HUM-1234  CVE-2025-1234  closed → known_affected  (VEX still open)
    Jira: https://redhat.atlassian.net/browse/HUM-1234
    VEX:  https://security.access.redhat.com/data/csaf/v2/vex-feed/2025/cve-2025-1234.json

  HUM-5678  CVE-2024-5678  open → fixed  (VEX not affected)
    Jira: https://redhat.atlassian.net/browse/HUM-5678
    VEX:  https://security.access.redhat.com/data/csaf/v2/vex-feed/2024/cve-2024-5678.json
```

### reconcile: exit codes

| Code | Meaning                                    |
|------|--------------------------------------------|
| `0`  | No mismatches found                        |
| `1`  | One or more mismatches detected            |
| `2`  | Missing `JIRA_EMAIL` or `JIRA_TOKEN`       |

## `catalog` — Catalog API / Jira coverage check

Fetches all CVEs currently detected in Hummingbird container images (via Grype
scan results stored in the catalog API) and cross-references them with Jira
HUM Security tickets. Reports CVEs that have no corresponding Jira ticket.

CVEs with a closed Jira ticket are reported separately as expected propagation
delay — the typical flow is CVE fix → RPM → image rebuild → Grype DB update,
and Jira tickets are closed early while the catalog API reflects the latest
Grype scan (updated ~once daily).

Non-CVE vulnerability identifiers (e.g. `GHSA-*`) from Grype are filtered out
since they are not tracked in Jira.

### catalog: usage

```bash
export JIRA_EMAIL=you@redhat.com
export JIRA_TOKEN=<your-api-token>
./check_vex.py catalog [--api-url URL] [--workers N] [--json]
```

Each CVE line shows severity, age since first detection, affected images, and
for tracked CVEs the Jira ticket key. Components with known fix versions are
shown in brackets when available.

Closed Jira tickets are cross-referenced with the VEX feed to distinguish
between different root causes:

- **Done-Errata, VEX still affected** -- fix shipped in RPM but not yet
  propagated to container images; needs rebuild or lockfile refresh.
- **Done-Errata, VEX resolved** -- fix fully propagated, Grype DB just
  needs its daily update to stop flagging.
- **Won't Do / Not a Bug, VEX resolved** -- VEX already marks CVE as
  not affected; Grype DB delay, will self-resolve.
- **Won't Do / Not a Bug, VEX still affected** -- genuine known issue
  that will persist in scans (accepted risk, upstream won't fix, etc.).

### catalog: options

| Option       | Description                                                                              |
|--------------|------------------------------------------------------------------------------------------|
| `--api-url`  | Catalog API base URL (default: `https://api-hummingbird.hummingbird-project.io/v1`)      |
| `--jira-url` | Jira base URL (default: `https://redhat.atlassian.net`)                                  |
| `--workers`  | Parallel VEX fetch workers (default: 20)                                                 |
| `--json, -j` | Output results as JSON                                                                   |

### catalog: example

```bash
$ ./check_vex.py catalog

Catalog: 72 CVEs across 29 images (scanned 2026-04-28T10:45:38Z)
Jira: 748 tickets (437 unique CVEs)

11 untracked CVE(s) (no Jira ticket):

  CVE-2008-2662  High     8d  ruby (12 tags)  [ruby3.3@3.3.10, +36 more]
  CVE-2026-2950  Medium   8d  aspnet-runtime, ... (30 tags)  [lodash@4.17.21]
  ...

45 tracked CVE(s) open in Jira:

  CVE-2025-68114  High  8d  php (3 tags)  [capstone@5.0.6]  HUM-925
  ...

8 closed in Jira (Done-Errata) but VEX still affected -- verify fix propagated:

  CVE-2026-27143  Critical  8d  caddy, go-fdo-client, ... (10 tags)  HUM-969

4 closed in Jira (Done-Errata), VEX resolved -- Grype DB delay:

  CVE-2025-61732  High  8d  go, xcaddy (6 tags)  HUM-1119

3 closed in Jira (Won't Do / Not a Bug), VEX resolved -- Grype DB delay:

  CVE-2026-27140  High  8d  caddy, go, ... (10 tags)  HUM-967

1 closed in Jira (Won't Do / Not a Bug), VEX still affected -- genuine:

  CVE-2025-12781  Medium  8d  postgresql, python, ... (17 tags)  HUM-1387
```

### catalog: exit codes

| Code | Meaning                                                    |
|------|------------------------------------------------------------|
| `0`  | All catalog CVEs are tracked (have a Jira ticket)          |
| `1`  | One or more untracked CVEs found                           |
| `2`  | Missing `JIRA_EMAIL` or `JIRA_TOKEN`                       |

## Development

```bash
# Run tests
cd vex-checker && python3 -m unittest discover tests -v

# Run linter (from repo root)
ruff check vex-checker
```

## License

This project is licensed under the GNU General Public License v3.0 or later -
see the [LICENSE][license] file for details.

[Red Hat CSAF VEX feed]: https://security.access.redhat.com/data/csaf/v2/vex-feed/
[license]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/LICENSE
