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)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- Nix (outputs in /nix/store)
- PHP (composer)
- Python (wheel, egg, poetry, requirements.txt)
- Red Hat (rpm)

View File

@ -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"
)

View File

@ -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

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

View File

@ -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 {

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

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

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 (
// 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{}),

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

View File

@ -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

View File

@ -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,
},
},

View File

@ -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",
},
},
}