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:
Alex Goodman 2023-04-04 10:53:56 -04:00 committed by GitHub
parent 8a574c9ed9
commit 7464079a09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2443 additions and 11 deletions

View File

@ -45,6 +45,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- Java (jar, ear, war, par, sar, native-image) - Java (jar, ear, war, par, sar, native-image)
- JavaScript (npm, yarn) - JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi) - Jenkins Plugins (jpi, hpi)
- Nix (outputs in /nix/store)
- PHP (composer) - PHP (composer)
- Python (wheel, egg, poetry, requirements.txt) - Python (wheel, egg, poetry, requirements.txt)
- Red Hat (rpm) - Red Hat (rpm)

View File

@ -6,5 +6,5 @@ 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 = "7.1.0" JSONSchemaVersion = "7.1.1"
) )

View File

@ -45,6 +45,7 @@ type artifactMetadataContainer struct {
Hackage pkg.HackageMetadata Hackage pkg.HackageMetadata
Java pkg.JavaMetadata Java pkg.JavaMetadata
KbPackage pkg.KbPackageMetadata KbPackage pkg.KbPackageMetadata
Nix pkg.NixStoreMetadata
NpmPackage pkg.NpmPackageJSONMetadata NpmPackage pkg.NpmPackageJSONMetadata
NpmPackageLock pkg.NpmPackageLockJSONMetadata NpmPackageLock pkg.NpmPackageLockJSONMetadata
MixLock pkg.MixLockMetadata MixLock pkg.MixLockMetadata

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from cabal or stack manifest files" answer = "acquired package info from cabal or stack manifest files"
case pkg.HexPkg: case pkg.HexPkg:
answer = "acquired package info from rebar3 or mix manifest file" answer = "acquired package info from rebar3 or mix manifest file"
case pkg.NixPkg:
answer = "acquired package info from nix store path"
default: default:
answer = "acquired package info from the following paths" answer = "acquired package info from the following paths"
} }

View File

@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) {
"from rebar3 or mix manifest file", "from rebar3 or mix manifest file",
}, },
}, },
{
input: pkg.Package{
Type: pkg.NixPkg,
},
expected: []string{
"from nix store path",
},
},
} }
var pkgTypes []pkg.Type var pkgTypes []pkg.Type
for _, test := range tests { for _, test := range tests {

View File

@ -89,7 +89,7 @@
} }
}, },
"schema": { "schema": {
"version": "7.1.0", "version": "7.1.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json"
} }
} }

View File

@ -185,7 +185,7 @@
} }
}, },
"schema": { "schema": {
"version": "7.1.0", "version": "7.1.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json"
} }
} }

View File

@ -112,7 +112,7 @@
} }
}, },
"schema": { "schema": {
"version": "7.1.0", "version": "7.1.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.0.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-7.1.1.json"
} }
} }

View File

@ -23,6 +23,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/haskell" "github.com/anchore/syft/syft/pkg/cataloger/haskell"
"github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript" "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/php"
"github.com/anchore/syft/syft/pkg/cataloger/portage" "github.com/anchore/syft/syft/pkg/cataloger/portage"
"github.com/anchore/syft/syft/pkg/cataloger/python" "github.com/anchore/syft/syft/pkg/cataloger/python"
@ -51,6 +52,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger {
golang.NewGoModuleBinaryCataloger(cfg.Go()), golang.NewGoModuleBinaryCataloger(cfg.Go()),
dotnet.NewDotnetDepsCataloger(), dotnet.NewDotnetDepsCataloger(),
portage.NewPortageCataloger(), portage.NewPortageCataloger(),
nix.NewStoreCataloger(),
sbom.NewSBOMCataloger(), sbom.NewSBOMCataloger(),
binary.NewCataloger(), binary.NewCataloger(),
}, cfg.Catalogers) }, cfg.Catalogers)
@ -85,6 +87,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
binary.NewCataloger(), binary.NewCataloger(),
elixir.NewMixLockCataloger(), elixir.NewMixLockCataloger(),
erlang.NewRebarLockCataloger(), erlang.NewRebarLockCataloger(),
nix.NewStoreCataloger(),
}, cfg.Catalogers) }, cfg.Catalogers)
} }
@ -121,6 +124,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
binary.NewCataloger(), binary.NewCataloger(),
elixir.NewMixLockCataloger(), elixir.NewMixLockCataloger(),
erlang.NewRebarLockCataloger(), erlang.NewRebarLockCataloger(),
nix.NewStoreCataloger(),
}, cfg.Catalogers) }, cfg.Catalogers)
} }

View 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()
}

View 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)
})
}
}

View 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()
}

View 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))
})
}
}

View 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
}

View 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))
})
}
}

View 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

View File

