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:
Alan Pope
2025-09-26 10:42:29 -04:00
committed by GitHub
co-authored by Christopher Phillips
parent 64b71ec04c
commit 0a36dabf23
27 changed files with 4417 additions and 3 deletions
@@ -47,6 +47,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.PythonUvLockEntry{},
pkg.RustBinaryAuditEntry{},
pkg.RustCargoLockEntry{},
pkg.SnapEntry{},
pkg.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{},
pkg.OpamPackage{},
@@ -61,6 +61,7 @@ func AllTypes() []any {
pkg.RubyGemspec{},
pkg.RustBinaryAuditEntry{},
pkg.RustCargoLockEntry{},
pkg.SnapEntry{},
pkg.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{},
pkg.TerraformLockProviderEntry{},
+1
View File
@@ -115,6 +115,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.OpamPackage{}, "opam-package"),
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
jsonNames(pkg.SnapEntry{}, "snap-entry"),
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
+28
View 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
View 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())
})
}
}
@@ -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
View 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()
}
@@ -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
}
@@ -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)
}
@@ -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)
}
@@ -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
}
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
@@ -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
@@ -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
@@ -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
View 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.)
}