// OSSeva Blog
ComplianceSBOMs and VEX for PostgreSQL, MySQL and Valkey: What to Generate and What Auditors Ask For
The short answer
For a database server, an SBOM has to describe four layers: the server and client packages, the shared libraries they load (OpenSSL, ICU, libxml2, the C library), the extensions or plugins installed in the server, and the operating system or base image underneath. Generate it from the artefact you deploy, the image or the host, with a tool such as syft or cdxgen. Then add what those tools cannot see from package metadata, chiefly extensions built from source and which ones are actually created in each database.
VEX is the other half. When a scanner reports a CVE that does not apply to how you run the database, a VEX statement records that decision in a machine-readable form, with a justification, so the scanner stops raising it and the auditor can check why. The basics are in what is VEX; this article applies them to database servers.
What goes in a database SBOM
| Layer | Examples | Where the data comes from |
|---|---|---|
| Server and client | postgresql-16, mysql-community-server, mariadb-server, valkey | The package database (dpkg, rpm) or binary detection |
| Libraries the server loads | libssl and libcrypto, libicu, libxml2, libldap, glibc | The package database of the host or image |
| Extensions, plugins and modules | PostGIS, pgvector, pg_stat_statements, MySQL component and plugin libraries, Valkey modules | Packages if installed from a repository; the database catalog in every case |
| Base image or host | Debian, UBI, Ubuntu, Alpine layers | The image layers or host package database |
| Operational tooling | Patroni, repmgr, pgBackRest, Orchestrator, Sentinel | Packages and language manifests, often Python or Go |
Leave out what is not shipped with the server. Client applications get their own SBOMs; mixing them in makes every database finding look like an application finding.
Generating it: images
For a container image, scan the exact digest you deploy and keep both common formats if your auditors have not chosen one:
# syft: CycloneDX and SPDX from one scan, straight from the registry
syft registry:registry.example.com/db/postgres@sha256:<digest> \
-o cyclonedx-json=postgres.cdx.json -o spdx-json=postgres.spdx.json
# cdxgen: CycloneDX from a container image
cdxgen -t docker registry.example.com/db/postgres@sha256:<digest> -o postgres.cdx.json
syft identifies PostgreSQL, MySQL, MariaDB, Redis and Valkey server binaries even when they were not installed from a package, using binary classifiers, so a server compiled into an image still shows up with a version.
Generating it: VMs and bare metal
# syft against the host filesystem, reading the dpkg or rpm database
sudo syft dir:/ -o cyclonedx-json=db01.cdx.json
# cdxgen's operating system mode (obom is an alias for cdxgen -t os)
sudo obom -o db01.obom.json
Run it on every database host, or on one host per golden image if the build is truly identical, and regenerate after every package change. An SBOM from last quarter does not describe a server that took two minor releases since.
The part scanners miss: what is installed in the database
Package metadata tells you a shared library is on disk. It does not tell you whether any database has loaded it, and it says nothing about an extension compiled from source into the library directory. Capture the catalog from each server and store it next to the SBOM:
-- PostgreSQL: run in every database, extensions are per database
SELECT current_database(), extname, extversion FROM pg_extension ORDER BY extname;
SHOW shared_preload_libraries;
-- MySQL and MariaDB: plugins loaded from a library file
SELECT PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_LIBRARY
FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_LIBRARY IS NOT NULL;
# Valkey or Redis
valkey-cli MODULE LIST
Where an extension came from source rather than a package, add it to the SBOM by hand or through your build pipeline, with the version and source URL. An SBOM that omits it is the kind of gap SBOMs for end-of-life components warns about.
VEX for database CVEs
A VEX statement says, for one product and one vulnerability, which of four statuses applies: not_affected, affected, fixed or under_investigation. A not_affected statement must carry either one of five justification labels or a free-text impact statement:
component_not_presentvulnerable_code_not_presentvulnerable_code_not_in_execute_pathvulnerable_code_cannot_be_controlled_by_adversaryinline_mitigations_already_exist
The PostgreSQL security page makes the first triage easy, because it names the component of each CVE: core server, client or contrib module. Take CVE-2026-15742, an 8.8 flaw in the fuzzystrmatch contrib module, fixed in 18.6, 17.11, 16.15, 15.19 and 14.24. Before you patch, the honest VEX status depends on the host:
- On RHEL-family hosts using PGDG packages, contrib modules ship in a separate package such as
postgresql16-contrib. If it is not installed, the code is not there:not_affectedwithvulnerable_code_not_present. On Debian and Ubuntu the contrib modules ship inside the server package itself, so this case does not arise. - If contrib is installed but no database has created fuzzystrmatch, be careful. The PostgreSQL documentation marks fuzzystrmatch as trusted, so any user with CREATE privilege on a database can install it. Unless you can show nobody holds that privilege, the status is
affecteduntil the minor release is deployed. - After the update, the status is
fixed.
That second case is the pattern auditors look for: a not_affected claim that is true today only because nobody has run one command yet. Write VEX for what an attacker could do, not for what your applications happen to do.
# Create an OpenVEX statement (vexctl, from the OpenVEX project)
vexctl create --author="Database Security Team" \
--product="pkg:rpm/redhat/postgresql16-server@<version>" \
--vuln="<CVE-ID>" \
--status="not_affected" \
--justification="vulnerable_code_not_present" \
--file=db01.vex.json
# Scan the SBOM and apply the VEX document
grype sbom:./db01.cdx.json --vex db01.vex.json
Grype moves findings marked not_affected or fixed to its ignored list by default, and Trivy has its own --vex option; check which VEX formats your scanner version reads. Set the author explicitly, since vexctl otherwise records an unknown one, keep each statement's timestamp, and review it when the configuration it depends on changes.
What auditors ask for
- Coverage. An SBOM for every production database cluster, matched to the version actually running, not the version in the change ticket.
- Freshness. Generation on every build or package change, with the date in the document.
- Format. SPDX or CycloneDX in JSON, which every mainstream scanner reads.
- Integrity. SBOMs stored with, or signed alongside, the artefact they describe.
- Exceptions. A VEX statement or exceptions register entry for every finding you are not fixing, with a justification, an owner and a review date.
- End-of-life components. A list of what no longer receives upstream fixes and who supplies fixes instead.
The open source audit evidence checklist maps these to specific control frameworks, and continuous SBOM compliance covers keeping them current.
Where OSSeva fits
For engines and versions upstream has stopped fixing, OSSeva ships patched, signed builds and the evidence that goes with them. OSSeva Assure includes SBOMs and CVE remediation records for MySQL and MariaDB, a compliance attestation package for PostgreSQL and a SOC 2 and PCI DSS attestation package for Redis, so the end-of-life rows in your SBOM have a named supplier and a record of what each build fixed. On OSSeva Operate, Critical CVEs (CVSS ≥ 9.0) are patched within 48 hours and High within 7 days. OSSeva for CISOs and the compliance resources describe the evidence in more detail. Pricing is per cluster; book a discovery call for a quote.
Frequently asked questions
How do I generate an SBOM for a PostgreSQL server?
Scan the deployed image digest or the host filesystem with syft or cdxgen, output CycloneDX or SPDX JSON, then add the output of pg_extension from every database and any extension built from source.
Should database extensions be in the SBOM?
Yes. Extensions run inside the server process with its privileges. Include packaged extensions automatically and add source-built ones by hand or from the build pipeline, with versions.
SPDX or CycloneDX for database SBOMs?
Either works for auditors and scanners. syft can write both from one scan, which avoids choosing until a customer or regulator asks for one.
Can we mark a CVE as not affected because no application uses the feature?
Only if an attacker could not use it either. If a user can enable the vulnerable code, as with trusted PostgreSQL extensions that anyone with CREATE privilege can install, the finding is still affected until it is patched or that privilege is removed.
Do scanners actually honour VEX?
Grype applies OpenVEX documents with --vex and hides not_affected and fixed findings by default. Trivy also accepts VEX through --vex. Check your own scanner's documentation for the formats it reads.
How often should a database SBOM be regenerated?
Whenever the deployed artefact changes: a new image digest, a minor release, an extension added or a library updated by the operating system.
Tags
Related articles
How to Document End-of-Life Software Risk: Risk Register, Exceptions and Compensating Controls
October 7, 2026ComplianceEnd-of-Life Software Policy Template for Open Source Infrastructure
October 7, 2026OperationsHow to Consolidate Open Source Database Support Contracts Without Migrating Anything
October 7, 2026Ready to get your open source under control?
Talk to an OSSeva engineer about CVE coverage, compliance, and migration support for your stack.