mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Add Nix cataloger (#1696)
* Add Basic Nix Cataloger Signed-off-by: Julio Tain Sueiras <juliosueiras@gmail.com> * Update nix def for the latest syft definition Signed-off-by: Julio Tain Sueiras <juliosueiras@gmail.com> * capture nix package files on pkg.NixStoreMetadata Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * fix unit tests and linting Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update JSON schema Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * address review comments Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * Update syft/pkg/cataloger/nix/parse_nix_store_path_test.go Co-authored-by: Florian Klink <flokli@flokli.de> Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * support unstable version conventions Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update json schema relative to main branch Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update syft json with v7.1.1 schema Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * fix CLI tests Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove extra continue statement Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * add Nix to list of supported ecosystems Signed-off-by: Alex Goodman <alex.goodman@anchore.com> --------- Signed-off-by: Julio Tain Sueiras <juliosueiras@gmail.com> Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Co-authored-by: Julio Tain Sueiras <juliosueiras@gmail.com> Co-authored-by: Florian Klink <flokli@flokli.de>
This commit is contained in:
parent
8a574c9ed9
commit
7464079a09
@ -45,6 +45,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
|
||||
- Java (jar, ear, war, par, sar, native-image)
|
||||
- JavaScript (npm, yarn)
|
||||
- Jenkins Plugins (jpi, hpi)
|
||||
- Nix (outputs in /nix/store)
|
||||
- PHP (composer)
|
||||
- Python (wheel, egg, poetry, requirements.txt)
|
||||
- Red Hat (rpm)
|
||||
|
||||
@ -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.0"
|
||||
JSONSchemaVersion = "7.1.1"
|
||||
)
|
||||
|
||||
@ -45,6 +45,7 @@ type artifactMetadataContainer struct {
|
||||
Hackage pkg.HackageMetadata
|
||||
Java pkg.JavaMetadata
|
||||
KbPackage pkg.KbPackageMetadata
|
||||
Nix pkg.NixStoreMetadata
|
||||
NpmPackage pkg.NpmPackageJSONMetadata
|
||||
NpmPackageLock pkg.NpmPackageLockJSONMetadata
|
||||
MixLock pkg.MixLockMetadata
|
||||
|
||||
1663
schema/json/schema-7.1.1.json
Normal file
1663
schema/json/schema-7.1.1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,8 @@ 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.NixPkg:
|
||||
answer = "acquired package info from nix store path"
|
||||
default:
|
||||
answer = "acquired package info from the following paths"
|
||||
}
|
||||
|
||||
@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) {
|
||||
"from rebar3 or mix manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.NixPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from nix store path",
|
||||
},
|
||||
},
|
||||
}
|
||||
var pkgTypes []pkg.Type
|
||||
for _, test := range tests {
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "7.1.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json"
|
||||
"version": "7.1.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json"
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,7 +185,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "7.1.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json"
|
||||
"version": "7.1.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json"
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "7.1.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json"
|
||||
"version": "7.1.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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/nix"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/php"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/portage"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
@ -51,6 +52,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger {
|
||||
golang.NewGoModuleBinaryCataloger(cfg.Go()),
|
||||
dotnet.NewDotnetDepsCataloger(),
|
||||
portage.NewPortageCataloger(),
|
||||
nix.NewStoreCataloger(),
|
||||
sbom.NewSBOMCataloger(),
|
||||
binary.NewCataloger(),
|
||||
}, cfg.Catalogers)
|
||||
@ -85,6 +87,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
|
||||
binary.NewCataloger(),
|
||||
elixir.NewMixLockCataloger(),
|
||||
erlang.NewRebarLockCataloger(),
|
||||
nix.NewStoreCataloger(),
|
||||
}, cfg.Catalogers)
|
||||
}
|
||||
|
||||
@ -121,6 +124,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
|
||||
binary.NewCataloger(),
|
||||
elixir.NewMixLockCataloger(),
|
||||
erlang.NewRebarLockCataloger(),
|
||||
nix.NewStoreCataloger(),
|
||||
}, cfg.Catalogers)
|
||||
}
|
||||
|
||||
|
||||
101
syft/pkg/cataloger/nix/cataloger.go
Normal file
101
syft/pkg/cataloger/nix/cataloger.go
Normal file
@ -0,0 +1,101 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
catalogerName = "nix-store-cataloger"
|
||||
nixStoreGlob = "**/nix/store/*"
|
||||
)
|
||||
|
||||
// StoreCataloger finds package outputs installed in the Nix store location (/nix/store/*).
|
||||
type StoreCataloger struct{}
|
||||
|
||||
func NewStoreCataloger() *StoreCataloger {
|
||||
return &StoreCataloger{}
|
||||
}
|
||||
|
||||
func (c *StoreCataloger) Name() string {
|
||||
return catalogerName
|
||||
}
|
||||
|
||||
func (c *StoreCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
// we want to search for only directories, which isn't possible via the stereoscope API, so we need to apply the glob manually on all returned paths
|
||||
var pkgs []pkg.Package
|
||||
var filesByPath = make(map[string]*source.LocationSet)
|
||||
for location := range resolver.AllLocations() {
|
||||
matchesStorePath, err := doublestar.Match(nixStoreGlob, location.RealPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to match nix store path: %w", err)
|
||||
}
|
||||
|
||||
parentStorePath := findParentNixStorePath(location.RealPath)
|
||||
if parentStorePath != "" {
|
||||
if _, ok := filesByPath[parentStorePath]; !ok {
|
||||
s := source.NewLocationSet()
|
||||
filesByPath[parentStorePath] = &s
|
||||
}
|
||||
filesByPath[parentStorePath].Add(location)
|
||||
}
|
||||
|
||||
if !matchesStorePath {
|
||||
continue
|
||||
}
|
||||
|
||||
storePath := parseNixStorePath(location.RealPath)
|
||||
|
||||
if storePath == nil || !storePath.isValidPackage() {
|
||||
continue
|
||||
}
|
||||
|
||||
p := newNixStorePackage(*storePath, location)
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
|
||||
// add file sets to packages
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
locations := p.Locations.ToSlice()
|
||||
if len(locations) == 0 {
|
||||
log.WithFields("package", p.Name).Warn("nix package has no evidence locations associated")
|
||||
continue
|
||||
}
|
||||
parentStorePath := locations[0].RealPath
|
||||
files, ok := filesByPath[parentStorePath]
|
||||
if !ok {
|
||||
log.WithFields("path", parentStorePath, "nix-store-path", parentStorePath).Warn("found a nix store file for a non-existent package")
|
||||
continue
|
||||
}
|
||||
appendFiles(p, files.ToSlice()...)
|
||||
}
|
||||
|
||||
return pkgs, nil, nil
|
||||
}
|
||||
|
||||
func appendFiles(p *pkg.Package, location ...source.Location) {
|
||||
metadata, ok := p.Metadata.(pkg.NixStoreMetadata)
|
||||
if !ok {
|
||||
log.WithFields("package", p.Name).Warn("nix package metadata missing")
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range location {
|
||||
metadata.Files = append(metadata.Files, l.RealPath)
|
||||
}
|
||||
|
||||
if metadata.Files == nil {
|
||||
// note: we always have an allocated collection for output
|
||||
metadata.Files = []string{}
|
||||
}
|
||||
|
||||
p.Metadata = metadata
|
||||
p.SetID()
|
||||
}
|
||||
55
syft/pkg/cataloger/nix/cataloger_test.go
Normal file
55
syft/pkg/cataloger/nix/cataloger_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package nix
|
||||
|
||||
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 TestCataloger_Catalog(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
fixture string
|
||||
wantPkgs []pkg.Package
|
||||
wantRel []artifact.Relationship
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/fixture-1",
|
||||
wantPkgs: []pkg.Package{
|
||||
{
|
||||
Name: "glibc",
|
||||
Version: "2.34-210",
|
||||
PURL: "pkg:nix/glibc@2.34-210?output=bin&outputhash=h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
Locations: source.NewLocationSet(source.NewLocation("nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin")),
|
||||
FoundBy: catalogerName,
|
||||
Type: pkg.NixPkg,
|
||||
MetadataType: pkg.NixStoreMetadataType,
|
||||
Metadata: pkg.NixStoreMetadata{
|
||||
OutputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
Output: "bin",
|
||||
Files: []string{
|
||||
"nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib",
|
||||
"nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so",
|
||||
"nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share",
|
||||
"nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man",
|
||||
"nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/share/man/glibc.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.fixture, func(t *testing.T) {
|
||||
c := NewStoreCataloger()
|
||||
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, tt.fixture).
|
||||
Expects(tt.wantPkgs, tt.wantRel).
|
||||
TestCataloger(t, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
59
syft/pkg/cataloger/nix/package.go
Normal file
59
syft/pkg/cataloger/nix/package.go
Normal file
@ -0,0 +1,59 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func newNixStorePackage(storePath nixStorePath, locations ...source.Location) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: storePath.name,
|
||||
Version: storePath.version,
|
||||
FoundBy: catalogerName,
|
||||
Locations: source.NewLocationSet(locations...),
|
||||
Type: pkg.NixPkg,
|
||||
PURL: packageURL(storePath),
|
||||
MetadataType: pkg.NixStoreMetadataType,
|
||||
Metadata: pkg.NixStoreMetadata{
|
||||
OutputHash: storePath.outputHash,
|
||||
Output: storePath.output,
|
||||
},
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func packageURL(storePath nixStorePath) string {
|
||||
var qualifiers packageurl.Qualifiers
|
||||
if storePath.output != "" {
|
||||
// since there is no nix pURL type yet, this is a guess, however, it is reasonable to assume that
|
||||
// if only a single output is installed the pURL should be able to express this.
|
||||
qualifiers = append(qualifiers,
|
||||
packageurl.Qualifier{
|
||||
Key: "output",
|
||||
Value: storePath.output,
|
||||
},
|
||||
)
|
||||
}
|
||||
if storePath.outputHash != "" {
|
||||
// it's not immediately clear if the hash found in the store path should be encoded in the pURL
|
||||
qualifiers = append(qualifiers,
|
||||
packageurl.Qualifier{
|
||||
Key: "outputhash",
|
||||
Value: storePath.outputHash,
|
||||
},
|
||||
)
|
||||
}
|
||||
pURL := packageurl.NewPackageURL(
|
||||
// TODO: nix pURL type has not been accepted yet (only proposed at this time)
|
||||
"nix",
|
||||
"",
|
||||
storePath.name,
|
||||
storePath.version,
|
||||
qualifiers,
|
||||
"")
|
||||
return pURL.ToString()
|
||||
}
|
||||
49
syft/pkg/cataloger/nix/package_test.go
Normal file
49
syft/pkg/cataloger/nix/package_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_packageURL(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
storePath nixStorePath
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "name + version",
|
||||
storePath: nixStorePath{
|
||||
name: "glibc",
|
||||
version: "2.34",
|
||||
},
|
||||
want: "pkg:nix/glibc@2.34",
|
||||
},
|
||||
{
|
||||
name: "hash qualifier",
|
||||
storePath: nixStorePath{
|
||||
name: "glibc",
|
||||
version: "2.34",
|
||||
outputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
},
|
||||
want: "pkg:nix/glibc@2.34?outputhash=h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
},
|
||||
{
|
||||
name: "output qualifier",
|
||||
storePath: nixStorePath{
|
||||
name: "glibc",
|
||||
version: "2.34",
|
||||
outputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
output: "bin",
|
||||
},
|
||||
want: "pkg:nix/glibc@2.34?output=bin&outputhash=h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, packageURL(tt.storePath))
|
||||
})
|
||||
}
|
||||
}
|
||||
134
syft/pkg/cataloger/nix/parse_nix_store_path.go
Normal file
134
syft/pkg/cataloger/nix/parse_nix_store_path.go
Normal file
@ -0,0 +1,134 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
numericPattern = regexp.MustCompile(`\d`)
|
||||
|
||||
// attempts to find the right-most example of something that appears to be a version (semver or otherwise)
|
||||
// example input: h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin
|
||||
// example output:
|
||||
// version: "2.34-210"
|
||||
// major: "2"
|
||||
// minor: "34"
|
||||
// patch: "210"
|
||||
// (there are other capture groups, but they can be ignored)
|
||||
rightMostVersionIshPattern = regexp.MustCompile(`-(?P<version>(?P<major>[0-9][a-zA-Z0-9]*)(\.(?P<minor>[0-9][a-zA-Z0-9]*))?(\.(?P<patch>0|[1-9][a-zA-Z0-9]*)){0,3}(?:-(?P<prerelease>\d*[.a-zA-Z-][.0-9a-zA-Z-]*)*)?(?:\+(?P<metadata>[.0-9a-zA-Z-]+(?:\.[.0-9a-zA-Z-]+)*))?)`)
|
||||
|
||||
unstableVersion = regexp.MustCompile(`-(?P<version>unstable-\d{4}-\d{2}-\d{2})$`)
|
||||
)
|
||||
|
||||
// checkout the package naming conventions here: https://nixos.org/manual/nixpkgs/stable/#sec-package-naming
|
||||
|
||||
type nixStorePath struct {
|
||||
outputHash string
|
||||
name string
|
||||
version string
|
||||
output string
|
||||
}
|
||||
|
||||
func (p nixStorePath) isValidPackage() bool {
|
||||
return p.name != "" && p.version != ""
|
||||
}
|
||||
|
||||
func findParentNixStorePath(source string) string {
|
||||
source = strings.TrimRight(source, "/")
|
||||
indicator := "nix/store/"
|
||||
start := strings.Index(source, indicator)
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
startOfHash := start + len(indicator)
|
||||
nextField := strings.Index(source[startOfHash:], "/")
|
||||
if nextField == -1 {
|
||||
return ""
|
||||
}
|
||||
startOfSubPath := startOfHash + nextField
|
||||
|
||||
return source[0:startOfSubPath]
|
||||
}
|
||||
|
||||
func parseNixStorePath(source string) *nixStorePath {
|
||||
if strings.HasSuffix(source, ".drv") {
|
||||
// ignore derivations
|
||||
return nil
|
||||
}
|
||||
|
||||
source = path.Base(source)
|
||||
|
||||
versionStartIdx, versionIsh, prerelease := findVersionIsh(source)
|
||||
if versionStartIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hashName := strings.TrimSuffix(source[0:versionStartIdx], "-")
|
||||
hashNameFields := strings.Split(hashName, "-")
|
||||
if len(hashNameFields) < 2 {
|
||||
return nil
|
||||
}
|
||||
hash, name := hashNameFields[0], strings.Join(hashNameFields[1:], "-")
|
||||
|
||||
prereleaseFields := strings.Split(prerelease, "-")
|
||||
lastPrereleaseField := prereleaseFields[len(prereleaseFields)-1]
|
||||
|
||||
var version = versionIsh
|
||||
var output string
|
||||
if !hasNumeric(lastPrereleaseField) {
|
||||
// this last prerelease field is probably a nix output
|
||||
version = strings.TrimSuffix(versionIsh, fmt.Sprintf("-%s", lastPrereleaseField))
|
||||
output = lastPrereleaseField
|
||||
}
|
||||
|
||||
return &nixStorePath{
|
||||
outputHash: hash,
|
||||
name: name,
|
||||
version: version,
|
||||
output: output,
|
||||
}
|
||||
}
|
||||
|
||||
func hasNumeric(s string) bool {
|
||||
return numericPattern.MatchString(s)
|
||||
}
|
||||
|
||||
func findVersionIsh(input string) (int, string, string) {
|
||||
// we want to return the index of the start of the "version" group (the first capture group).
|
||||
// note that the match indices are in the form of [start, end, start, end, ...]. Also note that the
|
||||
// capture group for version in both regexes are the same index, but if the regexes are changed
|
||||
// this code will start to fail.
|
||||
versionGroup := 1
|
||||
|
||||
match := unstableVersion.FindAllStringSubmatchIndex(input, -1)
|
||||
if len(match) > 0 && len(match[0]) > 0 {
|
||||
return match[0][versionGroup*2], input[match[0][versionGroup*2]:match[0][(versionGroup*2)+1]], ""
|
||||
}
|
||||
|
||||
match = rightMostVersionIshPattern.FindAllStringSubmatchIndex(input, -1)
|
||||
if len(match) == 0 || len(match[0]) == 0 {
|
||||
return -1, "", ""
|
||||
}
|
||||
|
||||
var version string
|
||||
versionStart, versionStop := match[0][versionGroup*2], match[0][(versionGroup*2)+1]
|
||||
if versionStart != -1 || versionStop != -1 {
|
||||
version = input[versionStart:versionStop]
|
||||
}
|
||||
|
||||
prereleaseGroup := 7
|
||||
|
||||
var prerelease string
|
||||
prereleaseStart, prereleaseStop := match[0][prereleaseGroup*2], match[0][(prereleaseGroup*2)+1]
|
||||
if prereleaseStart != -1 && prereleaseStop != -1 {
|
||||
prerelease = input[prereleaseStart:prereleaseStop]
|
||||
}
|
||||
|
||||
return versionStart,
|
||||
version,
|
||||
prerelease
|
||||
}
|
||||
304
syft/pkg/cataloger/nix/parse_nix_store_path_test.go
Normal file
304
syft/pkg/cataloger/nix/parse_nix_store_path_test.go
Normal file
@ -0,0 +1,304 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_findVersionIsh(t *testing.T) {
|
||||
// note: only the package version fields are tested here, the name is tested in parseNixStorePath below.
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantIdx int
|
||||
wantVersion string
|
||||
wantPreRelease string
|
||||
}{
|
||||
{
|
||||
name: "no version",
|
||||
input: "5q7vxm9lc4b9hifc3br4sr8dy7f2h0qa-source",
|
||||
wantIdx: -1,
|
||||
wantVersion: "",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
name: "semver with overbite into output",
|
||||
input: "/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin",
|
||||
wantIdx: 50,
|
||||
wantVersion: "2.34-210-bin",
|
||||
wantPreRelease: "210-bin",
|
||||
},
|
||||
{
|
||||
name: "multiple versions",
|
||||
input: "5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
wantIdx: 53,
|
||||
wantVersion: "2.33",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
name: "name ends with number",
|
||||
input: "55nswyz8335lk954y1ccx6as2jbq1z8f-libfido2-1.10.0",
|
||||
wantIdx: 42,
|
||||
wantVersion: "1.10.0",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
name: "major-minor only",
|
||||
input: "q8gnp7r8475p52k9gmdzsrcddw5hirbn-gdbm-1.23",
|
||||
wantIdx: 38,
|
||||
wantVersion: "1.23",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
name: "0-prefixed version field",
|
||||
input: "r705jm2icczpnmfccby3fzfrckfjakx3-perl5.34.1-URI-5.05",
|
||||
wantIdx: 48,
|
||||
wantVersion: "5.05",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
name: "prerelease with alpha prefix",
|
||||
input: "v48s6iddb518j9lc1pk3rcn3x8c2ff0j-bash-interactive-5.1-p16",
|
||||
wantIdx: 50,
|
||||
wantVersion: "5.1-p16",
|
||||
wantPreRelease: "p16",
|
||||
},
|
||||
{
|
||||
|
||||
name: "0-major version",
|
||||
input: "x2f9x5q6qrs6cssx09ylxqyg9q2isi1z-aws-c-http-0.6.15",
|
||||
wantIdx: 44,
|
||||
wantVersion: "0.6.15",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
|
||||
name: "several version fields",
|
||||
// note: this package version is fictitious
|
||||
input: "z24qs6f5d1mmwdp73n1jfc3swj4v2c5s-krb5-1.19.3.9.10",
|
||||
wantIdx: 38,
|
||||
wantVersion: "1.19.3.9.10",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
|
||||
name: "skip drv + major only version",
|
||||
input: "z0fqylhisz47krxv8fd0izm1i2qbswfr-readline63-006.drv",
|
||||
wantIdx: 44,
|
||||
wantVersion: "006",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
|
||||
name: "prerelease with multiple dashes",
|
||||
input: "zkgyp2vra0bgqm0dv1qi514l5fd0aksx-bash-interactive-5.1-p16-man",
|
||||
wantIdx: 50,
|
||||
wantVersion: "5.1-p16-man",
|
||||
wantPreRelease: "p16-man",
|
||||
},
|
||||
{
|
||||
|
||||
name: "date as major version",
|
||||
input: "0amf0d1dymv9gqcyhhjb9j0l8sn00c56-libedit-20210910-3.1",
|
||||
wantIdx: 41,
|
||||
wantVersion: "20210910-3.1",
|
||||
wantPreRelease: "3.1",
|
||||
},
|
||||
{
|
||||
|
||||
name: "long name",
|
||||
input: "0296qxvn30z9b2ah1g5p97k5wr9k8y78-busybox-static-x86_64-unknown-linux-musl-1.35.0",
|
||||
wantIdx: 74,
|
||||
wantVersion: "1.35.0",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
{
|
||||
// this accounts for https://nixos.org/manual/nixpkgs/stable/#sec-package-naming
|
||||
// > If a package is not a release but a commit from a repository, then the version attribute must
|
||||
// > be the date of that (fetched) commit. The date must be in "unstable-YYYY-MM-DD" format.
|
||||
// example: https://github.com/NixOS/nixpkgs/blob/798e23beab9b5cba4d6f05e8b243e1d4535770f3/pkgs/servers/webdav-server-rs/default.nix#L14
|
||||
name: "unstable version",
|
||||
input: "q5dhwzcn82by5ndc7g0q83wsnn13qkqw-webdav-server-rs-unstable-2021-08-16",
|
||||
wantIdx: 50,
|
||||
wantVersion: "unstable-2021-08-16",
|
||||
wantPreRelease: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotIdx, gotVersion, gotPreRelease := findVersionIsh(tt.input)
|
||||
assert.Equal(t, tt.wantIdx, gotIdx)
|
||||
assert.Equal(t, tt.wantVersion, gotVersion)
|
||||
assert.Equal(t, tt.wantPreRelease, gotPreRelease)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseNixStorePath(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
source string
|
||||
want *nixStorePath
|
||||
}{
|
||||
{
|
||||
source: "/nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin",
|
||||
want: &nixStorePath{
|
||||
outputHash: "h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
name: "glibc",
|
||||
version: "2.34-210",
|
||||
output: "bin",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/0296qxvn30z9b2ah1g5p97k5wr9k8y78-busybox-static-x86_64-unknown-linux-musl-1.35.0",
|
||||
want: &nixStorePath{
|
||||
outputHash: "0296qxvn30z9b2ah1g5p97k5wr9k8y78",
|
||||
name: "busybox-static-x86_64-unknown-linux-musl",
|
||||
version: "1.35.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
want: &nixStorePath{
|
||||
outputHash: "5zzrvdmlkc5rh3k5862krd3wfb3pqhyf",
|
||||
name: "perl5.34.1-TimeDate",
|
||||
version: "2.33",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/q38q8ng57zwjg1h15ry5zx0lb0xyax4b-libcap-2.63-lib",
|
||||
want: &nixStorePath{
|
||||
outputHash: "q38q8ng57zwjg1h15ry5zx0lb0xyax4b",
|
||||
name: "libcap",
|
||||
version: "2.63",
|
||||
output: "lib",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/p0y8fbpbqr2jm5zfrdll0rgyg2lvp5g2-util-linux-minimal-2.37.4-bin",
|
||||
want: &nixStorePath{
|
||||
outputHash: "p0y8fbpbqr2jm5zfrdll0rgyg2lvp5g2",
|
||||
name: "util-linux-minimal",
|
||||
version: "2.37.4",
|
||||
output: "bin",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/z24qs6f5d1mmwdp73n1jfc3swj4v2c5s-krb5-1.19.3.9.10",
|
||||
want: &nixStorePath{
|
||||
outputHash: "z24qs6f5d1mmwdp73n1jfc3swj4v2c5s",
|
||||
name: "krb5",
|
||||
version: "1.19.3.9.10",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/zkgyp2vra0bgqm0dv1qi514l5fd0aksx-bash-interactive-5.1-p16-man",
|
||||
want: &nixStorePath{
|
||||
outputHash: "zkgyp2vra0bgqm0dv1qi514l5fd0aksx",
|
||||
name: "bash-interactive",
|
||||
version: "5.1-p16",
|
||||
output: "man",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/nwf2y0nc48ybim56308cr5ccvwkabcqc-openssl-1.1.1q",
|
||||
want: &nixStorePath{
|
||||
outputHash: "nwf2y0nc48ybim56308cr5ccvwkabcqc",
|
||||
name: "openssl",
|
||||
version: "1.1.1q",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/nwv742f1bxv6g78hy9yc6slxdbxlmqhb-kmod-29",
|
||||
want: &nixStorePath{
|
||||
outputHash: "nwv742f1bxv6g78hy9yc6slxdbxlmqhb",
|
||||
name: "kmod",
|
||||
version: "29",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "/nix/store/n83qx7m848kg51lcjchwbkmlgdaxfckf-tzdata-2022a",
|
||||
want: &nixStorePath{
|
||||
outputHash: "n83qx7m848kg51lcjchwbkmlgdaxfckf",
|
||||
name: "tzdata",
|
||||
version: "2022a",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "'/nix/store/q5dhwzcn82by5ndc7g0q83wsnn13qkqw-webdav-server-rs-unstable-2021-08-16",
|
||||
want: &nixStorePath{
|
||||
outputHash: "q5dhwzcn82by5ndc7g0q83wsnn13qkqw",
|
||||
name: "webdav-server-rs",
|
||||
version: "unstable-2021-08-16",
|
||||
},
|
||||
},
|
||||
// negative cases...
|
||||
{
|
||||
source: "'z33yk02rsr6b4rb56lgb80bnvxx6yw39-?id=21ee35dde73aec5eba35290587d479218c6dd824.drv'",
|
||||
},
|
||||
{
|
||||
source: "/nix/store/yzahni8aig6mdrvcsccgwm2515lcpi5q-git-minimal-2.36.0.drv",
|
||||
},
|
||||
{
|
||||
source: "/nix/store/z9yvxs0s3xdkp5jgmzis4g50bfq3dgvm-0018-pkg-config-derive-prefix-from-prefix.patch",
|
||||
},
|
||||
{
|
||||
source: "/nix/store/w3hl7zrmc9qvzadc0k7cp9ysxiyz88j6-base-system",
|
||||
},
|
||||
{
|
||||
source: "/nix/store/zz1lc28x25fcx6al6xwk3dk8kp7wx47y-Test-RequiresInternet-0.05.tar.gz.drv",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(path.Base(tt.source), func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, parseNixStorePath(tt.source))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parentNixStorePath(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "exact path from absolute root",
|
||||
source: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "exact path from relative root",
|
||||
source: "nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "clean paths",
|
||||
source: "//nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33///",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "relative root with subdir file",
|
||||
source: "nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33/bin/perl-timedate",
|
||||
want: "nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
},
|
||||
{
|
||||
name: "absolute root with with subdir file",
|
||||
source: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33/bin/perl-timedate",
|
||||
want: "/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
},
|
||||
{
|
||||
name: "nexted root with with subdir file",
|
||||
source: "/somewhere/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33/bin/perl-timedate",
|
||||
want: "/somewhere/nix/store/5zzrvdmlkc5rh3k5862krd3wfb3pqhyf-perl5.34.1-TimeDate-2.33",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, findParentNixStorePath(tt.source))
|
||||
})
|
||||
}
|
||||
}
|
||||
2
syft/pkg/cataloger/nix/test-fixtures/fixture-1/.gitignore
vendored
Normal file
2
syft/pkg/cataloger/nix/test-fixtures/fixture-1/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# this is not a real binary, just a small text file
|
||||
!nix/store/h0cnbmfcn93xm5dg2x27ixhag1cwndga-glibc-2.34-210-bin/lib/glibc.so
|
||||
@ -0,0 +1 @@
|
||||
the binary
|
||||
@ -0,0 +1 @@
|
||||
the man pages
|
||||
@ -9,6 +9,7 @@ type MetadataType string
|
||||
|
||||
const (
|
||||
// this is the full set of data shapes that can be represented within the pkg.Package.Metadata field
|
||||
|
||||
UnknownMetadataType MetadataType = "UnknownMetadata"
|
||||
AlpmMetadataType MetadataType = "AlpmMetadata"
|
||||
ApkMetadataType MetadataType = "ApkMetadata"
|
||||
@ -26,6 +27,7 @@ const (
|
||||
JavaMetadataType MetadataType = "JavaMetadata"
|
||||
KbPackageMetadataType MetadataType = "KbPackageMetadata"
|
||||
MixLockMetadataType MetadataType = "MixLockMetadataType"
|
||||
NixStoreMetadataType MetadataType = "NixStoreMetadata"
|
||||
NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata"
|
||||
NpmPackageLockJSONMetadataType MetadataType = "NpmPackageLockJsonMetadata"
|
||||
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
|
||||
@ -54,6 +56,7 @@ var AllMetadataTypes = []MetadataType{
|
||||
JavaMetadataType,
|
||||
KbPackageMetadataType,
|
||||
MixLockMetadataType,
|
||||
NixStoreMetadataType,
|
||||
NpmPackageJSONMetadataType,
|
||||
NpmPackageLockJSONMetadataType,
|
||||
PhpComposerJSONMetadataType,
|
||||
@ -82,6 +85,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
|
||||
JavaMetadataType: reflect.TypeOf(JavaMetadata{}),
|
||||
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
|
||||
MixLockMetadataType: reflect.TypeOf(MixLockMetadata{}),
|
||||
NixStoreMetadataType: reflect.TypeOf(NixStoreMetadata{}),
|
||||
NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}),
|
||||
NpmPackageLockJSONMetadataType: reflect.TypeOf(NpmPackageLockJSONMetadata{}),
|
||||
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
|
||||
|
||||
25
syft/pkg/nix_store_metadata.go
Normal file
25
syft/pkg/nix_store_metadata.go
Normal file
@ -0,0 +1,25 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
type NixStoreMetadata struct {
|
||||
// OutputHash is the prefix of the nix store basename path
|
||||
OutputHash string `mapstructure:"outputHash" json:"outputHash"`
|
||||
|
||||
// Output allows for optionally specifying the specific nix package output this package represents (for packages that support multiple outputs).
|
||||
// Note: the default output for a package is an empty string, so will not be present in the output.
|
||||
Output string `mapstructure:"output" json:"output,omitempty"`
|
||||
|
||||
// Files is a listing a files that are under the nix/store path for this package
|
||||
Files []string `mapstructure:"files" json:"files"`
|
||||
}
|
||||
|
||||
func (m NixStoreMetadata) OwnedFiles() (result []string) {
|
||||
result = strset.New(m.Files...).List()
|
||||
sort.Strings(result)
|
||||
return
|
||||
}
|
||||
@ -26,6 +26,7 @@ const (
|
||||
JavaPkg Type = "java-archive"
|
||||
JenkinsPluginPkg Type = "jenkins-plugin"
|
||||
KbPkg Type = "msrc-kb"
|
||||
NixPkg Type = "nix"
|
||||
NpmPkg Type = "npm"
|
||||
PhpComposerPkg Type = "php-composer"
|
||||
PortagePkg Type = "portage"
|
||||
@ -51,6 +52,7 @@ var AllPkgs = []Type{
|
||||
JavaPkg,
|
||||
JenkinsPluginPkg,
|
||||
KbPkg,
|
||||
NixPkg,
|
||||
NpmPkg,
|
||||
PhpComposerPkg,
|
||||
PortagePkg,
|
||||
@ -92,6 +94,8 @@ func (t Type) PackageURLType() string {
|
||||
return packageurl.TypePyPi
|
||||
case PortagePkg:
|
||||
return "portage"
|
||||
case NixPkg:
|
||||
return "nix"
|
||||
case NpmPkg:
|
||||
return packageurl.TypeNPM
|
||||
case RpmPkg:
|
||||
@ -151,6 +155,8 @@ func TypeByName(name string) Type {
|
||||
return PortagePkg
|
||||
case packageurl.TypeHex:
|
||||
return HexPkg
|
||||
case "nix":
|
||||
return NixPkg
|
||||
default:
|
||||
return UnknownPkg
|
||||
}
|
||||
|
||||
@ -83,6 +83,10 @@ func TestTypeFromPURL(t *testing.T) {
|
||||
purl: "pkg:hex/hpax/hpax@0.1.1",
|
||||
expected: HexPkg,
|
||||
},
|
||||
{
|
||||
purl: "pkg:nix/glibc@2.34?hash=h0cnbmfcn93xm5dg2x27ixhag1cwndga",
|
||||
expected: NixPkg,
|
||||
},
|
||||
}
|
||||
|
||||
var pkgTypes []string
|
||||
|
||||
@ -97,7 +97,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
name: "squashed-scope-flag",
|
||||
args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage},
|
||||
assertions: []traitAssertion{
|
||||
assertPackageCount(34),
|
||||
assertPackageCount(35),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -214,7 +214,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(34),
|
||||
assertPackageCount(35),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -225,7 +225,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(34),
|
||||
assertPackageCount(35),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
@ -239,7 +239,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
assertions: []traitAssertion{
|
||||
assertNotInOutput("secret_password"),
|
||||
assertNotInOutput("secret_key_path"),
|
||||
assertPackageCount(34),
|
||||
assertPackageCount(35),
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
|
||||
@ -390,4 +390,11 @@ var commonTestCases = []testCase{
|
||||
"example-jenkins-plugin": "1.0-SNAPSHOT",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find nix store packages",
|
||||
pkgType: pkg.NixPkg,
|
||||
pkgInfo: map[string]string{
|
||||
"glibc": "2.34-210",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
the man pages
|
||||
Loading…
x
Reference in New Issue
Block a user