From f1851556a8f0a498266078c2a21df86564c3fb53 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 11 Jun 2020 10:02:15 -0400 Subject: [PATCH 1/6] distro: add a Name stringer Signed-off-by: Alfredo Deza --- imgbom/distro/distro.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/imgbom/distro/distro.go b/imgbom/distro/distro.go index cdd32fa51..c6a0e2a93 100644 --- a/imgbom/distro/distro.go +++ b/imgbom/distro/distro.go @@ -33,5 +33,10 @@ func (d Distro) FullVersion() string { } func (d Distro) String() string { - return fmt.Sprintf("%s %s", d.Type, d.Version) + return fmt.Sprintf("%s %s", d.Type, d.RawVersion) +} + +// Name provides a string repr of the distro +func (d Distro) Name() string { + return fmt.Sprintf("%s", d.Type) } From 56c2318ea19cb44ea01c528dc5552d6b3d7508b8 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 11 Jun 2020 10:02:33 -0400 Subject: [PATCH 2/6] add support for distro detection Signed-off-by: Alfredo Deza --- imgbom/distro/type.go | 55 +++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/imgbom/distro/type.go b/imgbom/distro/type.go index 49f1742b3..05f73ee83 100644 --- a/imgbom/distro/type.go +++ b/imgbom/distro/type.go @@ -3,41 +3,46 @@ package distro const ( UnknownDistro Type = iota Debian - // Ubuntu - // RedHat - // CentOS + Ubuntu + RedHat + CentOS // Fedora // Alpine - // Busybox + Busybox // AmazonLinux // OracleLinux // ArchLinux ) +const ( + // UnknownVersion is a default of 0.0.0 when it can't be parsed + UnknownVersion string = "0.0.0" +) + type Type int var distroStr = []string{ "UnknownDistro", - "Debian", - // "Ubuntu", - // "RedHat", - // "CentOS", - // "Fedora", - // "Alpine", - // "Busybox", - // "AmazonLinux", - // "OracleLinux", - // "ArchLinux", + "debian", + "ubuntu", + "redhat", + "centos", + // "fedora", + // "alpine", + "busybox", + // "amazn", + // "oraclelinux", + // "archlinux", } var All = []Type{ Debian, - // Ubuntu, - // RedHat, - // CentOS, + Ubuntu, + RedHat, + CentOS, // Fedora, // Alpine, - // Busybox, + Busybox, // AmazonLinux, // OracleLinux, // ArchLinux, @@ -50,3 +55,17 @@ func (t Type) String() string { return distroStr[t] } + +// Mappings connects a distro ID like "ubuntu" to a Distro type +var Mappings = map[string]Type{ + "debian": Debian, + "ubuntu": Ubuntu, + "rhel": RedHat, + "centos": CentOS, + // "fedora": Fedora, + // "alpine": Alpine, + "busybox": Busybox, + // "amazn": AmazonLinux, + // "oraclelinux": OracleLinux, + // "archlinux": ArchLinux, +} From dba5f0b197135c32e7b7d198ba9337500ccca7f6 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 11 Jun 2020 10:02:55 -0400 Subject: [PATCH 3/6] initial take on distro identification Signed-off-by: Alfredo Deza --- imgbom/distro/identify.go | 97 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/imgbom/distro/identify.go b/imgbom/distro/identify.go index d5468b4ca..b456762d4 100644 --- a/imgbom/distro/identify.go +++ b/imgbom/distro/identify.go @@ -1,11 +1,104 @@ package distro import ( + "regexp" + "strings" + + "github.com/anchore/imgbom/internal/log" + "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/image" ) -func Identify(img *image.Image) (Distro, error) { +// returns a distro or nil +type parseFunc func(string) *Distro + +// Identify parses distro-specific files to determine distro metadata like version and release +func Identify(img *image.Image) *Distro { // TODO: implement me based off of https://github.com/anchore/anchore-engine/blob/78b23d7e8f007005c070673405b5e23730a660e0/anchore_engine/analyzers/utils.py#L131 - return NewDistro(UnknownDistro, "0.0.0") + identityFiles := map[file.Path]parseFunc{ + "/etc/os-release": parseOsRelease, + // Debian and Debian-based distros have the same contents linked from this path + "/usr/lib/os-release": parseOsRelease, + // TODO: change this to /bin/busybox when stereoscope deals with hardlinks + "/bin/[": parseBusyBox, + } + + for path, fn := range identityFiles { + + contents, err := img.FileContentsFromSquash(path) + + if err != nil { + log.Errorf("unable to get contents from %s: %s", path, err) + continue + } + + if contents == "" { + log.Debugf("no contents in file, skipping: %s", path) + continue + } + distro := fn(contents) + + if distro == nil { + continue + } + + return distro + + } + // TODO: is it useful to know partially detected distros? where the ID is known but not the version (and viceversa?) + return nil +} + +func assembleDistro(name, version string) *Distro { + distroType, ok := Mappings[name] + + // Both distro and version must be present + if len(name) == 0 || len(version) == 0 { + return nil + } + + if ok { + distro, err := NewDistro(distroType, version) + if err != nil { + return nil + } + return &distro + } + + return nil +} + +func parseOsRelease(contents string) *Distro { + id, vers := "", "" + for _, line := range strings.Split(contents, "\n") { + + parts := strings.Split(line, "=") + prefix := parts[0] + value := strings.ReplaceAll(parts[len(parts)-1], `"`, "") + + switch prefix { + case "ID": + id = value + case "VERSION_ID": + vers = value + } + } + + return assembleDistro(id, vers) +} + +var busyboxVersionMatcher = regexp.MustCompile(`BusyBox v[\d\.]+`) + +func parseBusyBox(contents string) *Distro { + matches := busyboxVersionMatcher.FindAllString(contents, -1) + for _, match := range matches { + parts := strings.Split(match, " ") + version := strings.ReplaceAll(parts[1], "v", "") + distro := assembleDistro("busybox", version) + if distro != nil { + return distro + } + } + return nil } From 75375d0b5845067df1858c5035973cb152cfcd80 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Fri, 12 Jun 2020 10:45:49 -0400 Subject: [PATCH 4/6] cmd: IdentifyDistro returns a distro object which can be nil Signed-off-by: Alfredo Deza --- cmd/root.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 3677be8d8..dc80eb548 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,11 +56,11 @@ func doRunCmd(_ *cobra.Command, args []string) int { defer stereoscope.Cleanup() log.Info("Identifying Distro") - distro, err := imgbom.IdentifyDistro(img) - if err != nil { - log.Errorf("error identifying Distro: %w", err) + distro := imgbom.IdentifyDistro(img) + if distro == nil { + log.Errorf("error identifying distro") } else { - log.Info(" Distro: %s", distro) + log.Infof(" Distro: %s", distro) } log.Info("Cataloging image") From 889fa52b7db1df3fc48fe09921c363d6903d0265 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Fri, 12 Jun 2020 10:46:15 -0400 Subject: [PATCH 5/6] lib: update signature to *Distro Signed-off-by: Alfredo Deza --- imgbom/lib.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgbom/lib.go b/imgbom/lib.go index 036149103..176bcafdb 100644 --- a/imgbom/lib.go +++ b/imgbom/lib.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/stereoscope/pkg/image" ) -func IdentifyDistro(img *image.Image) (distro.Distro, error) { +func IdentifyDistro(img *image.Image) *distro.Distro { return distro.Identify(img) } From e460c031b6f288d69213a8c5558691369a0373e2 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Fri, 12 Jun 2020 10:47:07 -0400 Subject: [PATCH 6/6] distro: add tests and fixtures for identification Signed-off-by: Alfredo Deza --- imgbom/distro/identify_test.go | 108 +++++++++++++++++++++++ imgbom/distro/test-fixtures/bad-id | 15 ++++ imgbom/distro/test-fixtures/bad-version | 15 ++++ imgbom/distro/test-fixtures/centos-8 | 17 ++++ imgbom/distro/test-fixtures/debian-8 | 8 ++ imgbom/distro/test-fixtures/rhel-8 | 16 ++++ imgbom/distro/test-fixtures/ubuntu-20.04 | 12 +++ 7 files changed, 191 insertions(+) create mode 100644 imgbom/distro/identify_test.go create mode 100644 imgbom/distro/test-fixtures/bad-id create mode 100644 imgbom/distro/test-fixtures/bad-version create mode 100644 imgbom/distro/test-fixtures/centos-8 create mode 100644 imgbom/distro/test-fixtures/debian-8 create mode 100644 imgbom/distro/test-fixtures/rhel-8 create mode 100644 imgbom/distro/test-fixtures/ubuntu-20.04 diff --git a/imgbom/distro/identify_test.go b/imgbom/distro/identify_test.go new file mode 100644 index 000000000..b21899dac --- /dev/null +++ b/imgbom/distro/identify_test.go @@ -0,0 +1,108 @@ +package distro + +import ( + "fmt" + "io/ioutil" + "os" + "testing" +) + +func TestParseOsRelease(t *testing.T) { + + tests := []struct { + fixture string + name string + RawVersion string + }{ + { + fixture: "test-fixtures/ubuntu-20.04", + name: "ubuntu", + RawVersion: "20.04", + }, + { + fixture: "test-fixtures/debian-8", + name: "debian", + RawVersion: "8", + }, + { + fixture: "test-fixtures/centos-8", + name: "centos", + RawVersion: "8", + }, + { + fixture: "test-fixtures/rhel-8", + name: "redhat", + RawVersion: "8.1", + }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s:%s", test.name, test.RawVersion) + fixture, err := os.Open(test.fixture) + defer fixture.Close() + + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + b, err := ioutil.ReadAll(fixture) + if err != nil { + t.Fatalf("unable to read fixture file: %+v", err) + } + + contents := string(b) + + t.Run(name, func(t *testing.T) { + distro := parseOsRelease(contents) + if distro.Name() != test.name { + t.Errorf("mismatched name in distro: '%s' != '%s'", distro.Name(), test.name) + } + if distro.RawVersion != test.RawVersion { + t.Errorf("mismatched distro version: '%s' != '%s'", distro.RawVersion, test.RawVersion) + } + }) + } + +} + +func TestParseOsReleaseFailures(t *testing.T) { + + tests := []struct { + fixture string + name string + }{ + { + fixture: "test-fixtures/bad-version", + name: "No version", + }, + { + fixture: "test-fixtures/bad-id", + name: "No name ID", + }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s:%s", test.name, test.fixture) + fixture, err := os.Open(test.fixture) + defer fixture.Close() + + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + b, err := ioutil.ReadAll(fixture) + if err != nil { + t.Fatalf("unable to read fixture file: %+v", err) + } + + contents := string(b) + + t.Run(name, func(t *testing.T) { + distro := parseOsRelease(contents) + if distro != nil { + t.Errorf("unexpected non-nil distro: '%s' != nil", distro) + } + }) + } + +} diff --git a/imgbom/distro/test-fixtures/bad-id b/imgbom/distro/test-fixtures/bad-id new file mode 100644 index 000000000..871e65561 --- /dev/null +++ b/imgbom/distro/test-fixtures/bad-id @@ -0,0 +1,15 @@ +NAME="Red Hat Enterprise Linux" +VERSION="8.1 (Ootpa)" +ID_LIKE="fedora" +PLATFORM_ID="platform:el8" +PRETTY_NAME="Red Hat Enterprise Linux 8.1 (Ootpa)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:redhat:enterprise_linux:8.1:GA" +HOME_URL="https://www.redhat.com/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" + +REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8" +REDHAT_BUGZILLA_PRODUCT_VERSION=8.1 +REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" +REDHAT_SUPPORT_PRODUCT_VERSION="8.1" + diff --git a/imgbom/distro/test-fixtures/bad-version b/imgbom/distro/test-fixtures/bad-version new file mode 100644 index 000000000..fa940db6e --- /dev/null +++ b/imgbom/distro/test-fixtures/bad-version @@ -0,0 +1,15 @@ +NAME="Red Hat Enterprise Linux" +VERSION="8.1 (Ootpa)" +ID="rhel" +ID_LIKE="fedora" +PLATFORM_ID="platform:el8" +PRETTY_NAME="Red Hat Enterprise Linux 8.1 (Ootpa)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:redhat:enterprise_linux:8.1:GA" +HOME_URL="https://www.redhat.com/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" + +REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8" +REDHAT_BUGZILLA_PRODUCT_VERSION=8.1 +REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" +REDHAT_SUPPORT_PRODUCT_VERSION="8.1" diff --git a/imgbom/distro/test-fixtures/centos-8 b/imgbom/distro/test-fixtures/centos-8 new file mode 100644 index 000000000..7f10b7745 --- /dev/null +++ b/imgbom/distro/test-fixtures/centos-8 @@ -0,0 +1,17 @@ +NAME="CentOS Linux" +VERSION="8 (Core)" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="8" +PLATFORM_ID="platform:el8" +PRETTY_NAME="CentOS Linux 8 (Core)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:centos:centos:8" +HOME_URL="https://www.centos.org/" +BUG_REPORT_URL="https://bugs.centos.org/" + +CENTOS_MANTISBT_PROJECT="CentOS-8" +CENTOS_MANTISBT_PROJECT_VERSION="8" +REDHAT_SUPPORT_PRODUCT="centos" +REDHAT_SUPPORT_PRODUCT_VERSION="8" + diff --git a/imgbom/distro/test-fixtures/debian-8 b/imgbom/distro/test-fixtures/debian-8 new file mode 100644 index 000000000..120c51b08 --- /dev/null +++ b/imgbom/distro/test-fixtures/debian-8 @@ -0,0 +1,8 @@ +PRETTY_NAME="Debian GNU/Linux 8 (jessie)" +NAME="Debian GNU/Linux" +VERSION_ID="8" +VERSION="8 (jessie)" +ID=debian +HOME_URL="http://www.debian.org/" +SUPPORT_URL="http://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" diff --git a/imgbom/distro/test-fixtures/rhel-8 b/imgbom/distro/test-fixtures/rhel-8 new file mode 100644 index 000000000..216c1d226 --- /dev/null +++ b/imgbom/distro/test-fixtures/rhel-8 @@ -0,0 +1,16 @@ +NAME="Red Hat Enterprise Linux" +VERSION="8.1 (Ootpa)" +ID="rhel" +ID_LIKE="fedora" +VERSION_ID="8.1" +PLATFORM_ID="platform:el8" +PRETTY_NAME="Red Hat Enterprise Linux 8.1 (Ootpa)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:redhat:enterprise_linux:8.1:GA" +HOME_URL="https://www.redhat.com/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" + +REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8" +REDHAT_BUGZILLA_PRODUCT_VERSION=8.1 +REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" +REDHAT_SUPPORT_PRODUCT_VERSION="8.1" diff --git a/imgbom/distro/test-fixtures/ubuntu-20.04 b/imgbom/distro/test-fixtures/ubuntu-20.04 new file mode 100644 index 000000000..e07dd980d --- /dev/null +++ b/imgbom/distro/test-fixtures/ubuntu-20.04 @@ -0,0 +1,12 @@ +NAME="Ubuntu" +VERSION="20.04 LTS (Focal Fossa)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 20.04 LTS" +VERSION_ID="20.04" +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +VERSION_CODENAME=focal +UBUNTU_CODENAME=focal