Skip to content
raspberrypiPublic

About

Render grype JSON scans into single-file, interactive HTML CVE reports

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

cve-list

Auto-scanned CVE reports for every recent Raspberry Pi OS image release, plus the tooling that produces them.

Live index: https://raspberrypi.github.io/cve-list/

A scheduled GitHub Actions workflow walks the SBOMs published on downloads.raspberrypi.com, runs each through grype, and republishes the results as interactive HTML reports.

Repository layout

Path Purpose
scan-image.py Scan your own image and produce a report — the whole pipeline on an SBOM or rootfs you supply, no downloads. Start here.
scan-images.py Discover Pi OS SBOMs on downloads.raspberrypi.com, scan each, generate the dashboard. (Powers the hosted site.)
filter-sbom.py Drop packages from an SBOM (by name glob) before scanning — e.g. a kernel that's in dpkg but isn't booted.
inject-kernel.py Force a Linux kernel version into an SBOM so grype assesses it — for a custom / BSP kernel that isn't a dpkg package and is therefore invisible to syft.
rpt-crosscheck.py Recover CVEs grype hides on Pi +rpt packages via an artificial epoch.
kernel-osv-crosscheck.py Replace Debian's branch-based verdicts on the Pi kernel with Raspberry Pi's per-kernel OSV feed: each CVE assessed for each released kernel package by commit ancestry and the files compiled into it.
kernel-nvd-crosscheck.py Add kernel CVEs from NVD's per-branch ranges for CVEs the kernel CNA hasn't assessed (older ones from other CNAs).
apt_sources.py Which archives each image type installs from (Debian + Pi archive for arm64, Raspbian + Pi archive for armhf); shared by simulate-upgrade.py and debian-ahead.py.
debian-ahead.py Per-CVE fix availability: installable via apt (Debian / Raspbian, each with the Pi archive) or "Debian, not Raspbian" (armhf rebuild backlog). Epoch-safe for +rpt rebuilds; branch-aware for kernel/NVD fixes.
simulate-upgrade.py Build a synthetic post-apt full-upgrade SBOM against the archives the image really uses (apt_sources.py).
make-report.py Render a grype scan JSON into the interactive HTML report.
history/ One <variant>.jsonl per image: a record per weekly scan, both states. Appended and committed by CI; see history/README.md.
backfill-history.py One-off: recover past weeks' totals from old CI run logs into history/ (needs gh logged in).
.github/workflows/scan-and-publish.yml CI workflow: scans, commits history/, deploys to GitHub Pages weekly.

Scan your own image

scan-image.py runs the full Raspberry Pi-aware analysis on an image you already have — the same steps as the hosted dashboard, without any of the downloading/discovery. Each Pi-specific step self-skips when it doesn't apply, so it's also fine on a plain Debian image.

# From a mounted image rootfs (generates the SBOM with syft):
./scan-image.py --dir /mnt/myimage-root --out report.html
#   -> report.html (as scanned) and report-upgraded.html (after a simulated apt full-upgrade)

# From an SBOM you already produced (syft SPDX JSON, optionally .xz/.gz):
./scan-image.py --sbom myimage.spdx.json --out report.html

# The running system:
sudo ./scan-image.py --dir / --out report.html

Anything you can mount as a directory works. --dir just needs a rootfs tree.

To scan a .img file, loop-mount it first, then point --dir at the rootfs:

LOOP=$(sudo losetup -fP --show myimage.img)
sudo mount -o ro "${LOOP}p2" /mnt/root        # p2 = rootfs on Pi OS images
./scan-image.py --dir /mnt/root --out report.html
sudo umount /mnt/root && sudo losetup -d "$LOOP"

To scan a squashfs rootfs (e.g. an rpi-image-gen build, a live/installer filesystem.squashfs, or an appliance image), mount it read-only and point --dir at the mount:

sudo mkdir -p /mnt/root
sudo mount -t squashfs -o loop,ro myrootfs.squashfs /mnt/root
./scan-image.py --dir /mnt/root --out report.html
sudo umount /mnt/root

If the squashfs lives on a partition inside a larger .img, combine the two: losetup -fP the image, then mount -t squashfs -o loop,ro "${LOOP}pN" /mnt/root. No root or squashfs kernel module? Extract instead and scan the tree — the result is identical:

unsquashfs -d ./rootfs myrootfs.squashfs      # writes ./rootfs
./scan-image.py --dir ./rootfs --out report.html

The package database (/var/lib/dpkg/status) and the binaries syft inspects all live in the rootfs, so a mounted or extracted squashfs gives exactly the same SBOM — and therefore the same report — as scanning the running system.

