diff --git a/schema/json/generate.go b/schema/json/generate.go index 0f747879387..a1043845abc 100644 --- a/schema/json/generate.go +++ b/schema/json/generate.go @@ -41,6 +41,7 @@ type artifactMetadataContainer struct { Dart pkg.DartPubMetadata Dotnet pkg.DotnetDepsMetadata Portage pkg.PortageMetadata + Nix pkg.NixStoreMetadata } func main() { diff --git a/syft/file/classifier.go b/syft/file/classifier.go index d4efa538a1e..cbb34cc8bd8 100644 --- a/syft/file/classifier.go +++ b/syft/file/classifier.go @@ -22,6 +22,15 @@ var DefaultClassifiers = []Classifier{ `(?m)(?P{{ .version }}\.[0-9]+[-_a-zA-Z0-9]*)`, }, }, + { + Class: "nix-store", + FilepathPatterns: []*regexp.Regexp{ + regexp.MustCompile(`/nix/store/[^-]*-([^-]*)-(?P.*)/$`), + }, + EvidencePatternTemplates: []string{ + `(?m)(?P{{ .version }}\.[0-9]+[-_a-zA-Z0-9]*)`, + }, + }, { Class: "cpython-source", FilepathPatterns: []*regexp.Regexp{ diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 98d98436d2d..2158f50d75e 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -27,6 +27,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/rpmdb" "github.com/anchore/syft/syft/pkg/cataloger/ruby" "github.com/anchore/syft/syft/pkg/cataloger/rust" + "github.com/anchore/syft/syft/pkg/cataloger/nixstore" "github.com/anchore/syft/syft/pkg/cataloger/swift" "github.com/anchore/syft/syft/source" ) @@ -58,6 +59,7 @@ func ImageCatalogers(cfg Config) []Cataloger { golang.NewGoModuleBinaryCataloger(), dotnet.NewDotnetDepsCataloger(), portage.NewPortageCataloger(), + nixstore.NewNixStoreCataloger(), }, cfg.Catalogers) } diff --git a/syft/pkg/cataloger/nixstore/cataloger.go b/syft/pkg/cataloger/nixstore/cataloger.go new file mode 100644 index 00000000000..fc1edbf2070 --- /dev/null +++ b/syft/pkg/cataloger/nixstore/cataloger.go @@ -0,0 +1,64 @@ +package nixstore + +import ( + "fmt" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +type Cataloger struct{} + +func NewNixStoreCataloger() *Cataloger { + return &Cataloger{} +} + +func (c *Cataloger) Name() string { + return "nix-store-cataloger" +} + +func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) { + nixStoreFileMatches, err := resolver.FilesByGlob(pkg.NixStoreGlob) + if err != nil { + return nil, nil, fmt.Errorf("failed to find any nix store directory: %w", err) + } + + var allPackages []pkg.Package + pkgMap := make(map[string]interface{}) + for _, storeLocation := range nixStoreFileMatches { + name, version := extractNameAndVersion(storeLocation.VirtualPath) + + if version != "" { + nixLookupPkg := pkgMap[fmt.Sprintf("%s-%s", name, version)] + if nixLookupPkg == nil { + nixPkg := pkg.NixStoreMetadata{ + Source: name, + SourceVersion: version, + } + + p := newNixStorePackage(nixPkg) + p.Name = name + p.Version = version + p.FoundBy = c.Name() + p.Locations = source.NewLocationSet(storeLocation) + p.SetID() + + log.Debug(p) + + pkgMap[fmt.Sprintf("%s-%s", name, version)] = p + + } else { + nixLookupPkg := nixLookupPkg.(pkg.Package) + nixLookupPkg.Locations = source.NewLocationSet(append(nixLookupPkg.Locations.ToSlice(), storeLocation)...) + pkgMap[fmt.Sprintf("%s-%s", name, version)] = nixLookupPkg + } + } + } + + for _, p := range pkgMap { + allPackages = append(allPackages, p.(pkg.Package)) + } + return allPackages, nil, nil +} diff --git a/syft/pkg/cataloger/nixstore/parse_nix.go b/syft/pkg/cataloger/nixstore/parse_nix.go new file mode 100644 index 00000000000..17a71f0bf31 --- /dev/null +++ b/syft/pkg/cataloger/nixstore/parse_nix.go @@ -0,0 +1,31 @@ +package nixstore + +import ( + "fmt" + "regexp" + + "github.com/anchore/syft/internal" + + "github.com/anchore/syft/syft/pkg" +) + +var ( + errEndOfPackages = fmt.Errorf("no more packages to read") + sourceRegexp = regexp.MustCompile(`/nix/store/[^-]*-(?P[^-]*)-(?P[^-/]*).*/`) +) + +func newNixStorePackage(d pkg.NixStoreMetadata) pkg.Package { + return pkg.Package{ + Name: d.Package, + Version: d.Version, + Type: pkg.NixStorePkg, + MetadataType: pkg.NixStoreMetadataType, + Metadata: d, + } +} + +func extractNameAndVersion(source string) (string, string) { + // special handling for the Source field since it has formatted data + match := internal.MatchNamedCaptureGroups(sourceRegexp, source) + return match["name"], match["version"] +} diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index b2c74a66261..50c969b5fc0 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -14,6 +14,7 @@ const ( ApkMetadataType MetadataType = "ApkMetadata" AlpmMetadataType MetadataType = "AlpmMetadata" DpkgMetadataType MetadataType = "DpkgMetadata" + NixStoreMetadataType MetadataType = "NixStoreMetadata" GemMetadataType MetadataType = "GemMetadata" JavaMetadataType MetadataType = "JavaMetadata" NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata" @@ -35,6 +36,7 @@ var AllMetadataTypes = []MetadataType{ ApkMetadataType, AlpmMetadataType, DpkgMetadataType, + NixStoreMetadataType, GemMetadataType, JavaMetadataType, NpmPackageJSONMetadataType, diff --git a/syft/pkg/nix_store_metadata.go b/syft/pkg/nix_store_metadata.go new file mode 100644 index 00000000000..fddc3d3d2ec --- /dev/null +++ b/syft/pkg/nix_store_metadata.go @@ -0,0 +1,37 @@ +package pkg + +import ( + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/linux" +) + +const NixStoreGlob = "/nix/store/**" + +type NixStoreMetadata struct { + Package string `mapstructure:"Package" json:"package"` + Architecture string `mapstructure:"A" json:"architecture"` + Source string `mapstructure:"Source" json:"source"` + Version string `mapstructure:"Version" json:"version"` + SourceVersion string `mapstructure:"SourceVersion" json:"sourceVersion"` +} + +func (m NixStoreMetadata) PackageURL(d *linux.Release) string { + if d == nil { + return "" + } + pURL := packageurl.NewPackageURL( + // TODO: replace with `packageurl.TypeDebian` upon merge of https://github.com/package-url/packageurl-go/pull/21 + // TODO: or, since we're now using an Anchore fork of this module, we could do this sooner. + "nix", + "", + m.Package, + m.Version, + packageurl.Qualifiers{ + { + Key: "arch", + Value: m.Architecture, + }, + }, + "") + return pURL.ToString() +} diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 6dafed4727d..7f3748883a7 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -19,6 +19,7 @@ const ( JavaPkg Type = "java-archive" JenkinsPluginPkg Type = "jenkins-plugin" GoModulePkg Type = "go-module" + NixStorePkg Type = "nix-store" RustPkg Type = "rust-crate" KbPkg Type = "msrc-kb" DartPubPkg Type = "dart-pub" @@ -39,6 +40,7 @@ var AllPkgs = []Type{ NpmPkg, PythonPkg, PhpComposerPkg, + NixStorePkg, JavaPkg, JenkinsPluginPkg, GoModulePkg, @@ -63,6 +65,8 @@ func (t Type) PackageURLType() string { return packageurl.TypeGem case DebPkg: return "deb" + case NixStorePkg: + return "nix" case PythonPkg: return packageurl.TypePyPi case PhpComposerPkg: