Add Linux Kernel cataloger (#1694)

* add kernel handler

Signed-off-by: Avi Deitcher <avi@deitcher.net>

* [wip] combine kernel and kernel module cataloging

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* [wip] combine kernel and kernel module cataloging

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Signed-off-by: Avi Deitcher <avi@deitcher.net>

* rename Kernel package to LinuxKernel package

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* split kernel and module packages within cataloger

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* wire up application configuration with kernel cataloger options

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* dont use references for packages on relationships

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix linting and tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* kernel cataloger should be resistent to partial failure

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* log upon kernel module metadata missing

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add tests for linux kernel cataloger

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update integration tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update cli package test counts

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add evidence annotations for kernel packages

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* reduce noise in cli test output

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* missed cli test to reduce noise for

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix package counts

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update docs with linux kernel cataloging refs

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* bump json schema with new metadata fields

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

---------

Signed-off-by: Avi Deitcher <avi@deitcher.net>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Signed-off-by: <>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Avi Deitcher 2023-04-14 21:33:36 +03:00 committed by GitHub
parent 5d156b8241
commit cc731c7b19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2516 additions and 78 deletions

View file

@ -45,6 +45,8 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- Java (jar, ear, war, par, sar, nar, native-image)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- Linux kernel archives (vmlinz)
- Linux kernel modules (ko)
- Nix (outputs in /nix/store)
- PHP (composer)
- Python (wheel, egg, poetry, requirements.txt)
@ -513,6 +515,11 @@ golang:
# SYFT_GOLANG_LOCAL_MOD_CACHE_DIR env var
local-mod-cache-dir: ""
linux-kernel:
# whether to catalog linux kernel modules found within lib/modules/** directories
# SYFT_LINUX_KERNEL_CATALOG_MODULES env var
catalog-modules: true
# cataloging file contents is exposed through the power-user subcommand
file-contents:
cataloger:

1
go.mod
View file

@ -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-20230406143206-e95d60a265e3
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/docker/docker v23.0.3+incompatible
github.com/google/go-containerregistry v0.14.0
github.com/google/licensecheck v0.3.1

2
go.sum
View file

@ -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-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
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=

View file

@ -19,6 +19,7 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger"
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
)
var (
@ -50,6 +51,7 @@ type Application struct {
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
@ -76,6 +78,9 @@ func (cfg Application) ToCatalogerConfig() cataloger.Config {
SearchLocalModCacheLicenses: cfg.Golang.SearchLocalModCacheLicenses,
LocalModCacheDir: cfg.Golang.LocalModCacheDir,
},
LinuxKernel: kernel.LinuxCatalogerConfig{
CatalogModules: cfg.LinuxKernel.CatalogModules,
},
}
}

View file

@ -0,0 +1,11 @@
package config
import "github.com/spf13/viper"
type linuxKernel struct {
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
}
func (cfg linuxKernel) loadDefaultValues(v *viper.Viper) {
v.SetDefault("linux-kernel.catalog-modules", true)
}

View file

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "7.1.2"
JSONSchemaVersion = "7.1.3"
)

View file

@ -45,6 +45,8 @@ type artifactMetadataContainer struct {
Hackage pkg.HackageMetadata
Java pkg.JavaMetadata
KbPackage pkg.KbPackageMetadata
LinuxKernel pkg.LinuxKernelMetadata
LinuxKernelModule pkg.LinuxKernelModuleMetadata
Nix pkg.NixStoreMetadata
NpmPackage pkg.NpmPackageJSONMetadata
NpmPackageLock pkg.NpmPackageLockJSONMetadata

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg"
)
//nolint:funlen
func SourceInfo(p pkg.Package) string {
answer := ""
switch p.Type {
@ -45,6 +46,10 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from cabal or stack manifest files"
case pkg.HexPkg:
answer = "acquired package info from rebar3 or mix manifest file"
case pkg.LinuxKernelPkg:
answer = "acquired package info from linux kernel archive"
case pkg.LinuxKernelModulePkg:
answer = "acquired package info from linux kernel module files"
case pkg.NixPkg:
answer = "acquired package info from nix store path"
default:

View file

@ -199,6 +199,22 @@ func Test_SourceInfo(t *testing.T) {
"from rebar3 or mix manifest file",
},
},
{
input: pkg.Package{
Type: pkg.LinuxKernelPkg,
},
expected: []string{
"from linux kernel archive",
},
},
{
input: pkg.Package{
Type: pkg.LinuxKernelModulePkg,
},
expected: []string{
"from linux kernel module files",
},
},
{
input: pkg.Package{
Type: pkg.NixPkg,

View file

@ -89,7 +89,7 @@
}
},
"schema": {
"version": "7.1.2",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.2.json"
"version": "7.1.3",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.3.json"
}
}

View file

@ -185,7 +185,7 @@
}
},
"schema": {
"version": "7.1.2",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.2.json"
"version": "7.1.3",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.3.json"
}
}

View file

@ -112,7 +112,7 @@
}
},
"schema": {
"version": "7.1.2",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.2.json"
"version": "7.1.3",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.3.json"
}
}

View file

@ -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/nix"
"github.com/anchore/syft/syft/pkg/cataloger/php"
"github.com/anchore/syft/syft/pkg/cataloger/portage"
@ -55,6 +56,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger {
nix.NewStoreCataloger(),
sbom.NewSBOMCataloger(),
binary.NewCataloger(),
kernel.NewLinuxKernelCataloger(cfg.Kernel()),
}, cfg.Catalogers)
}
@ -88,6 +90,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
binary.NewCataloger(),
elixir.NewMixLockCataloger(),
erlang.NewRebarLockCataloger(),
kernel.NewLinuxKernelCataloger(cfg.Kernel()),
nix.NewStoreCataloger(),
}, cfg.Catalogers)
}
@ -126,6 +129,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
binary.NewCataloger(),
elixir.NewMixLockCataloger(),
erlang.NewRebarLockCataloger(),
kernel.NewLinuxKernelCataloger(cfg.Kernel()),
nix.NewStoreCataloger(),
}, cfg.Catalogers)
}

View file

