diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fab8c77..8cb64fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,9 +73,12 @@ package across our portfolio. - **Lane 2: OCI image PURL with package subcomponent.** Use only when the assertion varies per image. Subject is the OCI image PURL with the `repository_url` qualifier; the affected package is named in - `subcomponents`. Because OCI PURLs are registry-coupled, list one - product entry per distribution registry — typically both - `quay.io/stackstate/` and the Rancher-registry copy. + `subcomponents`. Because OCI PURLs are registry-coupled, author one + VEX file per distribution registry — typically both + `quay.io/stackstate/` and the Rancher Prime + `registry.rancher.com/suse-observability/` copy. This mirrors + Rancher's generated `rancher/vexhub` layout and lets Trivy's + `--vex repo` lookup resolve the exact image repository. ### Steps @@ -85,10 +88,12 @@ package across our portfolio. [tools/README.md](./tools/README.md) for command examples. - Lane 1 path: `pkg/maven/org.eclipse.jetty/jetty-http/scan.openvex.json`. - - Lane 2 path: + - Lane 2 paths: `pkg/oci/quay.io/stackstate/zookeeper/scan.openvex.json` - (and a sibling under the Rancher-registry path, or a single file - listing both in `products`). + and + `pkg/oci/registry.rancher.com/suse-observability/zookeeper/scan.openvex.json`. + Each file should contain the matching single OCI product, for + example `pkg:oci/zookeeper?repository_url=quay.io/stackstate/zookeeper`. 2. Run `python3 tools/build_index.py` to regenerate `index.json`. CI asserts the on-disk index matches the `pkg/` tree (`tools/build_index.py --check`). diff --git a/README.md b/README.md index 240ed92..73da97c 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,13 @@ findings that genuinely don't apply to our deployment. ## Scope VEX statements in this hub apply to SUSE Observability product -artefacts distributed under: +artefacts distributed as OCI images from: -- `pkg:oci/quay.io/stackstate/*` -- `pkg:oci//*` (Rancher Prime distribution) +- `quay.io/stackstate/*` +- `registry.rancher.com/suse-observability/*` (Rancher Prime distribution) + +OCI products are identified with the same PURL shape Rancher's hub +uses: `pkg:oci/?repository_url=//`. The hub is **complementary to** the SUSE-wide automated VEX pipeline operated by Rancher's @@ -48,6 +51,22 @@ image is scoped to how *we* configure the chart — it makes no claim about how SUSE Application Collection's source image behaves in another consumer's environment. +## Relationship to Rancher's VEX repos + +Rancher keeps authoring and publication separate: + +- `rancher/image-scanning` is the workflow repo where reviewed VEX + input is validated against scanner data and generated into OpenVEX. +- `rancher/vexhub` is the published static hub consumed by Trivy and + other VEX repository clients. + +This repo currently carries SUSE Observability-specific authoring and +publication together, but the published files intentionally mirror +`rancher/vexhub`: one `pkg/.../scan.openvex.json` file per indexed +product and an `index.json` with `version: 1` plus package IDs and +locations. If a statement can be handled by Rancher's `image-scanning` +workflow, prefer that route and let their automation publish it. + ## Layout ``` @@ -57,10 +76,10 @@ vexhub/ CODEOWNERS CONTRIBUTING.md vex-repository.json Aqua VEX Repository v0.1 descriptor - index.json PURL -> file mapping (generated) + index.json VEX repository index (generated) pkg/ OpenVEX statements, organised by PURL maven/ pkg:maven/... - oci/ pkg:oci/... (image-scoped, Lane 2) + oci/ pkg:oci/... (image-scoped, one file per OCI product) apk/, rpm/, npm/, ... one directory per PURL type as needed reports/ CSV exports for human review (future) docs/ @@ -68,8 +87,10 @@ vexhub/ tools/ build_index.py + vexctl usage docs ``` -Layout matches the Aqua VEX Hub convention so consumers familiar with -`aquasecurity/vexhub` find files where they expect. +Layout matches the Aqua/Rancher VEX Hub convention so consumers familiar +with `aquasecurity/vexhub` and `rancher/vexhub` find files where they +expect. `index.json` intentionally mirrors Rancher's generated index: +`version: 1` plus a `packages` list of PURL IDs and file locations. ## Consuming this hub with Trivy @@ -87,7 +108,7 @@ Then run scans with the repo enabled: ```bash trivy vex repo download -trivy image --vex repo --show-suppressed pkg:oci/quay.io/stackstate/zookeeper: +trivy image --vex repo --show-suppressed quay.io/stackstate/kafka: ``` Suppressed findings are annotated with the matching VEX statement and the diff --git a/index.json b/index.json index 9730ea8..faf54b1 100644 --- a/index.json +++ b/index.json @@ -1,4 +1,13 @@ { - "updated_at": "2026-05-01T07:25:29Z", - "packages": [] + "version": 1, + "packages": [ + { + "id": "pkg:oci/kafka?repository_url=quay.io%2Fstackstate%2Fkafka", + "location": "pkg/oci/quay.io/stackstate/kafka/scan.openvex.json" + }, + { + "id": "pkg:oci/kafka?repository_url=registry.rancher.com%2Fsuse-observability%2Fkafka", + "location": "pkg/oci/registry.rancher.com/suse-observability/kafka/scan.openvex.json" + } + ] } diff --git a/pkg/oci/quay.io/stackstate/kafka/scan.openvex.json b/pkg/oci/quay.io/stackstate/kafka/scan.openvex.json new file mode 100644 index 0000000..38919de --- /dev/null +++ b/pkg/oci/quay.io/stackstate/kafka/scan.openvex.json @@ -0,0 +1,143 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://github.com/StackVista/vexhub/pkg/oci/quay.io/stackstate/kafka/scan.openvex.json", + "author": "SUSE Observability Security Engineering", + "role": "Document Creator", + "version": 6, + "statements": [ + { + "vulnerability": { + "name": "CVE-2026-2332", + "aliases": [ + "GHSA-355h-qmc2-wpwf" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=quay.io/stackstate/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.eclipse.jetty/jetty-http@9.4.57.v20241219" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "SUSE Observability deploys this image as a Kafka broker StatefulSet. The Kafka chart runs /scripts/setup.sh, and that script execs kafka-server-start.sh with server.properties. The chart does not start Kafka Connect, does not expose a Jetty REST listener from this image, and JMX metrics are served by a separate jmx-exporter sidecar image. This statement does not apply to ad hoc use of this image for Kafka Connect or a custom command that starts Jetty-backed services.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2025-67030", + "aliases": [ + "GHSA-6fmv-xxpf-w3cw" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=quay.io/stackstate/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.codehaus.plexus/plexus-utils@3.5.1" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "The reported plexus-utils issue is in archive extraction. SUSE Observability starts the image as a Kafka broker using kafka-server-start.sh and does not run Kafka Connect, plugin archive installation, Maven tooling, or any product workflow that extracts attacker-provided archives through plexus-utils inside the broker container. This statement does not apply to custom commands, manual operator sessions, or Kafka Connect deployments that use this image to unpack untrusted plugin archives.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-24281", + "aliases": [ + "GHSA-7xrh-hqfc-g7qr" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=quay.io/stackstate/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.apache.zookeeper/zookeeper@3.8.4" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "This ZooKeeper finding is in TLS hostname verification through ZKTrustManager. The SUSE Observability Kafka chart defaults ZooKeeper client TLS to disabled and connects the brokers to the chart-managed ZooKeeper service through KAFKA_ZOOKEEPER_CONNECT without ZooKeeper TLS client material. The ZKTrustManager hostname-verification path is therefore not executed in the supported default broker deployment. This statement does not apply if auth.zookeeper.tls.enabled is enabled or if the image is run with custom ZooKeeper TLS client configuration.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-24308", + "aliases": [ + "GHSA-crhr-qqj8-rpxc" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=quay.io/stackstate/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.apache.zookeeper/zookeeper@3.8.4" + } + ] + } + ], + "status": "not_affected", + "justification": "inline_mitigations_already_exist", + "impact_statement": "This ZooKeeper finding concerns sensitive ZooKeeper client configuration values being logged. In the supported SUSE Observability Kafka broker deployment, ZooKeeper client TLS is disabled by default and auth.sasl.jaas.zookeeperUser/zookeeperPassword are empty, so the chart does not inject ZooKeeper client credentials or TLS secret paths into the broker's ZooKeeper client configuration. This statement does not apply to deployments that enable ZooKeeper SASL or ZooKeeper client TLS for Kafka brokers without re-validating the rendered configuration and log output.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-42577", + "aliases": [ + "GHSA-rwm7-x88c-3g2p" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=quay.io/stackstate/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/io.netty/netty-transport-native-epoll@4.1.125.Final" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "SUSE Observability starts this image as a Kafka broker. Kafka's broker listeners are configured through the chart's Kafka TCP listener settings and the chart does not configure ZooKeeper's Netty client socket implementation or Netty epoll transport for broker-to-ZooKeeper communication. The vulnerable Netty native epoll transport is present in the image but is not part of the broker execute path. This statement does not apply to custom commands or configuration that explicitly uses Netty epoll from this image.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-42583", + "aliases": [ + "GHSA-mj4r-2hfc-f8p6" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=quay.io/stackstate/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/io.netty/netty-codec@4.1.125.Final" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "SUSE Observability starts this image as a Kafka broker and does not run Kafka Connect, Jetty REST endpoints, Netty-based services, or ZooKeeper's Netty client socket implementation from this container. The vulnerable Netty codec package is present in the image but is not used by the supported broker runtime path. This statement does not apply to custom commands or configuration that explicitly uses Netty codecs from this image.", + "timestamp": "2026-05-08T05:03:13Z" + } + ], + "timestamp": "2026-05-08T05:03:13Z", + "last_updated": "2026-05-08T05:03:13Z" +} diff --git a/pkg/oci/registry.rancher.com/suse-observability/kafka/scan.openvex.json b/pkg/oci/registry.rancher.com/suse-observability/kafka/scan.openvex.json new file mode 100644 index 0000000..da0b9fa --- /dev/null +++ b/pkg/oci/registry.rancher.com/suse-observability/kafka/scan.openvex.json @@ -0,0 +1,143 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://github.com/StackVista/vexhub/pkg/oci/registry.rancher.com/suse-observability/kafka/scan.openvex.json", + "author": "SUSE Observability Security Engineering", + "role": "Document Creator", + "version": 6, + "statements": [ + { + "vulnerability": { + "name": "CVE-2026-2332", + "aliases": [ + "GHSA-355h-qmc2-wpwf" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.eclipse.jetty/jetty-http@9.4.57.v20241219" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "SUSE Observability deploys this image as a Kafka broker StatefulSet. The Kafka chart runs /scripts/setup.sh, and that script execs kafka-server-start.sh with server.properties. The chart does not start Kafka Connect, does not expose a Jetty REST listener from this image, and JMX metrics are served by a separate jmx-exporter sidecar image. This statement does not apply to ad hoc use of this image for Kafka Connect or a custom command that starts Jetty-backed services.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2025-67030", + "aliases": [ + "GHSA-6fmv-xxpf-w3cw" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.codehaus.plexus/plexus-utils@3.5.1" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "The reported plexus-utils issue is in archive extraction. SUSE Observability starts the image as a Kafka broker using kafka-server-start.sh and does not run Kafka Connect, plugin archive installation, Maven tooling, or any product workflow that extracts attacker-provided archives through plexus-utils inside the broker container. This statement does not apply to custom commands, manual operator sessions, or Kafka Connect deployments that use this image to unpack untrusted plugin archives.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-24281", + "aliases": [ + "GHSA-7xrh-hqfc-g7qr" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.apache.zookeeper/zookeeper@3.8.4" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "This ZooKeeper finding is in TLS hostname verification through ZKTrustManager. The SUSE Observability Kafka chart defaults ZooKeeper client TLS to disabled and connects the brokers to the chart-managed ZooKeeper service through KAFKA_ZOOKEEPER_CONNECT without ZooKeeper TLS client material. The ZKTrustManager hostname-verification path is therefore not executed in the supported default broker deployment. This statement does not apply if auth.zookeeper.tls.enabled is enabled or if the image is run with custom ZooKeeper TLS client configuration.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-24308", + "aliases": [ + "GHSA-crhr-qqj8-rpxc" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/org.apache.zookeeper/zookeeper@3.8.4" + } + ] + } + ], + "status": "not_affected", + "justification": "inline_mitigations_already_exist", + "impact_statement": "This ZooKeeper finding concerns sensitive ZooKeeper client configuration values being logged. In the supported SUSE Observability Kafka broker deployment, ZooKeeper client TLS is disabled by default and auth.sasl.jaas.zookeeperUser/zookeeperPassword are empty, so the chart does not inject ZooKeeper client credentials or TLS secret paths into the broker's ZooKeeper client configuration. This statement does not apply to deployments that enable ZooKeeper SASL or ZooKeeper client TLS for Kafka brokers without re-validating the rendered configuration and log output.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-42577", + "aliases": [ + "GHSA-rwm7-x88c-3g2p" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/io.netty/netty-transport-native-epoll@4.1.125.Final" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "SUSE Observability starts this image as a Kafka broker. Kafka's broker listeners are configured through the chart's Kafka TCP listener settings and the chart does not configure ZooKeeper's Netty client socket implementation or Netty epoll transport for broker-to-ZooKeeper communication. The vulnerable Netty native epoll transport is present in the image but is not part of the broker execute path. This statement does not apply to custom commands or configuration that explicitly uses Netty epoll from this image.", + "timestamp": "2026-05-08T05:03:13Z" + }, + { + "vulnerability": { + "name": "CVE-2026-42583", + "aliases": [ + "GHSA-mj4r-2hfc-f8p6" + ] + }, + "products": [ + { + "@id": "pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka", + "subcomponents": [ + { + "@id": "pkg:maven/io.netty/netty-codec@4.1.125.Final" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "SUSE Observability starts this image as a Kafka broker and does not run Kafka Connect, Jetty REST endpoints, Netty-based services, or ZooKeeper's Netty client socket implementation from this container. The vulnerable Netty codec package is present in the image but is not used by the supported broker runtime path. This statement does not apply to custom commands or configuration that explicitly uses Netty codecs from this image.", + "timestamp": "2026-05-08T05:03:13Z" + } + ], + "timestamp": "2026-05-08T05:03:13Z", + "last_updated": "2026-05-08T05:03:13Z" +} diff --git a/reports/kafka-appco-vex-evidence-2026-05-07.md b/reports/kafka-appco-vex-evidence-2026-05-07.md new file mode 100644 index 0000000..8a98304 --- /dev/null +++ b/reports/kafka-appco-vex-evidence-2026-05-07.md @@ -0,0 +1,86 @@ +# Kafka AppCo VEX Evidence - 2026-05-07 + +## Scope + +This draft covers the HIGH Kafka findings seen in cve-reporter for `quay.io/stackstate/kafka:3.9.2-f6a6e1a0-main-35` and in the SUSE Application Collection `apache-kafka:3.9.2-13.5` attestation. + +It intentionally scopes statements to the SUSE Observability Kafka broker image repositories: + +- `pkg:oci/kafka?repository_url=quay.io/stackstate/kafka` +- `pkg:oci/kafka?repository_url=registry.rancher.com/suse-observability/kafka` + +It does not VEX the upstream AppCo source image directly. The docker-images retag workflow scans that source image before copying, and temporary upstream-owned gate exceptions stay in `images/kafka/.trivyignore.yaml`. + +## Inputs Reviewed + +- `vexhub` base: `c949ff3f5339d11ef9ef9622038b7346eec7936f` +- `docker-images` `origin/main`: `e87c8d2fbad2abb7533778612282c5ff9e94928d` +- `helm-charts` `origin/master`: `77c420897d4eaee15dae80f4c8c8ace99cd63e96` +- Local cve-reporter query at `2026-05-07T14:45:56Z` +- User-provided AppCo attestation for `apache-kafka:3.9.2-13.5` + +## Scanner Rows + +cve-reporter currently reports ten HIGH rows for the Kafka image. Four are Trivy CVE rows and six are Grype GHSA rows: + +| Scanner IDs | Package | Version | Runtime interpretation | +|---|---|---:|---| +| `CVE-2026-2332`, `GHSA-355h-qmc2-wpwf` | `org.eclipse.jetty:jetty-http` | `9.4.57.v20241219` | Jetty is present for Kafka distribution tooling, but SUSE Observability starts only the Kafka broker, not Kafka Connect or a Jetty REST endpoint. | +| `CVE-2025-67030`, `GHSA-6fmv-xxpf-w3cw` | `org.codehaus.plexus:plexus-utils` | `3.5.1` | The reported archive-extraction path is not used by the broker startup path. | +| `CVE-2026-24281`, `GHSA-7xrh-hqfc-g7qr` | `org.apache.zookeeper:zookeeper` | `3.8.4` | The reported ZKTrustManager hostname-verification path requires ZooKeeper client TLS, which the chart disables by default. | +| `CVE-2026-24308`, `GHSA-crhr-qqj8-rpxc` | `org.apache.zookeeper:zookeeper` | `3.8.4` | The chart does not inject ZooKeeper SASL credentials or ZooKeeper client TLS secret paths by default. | +| `CVE-2026-42577`, `GHSA-rwm7-x88c-3g2p` | `io.netty:netty-transport-native-epoll` | `4.1.125.Final` | The broker deployment does not configure ZooKeeper's Netty client socket implementation or Netty epoll. | +| `CVE-2026-42583`, `GHSA-mj4r-2hfc-f8p6` | `io.netty:netty-codec` | `4.1.125.Final` | The broker deployment does not run Netty-based services from this image. | + +The AppCo attestation lists the same six underlying HIGH vulnerabilities as CVE IDs. cve-reporter shows ten HIGH rows because Trivy and Grype report several of the same advisories under different IDs. + +## Deployment Evidence + +The Kafka chart deploys the image as a broker StatefulSet. In `stable/kafka/templates/statefulset.yaml`, the `kafka` container runs the chart image and defaults to the chart command from `stable/kafka/values.yaml`. + +The command is `/scripts/setup.sh`. In `stable/kafka/templates/scripts-configmap.yaml`, `setup.sh` finishes with: + +```bash +exec /usr/share/kafka/bin/kafka-server-start.sh /usr/share/kafka/config/server.properties +``` + +There is no chart path that starts `connect-distributed.sh`, `connect-standalone.sh`, a Jetty-backed Kafka Connect REST worker, or Maven/Plexus archive extraction as part of the supported broker deployment. + +The Kafka Service exposes Kafka TCP listener ports only. JMX metrics are provided by a separate `jmx-exporter` sidecar image and service, not by Jetty from the Kafka image. + +Default Kafka auth values are plaintext for client and inter-broker protocols. `auth.zookeeper.tls.enabled` defaults to `false`, and `auth.sasl.jaas.zookeeperUser` / `auth.sasl.jaas.zookeeperPassword` default to empty strings. ZooKeeper client TLS material is copied only when `auth.zookeeper.tls.enabled` and an existing ZooKeeper TLS secret are configured. + +## Review Caveats + +These statements are deployment-context VEX, not an upstream claim about the SUSE Application Collection `apache-kafka` image in every possible use. + +The statements should be re-reviewed if any supported SUSE Observability deployment: + +- enables ZooKeeper client TLS or ZooKeeper SASL for Kafka brokers, +- uses this image for Kafka Connect, +- overrides the container command or args, +- runs plugin/archive extraction inside the broker container, or +- explicitly configures ZooKeeper's Netty client socket or Netty epoll transport. + +## Local Validation + +The per-product OpenVEX files were regenerated with `vexctl` 0.4.1 +(`vexctl create` plus `vexctl add`) and the generated +Rancher-compatible index was checked with: + +```bash +vexctl version +python3 tools/build_index.py --check +git diff --check +trivy image --quiet --skip-db-update --scanners vuln --severity HIGH \ + --vex pkg/oci/quay.io/stackstate/kafka/scan.openvex.json \ + --show-suppressed \ + quay.io/stackstate/kafka:3.9.2-f6a6e1a0-main-35 +``` + +The index helper was also checked with explicit PURL normalization assertions +for Maven package IDs and OCI `repository_url` IDs. + +Trivy 0.70.0 reported `Total: 0 (HIGH: 0)` and showed six suppressed HIGH CVEs from the Quay OpenVEX file: `CVE-2026-2332`, `CVE-2025-67030`, `CVE-2026-24281`, `CVE-2026-24308`, `CVE-2026-42577`, and `CVE-2026-42583`. + +This validates the Trivy/OpenVEX shape. cve-reporter's Grype rows still need scanner-side alias/VEX handling if the goal is to make the combined Trivy+Grype total match the AppCo attestation count exactly. diff --git a/tools/README.md b/tools/README.md index eea842e..7f141f8 100644 --- a/tools/README.md +++ b/tools/README.md @@ -40,7 +40,16 @@ resulting JSON — small file, infrequent edits. For Lane 2 (image PURL with subcomponent), see [vexctl docs](https://github.com/openvex/vexctl) and the OpenVEX spec. The `--product` flag accepts an OCI PURL; subcomponents can be added -to the resulting JSON. +to the resulting JSON. Keep one generated file per OCI repository URL, +for example: + +``` +pkg/oci/quay.io/stackstate/kafka/scan.openvex.json +pkg/oci/registry.rancher.com/suse-observability/kafka/scan.openvex.json +``` + +The product IDs inside those files should use the matching OCI PURL, +for example `pkg:oci/kafka?repository_url=quay.io/stackstate/kafka`. If multiple statements apply to the same package, append them to the same file's `statements` array, or use `vexctl merge` to combine @@ -56,9 +65,11 @@ python3 tools/build_index.py ``` The script walks `pkg/`, extracts product PURLs from every statement, -normalises them (drops version and qualifiers), and rewrites -`index.json` from scratch — sorted, deduplicated, and matching the -[Aqua VEX Repository specification](https://github.com/aquasecurity/vex-repo-spec). +normalises them for VEX repository lookup, and rewrites `index.json` +from scratch — sorted, deduplicated, and matching the index shape used +by [Rancher's VEX Hub](https://github.com/rancher/vexhub). Package +versions are stripped from index IDs, while qualifiers such as OCI +`repository_url` are preserved and percent-encoded. ## CI check diff --git a/tools/build_index.py b/tools/build_index.py index 004974e..e9f7c8a 100644 --- a/tools/build_index.py +++ b/tools/build_index.py @@ -2,9 +2,9 @@ """Regenerate index.json from the contents of pkg/. Walks every *.openvex.json file under pkg/, extracts the product PURLs -from each statement, normalises them (drops version and qualifiers), -and writes a fresh index.json conforming to the Aqua VEX Repository -specification. +from each statement, normalises them for VEX repository lookup, and +writes a fresh index.json conforming to the Aqua/Rancher VEX repository +index shape. Idempotent: produces the same `packages` list for the same on-disk state. Authors should run this whenever they add, modify, or remove a @@ -16,22 +16,30 @@ import argparse import json import sys -from datetime import datetime, timezone from pathlib import Path - - -def now_iso() -> str: - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") +from urllib.parse import parse_qsl, quote def index_id_for_purl(purl: str) -> str: - """Return the canonical index id for a PURL: version and qualifiers stripped.""" - head = purl - if "?" in head: - head, _ = head.split("?", 1) - if "@" in head: - head, _ = head.split("@", 1) - return head + """Return the Rancher-compatible VEX repository index ID for a PURL. + + Package versions are not part of the repository index key, but qualifiers + can be identity-bearing. OCI image products rely on `repository_url`, so + keep qualifiers and percent-encode their values the same way Rancher's hub + does in index.json. + """ + base, _, query = purl.partition("?") + if "@" in base: + base, _ = base.rsplit("@", 1) + if not query: + return base + + qualifiers = [] + for key, value in parse_qsl(query, keep_blank_values=True): + qualifiers.append(f"{quote(key, safe='')}={quote(value, safe='')}") + if not qualifiers: + return base + return f"{base}?{'&'.join(qualifiers)}" def collect_packages(hub_root: Path) -> list[dict]: @@ -65,7 +73,6 @@ def collect_packages(hub_root: Path) -> list[dict]: entries[pid] = { "id": pid, "location": rel_location, - "format": "openvex", } return sorted(entries.values(), key=lambda e: e["id"]) @@ -87,7 +94,7 @@ def main() -> None: packages = collect_packages(args.hub_root) fresh = { - "updated_at": now_iso(), + "version": 1, "packages": packages, } index_path = args.hub_root / "index.json" @@ -97,8 +104,8 @@ def main() -> None: with index_path.open() as f: current = json.load(f) else: - current = {"packages": []} - if current.get("packages") != packages: + current = {"version": 1, "packages": []} + if current != fresh: print( "index.json is out of sync with pkg/ contents.\n" "Run: python3 tools/build_index.py",