@ -9,6 +9,7 @@ type MetadataType string
const ( const (
// this is the full set of data shapes that can be represented within the pkg.Package.Metadata field // this is the full set of data shapes that can be represented within the pkg.Package.Metadata field
UnknownMetadataType MetadataType = "UnknownMetadata" UnknownMetadataType MetadataType = "UnknownMetadata"
AlpmMetadataType MetadataType = "AlpmMetadata" AlpmMetadataType MetadataType = "AlpmMetadata"
ApkMetadataType MetadataType = "ApkMetadata" ApkMetadataType MetadataType = "ApkMetadata"
@ -26,6 +27,7 @@ const (
JavaMetadataType MetadataType = "JavaMetadata" JavaMetadataType MetadataType = "JavaMetadata"
KbPackageMetadataType MetadataType = "KbPackageMetadata" KbPackageMetadataType MetadataType = "KbPackageMetadata"
MixLockMetadataType MetadataType = "MixLockMetadataType" MixLockMetadataType MetadataType = "MixLockMetadataType"
NixStoreMetadataType MetadataType = "NixStoreMetadata"
NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata" NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata"
NpmPackageLockJSONMetadataType MetadataType = "NpmPackageLockJsonMetadata" NpmPackageLockJSONMetadataType MetadataType = "NpmPackageLockJsonMetadata"
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata" PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
@ -54,6 +56,7 @@ var AllMetadataTypes = []MetadataType{
JavaMetadataType, JavaMetadataType,
KbPackageMetadataType, KbPackageMetadataType,
MixLockMetadataType, MixLockMetadataType,
NixStoreMetadataType,
NpmPackageJSONMetadataType, NpmPackageJSONMetadataType,
NpmPackageLockJSONMetadataType, NpmPackageLockJSONMetadataType,
PhpComposerJSONMetadataType, PhpComposerJSONMetadataType,
@ -82,6 +85,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
JavaMetadataType: reflect.TypeOf(JavaMetadata{}), JavaMetadataType: reflect.TypeOf(JavaMetadata{}),
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}), KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
MixLockMetadataType: reflect.TypeOf(MixLockMetadata{}), MixLockMetadataType: reflect.TypeOf(MixLockMetadata{}),
NixStoreMetadataType: reflect.TypeOf(NixStoreMetadata{}),
NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}), NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}),
NpmPackageLockJSONMetadataType: reflect.TypeOf(NpmPackageLockJSONMetadata{}), NpmPackageLockJSONMetadataType: reflect.TypeOf(NpmPackageLockJSONMetadata{}),
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}), PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),

View 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
}

View File

@ -26,6 +26,7 @@ const (
JavaPkg Type = "java-archive" JavaPkg Type = "java-archive"
JenkinsPluginPkg Type = "jenkins-plugin" JenkinsPluginPkg Type = "jenkins-plugin"
KbPkg Type = "msrc-kb" KbPkg Type = "msrc-kb"
NixPkg Type = "nix"
NpmPkg Type = "npm" NpmPkg Type = "npm"
PhpComposerPkg Type = "php-composer" PhpComposerPkg Type = "php-composer"
PortagePkg Type = "portage" PortagePkg Type = "portage"
@ -51,6 +52,7 @@ var AllPkgs = []Type{
JavaPkg, JavaPkg,
JenkinsPluginPkg, JenkinsPluginPkg,
KbPkg, KbPkg,
NixPkg,
NpmPkg, NpmPkg,
PhpComposerPkg, PhpComposerPkg,
PortagePkg, PortagePkg,
@ -92,6 +94,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypePyPi return packageurl.TypePyPi
case PortagePkg: case PortagePkg:
return "portage" return "portage"
case NixPkg:
return "nix"
case NpmPkg: case NpmPkg:
return packageurl.TypeNPM return packageurl.TypeNPM
case RpmPkg: case RpmPkg:
@ -151,6 +155,8 @@ func TypeByName(name string) Type {
return PortagePkg return PortagePkg
case packageurl.TypeHex: case packageurl.TypeHex:
return HexPkg return HexPkg
case "nix":
return NixPkg
default: default:
return UnknownPkg return UnknownPkg
} }

View File

@ -83,6 +83,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:hex/hpax/hpax@0.1.1", purl: "pkg:hex/hpax/hpax@0.1.1",
expected: HexPkg, expected: HexPkg,
}, },
{
purl: "pkg:nix/glibc@2.34?hash=h0cnbmfcn93xm5dg2x27ixhag1cwndga",
expected: NixPkg,
},
} }
var pkgTypes []string var pkgTypes []string

View File

@ -97,7 +97,7 @@ func TestPackagesCmdFlags(t *testing.T) {
name: "squashed-scope-flag", name: "squashed-scope-flag",
args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage}, args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertPackageCount(34), assertPackageCount(35),
assertSuccessfulReturnCode, 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. // the application config in the log matches that of what we expect to have been configured.
assertInOutput("parallelism: 2"), assertInOutput("parallelism: 2"),
assertInOutput("parallelism=2"), assertInOutput("parallelism=2"),
assertPackageCount(34), assertPackageCount(35),
assertSuccessfulReturnCode, 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. // the application config in the log matches that of what we expect to have been configured.
assertInOutput("parallelism: 1"), assertInOutput("parallelism: 1"),
assertInOutput("parallelism=1"), assertInOutput("parallelism=1"),
assertPackageCount(34), assertPackageCount(35),
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
}, },
}, },
@ -239,7 +239,7 @@ func TestPackagesCmdFlags(t *testing.T) {
assertions: []traitAssertion{ assertions: []traitAssertion{
assertNotInOutput("secret_password"), assertNotInOutput("secret_password"),
assertNotInOutput("secret_key_path"), assertNotInOutput("secret_key_path"),
assertPackageCount(34), assertPackageCount(35),
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
}, },
}, },

View File

@ -390,4 +390,11 @@ var commonTestCases = []testCase{
"example-jenkins-plugin": "1.0-SNAPSHOT", "example-jenkins-plugin": "1.0-SNAPSHOT",
}, },
}, },
{
name: "find nix store packages",
pkgType: pkg.NixPkg,
pkgInfo: map[string]string{
"glibc": "2.34-210",
},
},
} }