diff --git a/go.mod b/go.mod index 025671758d1..3d62b78882d 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 github.com/anchore/stereoscope v0.0.0-20230323161519-d7551b7f46f5 + github.com/deitch/magic v0.0.0-20230323094151-ad691daf393c github.com/docker/docker v23.0.1+incompatible github.com/google/go-containerregistry v0.14.0 github.com/google/licensecheck v0.3.1 diff --git a/go.sum b/go.sum index 8e9d7fb0510..ddb4a46a17a 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deitch/magic v0.0.0-20230323094151-ad691daf393c h1:oV9G+Jq+pW7pll/BWcZjbWB9RpNsJQ7v9ynrW6Z6tE8= +github.com/deitch/magic v0.0.0-20230323094151-ad691daf393c/go.mod h1:D5iaOhreaX/4N2s6RVZ7N1HQmqCatksnJHtatAdeq3Q= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= diff --git a/syft/pkg/cataloger/binary/classifier.go b/syft/pkg/cataloger/binary/classifier.go index 458b2857770..5ecbffd6843 100644 --- a/syft/pkg/cataloger/binary/classifier.go +++ b/syft/pkg/cataloger/binary/classifier.go @@ -19,6 +19,7 @@ import ( "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader" "github.com/anchore/syft/syft/source" + "github.com/deitch/magic/pkg/magic" ) var emptyPURL = packageurl.PackageURL{} @@ -124,6 +125,48 @@ func fileContentsVersionMatcher(pattern string) evidenceMatcher { } } +type versionFinder func([]string) string + +func fileTypeMatcher(filetype string, finder versionFinder) evidenceMatcher { + return func(resolver source.FileResolver, classifier classifier, location source.Location) ([]pkg.Package, error) { + reader, err := resolver.FileContentsByLocation(location) + if err != nil { + return nil, err + } + unionReader, err := unionreader.GetUnionReader(reader) + if err != nil { + return nil, fmt.Errorf("unable to get union reader for file: %w", err) + } + magicType, err := magic.GetType(unionReader) + if err != nil { + return nil, fmt.Errorf("unable to get magic type for file: %w", err) + } + if len(magicType) < 1 || magicType[0] != filetype { + return nil, nil + } + var version string + if finder != nil { + version = finder(magicType) + } + matchMetadata := map[string]string{ + "version": version, + } + + return singlePackage(classifier, location, matchMetadata), nil + } +} + +func prefixVersionFinder(prefix string) versionFinder { + return func(magicType []string) string { + for _, t := range magicType { + if strings.HasPrefix(t, prefix) { + return strings.TrimPrefix(t, prefix) + } + } + return "" + } +} + //nolint:gocognit func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher evidenceMatcher) evidenceMatcher { pat := regexp.MustCompile(sharedLibraryPattern) diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index dfc91bb5539..da91a9218cf 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -23,6 +23,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/haskell" "github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/javascript" + "github.com/anchore/syft/syft/pkg/cataloger/kernel" "github.com/anchore/syft/syft/pkg/cataloger/php" "github.com/anchore/syft/syft/pkg/cataloger/portage" "github.com/anchore/syft/syft/pkg/cataloger/python" @@ -85,6 +86,8 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger { binary.NewCataloger(), elixir.NewMixLockCataloger(), erlang.NewRebarLockCataloger(), + kernel.NewKernelCataloger(cfg.Kernel()), + kernel.NewKernelModuleCataloger(cfg.Kernel()), }, cfg.Catalogers) } @@ -121,6 +124,8 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { binary.NewCataloger(), elixir.NewMixLockCataloger(), erlang.NewRebarLockCataloger(), + kernel.NewKernelCataloger(cfg.Kernel()), + kernel.NewKernelModuleCataloger(cfg.Kernel()), }, cfg.Catalogers) } diff --git a/syft/pkg/cataloger/config.go b/syft/pkg/cataloger/config.go index 2074f16e45e..e69fdee2cda 100644 --- a/syft/pkg/cataloger/config.go +++ b/syft/pkg/cataloger/config.go @@ -3,11 +3,13 @@ package cataloger import ( "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/java" + "github.com/anchore/syft/syft/pkg/cataloger/kernel" ) type Config struct { Search SearchConfig Golang golang.GoCatalogerOpts + KernelOpts kernel.KernelCatalogerOpts Catalogers []string Parallelism int } @@ -29,3 +31,7 @@ func (c Config) Java() java.Config { func (c Config) Go() golang.GoCatalogerOpts { return c.Golang } + +func (c Config) Kernel() kernel.KernelCatalogerOpts { + return c.KernelOpts +} diff --git a/syft/pkg/cataloger/kernel/cataloger.go b/syft/pkg/cataloger/kernel/cataloger.go new file mode 100644 index 00000000000..bcbafc03f19 --- /dev/null +++ b/syft/pkg/cataloger/kernel/cataloger.go @@ -0,0 +1,52 @@ +/* +Package kernel provides a concrete Cataloger implementation for linux kernel and module files. +*/ +package kernel + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +type KernelCatalogerOpts struct { + KernelFilenameAppends []string + KernelModuleFilenameAppends []string +} + +var kernelFiles = []string{ + "kernel", + "kernel-*", + "vmlinux", + "vmlinux-*", + "vmlinuz", + "vmlinuz-*", +} + +var kernelModuleFiles = []string{ + "*.ko", +} + +// NewKernelCataloger returns a new kernel files cataloger object. +func NewKernelCataloger(opts KernelCatalogerOpts) *generic.Cataloger { + var fileList []string + for _, file := range kernelFiles { + fileList = append(fileList, "**/"+file) + } + for _, file := range opts.KernelFilenameAppends { + fileList = append(fileList, "**/"+file) + } + return generic.NewCataloger("linux-kernel-cataloger"). + WithParserByGlobs(parseKernelFile, fileList...) +} + +// NewKernelModuleCataloger returns a new kernel module files cataloger object. +func NewKernelModuleCataloger(opts KernelCatalogerOpts) *generic.Cataloger { + var fileList []string + for _, file := range kernelModuleFiles { + fileList = append(fileList, "**/"+file) + } + for _, file := range opts.KernelModuleFilenameAppends { + fileList = append(fileList, "**/"+file) + } + return generic.NewCataloger("linux-kernel-module-cataloger"). + WithParserByGlobs(parseKernelModuleFile, fileList...) +} diff --git a/syft/pkg/cataloger/kernel/consts.go b/syft/pkg/cataloger/kernel/consts.go new file mode 100644 index 00000000000..807a889ae8e --- /dev/null +++ b/syft/pkg/cataloger/kernel/consts.go @@ -0,0 +1,8 @@ +package kernel + +const ( + linuxKernelName = "Linux kernel" + linuxKernelVersionPrefix = "version " + packageName = "linux-kernel" + modinfoName = ".modinfo" +) diff --git a/syft/pkg/cataloger/kernel/package.go b/syft/pkg/cataloger/kernel/package.go new file mode 100644 index 00000000000..b79835507ce --- /dev/null +++ b/syft/pkg/cataloger/kernel/package.go @@ -0,0 +1,27 @@ +package kernel + +import ( + "strings" + + "github.com/anchore/packageurl-go" +) + +// packageURL returns the PURL for the specific Kernel package (see https://github.com/package-url/purl-spec) +func packageURL(name, version string) string { + var namespace string + + fields := strings.SplitN(name, "/", 2) + if len(fields) > 1 { + namespace = fields[0] + name = fields[1] + } + + return packageurl.NewPackageURL( + packageurl.TypeGeneric, + namespace, + name, + version, + nil, + "", + ).ToString() +} diff --git a/syft/pkg/cataloger/kernel/parse_kernel_file.go b/syft/pkg/cataloger/kernel/parse_kernel_file.go new file mode 100644 index 00000000000..094a430651a --- /dev/null +++ b/syft/pkg/cataloger/kernel/parse_kernel_file.go @@ -0,0 +1,93 @@ +package kernel + +import ( + "fmt" + "strconv" + "strings" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader" + "github.com/anchore/syft/syft/source" + "github.com/deitch/magic/pkg/magic" +) + +func parseKernelFile(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + unionReader, err := unionreader.GetUnionReader(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to get union reader for file: %w", err) + } + magicType, err := magic.GetType(unionReader) + if err != nil { + return nil, nil, fmt.Errorf("unable to get magic type for file: %w", err) + } + if len(magicType) < 1 || magicType[0] != linuxKernelName { + return nil, nil, nil + } + metadata := parseKernelMetadata(magicType) + if metadata.Version == "" { + return nil, nil, nil + } + p := pkg.Package{ + Name: packageName, + Version: metadata.Version, + PURL: packageURL(packageName, metadata.Version), + Type: pkg.KernelPkg, + MetadataType: pkg.KernelPackageMetadataType, + Metadata: metadata, + } + + p.SetID() + return []pkg.Package{p}, nil, nil +} + +func parseKernelMetadata(magicType []string) (p pkg.KernelPackageMetadata) { + // Linux kernel x86 boot executable bzImage, + // version 5.10.121-linuxkit (root@buildkitsandbox) #1 SMP Fri Dec 2 10:35:42 UTC 2022, + // RO-rootFS, + // swap_dev 0XA, + // Normal VGA + for _, t := range magicType { + switch { + case strings.HasPrefix(t, "x86 "): + p.Architecture = "x86" + case strings.Contains(t, "ARM64 "): + p.Architecture = "arm64" + case strings.Contains(t, "ARM "): + p.Architecture = "arm" + case t == "bzImage": + p.Format = "bzImage" + case t == "zImage": + p.Format = "zImage" + case strings.HasPrefix(t, "version "): + p.ExtendedVersion = strings.TrimPrefix(t, "version ") + fields := strings.Fields(p.ExtendedVersion) + if len(fields) > 0 { + p.Version = fields[0] + } + case strings.Contains(t, "rootFS") && strings.HasPrefix(t, "RW-"): + p.RWRootFS = true + case strings.HasPrefix(t, "swap_dev "): + swapDevStr := strings.TrimPrefix(t, "swap_dev ") + swapDev, err := strconv.ParseInt(swapDevStr, 16, 32) + if err != nil { + log.Warnf("unable to parse swap device: %s", err) + continue + } + p.SwapDevice = int(swapDev) + case strings.HasPrefix(t, "root_dev "): + rootDevStr := strings.TrimPrefix(t, "root_dev ") + rootDev, err := strconv.ParseInt(rootDevStr, 16, 32) + if err != nil { + log.Warnf("unable to parse root device: %s", err) + continue + } + p.SwapDevice = int(rootDev) + case strings.Contains(t, "VGA") || strings.Contains(t, "Video"): + p.VideoMode = t + } + } + return +} diff --git a/syft/pkg/cataloger/kernel/parse_kernel_module_file.go b/syft/pkg/cataloger/kernel/parse_kernel_module_file.go new file mode 100644 index 00000000000..02bcbbf7cc8 --- /dev/null +++ b/syft/pkg/cataloger/kernel/parse_kernel_module_file.go @@ -0,0 +1,182 @@ +package kernel + +import ( + "debug/elf" + "fmt" + "strings" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader" + "github.com/anchore/syft/syft/source" +) + +type parameter struct { + description string + ptype string +} +type kernelModuleMetadata struct { + kernelVersion string + versionMagic string + sourceVersion string + version string + author string + license string + name string + description string + parameters map[string]parameter +} + +type kvpair struct { + key string + value string +} + +func parseKernelModuleFile(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + unionReader, err := unionreader.GetUnionReader(reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to get union reader for file: %w", err) + } + metadata, err := parseKernelModuleMetadata(unionReader) + if err != nil { + return nil, nil, fmt.Errorf("unable to parse kernel module metadata: %w", err) + } + if metadata.kernelVersion == "" { + return nil, nil, nil + } + p := pkg.Package{ + Name: packageName, + Version: metadata.kernelVersion, + PURL: packageURL(packageName, metadata.kernelVersion), + Type: pkg.KernelPkg, + } + + p.SetID() + return []pkg.Package{p}, nil, nil +} + +func parseKernelModuleMetadata(r unionreader.UnionReader) (p *kernelModuleMetadata, err error) { + // filename: /lib/modules/5.15.0-1031-aws/kernel/zfs/zzstd.ko + // version: 1.4.5a + // license: Dual BSD/GPL + // description: ZSTD Compression for ZFS + // srcversion: F1F818A6E016499AB7F826E + // depends: spl + // retpoline: Y + // name: zzstd + // vermagic: 5.15.0-1031-aws SMP mod_unload modversions + // sig_id: PKCS#7 + // signer: Build time autogenerated kernel key + // sig_key: 49:A9:55:87:90:5B:33:41:AF:C0:A7:BE:2A:71:6C:D2:CA:34:E0:AE + // sig_hashalgo: sha512 + // + // OR + // + // filename: /home/ubuntu/eve/rootfs/lib/modules/5.10.121-linuxkit/kernel/drivers/net/wireless/realtek/rtl8821cu/8821cu.ko + // version: v5.4.1_28754.20180921_COEX20180712-3232 + // author: Realtek Semiconductor Corp. + // description: Realtek Wireless Lan Driver + // license: GPL + // srcversion: 960CCC648A0E0369171A2C9 + // depends: cfg80211 + // retpoline: Y + // name: 8821cu + // vermagic: 5.10.121-linuxkit SMP mod_unload + p = &kernelModuleMetadata{ + parameters: make(map[string]parameter), + } + f, err := elf.NewFile(r) + if err != nil { + return nil, err + } + defer f.Close() + modinfo := f.Section(modinfoName) + if modinfo == nil { + return nil, fmt.Errorf("no section %s", modinfoName) + } + b, err := modinfo.Data() + if err != nil { + return nil, fmt.Errorf("error reading secion %s: %w", modinfoName, err) + } + var ( + entry []byte + pairs []kvpair + ) + for _, b2 := range b { + if b2 == 0 { + if len(entry) > 0 { + var key, value string + parts := strings.SplitN(string(entry), "=", 2) + if len(parts) > 0 { + key = parts[0] + } + if len(parts) > 1 { + value = parts[1] + } + if key != "" { + pairs = append(pairs, kvpair{key: key, value: value}) + } + } + entry = []byte{} + continue + } + entry = append(entry, b2) + } + if len(entry) != 0 { + var key, value string + parts := strings.SplitN(string(entry), "=", 2) + if len(parts) > 0 { + key = parts[0] + } + if len(parts) > 1 { + value = parts[1] + } + if key != "" { + pairs = append(pairs, kvpair{key: key, value: value}) + } + } + for _, entry := range pairs { + switch entry.key { + case "version": + p.version = entry.value + case "license": + p.license = entry.value + case "author": + p.author = entry.value + case "name": + p.name = entry.value + case "vermagic": + p.versionMagic = entry.value + fields := strings.Fields(entry.value) + if len(fields) > 0 { + p.kernelVersion = fields[0] + } + case "srcversion": + p.sourceVersion = entry.value + case "description": + p.description = entry.value + case "parm": + parts := strings.SplitN(entry.value, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid parm entry: %s", entry.value) + } + if m, ok := p.parameters[parts[0]]; !ok { + p.parameters[parts[0]] = parameter{description: parts[1]} + } else { + m.description = parts[1] + } + case "parmtype": + parts := strings.SplitN(entry.value, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid parmtype entry: %s", entry.value) + } + if m, ok := p.parameters[parts[0]]; !ok { + p.parameters[parts[0]] = parameter{ptype: parts[1]} + } else { + m.ptype = parts[1] + } + } + } + return +} diff --git a/syft/pkg/kernel_package_metadata.go b/syft/pkg/kernel_package_metadata.go new file mode 100644 index 00000000000..d1e165a3907 --- /dev/null +++ b/syft/pkg/kernel_package_metadata.go @@ -0,0 +1,16 @@ +package pkg + +// KernelPackageMetadata represents all captured data for a Linux kernel +type KernelPackageMetadata struct { + Name string `mapstructure:"name" json:"name"` + Architecture string `mapstructure:"architecture" json:"architecture"` + Version string `mapstructure:"version" json:"version"` + ExtendedVersion string `mapstructure:"extendedVersion" json:"extendedVersion,omitempty"` + BuildTime string `mapstructure:"buildTime" json:"buildTime,omitempty"` + Author string `mapstructure:"author" json:"author,omitempty"` + Format string `mapstructure:"format" json:"format,omitempty"` + RWRootFS bool `mapstructure:"rwRootFS" json:"rwRootFS,omitempty"` + SwapDevice int `mapstructure:"swapDevice" json:"swapDevice,omitempty"` + RootDevice int `mapstructure:"rootDevice" json:"rootDevice,omitempty"` + VideoMode string `mapstructure:"videoMode" json:"videoMode,omitempty"` +} diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index 3fff9e5d729..2675bb6be52 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -25,6 +25,7 @@ const ( HackageMetadataType MetadataType = "HackageMetadataType" JavaMetadataType MetadataType = "JavaMetadata" KbPackageMetadataType MetadataType = "KbPackageMetadata" + KernelPackageMetadataType MetadataType = "KernelPackageMetadata" MixLockMetadataType MetadataType = "MixLockMetadataType" NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata" NpmPackageLockJSONMetadataType MetadataType = "NpmPackageLockJsonMetadata" @@ -53,6 +54,7 @@ var AllMetadataTypes = []MetadataType{ HackageMetadataType, JavaMetadataType, KbPackageMetadataType, + KernelPackageMetadataType, MixLockMetadataType, NpmPackageJSONMetadataType, NpmPackageLockJSONMetadataType, @@ -81,6 +83,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{ HackageMetadataType: reflect.TypeOf(HackageMetadata{}), JavaMetadataType: reflect.TypeOf(JavaMetadata{}), KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}), + KernelPackageMetadataType: reflect.TypeOf(KernelPackageMetadata{}), MixLockMetadataType: reflect.TypeOf(MixLockMetadata{}), NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}), NpmPackageLockJSONMetadataType: reflect.TypeOf(NpmPackageLockJSONMetadata{}), diff --git a/syft/pkg/type.go b/syft/pkg/type.go index a275ed14035..cc814971a37 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -26,6 +26,7 @@ const ( JavaPkg Type = "java-archive" JenkinsPluginPkg Type = "jenkins-plugin" KbPkg Type = "msrc-kb" + KernelPkg Type = "linux-kernel" NpmPkg Type = "npm" PhpComposerPkg Type = "php-composer" PortagePkg Type = "portage" @@ -51,6 +52,7 @@ var AllPkgs = []Type{ JavaPkg, JenkinsPluginPkg, KbPkg, + KernelPkg, NpmPkg, PhpComposerPkg, PortagePkg, @@ -86,6 +88,8 @@ func (t Type) PackageURLType() string { return packageurl.TypeHackage case JavaPkg, JenkinsPluginPkg: return packageurl.TypeMaven + case KernelPkg: + return "linux-kernel" case PhpComposerPkg: return packageurl.TypeComposer case PythonPkg: @@ -151,6 +155,8 @@ func TypeByName(name string) Type { return PortagePkg case packageurl.TypeHex: return HexPkg + case "linux-kernel": + return KernelPkg default: return UnknownPkg }