pkgdb

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 11 Imported by: 0

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

View Source
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.

View Source
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

func ReadFile(r io.Reader) (Package, Meta, error)

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

func SourceRPMName(srpm string) string

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.

func (*APK) Detect

func (*APK) Detect(fsys target.RootFS) (string, bool)

func (*APK) Format

func (*APK) Format() Format

func (*APK) Read

func (a *APK) Read(fsys target.RootFS) ([]Package, error)

type Deb

type Deb struct{}

Deb reads dpkg's database.

func (*Deb) Detect

func (*Deb) Detect(fsys target.RootFS) (string, bool)

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.

func (*Deb) Format

func (*Deb) Format() Format

func (*Deb) Read

func (d *Deb) Read(fsys target.RootFS) ([]Package, error)

type Format

type Format string

Format identifies a package manager.

const (
	FormatDeb Format = "deb"
	FormatAPK Format = "apk"
	FormatRPM Format = "rpm"
)

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

func (m Meta) CanRuleOutCode() bool

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.

func (Meta) HasELF added in v0.6.0

func (m Meta) HasELF() bool

HasELF reports whether the package ships any executable object.

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

func (p Package) OSVNames() []string

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 RPM

type RPM struct{}

RPM reads rpm's database.

func (*RPM) Detect

func (*RPM) Detect(fsys target.RootFS) (string, bool)

func (*RPM) Format

func (*RPM) Format() Format

func (*RPM) Read

func (r *RPM) Read(fsys target.RootFS) ([]Package, error)

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.

func Readers

func Readers() []Reader

Readers returns every backend, in a stable order.

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

func Read(fsys target.RootFS) ([]Result, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL