portstatus shows what pkg audit does not tell you

As a member of the ports-secteam, it is my job, and that of the others on the team, to respond quickly to CVEs. Until now, unfortunately, I did not know of any site that showed me which CVEs are relevant to the FreeBSD ports and which ports have vulnerabilities at all. It was also hard to connect CVEs, VuXML and the ports. So I decided to build my own site that lets me respond quickly and effectively to new vulnerabilities in the ports. The result is portstatus.bsdproject.de.

One example of why the site is needed: print/py-pypdf is at version 6.9.2 in both latest and quarterly. I ask pkg audit whether this package is something to worry about:

# pkg audit -F py312-pypdf-6.9.2
vulnxml file up-to-date
0 problem(s) in 0 package(s) found.

Zero problems. The CVE records disagree: 43 vulnerabilities affect exactly this version, 13 of them with a CVSS score of 7 or higher, and the oldest have been known since April. Upstream fixed all 43 by version 6.21.2. Almost all of them come down to excessive memory or CPU use caused by crafted PDFs. That becomes critical when your service accepts PDFs from outside, such as Paperless, a web shop or a mail filter. pkg audit is not lying here. It only knows what someone has entered in VuXML, and for pypdf nobody has done that yet.

This is exactly the gap portstatus makes visible. The site matches every port it can map to a product against the CVE databases and shows whether there is a VuXML entry and whether upstream has a newer version. Across the whole tree, that currently adds up to 3,295 CVEs in 670 ports that pkg audit does not report. The figure covers all of the roughly 34,000 ports and the packages in latest. On your server it is a fraction of that, but not zero.

Important: portstatus is still my private project and not an official service of the FreeBSD Project or the ports-secteam.

portstatus home page: the latest commits to the ports tree with version, package state and open CVEs, on the right the list of new CVEs
The home page: every commit to main with the version it brings the port to, the built package, and what is still open.

Why VuXML alone is not enough

A VuXML entry says: package X up to version Y is affected, here are the CVEs, here is the source. pkg audit checks that against your installed packages, quickly and with a single download. For everything that is in there, this works very well.

The problem is the word “someone”. A VuXML entry is written by a committer, by the ports-secteam or, through a problem report, by the maintainer. For Firefox, Chromium, OpenSSL or nginx this often happens the same day. For a Perl library, a PHP web project or a Python module like pypdf, sometimes never. There are too many CVEs, and nobody reads them all.

The second catch lies in the ports tree itself. To map a CVE to a port automatically, you need a CPE, the “vendor and product” identifier that the CVE records use as well. Of 34,339 ports, 3,306 carry such an identifier. That is less than ten percent. For maintainers this is the easiest lever: USES=cpe in the Makefile (see the Porter’s Handbook), and the port becomes visible to every tool.

Nobody failed here. The process relies on people looking, and there was no place that showed where nobody had looked.

A tour of a port page

Every pair of CVE and port ends up in exactly one state. Red means unaddressed: the CVE record lists the built package version as affected, and no VuXML entry names the CVE. Yellow means “VuXML only”: pkg audit warns you, only the update is still missing. Anything that cannot be compared cleanly is listed separately and not counted. Better a number that is slightly too low than one that nobody believes.

Decision path: is the package version in the affected range, does a VuXML entry name the CVE, are the versions comparable; the resulting states are not affected, VuXML only or fixed, unaddressed and not comparable
Three questions decide whether a CVE counts as red, yellow or not at all.

If you type print/py-pypdf into the search, you get this:

Port page of print/py-pypdf: 43 unaddressed CVEs, a note that the update to 6.21.2 fixes all 43, below it the list of CVEs
The tiles at the top, below them the answer to the real question: which update helps.

At the top are four tiles: unaddressed, VuXML only, fixed and open problem reports. They refer to the package in latest. Below them is the sentence I consider the most important on the page: “Update to 6.21.2 fixes all 43 unaddressed CVEs.” The line below shows how many CVEs each intermediate version already fixes. That helps when jumping to the newest version is not an option right now. If an old branch no longer gets fixes, the page names the successor, for net/freerdp that is net/freerdp3.

