Documentation
¶
Overview ¶
Package pkgdb reads the installed-package databases of the three OS package managers that show up in container images: dpkg, apk and rpm.
It answers two questions the OS ecosystem plugin needs: which packages are installed at which versions, and which files each one owns. The second is what connects a CVE against "openssl" to the ELF objects that would have to be loaded for it to matter.
Nothing here talks to OSV or to the network. Readers parse a filesystem and return what they found, or an error -- never a partial answer.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNotRPM = errors.New("not an rpm package file (bad lead magic)")
ErrNotRPM is returned when a file does not begin with rpm's lead magic. It is separate so a directory walk can tell "this .rpm is not one" from "this .rpm is one and is broken", and report the two differently.
var ErrTruncated = errors.New("unexpected end of file")
ErrTruncated means the file stopped before its header did -- a download cut short, or a package that was never whole.
Functions ¶
func ReadFile ¶ added in v0.6.0
ReadFile parses an RPM package file's headers into the same Package the rpm database reader produces, so nothing downstream can tell the difference.
It reads the lead, the signature header and the main header, and stops -- it never touches the payload, and never seeks, so r may be a network stream. On the two packages measured that is 0.7% and 4.6% of the file.
An error means the file could not be understood, never that it was understood to contain nothing: a package with no files and a package whose file list failed to decode must not produce the same Package.
func SourceRPMName ¶ added in v0.7.0
SourceRPMName extracts the source package name from a SOURCERPM value like "openssl-3.2.2-16.el10.src.rpm", which is name-version-release.src.rpm. The name itself may contain hyphens ("java-21-openjdk"), so the tail is stripped by position from the right rather than by splitting from the left.
Types ¶
type APK ¶
type APK struct{}
APK reads apk's database, as used by Alpine, Wolfi, Chainguard and MinimOS.
type Deb ¶
type Deb struct{}
Deb reads dpkg's database.
func (*Deb) Detect ¶
Detect looks for either shape of dpkg database.
Debian and Ubuntu ship one concatenated /var/lib/dpkg/status. Google's distroless images ship /var/lib/dpkg/status.d/ instead, a directory holding one status paragraph per package, because the images are assembled by Bazel rather than by dpkg. Both are real and both appear in images people scan.
type Meta ¶ added in v0.6.0
type Meta struct {
// Vendor and Distribution are the VENDOR and DISTRIBUTION tags, verbatim:
// "Rocky Enterprise Software Foundation" / "Rocky Linux 9", "SUSE LLC" /
// "SUSE Linux Enterprise 15". Either may be empty; some rebuilders set
// neither, which is why --osv-ecosystem exists.
Vendor string `json:"vendor,omitempty"`
Distribution string `json:"distribution,omitempty"`
// ELF are the installed paths whose FILECLASS entry names an ELF object.
//
// rpm stores file(1)'s output for every file it packages, in the header,
// which means "does this package ship any code at all" is answerable
// without decompressing the cpio payload -- no xz, no zstd, no cpio.
// That single fact is what makes a metadata-only scan able to produce a
// real not_present verdict rather than only undetermined ones.
ELF []string `json:"elf,omitempty"`
// FilesKnown reports whether the file list behind ELF was actually read.
//
// A package that genuinely ships no code and a package nobody looked at
// are indistinguishable by ELF alone -- both have len(ELF) == 0 -- and the
// two call for opposite verdicts: one is "there is no code here", the
// other is "nothing was learned". Only a reader with the header in front
// of it can tell them apart, so only ReadFile sets this. Every other way a
// Meta comes into being -- an SBOM component, a hand-built literal --
// leaves it false and gets the cautious answer, which is the direction a
// zero value has to fall in a tool whose worst failure is a clean report.
FilesKnown bool `json:"files_known,omitempty"`
// SourcePackage is true for a .src.rpm. Source packages install nothing and
// are not what a distribution files advisories against, so callers skip
// them rather than report a package that cannot be installed.
SourcePackage bool `json:"source_package,omitempty"`
}
Meta is what an RPM file says about itself beyond the package identity: which distribution built it, and whether it ships any executable code.
It exists because an RPM file arrives with no filesystem around it. An installed package is found inside a tree that has an /etc/os-release to name the ecosystem and ELF objects to trace; a package file has to carry both facts in its own header or they are not available at all.
func (Meta) CanRuleOutCode ¶ added in v0.7.0
CanRuleOutCode reports whether this metadata is enough to say the package installs nothing that could execute.
An empty ELF list is only evidence when its emptiness was observed, so the file list has to be known before its silence means anything.
type Package ¶
type Package struct {
Format Format `json:"format"`
Name string `json:"name"`
// Version is the version string to compare against OSV ranges. For rpm
// this is the full EVR with the epoch always present ("0:1.43.0-5.el9_3"),
// because that is how the Red Hat, Rocky and AlmaLinux records are
// written. See rpmEVR.
Version string `json:"version"`
Arch string `json:"arch,omitempty"`
// Epoch is the rpm epoch, broken out so a consumer can reconstruct the
// epoch-free version for the ecosystems whose records omit it (Azure
// Linux). Zero and meaningless for deb and apk.
Epoch int `json:"epoch,omitempty"`
// Source is the source (dpkg "Source:", apk "o:", rpm SOURCERPM) package
// this was built from, when the database records one and it differs from
// Name.
Source string `json:"source,omitempty"`
// SourceVersion is the source version when dpkg records one explicitly,
// which it only does when it differs from Version.
SourceVersion string `json:"source_version,omitempty"`
// Files are the tree-absolute paths the package installs, as the database
// records them: directories included, nothing stat'ed.
Files []string `json:"files,omitempty"`
// DB is the tree-absolute path of the database this came from, for
// evidence and error messages.
DB string `json:"db,omitempty"`
}
Package is one installed package.
func (Package) OSVNames ¶
OSVNames returns the package names to query OSV with, likeliest first.
Which name a distribution files advisories against is not consistent, and not even consistent within one package format. Verified against the live api.osv.dev, querying with no version so the count is "does this name exist in the database at all":
Debian:12 openssl 255 libssl3 0 <- source
Debian:12 glibc 158 libc6 0 <- source
Alpine:v3.19 openssl 55 libssl3 0 <- origin
Red Hat openssl 168 openssl-libs 113 <- binary
AlmaLinux:9 openssl 10 openssl-libs 15 <- binary
Rocky Linux:9 openssl 10 openssl-libs 0 <- source, unlike its
upstream and its peer
So the rule cannot be "deb and apk use the source name, rpm uses the binary name": Rocky and AlmaLinux are both RPM rebuilds of Red Hat and disagree. Both names are returned instead. A name that matches nothing costs one entry in a batch query; choosing the wrong single name reports a vulnerable package as clean, which is the one outcome this tool must never produce.
The order is a display and tie-break preference only -- callers query every name returned.
type Reader ¶
type Reader interface {
// Format names the package manager this reader understands.
Format() Format
// Detect reports whether the tree carries this kind of database, and the
// tree-absolute path of the one it found.
Detect(fsys target.RootFS) (string, bool)
// Read parses the database. A database that Detect found but Read cannot
// parse is an error, never an empty slice.
Read(fsys target.RootFS) ([]Package, error)
}
Reader parses one kind of package database out of a filesystem tree.
type Result ¶
type Result struct {
Format Format `json:"format"`
DB string `json:"db"`
Packages []Package `json:"packages"`
}
Result is what one reader found.
func Read ¶
Read runs every reader that detects a database in fsys.
A reader whose database is present but unparseable fails the whole call. Reporting the packages from the other databases and quietly omitting that one would render as "these are all the packages in the image", and every package the unread database owns would be attested as not present.