mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
feat(cataloger): add snap package cataloger for metadata extraction (#4151)
--------- Signed-off-by: Alan Pope <alan.pope@anchore.com> Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
parent
64b71ec04c
commit
0a36dabf23
2
go.mod
2
go.mod
@ -281,7 +281,7 @@ require (
|
|||||||
google.golang.org/grpc v1.67.3 // indirect
|
google.golang.org/grpc v1.67.3 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/libc v1.66.3 // indirect
|
modernc.org/libc v1.66.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
@ -3,5 +3,5 @@ package internal
|
|||||||
const (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// 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.
|
// 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 = "16.0.39"
|
JSONSchemaVersion = "16.0.40"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||||
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/snap"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/swipl"
|
"github.com/anchore/syft/syft/pkg/cataloger/swipl"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/terraform"
|
"github.com/anchore/syft/syft/pkg/cataloger/terraform"
|
||||||
@ -173,6 +174,7 @@ func DefaultPackageTaskFactories() Factories {
|
|||||||
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
|
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
|
||||||
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
|
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
|
||||||
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
|
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
|
||||||
|
newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"),
|
||||||
|
|
||||||
// deprecated catalogers ////////////////////////////////////////
|
// deprecated catalogers ////////////////////////////////////////
|
||||||
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
|
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
|
||||||
|
|||||||
3375
schema/json/schema-16.0.40.json
Normal file
3375
schema/json/schema-16.0.40.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "anchore.io/schema/syft/json/16.0.39/document",
|
"$id": "anchore.io/schema/syft/json/16.0.40/document",
|
||||||
"$ref": "#/$defs/Document",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@ -2194,6 +2194,9 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/$defs/RustCargoLockEntry"
|
"$ref": "#/$defs/RustCargoLockEntry"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/SnapEntry"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/$defs/SwiftPackageManagerLockEntry"
|
"$ref": "#/$defs/SwiftPackageManagerLockEntry"
|
||||||
},
|
},
|
||||||
@ -3205,6 +3208,33 @@
|
|||||||
"url"
|
"url"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"SnapEntry": {
|
||||||
|
"properties": {
|
||||||
|
"snapType": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"snapName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"snapVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"architecture": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"snapType",
|
||||||
|
"base",
|
||||||
|
"snapName",
|
||||||
|
"snapVersion",
|
||||||
|
"architecture"
|
||||||
|
]
|
||||||
|
},
|
||||||
"Source": {
|
"Source": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
|||||||
@ -47,6 +47,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
|||||||
pkg.PythonUvLockEntry{},
|
pkg.PythonUvLockEntry{},
|
||||||
pkg.RustBinaryAuditEntry{},
|
pkg.RustBinaryAuditEntry{},
|
||||||
pkg.RustCargoLockEntry{},
|
pkg.RustCargoLockEntry{},
|
||||||
|
pkg.SnapEntry{},
|
||||||
pkg.SwiftPackageManagerResolvedEntry{},
|
pkg.SwiftPackageManagerResolvedEntry{},
|
||||||
pkg.SwiplPackEntry{},
|
pkg.SwiplPackEntry{},
|
||||||
pkg.OpamPackage{},
|
pkg.OpamPackage{},
|
||||||
|
|||||||
@ -61,6 +61,7 @@ func AllTypes() []any {
|
|||||||
pkg.RubyGemspec{},
|
pkg.RubyGemspec{},
|
||||||
pkg.RustBinaryAuditEntry{},
|
pkg.RustBinaryAuditEntry{},
|
||||||
pkg.RustCargoLockEntry{},
|
pkg.RustCargoLockEntry{},
|
||||||
|
pkg.SnapEntry{},
|
||||||
pkg.SwiftPackageManagerResolvedEntry{},
|
pkg.SwiftPackageManagerResolvedEntry{},
|
||||||
pkg.SwiplPackEntry{},
|
pkg.SwiplPackEntry{},
|
||||||
pkg.TerraformLockProviderEntry{},
|
pkg.TerraformLockProviderEntry{},
|
||||||
|
|||||||
@ -115,6 +115,7 @@ var jsonTypes = makeJSONTypes(
|
|||||||
jsonNames(pkg.OpamPackage{}, "opam-package"),
|
jsonNames(pkg.OpamPackage{}, "opam-package"),
|
||||||
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
|
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
|
||||||
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
|
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||||
|
jsonNames(pkg.SnapEntry{}, "snap-entry"),
|
||||||
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
|
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
|
||||||
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
|
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
|
||||||
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
|
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
|
||||||
|
|||||||
28
syft/pkg/cataloger/snap/cataloger.go
Normal file
28
syft/pkg/cataloger/snap/cataloger.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Package snap provides a concrete Cataloger implementation for snap packages, extracting metadata
|
||||||
|
from different types of snap files (base, kernel, system/gadget, snapd) rather than just scanning
|
||||||
|
the filesystem.
|
||||||
|
*/
|
||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const catalogerName = "snap-cataloger"
|
||||||
|
|
||||||
|
// NewCataloger returns a new Snap cataloger object that can parse snap package metadata.
|
||||||
|
func NewCataloger() pkg.Cataloger {
|
||||||
|
return generic.NewCataloger(catalogerName).
|
||||||
|
// Look for snap.yaml to identify snap type and base snap info
|
||||||
|
WithParserByGlobs(parseSnapYaml, "**/meta/snap.yaml").
|
||||||
|
// Base snaps: dpkg.yaml files containing package manifests
|
||||||
|
WithParserByGlobs(parseBaseDpkgYaml, "**/usr/share/snappy/dpkg.yaml").
|
||||||
|
// Kernel snaps: changelog files for kernel version info
|
||||||
|
WithParserByGlobs(parseKernelChangelog, "**/doc/linux-modules-*/changelog.Debian.gz").
|
||||||
|
// System/Gadget snaps: manifest files with primed-stage-packages
|
||||||
|
WithParserByGlobs(parseSystemManifest, "**/snap/manifest.yaml").
|
||||||
|
// Snapd snaps: snapcraft.yaml files
|
||||||
|
WithParserByGlobs(parseSnapdSnapcraft, "**/snap/snapcraft.yaml")
|
||||||
|
}
|
||||||
36
syft/pkg/cataloger/snap/cataloger_test.go
Normal file
36
syft/pkg/cataloger/snap/cataloger_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCataloger_Globs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fixture string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "base snap with dpkg.yaml",
|
||||||
|
fixture: "test-fixtures/glob-paths/base",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "system snap with manifest.yaml",
|
||||||
|
fixture: "test-fixtures/glob-paths/system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "snap with meta/snap.yaml",
|
||||||
|
fixture: "test-fixtures/glob-paths/meta",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
pkgtest.NewCatalogTester().
|
||||||
|
FromDirectory(t, test.fixture).
|
||||||
|
IgnoreUnfulfilledPathResponses("**/meta/snap.yaml", "**/usr/share/snappy/dpkg.yaml", "**/doc/linux-modules-*/changelog.Debian.gz", "**/snap/manifest.yaml", "**/snap/snapcraft.yaml").
|
||||||
|
TestCataloger(t, NewCataloger())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
55
syft/pkg/cataloger/snap/integration_test.go
Normal file
55
syft/pkg/cataloger/snap/integration_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRealDpkgYamlParsing(t *testing.T) {
|
||||||
|
fixture := "test-fixtures/real-dpkg.yaml"
|
||||||
|
|
||||||
|
// Open the file
|
||||||
|
f, err := os.Open(fixture)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
reader := file.LocationReadCloser{
|
||||||
|
Location: file.NewLocation(fixture),
|
||||||
|
ReadCloser: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse using our function
|
||||||
|
packages, relationships, err := parseBaseDpkgYaml(context.Background(), nil, &generic.Environment{}, reader)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, relationships) // relationships should be nil for this parser
|
||||||
|
|
||||||
|
// We should have 10 packages from the fixture
|
||||||
|
assert.Equal(t, 10, len(packages))
|
||||||
|
|
||||||
|
// Check some specific packages
|
||||||
|
foundPackages := make(map[string]pkg.Package)
|
||||||
|
for _, p := range packages {
|
||||||
|
foundPackages[p.Name] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify key packages exist
|
||||||
|
require.Contains(t, foundPackages, "adduser")
|
||||||
|
require.Contains(t, foundPackages, "systemd")
|
||||||
|
require.Contains(t, foundPackages, "gcc-10-base")
|
||||||
|
|
||||||
|
// Check that architecture is parsed correctly from package names
|
||||||
|
gccPkg := foundPackages["gcc-10-base"]
|
||||||
|
metadata, ok := gccPkg.Metadata.(pkg.SnapEntry)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "amd64", metadata.Architecture)
|
||||||
|
assert.Equal(t, pkg.SnapTypeBase, metadata.SnapType)
|
||||||
|
}
|
||||||
94
syft/pkg/cataloger/snap/package.go
Normal file
94
syft/pkg/cataloger/snap/package.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/packageurl-go"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newPackage creates a new Package from snap metadata
|
||||||
|
func newPackage(name, version string, metadata pkg.SnapEntry, locations ...file.Location) pkg.Package {
|
||||||
|
p := pkg.Package{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
Locations: file.NewLocationSet(locations...),
|
||||||
|
PURL: packageURL(name, version, metadata),
|
||||||
|
Type: pkg.DebPkg, // Use Debian package type for compatibility
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SetID()
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageURL returns the PURL for a snap package
|
||||||
|
func packageURL(name, version string, metadata pkg.SnapEntry) string {
|
||||||
|
var qualifiers packageurl.Qualifiers
|
||||||
|
|
||||||
|
if metadata.Architecture != "" {
|
||||||
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
|
Key: "arch",
|
||||||
|
Value: metadata.Architecture,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.Base != "" {
|
||||||
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
|
Key: "base",
|
||||||
|
Value: metadata.Base,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.SnapType != "" {
|
||||||
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
|
Key: "type",
|
||||||
|
Value: metadata.SnapType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageurl.NewPackageURL(
|
||||||
|
packageurl.TypeGeneric,
|
||||||
|
"snap",
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
qualifiers,
|
||||||
|
"",
|
||||||
|
).ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDebianPackageFromSnap creates a Debian-style package entry from snap manifest data
|
||||||
|
func newDebianPackageFromSnap(name, version string, snapMetadata pkg.SnapEntry, locations ...file.Location) pkg.Package {
|
||||||
|
p := pkg.Package{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
Locations: file.NewLocationSet(locations...),
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: debianPackageURL(name, version, snapMetadata.Architecture),
|
||||||
|
Metadata: snapMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SetID()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// debianPackageURL creates a PURL for Debian packages found in snaps
|
||||||
|
func debianPackageURL(name, version, architecture string) string {
|
||||||
|
var qualifiers packageurl.Qualifiers
|
||||||
|
|
||||||
|
if architecture != "" {
|
||||||
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
|
Key: "arch",
|
||||||
|
Value: architecture,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageurl.NewPackageURL(
|
||||||
|
packageurl.TypeDebian,
|
||||||
|
"ubuntu", // Assume Ubuntu since most snaps are built on Ubuntu
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
qualifiers,
|
||||||
|
"",
|
||||||
|
).ToString()
|
||||||
|
}
|
||||||
80
syft/pkg/cataloger/snap/parse_base_dpkg.go
Normal file
80
syft/pkg/cataloger/snap/parse_base_dpkg.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dpkgYaml represents the structure of dpkg.yaml files found in base snaps
|
||||||
|
type dpkgYaml struct {
|
||||||
|
PackageRepositories []packageRepository `yaml:"package-repositories"`
|
||||||
|
Packages []string `yaml:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type packageRepository struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
PPA string `yaml:"ppa,omitempty"`
|
||||||
|
URL string `yaml:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBaseDpkgYaml parses dpkg.yaml files from base snaps
|
||||||
|
func parseBaseDpkgYaml(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var dpkg dpkgYaml
|
||||||
|
|
||||||
|
decoder := yaml.NewDecoder(reader)
|
||||||
|
if err := decoder.Decode(&dpkg); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to parse dpkg.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var packages []pkg.Package
|
||||||
|
|
||||||
|
snapMetadata := pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeBase,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each package entry in "name=version" format
|
||||||
|
for _, pkgEntry := range dpkg.Packages {
|
||||||
|
if !strings.Contains(pkgEntry, "=") {
|
||||||
|
continue // Skip malformed entries
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(pkgEntry, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(parts[0])
|
||||||
|
version := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
// Skip empty names or versions
|
||||||
|
if name == "" || version == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle architecture suffixes (e.g., "libssl1.1:amd64")
|
||||||
|
if strings.Contains(name, ":") {
|
||||||
|
archParts := strings.SplitN(name, ":", 2)
|
||||||
|
name = archParts[0]
|
||||||
|
snapMetadata.Architecture = archParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
debPkg := newDebianPackageFromSnap(
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
snapMetadata,
|
||||||
|
reader.Location,
|
||||||
|
)
|
||||||
|
|
||||||
|
packages = append(packages, debPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages, nil, nil
|
||||||
|
}
|
||||||
50
syft/pkg/cataloger/snap/parse_base_dpkg_test.go
Normal file
50
syft/pkg/cataloger/snap/parse_base_dpkg_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseBaseDpkgYaml(t *testing.T) {
|
||||||
|
fixture := "test-fixtures/dpkg.yaml"
|
||||||
|
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||||
|
|
||||||
|
expected := []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "adduser",
|
||||||
|
Version: "3.118ubuntu2",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: "pkg:deb/ubuntu/adduser@3.118ubuntu2",
|
||||||
|
Locations: locations,
|
||||||
|
Metadata: pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeBase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "apparmor",
|
||||||
|
Version: "2.13.3-7ubuntu5.3",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: "pkg:deb/ubuntu/apparmor@2.13.3-7ubuntu5.3",
|
||||||
|
Locations: locations,
|
||||||
|
Metadata: pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeBase,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "gcc-10-base",
|
||||||
|
Version: "10.5.0-1ubuntu1~20.04",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: "pkg:deb/ubuntu/gcc-10-base@10.5.0-1ubuntu1~20.04?arch=amd64",
|
||||||
|
Locations: locations,
|
||||||
|
Metadata: pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeBase,
|
||||||
|
Architecture: "amd64", // from package name suffix
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgtest.TestFileParser(t, fixture, parseBaseDpkgYaml, expected, nil)
|
||||||
|
}
|
||||||
71
syft/pkg/cataloger/snap/parse_integration_test.go
Normal file
71
syft/pkg/cataloger/snap/parse_integration_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseSnapYaml(t *testing.T) {
|
||||||
|
fixture := "test-fixtures/snap.yaml"
|
||||||
|
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||||
|
|
||||||
|
expected := []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "test-snap",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: "pkg:generic/snap/test-snap@1.0.0?arch=amd64&base=core20&type=app",
|
||||||
|
Locations: locations,
|
||||||
|
Metadata: pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeApp,
|
||||||
|
Base: "core20",
|
||||||
|
SnapName: "test-snap",
|
||||||
|
SnapVersion: "1.0.0",
|
||||||
|
Architecture: "amd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgtest.TestFileParser(t, fixture, parseSnapYaml, expected, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSystemManifest(t *testing.T) {
|
||||||
|
fixture := "test-fixtures/manifest.yaml"
|
||||||
|
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||||
|
|
||||||
|
expected := []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "grub-efi-amd64-signed",
|
||||||
|
Version: "1.202+2.12-1ubuntu7",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: "pkg:deb/ubuntu/grub-efi-amd64-signed@1.202%2B2.12-1ubuntu7?arch=amd64", // URL encoded
|
||||||
|
Locations: locations,
|
||||||
|
Metadata: pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeApp, // Default type when gadget not detected from name
|
||||||
|
Base: "core24",
|
||||||
|
SnapName: "pc",
|
||||||
|
SnapVersion: "24-0.1",
|
||||||
|
Architecture: "amd64", // From architectures array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "shim-signed",
|
||||||
|
Version: "1.56+15.7-0ubuntu1",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
PURL: "pkg:deb/ubuntu/shim-signed@1.56%2B15.7-0ubuntu1?arch=amd64", // URL encoded
|
||||||
|
Locations: locations,
|
||||||
|
Metadata: pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeApp, // Default type when gadget not detected from name
|
||||||
|
Base: "core24",
|
||||||
|
SnapName: "pc",
|
||||||
|
SnapVersion: "24-0.1",
|
||||||
|
Architecture: "amd64", // From architectures array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgtest.TestFileParser(t, fixture, parseSystemManifest, expected, nil)
|
||||||
|
}
|
||||||
160
syft/pkg/cataloger/snap/parse_kernel_changelog.go
Normal file
160
syft/pkg/cataloger/snap/parse_kernel_changelog.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kernelVersionInfo holds parsed kernel version information
|
||||||
|
type kernelVersionInfo struct {
|
||||||
|
baseVersion string // e.g., "5.4.0-195"
|
||||||
|
releaseVersion string // e.g., "215"
|
||||||
|
fullVersion string // e.g., "5.4.0-195.215"
|
||||||
|
majorVersion string // e.g., "5.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseKernelChangelog parses changelog files from kernel snaps to extract kernel version
|
||||||
|
func parseKernelChangelog(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
// The file should be gzipped
|
||||||
|
lines, err := readChangelogLines(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pull from first line
|
||||||
|
versionInfo, err := extractKernelVersion(lines[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapMetadata := pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeKernel,
|
||||||
|
}
|
||||||
|
|
||||||
|
packages := createMainKernelPackage(versionInfo, snapMetadata, reader.Location)
|
||||||
|
|
||||||
|
// Check for base kernel package
|
||||||
|
basePackage := findBaseKernelPackage(lines, versionInfo, snapMetadata, reader.Location)
|
||||||
|
if basePackage != nil {
|
||||||
|
packages = append(packages, *basePackage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readChangelogLines reads and decompresses the changelog content
|
||||||
|
func readChangelogLines(reader file.LocationReadCloser) ([]string, error) {
|
||||||
|
gzReader, err := gzip.NewReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create gzip reader for changelog: %w", err)
|
||||||
|
}
|
||||||
|
defer gzReader.Close()
|
||||||
|
|
||||||
|
content, err := readAll(gzReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read changelog content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return nil, fmt.Errorf("changelog file is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the first line to extract kernel version information
|
||||||
|
// Format: "linux (5.4.0-195.215) focal; urgency=medium"
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractKernelVersion parses version information from the first changelog line
|
||||||
|
func extractKernelVersion(firstLine string) (*kernelVersionInfo, error) {
|
||||||
|
// Format: "linux (5.4.0-195.215) focal; urgency=medium"
|
||||||
|
kernelVersionRegex := regexp.MustCompile(`linux \(([0-9]+\.[0-9]+\.[0-9]+-[0-9]+)\.([0-9]+)\)`)
|
||||||
|
matches := kernelVersionRegex.FindStringSubmatch(firstLine)
|
||||||
|
|
||||||
|
if len(matches) < 3 {
|
||||||
|
return nil, fmt.Errorf("could not parse kernel version from changelog: %s", firstLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &kernelVersionInfo{
|
||||||
|
baseVersion: matches[1], // e.g., "5.4.0-195"
|
||||||
|
releaseVersion: matches[2], // eg., "215"
|
||||||
|
}
|
||||||
|
// eg "5.4.0-195.215"
|
||||||
|
info.fullVersion = fmt.Sprintf("%s.%s", info.baseVersion, info.releaseVersion)
|
||||||
|
|
||||||
|
// Extract major version; package naming
|
||||||
|
majorVersionRegex := regexp.MustCompile(`([0-9]+\.[0-9]+)\.[0-9]+-[0-9]+`)
|
||||||
|
majorMatches := majorVersionRegex.FindStringSubmatch(info.baseVersion)
|
||||||
|
|
||||||
|
if len(majorMatches) >= 2 {
|
||||||
|
info.majorVersion = majorMatches[1]
|
||||||
|
} else {
|
||||||
|
info.majorVersion = info.baseVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMainKernelPackage creates the main kernel package
|
||||||
|
func createMainKernelPackage(versionInfo *kernelVersionInfo, snapMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
|
||||||
|
kernelPackageName := fmt.Sprintf("linux-image-%s-generic", versionInfo.baseVersion)
|
||||||
|
kernelPkg := newDebianPackageFromSnap(
|
||||||
|
kernelPackageName,
|
||||||
|
versionInfo.fullVersion,
|
||||||
|
snapMetadata,
|
||||||
|
location,
|
||||||
|
)
|
||||||
|
|
||||||
|
return []pkg.Package{kernelPkg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBaseKernelPackage searches for and creates base kernel package if present
|
||||||
|
func findBaseKernelPackage(lines []string, versionInfo *kernelVersionInfo, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package {
|
||||||
|
baseKernelEntry := fmt.Sprintf("%s/linux:", strings.ReplaceAll(versionInfo.releaseVersion, ";", "/"))
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, baseKernelEntry) {
|
||||||
|
return parseBaseKernelLine(line, versionInfo.majorVersion, snapMetadata, location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBaseKernelLine extracts base kernel version from a changelog line
|
||||||
|
func parseBaseKernelLine(line string, majorVersion string, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package {
|
||||||
|
baseKernelRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+)\.?[0-9]*`, regexp.QuoteMeta(majorVersion)))
|
||||||
|
baseMatches := baseKernelRegex.FindStringSubmatch(line)
|
||||||
|
|
||||||
|
if len(baseMatches) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
baseKernelVersion := baseMatches[1]
|
||||||
|
baseKernelFullRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+\.[0-9]+)`, regexp.QuoteMeta(majorVersion)))
|
||||||
|
baseFullMatches := baseKernelFullRegex.FindStringSubmatch(line)
|
||||||
|
|
||||||
|
var baseFullVersion string
|
||||||
|
if len(baseFullMatches) >= 2 {
|
||||||
|
baseFullVersion = baseFullMatches[1]
|
||||||
|
} else {
|
||||||
|
baseFullVersion = baseKernelVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
baseKernelPkg := newDebianPackageFromSnap(
|
||||||
|
fmt.Sprintf("linux-image-%s-generic", baseKernelVersion),
|
||||||
|
baseFullVersion,
|
||||||
|
snapMetadata,
|
||||||
|
location,
|
||||||
|
)
|
||||||
|
|
||||||
|
return &baseKernelPkg
|
||||||
|
}
|
||||||
68
syft/pkg/cataloger/snap/parse_snap_yaml.go
Normal file
68
syft/pkg/cataloger/snap/parse_snap_yaml.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// snapYaml represents the structure of meta/snap.yaml files
|
||||||
|
type snapYaml struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Base string `yaml:"base"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Architecture string `yaml:"architecture"`
|
||||||
|
Summary string `yaml:"summary"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSnapYaml parses meta/snap.yaml files to identify snap type and basic metadata
|
||||||
|
func parseSnapYaml(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var snap snapYaml
|
||||||
|
|
||||||
|
decoder := yaml.NewDecoder(reader)
|
||||||
|
if err := decoder.Decode(&snap); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to parse snap.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if snap.Name == "" {
|
||||||
|
return nil, nil, fmt.Errorf("snap.yaml missing required 'name' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine snap type - default to "app" if not specified
|
||||||
|
snapType := snap.Type
|
||||||
|
if snapType == "" {
|
||||||
|
snapType = pkg.SnapTypeApp
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := pkg.SnapEntry{
|
||||||
|
SnapType: snapType,
|
||||||
|
Base: snap.Base,
|
||||||
|
SnapName: snap.Name,
|
||||||
|
SnapVersion: snap.Version,
|
||||||
|
Architecture: snap.Architecture,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a package representing the snap itself
|
||||||
|
snapPkg := newPackage(
|
||||||
|
snap.Name,
|
||||||
|
snap.Version,
|
||||||
|
metadata,
|
||||||
|
reader.Location,
|
||||||
|
)
|
||||||
|
|
||||||
|
return []pkg.Package{snapPkg}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readAll reads all content from a reader and returns it as bytes
|
||||||
|
func readAll(r io.Reader) ([]byte, error) {
|
||||||
|
return io.ReadAll(r)
|
||||||
|
}
|
||||||
188
syft/pkg/cataloger/snap/parse_snapd_snapcraft.go
Normal file
188
syft/pkg/cataloger/snap/parse_snapd_snapcraft.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// snapcraftYaml represents the structure of snapcraft.yaml files found in snapd snaps
|
||||||
|
type snapcraftYaml struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Summary string `yaml:"summary"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Base string `yaml:"base"`
|
||||||
|
Grade string `yaml:"grade"`
|
||||||
|
Confinement string `yaml:"confinement"`
|
||||||
|
Architectures []string `yaml:"architectures"`
|
||||||
|
Parts map[string]snapcraftPart `yaml:"parts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapcraftPart represents a part in a snapcraft.yaml file
|
||||||
|
type snapcraftPart struct {
|
||||||
|
Plugin string `yaml:"plugin"`
|
||||||
|
Source string `yaml:"source"`
|
||||||
|
SourceType string `yaml:"source-type"`
|
||||||
|
SourceTag string `yaml:"source-tag"`
|
||||||
|
SourceCommit string `yaml:"source-commit"`
|
||||||
|
BuildPackages []string `yaml:"build-packages"`
|
||||||
|
StagePackages []string `yaml:"stage-packages"`
|
||||||
|
BuildSnaps []string `yaml:"build-snaps"`
|
||||||
|
StageSnaps []string `yaml:"stage-snaps"`
|
||||||
|
BuildEnvironment []map[string]string `yaml:"build-environment"`
|
||||||
|
Override map[string]string `yaml:"override-build"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSnapdSnapcraft parses snapcraft.yaml files from snapd snaps
|
||||||
|
func parseSnapdSnapcraft(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var snapcraft snapcraftYaml
|
||||||
|
|
||||||
|
decoder := yaml.NewDecoder(reader)
|
||||||
|
if err := decoder.Decode(&snapcraft); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to parse snapcraft.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapMetadata := createMetadata(snapcraft)
|
||||||
|
packages := extractPackagesFromParts(snapcraft, snapMetadata, reader.Location)
|
||||||
|
|
||||||
|
return packages, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMetadata creates metadata from snapcraft.yaml
|
||||||
|
func createMetadata(snapcraft snapcraftYaml) pkg.SnapEntry {
|
||||||
|
metadata := pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeSnapd,
|
||||||
|
Base: snapcraft.Base,
|
||||||
|
SnapName: snapcraft.Name,
|
||||||
|
SnapVersion: snapcraft.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snapcraft.Architectures) > 0 {
|
||||||
|
metadata.Architecture = snapcraft.Architectures[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPackagesFromParts processes all parts to extract packages
|
||||||
|
func extractPackagesFromParts(snapcraft snapcraftYaml, baseMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
|
||||||
|
var packages []pkg.Package
|
||||||
|
|
||||||
|
for _, part := range snapcraft.Parts {
|
||||||
|
buildPackages := processBuildPackages(part.BuildPackages, baseMetadata, location)
|
||||||
|
packages = append(packages, buildPackages...)
|
||||||
|
|
||||||
|
stagePackages := processStagePackages(part.StagePackages, baseMetadata, location)
|
||||||
|
packages = append(packages, stagePackages...)
|
||||||
|
|
||||||
|
snapPackages := processSnapPackages(part.BuildSnaps, part.StageSnaps, baseMetadata, location)
|
||||||
|
packages = append(packages, snapPackages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// processBuildPackages creates packages from build-packages list
|
||||||
|
func processBuildPackages(buildPackages []string, metadata pkg.SnapEntry, location file.Location) []pkg.Package {
|
||||||
|
var packages []pkg.Package
|
||||||
|
|
||||||
|
for _, pkgName := range buildPackages {
|
||||||
|
if pkgName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPkg := newDebianPackageFromSnap(
|
||||||
|
pkgName,
|
||||||
|
"unknown",
|
||||||
|
metadata,
|
||||||
|
location,
|
||||||
|
)
|
||||||
|
packages = append(packages, buildPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// processStagePackages creates packages from stage-packages list with version parsing
|
||||||
|
func processStagePackages(stagePackages []string, metadata pkg.SnapEntry, location file.Location) []pkg.Package {
|
||||||
|
var packages []pkg.Package
|
||||||
|
|
||||||
|
for _, pkgEntry := range stagePackages {
|
||||||
|
if pkgEntry == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name, version := parsePackageWithVersion(pkgEntry)
|
||||||
|
stagePkg := newDebianPackageFromSnap(
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
metadata,
|
||||||
|
location,
|
||||||
|
)
|
||||||
|
packages = append(packages, stagePkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePackageWithVersion extracts package name and version from version-constrained entries
|
||||||
|
func parsePackageWithVersion(pkgEntry string) (string, string) {
|
||||||
|
name := pkgEntry
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
if !strings.ContainsAny(pkgEntry, "=<>") {
|
||||||
|
return name, version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to split on version operators
|
||||||
|
operators := []string{">=", "<=", "==", "!=", "=", ">", "<"}
|
||||||
|
for _, op := range operators {
|
||||||
|
if strings.Contains(pkgEntry, op) {
|
||||||
|
parts := strings.SplitN(pkgEntry, op, 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, version
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSnapPackages creates packages from snap dependencies
|
||||||
|
func processSnapPackages(buildSnaps, stageSnaps []string, baseMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
|
||||||
|
var packages []pkg.Package
|
||||||
|
allSnaps := make([]string, 0, len(buildSnaps)+len(stageSnaps))
|
||||||
|
allSnaps = append(allSnaps, buildSnaps...)
|
||||||
|
allSnaps = append(allSnaps, stageSnaps...)
|
||||||
|
|
||||||
|
for _, snapName := range allSnaps {
|
||||||
|
if snapName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
snapMetadata := pkg.SnapEntry{
|
||||||
|
SnapType: pkg.SnapTypeApp,
|
||||||
|
SnapName: snapName,
|
||||||
|
SnapVersion: "unknown",
|
||||||
|
Architecture: baseMetadata.Architecture,
|
||||||
|
}
|
||||||
|
|
||||||
|
snapPkg := newPackage(
|
||||||
|
snapName,
|
||||||
|
"unknown",
|
||||||
|
snapMetadata,
|
||||||
|
location,
|
||||||
|
)
|
||||||
|
packages = append(packages, snapPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
102
syft/pkg/cataloger/snap/parse_system_manifest.go
Normal file
102
syft/pkg/cataloger/snap/parse_system_manifest.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// systemManifest represents the structure of manifest.yaml files found in system/gadget snaps
|
||||||
|
type systemManifest struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Base string `yaml:"base"`
|
||||||
|
Grade string `yaml:"grade"`
|
||||||
|
Confinement string `yaml:"confinement"`
|
||||||
|
PrimedStagePackages []string `yaml:"primed-stage-packages"`
|
||||||
|
Architectures []string `yaml:"architectures"`
|
||||||
|
SnapcraftVersion string `yaml:"snapcraft-version"`
|
||||||
|
SnapcraftOSReleaseID string `yaml:"snapcraft-os-release-id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSystemManifest parses manifest.yaml files from system/gadget snaps
|
||||||
|
func parseSystemManifest(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var manifest systemManifest
|
||||||
|
|
||||||
|
decoder := yaml.NewDecoder(reader)
|
||||||
|
if err := decoder.Decode(&manifest); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to parse manifest.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var packages []pkg.Package
|
||||||
|
|
||||||
|
// Determine snap type - could be system, gadget, or app
|
||||||
|
snapType := pkg.SnapTypeApp // Default
|
||||||
|
if manifest.Name != "" {
|
||||||
|
// Try to infer type from name patterns or content
|
||||||
|
switch {
|
||||||
|
case strings.Contains(strings.ToLower(manifest.Name), "gadget"):
|
||||||
|
snapType = pkg.SnapTypeGadget
|
||||||
|
default:
|
||||||
|
snapType = pkg.SnapTypeApp // System snaps are often just regular apps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapMetadata := pkg.SnapEntry{
|
||||||
|
SnapType: snapType,
|
||||||
|
Base: manifest.Base,
|
||||||
|
SnapName: manifest.Name,
|
||||||
|
SnapVersion: manifest.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set architecture if available
|
||||||
|
if len(manifest.Architectures) > 0 {
|
||||||
|
snapMetadata.Architecture = manifest.Architectures[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse primed-stage-packages entries
|
||||||
|
for _, pkgEntry := range manifest.PrimedStagePackages {
|
||||||
|
if !strings.Contains(pkgEntry, "=") {
|
||||||
|
continue // Skip malformed entries
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(pkgEntry, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(parts[0])
|
||||||
|
version := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
// Skip empty names or versions
|
||||||
|
if name == "" || version == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle architecture suffixes if present
|
||||||
|
currentMetadata := snapMetadata
|
||||||
|
if strings.Contains(name, ":") {
|
||||||
|
archParts := strings.SplitN(name, ":", 2)
|
||||||
|
name = archParts[0]
|
||||||
|
currentMetadata.Architecture = archParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
debPkg := newDebianPackageFromSnap(
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
currentMetadata,
|
||||||
|
reader.Location,
|
||||||
|
)
|
||||||
|
|
||||||
|
packages = append(packages, debPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages, nil, nil
|
||||||
|
}
|
||||||
7
syft/pkg/cataloger/snap/test-fixtures/dpkg.yaml
Normal file
7
syft/pkg/cataloger/snap/test-fixtures/dpkg.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package-repositories:
|
||||||
|
- type: apt
|
||||||
|
ppa: ucdev/ubuntu/base-ppa
|
||||||
|
packages:
|
||||||
|
- adduser=3.118ubuntu2
|
||||||
|
- apparmor=2.13.3-7ubuntu5.3
|
||||||
|
- gcc-10-base:amd64=10.5.0-1ubuntu1~20.04
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- test-package=1.0.0
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
name: test-snap
|
||||||
|
version: 1.0
|
||||||
|
type: app
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
name: test-snap
|
||||||
|
version: 1.0
|
||||||
|
primed-stage-packages:
|
||||||
|
- test-package=1.0.0
|
||||||
17
syft/pkg/cataloger/snap/test-fixtures/manifest.yaml
Normal file
17
syft/pkg/cataloger/snap/test-fixtures/manifest.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
snapcraft-version: 8.2.1.post5+git07b4ee15
|
||||||
|
snapcraft-started-at: '2024-04-29T06:47:56.067540Z'
|
||||||
|
snapcraft-os-release-id: ubuntu
|
||||||
|
snapcraft-os-release-version-id: '24.04'
|
||||||
|
name: pc
|
||||||
|
version: 24-0.1
|
||||||
|
summary: PC gadget for generic devices
|
||||||
|
description: |
|
||||||
|
This gadget enables generic pc devices to work with Ubuntu Core
|
||||||
|
base: core24
|
||||||
|
grade: stable
|
||||||
|
confinement: strict
|
||||||
|
architectures:
|
||||||
|
- amd64
|
||||||
|
primed-stage-packages:
|
||||||
|
- grub-efi-amd64-signed=1.202+2.12-1ubuntu7
|
||||||
|
- shim-signed=1.56+15.7-0ubuntu1
|
||||||
14
syft/pkg/cataloger/snap/test-fixtures/real-dpkg.yaml
Normal file
14
syft/pkg/cataloger/snap/test-fixtures/real-dpkg.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package-repositories:
|
||||||
|
- type: apt
|
||||||
|
ppa: ucdev/ubuntu/base-ppa
|
||||||
|
packages:
|
||||||
|
- adduser=3.118ubuntu2
|
||||||
|
- apparmor=2.13.3-7ubuntu5.3
|
||||||
|
- apt=2.0.10
|
||||||
|
- bash=5.0-6ubuntu1.2
|
||||||
|
- coreutils=8.30-3ubuntu2
|
||||||
|
- dpkg=1.19.7ubuntu3.2
|
||||||
|
- gcc-10-base:amd64=10.5.0-1ubuntu1~20.04
|
||||||
|
- libc6:amd64=2.31-0ubuntu9.16
|
||||||
|
- systemd=245.4-4ubuntu3.23
|
||||||
|
- ubuntu-keyring=2020.02.11.4
|
||||||
7
syft/pkg/cataloger/snap/test-fixtures/snap.yaml
Normal file
7
syft/pkg/cataloger/snap/test-fixtures/snap.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
name: test-snap
|
||||||
|
version: 1.0.0
|
||||||
|
summary: A test snap
|
||||||
|
description: This is a test snap for testing purposes
|
||||||
|
base: core20
|
||||||
|
type: app
|
||||||
|
architecture: amd64
|
||||||
18
syft/pkg/snap.go
Normal file
18
syft/pkg/snap.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
const (
|
||||||
|
SnapTypeBase = "base"
|
||||||
|
SnapTypeKernel = "kernel"
|
||||||
|
SnapTypeApp = "app"
|
||||||
|
SnapTypeGadget = "gadget"
|
||||||
|
SnapTypeSnapd = "snapd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnapEntry struct {
|
||||||
|
SnapType string `json:"snapType" yaml:"snapType"` // base, kernel, system, gadget, snapd
|
||||||
|
Base string `json:"base" yaml:"base"` // base snap name (e.g., core20, core22)
|
||||||
|
SnapName string `json:"snapName" yaml:"snapName"` // name of the snap
|
||||||
|
SnapVersion string `json:"snapVersion" yaml:"snapVersion"` // version of the snap
|
||||||
|
Architecture string `json:"architecture" yaml:"architecture"` // architecture (amd64, arm64, etc.)
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user