@ -3,11 +3,15 @@ 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"
)
// TODO: these field naming vs helper function naming schemes are inconsistent.
type Config struct {
Search SearchConfig
Golang golang.GoCatalogerOpts
LinuxKernel kernel.LinuxCatalogerConfig
Catalogers []string
Parallelism int
}
@ -16,6 +20,7 @@ func DefaultConfig() Config {
return Config{
Search: DefaultSearchConfig(),
Parallelism: 1,
LinuxKernel: kernel.DefaultLinuxCatalogerConfig(),
}
}
@ -29,3 +34,7 @@ func (c Config) Java() java.Config {
func (c Config) Go() golang.GoCatalogerOpts {
return c.Golang
}
func (c Config) Kernel() kernel.LinuxCatalogerConfig {
return c.LinuxKernel
}

View file

@ -0,0 +1,127 @@
/*
Package kernel provides a concrete Cataloger implementation for linux kernel and module files.
*/
package kernel
import (
"github.com/hashicorp/go-multierror"
"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/source"
)
var _ pkg.Cataloger = (*LinuxKernelCataloger)(nil)
type LinuxCatalogerConfig struct {
CatalogModules bool
}
type LinuxKernelCataloger struct {
cfg LinuxCatalogerConfig
}
func DefaultLinuxCatalogerConfig() LinuxCatalogerConfig {
return LinuxCatalogerConfig{
CatalogModules: true,
}
}
var kernelArchiveGlobs = []string{
"**/kernel",
"**/kernel-*",
"**/vmlinux",
"**/vmlinux-*",
"**/vmlinuz",
"**/vmlinuz-*",
}
var kernelModuleGlobs = []string{
"**/lib/modules/**/*.ko",
}
// NewLinuxKernelCataloger returns a new kernel files cataloger object.
func NewLinuxKernelCataloger(cfg LinuxCatalogerConfig) *LinuxKernelCataloger {
return &LinuxKernelCataloger{
cfg: cfg,
}
}
func (l LinuxKernelCataloger) Name() string {
return "linux-kernel-cataloger"
}
func (l LinuxKernelCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
var allPackages []pkg.Package
var allRelationships []artifact.Relationship
var errs error
kernelPackages, kernelRelationships, err := generic.NewCataloger(l.Name()).WithParserByGlobs(parseLinuxKernelFile, kernelArchiveGlobs...).Catalog(resolver)
if err != nil {
errs = multierror.Append(errs, err)
}
allRelationships = append(allRelationships, kernelRelationships...)
allPackages = append(allPackages, kernelPackages...)
if l.cfg.CatalogModules {
modulePackages, moduleRelationships, err := generic.NewCataloger(l.Name()).WithParserByGlobs(parseLinuxKernelModuleFile, kernelModuleGlobs...).Catalog(resolver)
if err != nil {
errs = multierror.Append(errs, err)
}
allPackages = append(allPackages, modulePackages...)
moduleToKernelRelationships := createKernelToModuleRelationships(kernelPackages, modulePackages)
allRelationships = append(allRelationships, moduleRelationships...)
allRelationships = append(allRelationships, moduleToKernelRelationships...)
}
return allPackages, allRelationships, errs
}
func createKernelToModuleRelationships(kernelPackages, modulePackages []pkg.Package) []artifact.Relationship {
// organize kernel and module packages by kernel version
kernelPackagesByVersion := make(map[string][]*pkg.Package)
for idx, p := range kernelPackages {
kernelPackagesByVersion[p.Version] = append(kernelPackagesByVersion[p.Version], &kernelPackages[idx])
}
modulesByKernelVersion := make(map[string][]*pkg.Package)
for idx, p := range modulePackages {
m, ok := p.Metadata.(pkg.LinuxKernelModuleMetadata)
if !ok {
log.Debug("linux-kernel-module package found without metadata: %s@%s", p.Name, p.Version)
continue
}
modulesByKernelVersion[m.KernelVersion] = append(modulesByKernelVersion[m.KernelVersion], &modulePackages[idx])
}
// create relationships between kernel and modules: [module] --(depends on)--> [kernel]
// since we try to use singular directions for relationships, we'll use "dependency of" here instead:
// [kernel] --(dependency of)--> [module]
var moduleToKernelRelationships []artifact.Relationship
for kernelVersion, modules := range modulesByKernelVersion {
kps, ok := kernelPackagesByVersion[kernelVersion]
if !ok {
// it's ok if there is a module that has no installed kernel...
continue
}
// we don't know which kernel is the "right" one, so we'll create a relationship for each one
for _, kp := range kps {
for _, mp := range modules {
moduleToKernelRelationships = append(moduleToKernelRelationships, artifact.Relationship{
// note: relationships should have Package objects, not pointers
From: *kp,
To: *mp,
Type: artifact.DependencyOfRelationship,
})
}
}
}
return moduleToKernelRelationships
}

View file

@ -0,0 +1,92 @@
package kernel
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/source"
)
func Test_JavascriptCataloger(t *testing.T) {
kernelPkg := pkg.Package{
Name: "linux-kernel",
Version: "6.2.9-200.fc37.x86_64",
FoundBy: "linux-kernel-cataloger",
Locations: source.NewLocationSet(
source.NewVirtualLocation(
"/lib/modules/6.2.9-200.fc37.x86_64/vmlinuz",
"/lib/modules/6.2.9-200.fc37.x86_64/vmlinuz",
),
),
Type: pkg.LinuxKernelPkg,
PURL: "pkg:generic/linux-kernel@6.2.9-200.fc37.x86_64",
MetadataType: pkg.LinuxKernelMetadataType,
Metadata: pkg.LinuxKernelMetadata{
Name: "",
Architecture: "x86",
Version: "6.2.9-200.fc37.x86_64",
ExtendedVersion: "6.2.9-200.fc37.x86_64 (mockbuild@bkernel02.iad2.fedoraproject.org) #1 SMP PREEMPT_DYNAMIC Thu Mar 30 22:31:57 UTC 2023",
BuildTime: "",
Author: "",
Format: "bzImage",
RWRootFS: false,
SwapDevice: 0,
RootDevice: 0,
VideoMode: "Video mode 65535",
},
}
kernelModulePkg := pkg.Package{
Name: "fsa4480",
Version: "",
FoundBy: "linux-kernel-cataloger",
Locations: source.NewLocationSet(
source.NewVirtualLocation("/lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko",
"/lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko",
),
),
Licenses: []string{
"GPL v2",
},
Type: pkg.LinuxKernelModulePkg,
PURL: "pkg:generic/fsa4480",
MetadataType: pkg.LinuxKernelModuleMetadataType,
Metadata: pkg.LinuxKernelModuleMetadata{
Name: "fsa4480",
Version: "",
SourceVersion: "",
License: "GPL v2",
Path: "/lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko",
Description: "ON Semiconductor FSA4480 driver",
KernelVersion: "6.2.9-200.fc37.x86_64",
VersionMagic: "6.2.9-200.fc37.x86_64 SMP preempt mod_unload ",
Parameters: map[string]pkg.LinuxKernelModuleParameter{},
},
}
expectedPkgs := []pkg.Package{
kernelPkg,
kernelModulePkg,
}
expectedRelationships := []artifact.Relationship{
{
From: kernelPkg,
To: kernelModulePkg,
Type: artifact.DependencyOfRelationship,
},
}
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-kernel-and-modules").
IgnoreLocationLayer().
Expects(expectedPkgs, expectedRelationships).
TestCataloger(t,
NewLinuxKernelCataloger(
LinuxCatalogerConfig{
CatalogModules: true,
},
),
)
}

View file

@ -0,0 +1,71 @@
package kernel
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
const linuxKernelPackageName = "linux-kernel"
func newLinuxKernelPackage(metadata pkg.LinuxKernelMetadata, locations ...source.Location) pkg.Package {
p := pkg.Package{
Name: linuxKernelPackageName,
Version: metadata.Version,
Locations: source.NewLocationSet(locations...),
PURL: packageURL(linuxKernelPackageName, metadata.Version),
Type: pkg.LinuxKernelPkg,
MetadataType: pkg.LinuxKernelMetadataType,
Metadata: metadata,
}
p.SetID()
return p
}
func newLinuxKernelModulePackage(metadata pkg.LinuxKernelModuleMetadata, locations ...source.Location) pkg.Package {
var licenses []string
if metadata.License != "" {
licenses = []string{metadata.License}
} else {
licenses = []string{}
}
p := pkg.Package{
Name: metadata.Name,
Version: metadata.Version,
Locations: source.NewLocationSet(locations...),
Licenses: licenses,
PURL: packageURL(metadata.Name, metadata.Version),
Type: pkg.LinuxKernelModulePkg,
MetadataType: pkg.LinuxKernelModuleMetadataType,
Metadata: metadata,
}
p.SetID()
return p
}
// 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()
}

View file

@ -0,0 +1,92 @@
package kernel
import (
"fmt"
"strconv"
"strings"
"github.com/deitch/magic/pkg/magic"
"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"
)
const linuxKernelMagicName = "Linux kernel"
func parseLinuxKernelFile(_ 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] != linuxKernelMagicName {
return nil, nil, nil
}
metadata := parseLinuxKernelMetadata(magicType)
if metadata.Version == "" {
return nil, nil, nil
}
return []pkg.Package{
newLinuxKernelPackage(
metadata,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
}, nil, nil
}
func parseLinuxKernelMetadata(magicType []string) (p pkg.LinuxKernelMetadata) {
// 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 p
}

View file

@ -0,0 +1,157 @@
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"
)
const modinfoName = ".modinfo"
func parseLinuxKernelModuleFile(_ 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 := parseLinuxKernelModuleMetadata(unionReader)
if err != nil {
return nil, nil, fmt.Errorf("unable to parse kernel module metadata: %w", err)
}
if metadata == nil || metadata.KernelVersion == "" {
return nil, nil, nil
}
metadata.Path = reader.Location.RealPath
return []pkg.Package{
newLinuxKernelModulePackage(
*metadata,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
}, nil, nil
}
func parseLinuxKernelModuleMetadata(r unionreader.UnionReader) (p *pkg.LinuxKernelModuleMetadata, 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 = &pkg.LinuxKernelModuleMetadata{
Parameters: make(map[string]pkg.LinuxKernelModuleParameter),
}
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
)
for _, b2 := range b {
if b2 == 0 {
if err := addLinuxKernelModuleEntry(p, entry); err != nil {
return nil, fmt.Errorf("error parsing entry %s: %w", string(entry), err)
}
entry = []byte{}
continue
}
entry = append(entry, b2)
}
if err := addLinuxKernelModuleEntry(p, entry); err != nil {
return nil, fmt.Errorf("error parsing entry %s: %w", string(entry), err)
}
return p, nil
}
func addLinuxKernelModuleEntry(k *pkg.LinuxKernelModuleMetadata, entry []byte) error {
if len(entry) == 0 {
return nil
}
var key, value string
parts := strings.SplitN(string(entry), "=", 2)
if len(parts) > 0 {
key = parts[0]
}
if len(parts) > 1 {
value = parts[1]
}
switch key {
case "version":
k.Version = value
case "license":
k.License = value
case "author":
k.Author = value
case "name":
k.Name = value
case "vermagic":
k.VersionMagic = value
fields := strings.Fields(value)
if len(fields) > 0 {
k.KernelVersion = fields[0]
}
case "srcversion":
k.SourceVersion = value
case "description":
k.Description = value
case "parm":
parts := strings.SplitN(value, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid parm entry: %s", value)
}
if m, ok := k.Parameters[parts[0]]; !ok {
k.Parameters[parts[0]] = pkg.LinuxKernelModuleParameter{Description: parts[1]}
} else {
m.Description = parts[1]
}
case "parmtype":
parts := strings.SplitN(value, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid parmtype entry: %s", value)
}
if m, ok := k.Parameters[parts[0]]; !ok {
k.Parameters[parts[0]] = pkg.LinuxKernelModuleParameter{Type: parts[1]}
} else {
m.Type = parts[1]
}
}
return nil
}

View file

@ -0,0 +1,17 @@
FROM fedora:37@sha256:3f987b7657e944cf87a129cc262982d4f80e38bd98f7db313ccaf90ca7069dd2
RUN dnf install 'dnf-command(download)' cpio xz -y
RUN dnf download kernel-core kernel-modules-core -y
RUN rpm2cpio kernel-core-*.rpm | cpio -t && \
rpm2cpio kernel-core-*.rpm | cpio -idmv ./lib/modules/6.2.9-200.fc37.x86_64/vmlinuz
RUN rpm2cpio kernel-modules-core-*.rpm | cpio -t && \
rpm2cpio kernel-modules-core-*.rpm | cpio -idmv ./lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko.xz
RUN unxz /lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko.xz
FROM scratch
COPY --from=0 /lib/modules/6.2.9-200.fc37.x86_64/vmlinuz /lib/modules/6.2.9-200.fc37.x86_64/vmlinuz
COPY --from=0 /lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko /lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko

View file

@ -0,0 +1,34 @@
package pkg
// LinuxKernelMetadata represents all captured data for a Linux kernel
type LinuxKernelMetadata struct {
Name string `mapstructure:"name" json:"name" cyclonedx:"name"`
Architecture string `mapstructure:"architecture" json:"architecture" cyclonedx:"architecture"`
Version string `mapstructure:"version" json:"version" cyclonedx:"version"`
ExtendedVersion string `mapstructure:"extendedVersion" json:"extendedVersion,omitempty" cyclonedx:"extendedVersion"`
BuildTime string `mapstructure:"buildTime" json:"buildTime,omitempty" cyclonedx:"buildTime"`
Author string `mapstructure:"author" json:"author,omitempty" cyclonedx:"author"`
Format string `mapstructure:"format" json:"format,omitempty" cyclonedx:"format"`
RWRootFS bool `mapstructure:"rwRootFS" json:"rwRootFS,omitempty" cyclonedx:"rwRootFS"`
SwapDevice int `mapstructure:"swapDevice" json:"swapDevice,omitempty" cyclonedx:"swapDevice"`
RootDevice int `mapstructure:"rootDevice" json:"rootDevice,omitempty" cyclonedx:"rootDevice"`
VideoMode string `mapstructure:"videoMode" json:"videoMode,omitempty" cyclonedx:"videoMode"`
}
type LinuxKernelModuleMetadata struct {
Name string `mapstructure:"name" json:"name,omitempty" cyclonedx:"name"`
Version string `mapstructure:"version" json:"version,omitempty" cyclonedx:"version"`
SourceVersion string `mapstructure:"sourceVersion" json:"sourceVersion,omitempty" cyclonedx:"sourceVersion"`
Path string `mapstructure:"path" json:"path,omitempty" cyclonedx:"path"`
Description string `mapstructure:"description" json:"description,omitempty" cyclonedx:"description"`
Author string `mapstructure:"author" json:"author,omitempty" cyclonedx:"author"`
License string `mapstructure:"license" json:"license,omitempty" cyclonedx:"license"`
KernelVersion string `mapstructure:"kernelVersion" json:"kernelVersion,omitempty" cyclonedx:"kernelVersion"`
VersionMagic string `mapstructure:"versionMagic" json:"versionMagic,omitempty" cyclonedx:"versionMagic"`
Parameters map[string]LinuxKernelModuleParameter `mapstructure:"parameters" json:"parameters,omitempty" cyclonedx:"parameters"`
}
type LinuxKernelModuleParameter struct {
Type string `mapstructure:"type" json:"type,omitempty" cyclonedx:"type"`
Description string `mapstructure:"description" json:"description,omitempty" cyclonedx:"description"`
}

View file

@ -26,6 +26,8 @@ const (
HackageMetadataType MetadataType = "HackageMetadataType"
JavaMetadataType MetadataType = "JavaMetadata"
KbPackageMetadataType MetadataType = "KbPackageMetadata"
LinuxKernelMetadataType MetadataType = "LinuxKernelMetadata"
LinuxKernelModuleMetadataType MetadataType = "LinuxKernelModuleMetadata"
MixLockMetadataType MetadataType = "MixLockMetadataType"
NixStoreMetadataType MetadataType = "NixStoreMetadata"
NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata"
@ -55,6 +57,8 @@ var AllMetadataTypes = []MetadataType{
HackageMetadataType,
JavaMetadataType,
KbPackageMetadataType,
LinuxKernelMetadataType,
LinuxKernelModuleMetadataType,
MixLockMetadataType,
NixStoreMetadataType,
NpmPackageJSONMetadataType,
@ -84,6 +88,8 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
HackageMetadataType: reflect.TypeOf(HackageMetadata{}),
JavaMetadataType: reflect.TypeOf(JavaMetadata{}),
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
LinuxKernelMetadataType: reflect.TypeOf(LinuxKernelMetadata{}),
LinuxKernelModuleMetadataType: reflect.TypeOf(LinuxKernelModuleMetadata{}),
MixLockMetadataType: reflect.TypeOf(MixLockMetadata{}),
NixStoreMetadataType: reflect.TypeOf(NixStoreMetadata{}),
NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}),

View file

@ -26,6 +26,8 @@ const (
JavaPkg Type = "java-archive"
JenkinsPluginPkg Type = "jenkins-plugin"
KbPkg Type = "msrc-kb"
LinuxKernelPkg Type = "linux-kernel"
LinuxKernelModulePkg Type = "linux-kernel-module"
NixPkg Type = "nix"
NpmPkg Type = "npm"
PhpComposerPkg Type = "php-composer"
@ -52,6 +54,8 @@ var AllPkgs = []Type{
JavaPkg,
JenkinsPluginPkg,
KbPkg,
LinuxKernelPkg,
LinuxKernelModulePkg,
NixPkg,
NpmPkg,
PhpComposerPkg,
@ -88,6 +92,10 @@ func (t Type) PackageURLType() string {
return packageurl.TypeHackage
case JavaPkg, JenkinsPluginPkg:
return packageurl.TypeMaven
case LinuxKernelPkg:
return "generic/linux-kernel"
case LinuxKernelModulePkg:
return packageurl.TypeGeneric
case PhpComposerPkg:
return packageurl.TypeComposer
case PythonPkg:
@ -114,7 +122,11 @@ func TypeFromPURL(p string) Type {
return UnknownPkg
}
return TypeByName(purl.Type)
ptype := purl.Type
if ptype == "generic" {
ptype = purl.Name
}
return TypeByName(ptype)
}
func TypeByName(name string) Type {
@ -155,6 +167,10 @@ func TypeByName(name string) Type {
return PortagePkg
case packageurl.TypeHex:
return HexPkg
case "linux-kernel":
return LinuxKernelPkg
case "linux-kernel-module":
return LinuxKernelModulePkg
case "nix":
return NixPkg
default:

View file

@ -83,6 +83,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:hex/hpax/hpax@0.1.1",
expected: HexPkg,
},
{
purl: "pkg:generic/linux-kernel@5.10.15",
expected: LinuxKernelPkg,
},
{
purl: "pkg:nix/glibc@2.34?hash=h0cnbmfcn93xm5dg2x27ixhag1cwndga",
expected: NixPkg,
@ -101,6 +105,7 @@ func TestTypeFromPURL(t *testing.T) {
expectedTypes.Remove(string(JenkinsPluginPkg))
expectedTypes.Remove(string(PortagePkg))
expectedTypes.Remove(string(BinaryPkg))
expectedTypes.Remove(string(LinuxKernelModulePkg))
for _, test := range tests {
t.Run(string(test.expected), func(t *testing.T) {

View file

@ -55,11 +55,7 @@ func TestAllFormatsConvertable(t *testing.T) {
for _, traitFn := range assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}

View file

@ -2,7 +2,6 @@ package cli
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
@ -34,11 +33,7 @@ func TestAllFormatsExpressible(t *testing.T) {
for _, traitFn := range commonAssertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}

View file

@ -40,11 +40,7 @@ func TestConvertCmd(t *testing.T) {
for _, traitFn := range assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}

View file

@ -52,11 +52,7 @@ func TestValidCycloneDX(t *testing.T) {
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
validateCycloneDXJSON(t, stdout)
})
@ -95,11 +91,7 @@ func assertValidCycloneDX(tb testing.TB, stdout, stderr string, rc int) {
tb.Errorf("expected no validation failures for cyclonedx-cli but got rc=%d", rc)
}
if tb.Failed() {
tb.Log("STDOUT:\n", stdout)
tb.Log("STDERR:\n", stderr)
tb.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(tb, cmd, stdout, stderr)
}
// validate --input-format json --input-version v1_4 --input-file bom.json
@ -134,9 +126,5 @@ func validateCycloneDXJSON(t *testing.T, stdout string) {
t.Errorf("expected no validation failures for cyclonedx-cli but found invalid BOM")
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
}

View file

@ -2,7 +2,6 @@ package cli
import (
"os/exec"
"strings"
"testing"
"time"
)
@ -37,10 +36,6 @@ func TestDirectoryScanCompletesWithinTimeout(t *testing.T) {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
}

View file

@ -3,7 +3,6 @@ package cli
import (
"fmt"
"path/filepath"
"strings"
"testing"
)
@ -97,7 +96,7 @@ func TestPackagesCmdFlags(t *testing.T) {
name: "squashed-scope-flag",
args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage},
assertions: []traitAssertion{
assertPackageCount(35),
assertPackageCount(37),
assertSuccessfulReturnCode,
},
},
@ -214,7 +213,7 @@ func TestPackagesCmdFlags(t *testing.T) {
// the application config in the log matches that of what we expect to have been configured.
assertInOutput("parallelism: 2"),
assertInOutput("parallelism=2"),
assertPackageCount(35),
assertPackageCount(37),
assertSuccessfulReturnCode,
},
},
@ -225,7 +224,7 @@ func TestPackagesCmdFlags(t *testing.T) {
// the application config in the log matches that of what we expect to have been configured.
assertInOutput("parallelism: 1"),
assertInOutput("parallelism=1"),
assertPackageCount(35),
assertPackageCount(37),
assertSuccessfulReturnCode,
},
},
@ -239,7 +238,7 @@ func TestPackagesCmdFlags(t *testing.T) {
assertions: []traitAssertion{
assertNotInOutput("secret_password"),
assertNotInOutput("secret_key_path"),
assertPackageCount(35),
assertPackageCount(37),
assertSuccessfulReturnCode,
},
},
@ -251,11 +250,7 @@ func TestPackagesCmdFlags(t *testing.T) {
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}
@ -337,11 +332,7 @@ func TestRegistryAuth(t *testing.T) {
for _, traitAssertionFn := range test.assertions {
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}

View file

@ -1,7 +1,6 @@
package cli
import (
"strings"
"testing"
)
@ -96,11 +95,7 @@ func TestPowerUserCmdFlags(t *testing.T) {
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}

View file

@ -3,7 +3,6 @@ package cli
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
@ -109,11 +108,7 @@ func TestPersistentFlags(t *testing.T) {
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}
@ -151,11 +146,7 @@ func TestLogFile(t *testing.T) {
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
logOutputOnFailure(t, cmd, stdout, stderr)
})
}
}

View file

@ -3,6 +3,7 @@ package cli
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"math"
@ -21,6 +22,16 @@ import (
"github.com/anchore/stereoscope/pkg/imagetest"
)
var showOutput = flag.Bool("show-output", false, "show stdout and stderr for failing tests")
func logOutputOnFailure(t testing.TB, cmd *exec.Cmd, stdout, stderr string) {
if t.Failed() && showOutput != nil && *showOutput {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
}
func runAndShow(t *testing.T, cmd *exec.Cmd) {
t.Helper()

View file

@ -11,6 +11,20 @@ type testCase struct {
}
var imageOnlyTestCases = []testCase{
{
name: "find kernel packages",
pkgType: pkg.LinuxKernelPkg,
pkgInfo: map[string]string{
"linux-kernel": "6.2.9-200.fc37.x86_64",
},
},
{
name: "find kernel module packages",
pkgType: pkg.LinuxKernelModulePkg,
pkgInfo: map[string]string{
"fsa4480": "",
},
},
{
name: "find gemspec packages",
pkgType: pkg.GemPkg,

View file

@ -221,6 +221,8 @@ func TestPkgCoverageDirectory(t *testing.T) {
observedPkgs.Remove(string(pkg.UnknownPkg))
definedPkgs.Remove(string(pkg.BinaryPkg))
definedPkgs.Remove(string(pkg.UnknownPkg))
definedPkgs.Remove(string(pkg.LinuxKernelPkg))
definedPkgs.Remove(string(pkg.LinuxKernelModulePkg))
// for directory scans we should not expect to see any of the following package types
definedPkgs.Remove(string(pkg.KbPkg))

View file

@ -1,5 +1,22 @@
FROM fedora:37@sha256:3f987b7657e944cf87a129cc262982d4f80e38bd98f7db313ccaf90ca7069dd2
RUN dnf install 'dnf-command(download)' cpio xz -y
RUN dnf download kernel-core kernel-modules-core -y
RUN rpm2cpio kernel-core-*.rpm | cpio -t && \
rpm2cpio kernel-core-*.rpm | cpio -idmv ./lib/modules/6.2.9-200.fc37.x86_64/vmlinuz
RUN rpm2cpio kernel-modules-core-*.rpm | cpio -t && \
rpm2cpio kernel-modules-core-*.rpm | cpio -idmv ./lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko.xz
RUN unxz /lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko.xz
FROM scratch
COPY --from=0 /lib/modules/6.2.9-200.fc37.x86_64/vmlinuz /lib/modules/6.2.9-200.fc37.x86_64/vmlinuz
COPY --from=0 /lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko /lib/modules/6.2.9-200.fc37.x86_64/kernel/drivers/usb/typec/mux/fsa4480.ko
COPY pkgs/ .
# we duplicate to show a package count difference between all-layers and squashed scopes
COPY lib lib
COPY etc/ .
COPY etc/ .