From c65d0236681800ee5ccf9b87ed99af40d2451d77 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 23 Jan 2026 17:52:21 -0500 Subject: [PATCH] feat: detect Debian version from /etc/debian_version (#4569) Signed-off-by: Keith Zantow --- syft/linux/identify_release.go | 9 ++++ syft/linux/identify_release_test.go | 16 +++++- syft/linux/supplement_release.go | 51 +++++++++++++++++++ .../from-debian_version/etc/debian_version | 1 + .../debian/from-debian_version/etc/os-release | 6 +++ .../{ => from-os-release}/usr/lib/os-release | 0 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 syft/linux/supplement_release.go create mode 100644 syft/linux/test-fixtures/os/debian/from-debian_version/etc/debian_version create mode 100644 syft/linux/test-fixtures/os/debian/from-debian_version/etc/os-release rename syft/linux/test-fixtures/os/debian/{ => from-os-release}/usr/lib/os-release (100%) diff --git a/syft/linux/identify_release.go b/syft/linux/identify_release.go index 6f0fd3a5c..0ff7ab280 100644 --- a/syft/linux/identify_release.go +++ b/syft/linux/identify_release.go @@ -54,6 +54,12 @@ var identityFiles = []parseEntry{ // ///////////////////////////////////////////////////////////////////////////////////////////////////// } +// after a parser function returns a Release, it may have incomplete information; supplementers can be used to +// fill in missing details based on other files present in the filesystem +var supplementers = []func(file.Resolver, *Release){ + supplementDebianVersion, +} + // IdentifyRelease parses distro-specific files to discover and raise linux distribution release details. func IdentifyRelease(resolver file.Resolver) *Release { logger := log.Nested("operation", "identify-release") @@ -67,6 +73,9 @@ func IdentifyRelease(resolver file.Resolver) *Release { for _, location := range locations { release := tryParseReleaseInfo(resolver, location, logger, entry) if release != nil { + for _, supplementer := range supplementers { + supplementer(resolver, release) + } return release } } diff --git a/syft/linux/identify_release_test.go b/syft/linux/identify_release_test.go index e6e39f5aa..c81a5ad1f 100644 --- a/syft/linux/identify_release_test.go +++ b/syft/linux/identify_release_test.go @@ -74,7 +74,7 @@ func TestIdentifyRelease(t *testing.T) { }, }, { - fixture: "test-fixtures/os/debian", + fixture: "test-fixtures/os/debian/from-os-release", release: &Release{ PrettyName: "Debian GNU/Linux 8 (jessie)", Name: "Debian GNU/Linux", @@ -87,6 +87,20 @@ func TestIdentifyRelease(t *testing.T) { BugReportURL: "https://bugs.debian.org/", }, }, + { + fixture: "test-fixtures/os/debian/from-debian_version", + release: &Release{ + PrettyName: "Distroless", + Name: "Debian GNU/Linux", + ID: "debian", + IDLike: nil, + Version: "10.8", + VersionID: "10.8", + HomeURL: "https://github.com/GoogleContainerTools/distroless", + SupportURL: "https://github.com/GoogleContainerTools/distroless/blob/master/README.md", + BugReportURL: "https://github.com/GoogleContainerTools/distroless/issues/new", + }, + }, { fixture: "test-fixtures/os/fedora", release: &Release{ diff --git a/syft/linux/supplement_release.go b/syft/linux/supplement_release.go new file mode 100644 index 000000000..184da9379 --- /dev/null +++ b/syft/linux/supplement_release.go @@ -0,0 +1,51 @@ +package linux + +import ( + "io" + "regexp" + "strings" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" +) + +func supplementDebianVersion(resolver file.Resolver, release *Release) { + // we're only looking for version information for debian when none is present in /etc/os-release + if release.Version != "" || release.VersionID != "" || !strings.EqualFold(release.ID, "debian") { + return + } + // if we have a debian release with no version, look for a debian_version + locations, err := resolver.FilesByGlob("/etc/debian_version") + if err != nil { + log.Debugf("error reading /etc/debian_version: %v", err) + return + } + for _, location := range locations { + version := readDebianVersionFile(resolver, location) + if version != "" { + release.Version = version + release.VersionID = version + return // keep the first result + } + } +} + +func readDebianVersionFile(resolver file.Resolver, location file.Location) string { + rdr, err := resolver.FileContentsByLocation(location) + if err != nil { + log.Debugf("error getting contents for %s: %v", location.RealPath, err) + return "" + } + defer internal.CloseAndLogError(rdr, location.RealPath) + contents, err := io.ReadAll(rdr) + if err != nil { + log.Debugf("error reading %s: %v", location.RealPath, err) + return "" + } + version := strings.TrimSpace(string(contents)) + if regexp.MustCompile(`^\d+(?:\.\d+)?$`).MatchString(version) { + return version + } + return "" +} diff --git a/syft/linux/test-fixtures/os/debian/from-debian_version/etc/debian_version b/syft/linux/test-fixtures/os/debian/from-debian_version/etc/debian_version new file mode 100644 index 000000000..f5d398917 --- /dev/null +++ b/syft/linux/test-fixtures/os/debian/from-debian_version/etc/debian_version @@ -0,0 +1 @@ +10.8 diff --git a/syft/linux/test-fixtures/os/debian/from-debian_version/etc/os-release b/syft/linux/test-fixtures/os/debian/from-debian_version/etc/os-release new file mode 100644 index 000000000..b6a550d49 --- /dev/null +++ b/syft/linux/test-fixtures/os/debian/from-debian_version/etc/os-release @@ -0,0 +1,6 @@ +PRETTY_NAME="Distroless" +NAME="Debian GNU/Linux" +ID="debian" +HOME_URL="https://github.com/GoogleContainerTools/distroless" +SUPPORT_URL="https://github.com/GoogleContainerTools/distroless/blob/master/README.md" +BUG_REPORT_URL="https://github.com/GoogleContainerTools/distroless/issues/new" diff --git a/syft/linux/test-fixtures/os/debian/usr/lib/os-release b/syft/linux/test-fixtures/os/debian/from-os-release/usr/lib/os-release similarity index 100% rename from syft/linux/test-fixtures/os/debian/usr/lib/os-release rename to syft/linux/test-fixtures/os/debian/from-os-release/usr/lib/os-release