Your package is red, now what?

  1. Find your ports. pkg query '%o' lists the origins of all installed packages, pkg info -r <package> shows what pulled a package in. Check first what is reachable from outside or processes untrusted data.
  2. Read the CVEs. If one only affects the Windows client or a feature you do not use, you can take it more calmly.
  3. Look at the update line. Is there a newer version or a successor port?
  4. Is the fix only in main? If the MFH has been requested or a problem report is open, wait or follow up politely.
  5. No update and no problem report? Then open one on bugs.freebsd.org under “Ports & Packages”, with a subject like print/py-pypdf: Update to 6.21.2 (security) and the CVE numbers plus the link to the portstatus page in the text. A Bugzilla account is enough, you do not need to be a committer.
  6. Need the fix right away? Build the port yourself with poudriere from a quarterly checkout (branch 2026Q4) plus the fix. The page shows the hash on the commit. Mixing single packages from latest into a quarterly system often goes wrong because library versions drift apart.
% git -C ~/ports switch 2026Q4                 # checkout on the quarterly branch
% git -C ~/ports cherry-pick -x 1a66662c5b   # fix from main, hash from the port page
# poudriere bulk -j 151amd64 -p quarterly mail/roundcube

Quarterly: fixed, but not on your system yet

If you use pkg on a RELEASE, your packages come from quarterly. Only fixes that someone carries over from main by MFH (“merged from head”) end up there. mail/roundcube got the security update to 1.7.5 in main on October 19. The commit requests the MFH to 2026Q4, but it has not been merged yet:

Port page of mail/roundcube with a red note: fixed in main, not merged to 2026Q4; quarterly 1.7.4 with a VuXML entry, plus an open problem report
Fixed in main since October 19, still open in 2026Q4.

Here VuXML does its job, at least for php82 to php85: pkg audit warns you on quarterly as well.

# pkg audit roundcube-php84-1.7.4,1
roundcube-php84-1.7.4,1 is vulnerable:
  Roundcube -- Multiple vulnerabilities
  WWW: https://vuxml.FreeBSD.org/freebsd/69adacad-cba3-11f1-8135-8447094a420f.html

1 problem(s) in 1 package(s) found.

The entry, however, lists roundcube-php85 twice and roundcube-php86 not at all. For php86, pkg audit stays silent, and portstatus shows exactly these packages in red, with a note about the missing package name. What pkg audit does not tell you either: that the fix has long been in main, that the MFH has been requested, and that an open problem report is asking for exactly that.

Install and packages per platform

Below the tiles are three tabs. “Port info” sums up maintainer, package state, upstream and quarterly. “Install” shows what you get before you install it: size, dependencies, license, which users the package creates, which options it was built with, and what the pkg-message says.

Install tab for www/nginx: pkg install and port build, size, dependency pcre2, license, user www and the 29 enabled options
The package options at a glance: for nginx, 29 of 118 are enabled.

The options are my favorite. Anyone who has installed a package only to find that the one module they needed is missing knows why. For nginx you see right away: HTTPV3 yes, BROTLI no.

The third tab shows which version is available on which FreeBSD version and architecture, split into latest and quarterly:

Table of nginx package versions on FreeBSD 14, 15 and 16 for amd64, aarch64 and i386, split into latest and quarterly; versions that differ are marked yellow
Yellow marks what differs from the version in the tree. aarch64 is lagging behind here.

On FreeBSD 14 and 16, aarch64 is still at 1.30.5. Package sets that have not been rebuilt for 60 days are hidden. That is why armv6 and armv7 do not show up at all, their indexes date from 2023 and 2025.

No package does not mean “coming soon”

If a port has no package, the page tells you why. www/py-grimoirelab, for example, has no package on FreeBSD 15 amd64 because one of its dependencies has none. But which one?

Port page of www/py-grimoirelab: no package in the latest repository because the dependency archivers/py-brotli has no package
Instead of “no package built yet”, the page names the port that blocks it.

The chain is longer than you would think: grimoirelab needs py-graal, which needs py-networkx, which in turn needs py-matplotlib, matplotlib needs py-fonttools, and fonttools depends on archivers/py-brotli. py-brotli broke with the update of brotli to 1.2.1. I would have bet on networkx and was wrong. The py-brotli page shows the damage: 350 ports without a package, all because of this one. The fix has been in the tree since October 24. Because package builds lag days behind, the page names both possibilities, a real build failure or a fix that has not been built yet, and shows the latest commit.

Port page of archivers/py-brotli: no package, latest commit on October 24 probably not built yet, blocks 350 ports
One broken port, 350 ports without a package.

For committers and maintainers

If you maintain ports, the maintainer page is the quickest way in. Search for your address and you see all your ports with open CVEs, VuXML hits and available updates on one page. Not always pleasant, but useful.

This is how you resolve an “unaddressed”: update the port past the affected range and, in the same go, write a VuXML entry so users of the old version get warned. If there is no fix yet, the VuXML entry alone is enough. For security fixes, the MFH to quarterly belongs with it. The tree provides the skeleton for the entry, and with CVE_ID it pre-fills the details from the CVE:

% cd ~/ports/security/vuxml
% make newentry CVE_ID=CVE-2026-103000
% make validate

After the next hourly run the port shows yellow instead of red, and once the new package is out, not at all. For every commit the page also shows whether the MFH has been requested or is already done. /vuxml-gaps lists the VuXML entries that are missing CVE numbers or package names.

There is also /updates: 6,318 ports for which upstream has a newer version, by category, type of update (major, minor, patch) and the number of CVEs the update would fix. Ports that deliberately track a branch, like mysql84, do not count as outdated there.

What runs in the background

The site itself computes nothing. Everything happens in a Python collector that runs on its own VM and writes its results to MySQL. The website only reads, sets no cookies, does no tracking and needs no login. All sources are open:

  • Package index from pkg.freebsd.org for latest and quarterly, plus the indexes of all FreeBSD versions and architectures that still get packages
  • ports.git: every commit to main since 1994, the quarterly branches, VuXML, MOVED
  • NVD and cvelistV5 from CVE.org: the CVEs with affected products and version ranges
  • Repology: CPEs for ports that have none of their own, and the newest upstream version
  • Bugzilla: open problem reports per port
Diagram: sources such as the package index, ports.git, NVD, cvelistV5, Repology and Bugzilla feed the collector, which writes to MySQL; the PHP site only reads
All computing happens in the collector, the site only displays. If a source fails, its last state stays in place.

The collector fetches new commits every minute. VuXML, the CVE records from cvelistV5 and the problem reports come hourly, NVD, packages and upstream versions once a day. It compares versions with libpkg, exactly like pkg version -t. Compare 1.30.6_5,3 wrongly against “before 1.30.6” and the list is full of false alarms and therefore worthless. Missing packages are traced back through the dependencies in the package index to the root:

def root(o, path):
    # missing dependencies that still exist in main
    sub = [d for d in gaps(o) if d not in path]
    if not sub:
        return {o}        # the chain ends here
    res = set()
    for d in sub:
        res |= root(d, path | {o})
    return res

Where the site still falls short

  • CVE records are often too broad. When a CNA, the organization that publishes the CVE record, writes “before 3.29.0”, that formally applies to every older branch, whether or not the old code has the flaw. That is why net/freerdp 2.x shows 96 CVEs, even though the port is deprecated, will be removed at the end of 2026 and points to net/freerdp3 in its pkg-message. portstatus works with what the record says.
  • VuXML does not always list every CVE. If pkg audit already warns about the version and the entry reaches up to the fixed version, portstatus counts the CVE as yellow, because the update fixes it too. Today that affects 582 CVEs from 7 entries, 550 of them in the Chromium entry alone. Without this rule, 3,865 instead of 3,295 CVEs would be on the red list.
  • Backports do not show in the version. Especially with the Linux ports (linux-rl9-*), fixes go into the RPM release while the upstream version stays the same. These are therefore listed separately as “possibly backported”.
  • No CPE, no reliable match. For more than 90 percent of the ports the site needs fallbacks via Repology or the package name on PyPI and CPAN. That catches a lot, but not everything.
  • Package builds lag behind. What is fixed in the tree only reaches the package days later. The site deliberately bases the security state on the built package, because that is what is installed on your system.

How the site counts is explained on the About page. I collect false positives through the comments here. What I still miss: a feed per maintainer or port, so nobody has to visit the site actively, and a way to mark a hit as “checked, does not affect us”, with a reason and visible to everyone.

Conclusion

  • pkg audit is as good as VuXML. What is missing there, it does not report: across the whole tree, currently 3,295 CVEs in 670 ports.
  • portstatus matches the ports against cvelistV5 hourly and against NVD daily, and tells you per port whether there is a fix and where.
  • Quarterly users see what is fixed in main but still open on their systems.
  • Committers use the maintainer search to quickly find where a VuXML entry, an update or an MFH is missing, and /vuxml-gaps to see which entries lack CVE numbers or package names.

The site does not replace pkg audit. It shows the gap next to it.

Your turn: look up your most important ports on portstatus.bsdproject.de. Which one shows red for you even though you are sure it is clean? Post it with the port name and CVE in the comments.

Schreibe einen Kommentar