Prerequisites: python3, grype (+ its DB, grype db update), dpkg; plus syft if you use --dir. Step 3 downloads the kernel OSV feed (cached for 6 hours) and steps 5–6 (fix availability and the upgrade simulation) fetch apt Package indexes over the network — pass --offline to use the cached feed and skip 5–6. Distro (trixie/bookworm) and arch (arm64/armhf) are auto-detected from the SBOM; override with --distro/--arch if needed.

The helper scripts scan-image.py orchestrates (filter-sbom.py, rpt-crosscheck.py, kernel-osv-crosscheck.py, kernel-nvd-crosscheck.py, debian-ahead.py, simulate-upgrade.py, make-report.py) are the reusable, image-agnostic core — scan-images.py uses the same ones.

Excluding packages that shouldn't be assessed

Sometimes a package is registered in dpkg but isn't really part of the running system — e.g. an appliance can carry a kernel (linux-image-6.6.31+rpt-*) that isn't the one it boots, so every CVE for it is noise. Filter such packages out of the SBOM before it reaches grype, so the references never exist in the scan, the cross-checks, or the report:

# drop a non-booted kernel by version (glob on package name; repeatable)
./scan-image.py --sbom myimage.spdx.json --out report.html \
    --exclude '*6.6.31*'

# or keep the patterns in a file (one glob per line, '#' comments allowed)
./scan-image.py --dir /mnt/root --out report.html --exclude-file exclude.txt

Patterns are fnmatch globs against the package name (*6.6.31*, linux-*-6.6.31+rpt*, exact names, …). filter-sbom.py can also be run on its own to produce a filtered SBOM. This is the right place to suppress false-positive packages: unlike hiding rows in the report, a filtered package contributes nothing to any count, cross-check, or the upgrade simulation.

Forcing in a kernel grype can't see

A custom, Buildroot, or vendor-BSP kernel is not a dpkg package, so syft never records it and grype cannot assess it at all. The only kernels in the SBOM are whatever linux-image-* packages sit in the dpkg database — and on an appliance those are frequently not the kernel that actually boots. A common pattern: dpkg still lists the stock Pi kernels (e.g. 6.12.93+rpt, 6.6.31+rpt) pulled in with the base image, but the device actually boots a custom kernel with a vendor suffix (e.g. 6.12.94-myvendor) that has no dpkg record and no linux-image package — only its /lib/modules/6.12.94-myvendor tree gives it away.

--kernel-version injects a Debian-shaped linux-image-<version> package at the version you name, so the pipeline assesses the real kernel: grype's Debian linux advisory matcher fires (branch-tracked CVEs) and kernel-nvd-crosscheck adds NVD's per-branch ranges (the Raspberry Pi OSV feed covers released Pi kernel packages only, so it leaves an injected kernel alone). Pair it with --exclude of the non-booted dpkg kernels so they don't double-count:

./scan-image.py --sbom myimage.sbom.json --out report.html \
    --exclude '*6.6.31*' --exclude '*6.12.93*' \
    --kernel-version 6.12.94-myvendor

The version string is used verbatim (so the report is truthful) and its upstream X.Y.Z is what the CVE ranges compare against. This is an upstream-baseline assessment: a custom kernel may carry backports the baseline lacks, or omit some, so treat the result as the closest automatable picture rather than proof — the branch hint (below) tells you which findings are fixable within your branch. inject-kernel.py can also be run standalone to produce the augmented SBOM.

Tracing why a CVE is present

Expand a CVE row to see its affected packages. Two details help explain the hit:

  • Found in — for non-deb packages (go-modules, embedded binaries, …), the file the package was detected in, so you can see which binary pulls in a vulnerable module (e.g. a stdlib CVE traced to usr/bin/ncwstunnel). This comes from the SBOM, so pass it: make-report.py … --sbom <sbom> (scan-image.py/scan-images.py do this automatically).
  • Branch hint — for kernel-style X.Y.Z packages, the fix cell notes whether a fix exists on the installed version's own branch (“→ fixed in your 6.12 branch at 6.12.95”, an in-place patch upgrade) or only on newer branches (“⚠ no 6.12 fix — only newer branches”). NVD often lists per-branch fix points, e.g. 6.1.177, 6.6.144, 6.12.95, 6.18.38 — the hint picks out the one that applies to your kernel.

The HTML reports

Turn a grype JSON scan into a single self-contained, interactive HTML report you can open in any browser.

A report describes one state of a system: one row per distinct CVE found in the SBOM it was given, risk-sorted. Filter by severity chip, KEV, fix status, package type, or free text; click a row for the description, every affected package with its installed version, the fix version, and (for embedded packages) the file it was found in. To compare states, for example an image as released against the same image after apt full-upgrade, render one report per state and link them from an overview page, which is what scan-images.py does. There is deliberately no switching between states inside a report.

