Merge pull request #57 from anchore/issue-33

Initial take on distro detection
This commit is contained in:
Alfredo Deza 2020-06-12 15:58:36 -04:00 committed by GitHub
commit 812934c964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 334 additions and 26 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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/"

View File

@ -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"

View File

@ -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

View File

@ -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,
}

View File

@ -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)
}