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.
| 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-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.htmlAnything 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/rootIf 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.htmlThe 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.
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.txtPatterns 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.
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-myvendorThe 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.
Expand a CVE row to see its affected packages. Two details help explain the hit:
- Found in — for non-
debpackages (go-modules, embedded binaries, …), the file the package was detected in, so you can see which binary pulls in a vulnerable module (e.g. astdlibCVE traced tousr/bin/ncwstunnel). This comes from the SBOM, so pass it:make-report.py … --sbom <sbom>(scan-image.py/scan-images.pydo this automatically). - Branch hint — for kernel-style
X.Y.Zpackages, 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.
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.
The examples/ directory has two pre-built reports, also served live via GitHub Pages:
- Raspberry Pi OS desktop image (
2026-04-21-raspios-trixie-arm64, 3826 findings): https://raspberrypi.github.io/cve-list/examples/raspios-2026-04-21-desktop-arm64.html - Live system scan (running Pi with a dev workspace, 14 666 findings): https://raspberrypi.github.io/cve-list/examples/live-system-example.html
# 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.jsonpython3 make-report.py scan.json report.html --title "My scan"Optional flags:
--title "..."— page title (defaults toGrype 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 ofdebian-ahead.py; adds the “Fix available from” column.--link LABEL=URL— navigation link shown under the title (repeatable), e.g.--link Overview=../index.html.
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"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.
- Python 3 (standard library only — no third-party packages).
grypeto produce the input JSON.