The page is built to stay responsive on large scans (10k+ CVEs) and on slow machines: only the rows matching the current filters are in the DOM, 500 at a time with “show more” (appended a couple of hundred rows per frame), and a CVE's detail block is created when you expand it and thrown away when you collapse it.

No server, no JS dependencies, no internet at view time — everything is inlined into one HTML file.

Examples

The examples/ directory has two pre-built reports, also served live via GitHub Pages:

Usage

1. Produce a grype JSON

# Scan a directory (e.g. a mounted Pi OS image)
sudo grype dir:/mnt/raspios-root \
  --exclude './proc/**' --exclude './sys/**' \
  --exclude './dev/**'  --exclude './run/**' \
  -o json --file scan.json

# …or a container image
grype <image-ref> -o json --file scan.json

# …or the live host filesystem
sudo grype dir:/ \
  --exclude './proc/**' --exclude './sys/**' \
  --exclude './dev/**'  --exclude './run/**' \
  --exclude './mnt/**' --exclude './media/**' \
  -o json --file scan.json

2. Render the report

python3 make-report.py scan.json report.html --title "My scan"

Optional flags:

  • --title "..." — page title (defaults to Grype report: <basename>).
  • --source-label "..." — text shown under the title (defaults to the input path; override when sharing publicly to avoid leaking local paths).
  • --sbom <sbom> — the SBOM the scan came from; adds the “Found in” file paths for embedded (non-deb) packages.
  • --debian-ahead <json> — output of debian-ahead.py; adds the “Fix available from” column.
  • --link LABEL=URL — navigation link shown under the title (repeatable), e.g. --link Overview=../index.html.

Scanning a Raspberry Pi .img directly

Grype doesn't read disk-image partition tables natively, but you can loop-mount the rootfs:

xz -dk raspios.img.xz                                  # if compressed
LOOP=$(sudo losetup -fP --show raspios.img)
sudo mkdir -p /mnt/raspios-root
sudo mount -o ro "${LOOP}p2" /mnt/raspios-root
sudo grype dir:/mnt/raspios-root \
  --exclude './proc/**' --exclude './sys/**' \
  --exclude './dev/**'  --exclude './run/**' \
  -o json --file scan.json
sudo umount /mnt/raspios-root
sudo losetup -d "$LOOP"

Automated Pi OS image scanning

scan-images.py is the entry point used by CI. It picks the most recent release of each of the four full Raspberry Pi OS images — full is a superset of the lite and desktop variants, so scanning it covers them — across two arches and two channels:

Variant Arch Channel
raspios_full_arm64 64-bit current (trixie)
raspios_full_armhf 32-bit current (trixie)
raspios_oldstable_full_arm64 64-bit oldstable (bookworm)
raspios_oldstable_full_armhf 32-bit oldstable (bookworm)

For each, it runs the full grype + cross-check pipeline on the image as released and on a simulated apt full-upgrade, and renders one report per state: <date>.html for the image as released and <date>-upgraded.html for the post-upgrade state. The index page shows the same summary columns for both states side by side, each linking to its report.

# Default — the four full images, into ./site
python3 scan-images.py

# Test on a single variant
python3 scan-images.py --variant raspios_full_arm64

# Just print what would be scanned, no work
python3 scan-images.py --dry-run

# Useful flags
#   --variant X          repeatable; override the default four-variant set
#   --no-simulate        skip the apt-upgrade simulation (baseline report only)
#   --force              ignore cache, re-download / re-scan everything
#   --out DIR            output directory (default: site/)
#   --cache DIR          cache directory (default: cache/)
#   --history DIR        append this run's numbers to DIR/<variant>.jsonl (CI passes `history`;
#                        leave it off for local experiments so the committed history stays clean)

The generated site/ directory is the Pages root:

site/
├── index.html                       # 4-row overview: image as released | after apt full-upgrade, each with its report
├── summary.json                     # per-image rollups for this run (machine-readable)
├── history/                         # copy of the committed history/<variant>.jsonl files
├── raspios_full_arm64/
│   ├── 2026-06-19.html              # report: image as released
│   ├── 2026-06-19.json              # raw grype output (as released)
│   ├── 2026-06-19-upgraded.html     # report: after simulated apt full-upgrade
│   └── 2026-06-19-upgraded.json     # raw grype output (simulated upgrade)
└── ...

The CI workflow runs weekly (Monday 06:00 UTC) and on manual dispatch, caches SBOM downloads (each release's SBOM is immutable), commits the week's numbers to history/, and deploys directly to GitHub Pages. The Pages deployment is replaced each week; history/ in the repo is the durable record and the input for trend graphs.

Requirements

  • Python 3 (standard library only — no third-party packages).
  • grype to produce the input JSON.

About

Render grype JSON scans into single-file, interactive HTML CVE reports

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages