github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/snap/parse_snapd_snapcraft.go (about)

     1  package snap
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"gopkg.in/yaml.v3"
     9  
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/pkg"
    13  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    14  )
    15  
    16  // snapcraftYaml represents the structure of snapcraft.yaml files found in snapd snaps
    17  type snapcraftYaml struct {
    18  	Name          string                   `yaml:"name"`
    19  	Version       string                   `yaml:"version"`
    20  	Summary       string                   `yaml:"summary"`
    21  	Description   string                   `yaml:"description"`
    22  	Base          string                   `yaml:"base"`
    23  	Grade         string                   `yaml:"grade"`
    24  	Confinement   string                   `yaml:"confinement"`
    25  	Architectures []string                 `yaml:"architectures"`
    26  	Parts         map[string]snapcraftPart `yaml:"parts"`
    27  }
    28  
    29  // snapcraftPart represents a part in a snapcraft.yaml file
    30  type snapcraftPart struct {
    31  	Plugin           string              `yaml:"plugin"`
    32  	Source           string              `yaml:"source"`
    33  	SourceType       string              `yaml:"source-type"`
    34  	SourceTag        string              `yaml:"source-tag"`
    35  	SourceCommit     string              `yaml:"source-commit"`
    36  	BuildPackages    []string            `yaml:"build-packages"`
    37  	StagePackages    []string            `yaml:"stage-packages"`
    38  	BuildSnaps       []string            `yaml:"build-snaps"`
    39  	StageSnaps       []string            `yaml:"stage-snaps"`
    40  	BuildEnvironment []map[string]string `yaml:"build-environment"`
    41  	Override         map[string]string   `yaml:"override-build"`
    42  }
    43  
    44  // parseSnapdSnapcraft parses snapcraft.yaml files from snapd snaps
    45  func parseSnapdSnapcraft(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    46  	var snapcraft snapcraftYaml
    47  
    48  	decoder := yaml.NewDecoder(reader)
    49  	if err := decoder.Decode(&snapcraft); err != nil {
    50  		return nil, nil, fmt.Errorf("failed to parse snapcraft.yaml: %w", err)
    51  	}
    52  
    53  	snapMetadata := createMetadata(snapcraft)
    54  	packages := extractPackagesFromParts(snapcraft, snapMetadata, reader.Location)
    55  
    56  	return packages, nil, nil
    57  }
    58  
    59  // createMetadata creates metadata from snapcraft.yaml
    60  func createMetadata(snapcraft snapcraftYaml) pkg.SnapEntry {
    61  	metadata := pkg.SnapEntry{
    62  		SnapType:    pkg.SnapTypeSnapd,
    63  		Base:        snapcraft.Base,
    64  		SnapName:    snapcraft.Name,
    65  		SnapVersion: snapcraft.Version,
    66  	}
    67  
    68  	if len(snapcraft.Architectures) > 0 {
    69  		metadata.Architecture = snapcraft.Architectures[0]
    70  	}
    71  
    72  	return metadata
    73  }
    74  
    75  // extractPackagesFromParts processes all parts to extract packages
    76  func extractPackagesFromParts(snapcraft snapcraftYaml, baseMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
    77  	var packages []pkg.Package
    78  
    79  	for _, part := range snapcraft.Parts {
    80  		buildPackages := processBuildPackages(part.BuildPackages, baseMetadata, location)
    81  		packages = append(packages, buildPackages...)
    82  
    83  		stagePackages := processStagePackages(part.StagePackages, baseMetadata, location)
    84  		packages = append(packages, stagePackages...)
    85  
    86  		snapPackages := processSnapPackages(part.BuildSnaps, part.StageSnaps, baseMetadata, location)
    87  		packages = append(packages, snapPackages...)
    88  	}
    89  
    90  	return packages
    91  }
    92  
    93  // processBuildPackages creates packages from build-packages list
    94  func processBuildPackages(buildPackages []string, metadata pkg.SnapEntry, location file.Location) []pkg.Package {
    95  	var packages []pkg.Package
    96  
    97  	for _, pkgName := range buildPackages {
    98  		if pkgName == "" {
    99  			continue
   100  		}
   101  
   102  		buildPkg := newDebianPackageFromSnap(
   103  			pkgName,
   104  			"unknown",
   105  			metadata,
   106  			location,
   107  		)
   108  		packages = append(packages, buildPkg)
   109  	}
   110  
   111  	return packages
   112  }
   113  
   114  // processStagePackages creates packages from stage-packages list with version parsing
   115  func processStagePackages(stagePackages []string, metadata pkg.SnapEntry, location file.Location) []pkg.Package {
   116  	var packages []pkg.Package
   117  
   118  	for _, pkgEntry := range stagePackages {
   119  		if pkgEntry == "" {
   120  			continue
   121  		}
   122  
   123  		name, version := parsePackageWithVersion(pkgEntry)
   124  		stagePkg := newDebianPackageFromSnap(
   125  			name,
   126  			version,
   127  			metadata,
   128  			location,
   129  		)
   130  		packages = append(packages, stagePkg)
   131  	}
   132  
   133  	return packages
   134  }
   135  
   136  // parsePackageWithVersion extracts package name and version from version-constrained entries
   137  func parsePackageWithVersion(pkgEntry string) (string, string) {
   138  	name := pkgEntry
   139  	version := "unknown"
   140  
   141  	if !strings.ContainsAny(pkgEntry, "=<>") {
   142  		return name, version
   143  	}
   144  
   145  	// Try to split on version operators
   146  	operators := []string{">=", "<=", "==", "!=", "=", ">", "<"}
   147  	for _, op := range operators {
   148  		if strings.Contains(pkgEntry, op) {
   149  			parts := strings.SplitN(pkgEntry, op, 2)
   150  			if len(parts) == 2 {
   151  				return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
   152  			}
   153  		}
   154  	}
   155  
   156  	return name, version
   157  }
   158  
   159  // processSnapPackages creates packages from snap dependencies
   160  func processSnapPackages(buildSnaps, stageSnaps []string, baseMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
   161  	var packages []pkg.Package
   162  	allSnaps := make([]string, 0, len(buildSnaps)+len(stageSnaps))
   163  	allSnaps = append(allSnaps, buildSnaps...)
   164  	allSnaps = append(allSnaps, stageSnaps...)
   165  
   166  	for _, snapName := range allSnaps {
   167  		if snapName == "" {
   168  			continue
   169  		}
   170  
   171  		snapMetadata := pkg.SnapEntry{
   172  			SnapType:     pkg.SnapTypeApp,
   173  			SnapName:     snapName,
   174  			SnapVersion:  "unknown",
   175  			Architecture: baseMetadata.Architecture,
   176  		}
   177  
   178  		snapPkg := newPackage(
   179  			snapName,
   180  			"unknown",
   181  			snapMetadata,
   182  			location,
   183  		)
   184  		packages = append(packages, snapPkg)
   185  	}
   186  
   187  	return packages
   188  }