feat(javascript): Add dependency parsing (#4304)

* feat: Add dependency parsing to javascript package locks

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

* Bump schema version

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

* Add support for yarn and pnpm, excl. yarn v1

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

* Add support for dependencies for v1 yarn lock files

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

* Ensure schema is correctly generated

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

* Fix tests

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

* PR feedback

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>

---------

Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>
This commit is contained in:
Tim Olshansky 2025-11-06 13:03:43 -08:00 committed by GitHub
parent e5711e9b42
commit 4e06a7ab32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 5346 additions and 215 deletions

View File

@ -3,5 +3,5 @@ package internal
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 = "16.0.41"
JSONSchemaVersion = "16.0.42"
)

View File

@ -49,6 +49,7 @@ func AllTypes() []any {
pkg.PhpComposerLockEntry{},
pkg.PhpPearEntry{},
pkg.PhpPeclEntry{},
pkg.PnpmLockEntry{},
pkg.PortageEntry{},
pkg.PythonPackage{},
pkg.PythonPdmLockEntry{},

View File

@ -95,6 +95,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.NpmPackage{}, "javascript-npm-package", "NpmPackageJsonMetadata"),
jsonNames(pkg.NpmPackageLockEntry{}, "javascript-npm-package-lock-entry", "NpmPackageLockJsonMetadata"),
jsonNames(pkg.YarnLockEntry{}, "javascript-yarn-lock-entry", "YarnLockJsonMetadata"),
jsonNames(pkg.PnpmLockEntry{}, "javascript-pnpm-lock-entry"),
jsonNames(pkg.PEBinary{}, "pe-binary"),
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.41/document",
"$id": "anchore.io/schema/syft/json/16.0.42/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -1876,15 +1876,48 @@
"integrity": {
"type": "string",
"description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected."
},
"dependencies": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object",
"description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\""
}
},
"type": "object",
"required": [
"resolved",
"integrity"
"integrity",
"dependencies"
],
"description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file."
},
"JavascriptPnpmLockEntry": {
"properties": {
"resolution": {
"$ref": "#/$defs/PnpmLockResolution",
"description": "Resolution is the resolution information for the package"
},
"dependencies": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object",
"description": "Dependencies is a map of dependencies and their versions"
}
},
"type": "object",
"required": [
"resolution",
"dependencies"
],
"description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml file."
},
"JavascriptYarnLockEntry": {
"properties": {
"resolved": {
@ -1894,12 +1927,22 @@
"integrity": {
"type": "string",
"description": "Integrity is Subresource Integrity hash for verification (SRI format)"
},
"dependencies": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object",
"description": "Dependencies is a map of dependencies and their versions"
}
},
"type": "object",
"required": [
"resolved",
"integrity"
"integrity",
"dependencies"
],
"description": "YarnLockEntry represents a single entry section of a yarn.lock file."
},
@ -2507,6 +2550,9 @@
{
"$ref": "#/$defs/JavascriptNpmPackageLockEntry"
},
{
"$ref": "#/$defs/JavascriptPnpmLockEntry"
},
{
"$ref": "#/$defs/JavascriptYarnLockEntry"
},
@ -2958,6 +3004,18 @@
],
"description": "PhpPeclEntry represents a single package entry found within php pecl metadata files."
},
"PnpmLockResolution": {
"properties": {
"integrity": {
"type": "string",
"description": "Integrity is Subresource Integrity hash for verification (SRI format)"
}
},
"type": "object",
"required": [
"integrity"
]
},
"PortageDbEntry": {
"properties": {
"installedSize": {

View File

@ -40,6 +40,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.PhpComposerInstalledEntry{},
pkg.PhpPearEntry{},
pkg.PhpPeclEntry{},
pkg.PnpmLockEntry{},
pkg.PortageEntry{},
pkg.PythonPipfileLockEntry{},
pkg.PythonPdmLockEntry{},

View File

@ -0,0 +1,87 @@
package javascript
import (
"fmt"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)
func packageLockDependencySpecifier(p pkg.Package) dependency.Specification {
meta, ok := p.Metadata.(pkg.NpmPackageLockEntry)
if !ok {
log.Tracef("cataloger failed to extract package lock metadata for package %+v", p.Name)
return dependency.Specification{}
}
provides := []string{p.Name}
var requires []string
for name, dependencySpecifier := range meta.Dependencies {
purl, err := packageurl.FromString(strings.ReplaceAll(dependencySpecifier, "npm:", "pkg:npm/"))
if err == nil {
// if the package url is valid, include the name from the package url since this is likely an alias
var fullName = fmt.Sprintf("%s/%s", purl.Namespace, purl.Name)
requires = append(requires, fullName)
} else {
fmt.Println("error", err)
}
requires = append(requires, name)
}
return dependency.Specification{
ProvidesRequires: dependency.ProvidesRequires{
Provides: provides,
Requires: requires,
},
}
}
func pnpmLockDependencySpecifier(p pkg.Package) dependency.Specification {
meta, ok := p.Metadata.(pkg.PnpmLockEntry)
if !ok {
log.Tracef("cataloger failed to extract pnpm lock metadata for package %+v", p.Name)
return dependency.Specification{}
}
provides := []string{p.Name}
var requires []string
for name := range meta.Dependencies {
requires = append(requires, name)
}
return dependency.Specification{
ProvidesRequires: dependency.ProvidesRequires{
Provides: provides,
Requires: requires,
},
}
}
func yarnLockDependencySpecifier(p pkg.Package) dependency.Specification {
meta, ok := p.Metadata.(pkg.YarnLockEntry)
if !ok {
log.Tracef("cataloger failed to extract yarn lock metadata for package %+v", p.Name)
return dependency.Specification{}
}
provides := []string{p.Name}
var requires []string
for name := range meta.Dependencies {
requires = append(requires, name)
}
return dependency.Specification{
ProvidesRequires: dependency.ProvidesRequires{
Provides: provides,
Requires: requires,
},
}
}

View File

@ -158,12 +158,12 @@ func newPackageLockV2Package(ctx context.Context, cfg CatalogerConfig, resolver
PURL: packageURL(name, u.Version),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: u.Resolved, Integrity: u.Integrity},
Metadata: pkg.NpmPackageLockEntry{Resolved: u.Resolved, Integrity: u.Integrity, Dependencies: u.Dependencies},
},
)
}
func newPnpmPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string) pkg.Package {
func newPnpmPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, integrity string, dependencies map[string]string) pkg.Package {
var licenseSet pkg.LicenseSet
if cfg.SearchRemoteLicenses {
@ -187,11 +187,12 @@ func newPnpmPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Reso
PURL: packageURL(name, version),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: integrity}, Dependencies: dependencies},
},
)
}
func newYarnLockPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, resolved string, integrity string) pkg.Package {
func newYarnLockPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, resolved string, integrity string, dependencies map[string]string) pkg.Package {
var licenseSet pkg.LicenseSet
if cfg.SearchRemoteLicenses {
@ -215,7 +216,7 @@ func newYarnLockPackage(ctx context.Context, cfg CatalogerConfig, resolver file.
PURL: packageURL(name, version),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{Resolved: resolved, Integrity: integrity},
Metadata: pkg.YarnLockEntry{Resolved: resolved, Integrity: integrity, Dependencies: dependencies},
},
)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)
// packageLock represents a JavaScript package.lock json file
@ -33,12 +34,13 @@ type lockDependency struct {
}
type lockPackage struct {
Name string `json:"name"` // only present in the root package entry (named "")
Version string `json:"version"`
Resolved string `json:"resolved"`
Integrity string `json:"integrity"`
License packageLockLicense `json:"license"`
Dev bool `json:"dev"`
Name string `json:"name"` // only present in the root package entry (named "")
Version string `json:"version"`
Resolved string `json:"resolved"`
Integrity string `json:"integrity"`
License packageLockLicense `json:"license"`
Dev bool `json:"dev"`
Dependencies map[string]string `json:"dependencies"`
}
// packageLockLicense
@ -104,16 +106,14 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve
name = pkgMeta.Name
}
pkgs = append(
pkgs,
newPackageLockV2Package(ctx, a.cfg, resolver, reader.Location, getNameFromPath(name), pkgMeta),
)
newPkg := newPackageLockV2Package(ctx, a.cfg, resolver, reader.Location, getNameFromPath(name), pkgMeta)
pkgs = append(pkgs, newPkg)
}
}
pkg.Sort(pkgs)
return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages")
return pkgs, dependency.Resolve(packageLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages")
}
func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) {

View File

@ -122,7 +122,7 @@ func TestParsePackageLockV2(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
PURL: "pkg:npm/npm@6.14.6",
Metadata: pkg.NpmPackageLockEntry{},
Metadata: pkg.NpmPackageLockEntry{Dependencies: map[string]string{"@types/react": "^18.0.9"}},
},
{
Name: "@types/prop-types",
@ -144,7 +144,7 @@ func TestParsePackageLockV2(t *testing.T) {
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", file.NewLocation(fixture)),
),
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ="},
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz", Integrity: "sha1-RYPZwyLWfv5LOak10iPtzHBQzPQ=", Dependencies: map[string]string{"@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2"}},
},
{
Name: "@types/scheduler",
@ -172,6 +172,28 @@ func TestParsePackageLockV2(t *testing.T) {
for i := range expectedPkgs {
expectedPkgs[i].Locations.Add(file.NewLocation(fixture))
}
expectedRelationships = []artifact.Relationship{
{
From: expectedPkgs[1],
To: expectedPkgs[2],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[3],
To: expectedPkgs[2],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[4],
To: expectedPkgs[2],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[2],
To: expectedPkgs[0],
Type: artifact.DependencyOfRelationship,
},
}
adapter := newGenericPackageLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, expectedRelationships)
}
@ -186,7 +208,7 @@ func TestParsePackageLockV3(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
PURL: "pkg:npm/lock-v3-fixture@1.0.0",
Metadata: pkg.NpmPackageLockEntry{},
Metadata: pkg.NpmPackageLockEntry{Dependencies: map[string]string{"@types/react": "^18.0.9"}},
},
{
Name: "@types/prop-types",
@ -202,7 +224,7 @@ func TestParsePackageLockV3(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
PURL: "pkg:npm/%40types/react@18.0.20",
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz", Integrity: "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA=="},
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz", Integrity: "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==", Dependencies: map[string]string{"@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2"}},
},
{
Name: "@types/scheduler",
@ -224,6 +246,28 @@ func TestParsePackageLockV3(t *testing.T) {
for i := range expectedPkgs {
expectedPkgs[i].Locations.Add(file.NewLocation(fixture))
}
expectedRelationships = []artifact.Relationship{
{
From: expectedPkgs[1],
To: expectedPkgs[2],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[3],
To: expectedPkgs[2],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[4],
To: expectedPkgs[2],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[2],
To: expectedPkgs[0],
Type: artifact.DependencyOfRelationship,
},
}
adapter := newGenericPackageLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, expectedRelationships)
}
@ -271,7 +315,7 @@ func TestParsePackageLockAlias(t *testing.T) {
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "ISC", file.NewLocation(packageLockV2)),
),
Metadata: pkg.NpmPackageLockEntry{},
Metadata: pkg.NpmPackageLockEntry{Dependencies: map[string]string{"case": "1.6.2", "case-alias": "npm:case@^1.6.3", "chai": "npm:@bundled-es-modules/chai@^4.2.2"}},
}
for _, pl := range packageLocks {
@ -285,6 +329,26 @@ func TestParsePackageLockAlias(t *testing.T) {
for i := range expected {
expected[i].Locations.Add(file.NewLocation(pl))
}
if pl == packageLockV2 {
expectedRelationships = []artifact.Relationship{
{
From: expected[0],
To: expected[3],
Type: artifact.DependencyOfRelationship,
},
{
From: expected[1],
To: expected[3],
Type: artifact.DependencyOfRelationship,
},
{
From: expected[2],
To: expected[3],
Type: artifact.DependencyOfRelationship,
},
}
}
adapter := newGenericPackageLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, pl, adapter.parsePackageLock, expected, expectedRelationships)
}
@ -304,7 +368,7 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
pkg.NewLicenseFromLocationsWithContext(ctx, "ISC", file.NewLocation(fixture)),
),
PURL: "pkg:npm/tmp@1.0.0",
Metadata: pkg.NpmPackageLockEntry{},
Metadata: pkg.NpmPackageLockEntry{Dependencies: map[string]string{"pause-stream": "0.0.11"}},
},
{
Name: "pause-stream",
@ -317,7 +381,7 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
pkg.NewLicenseFromLocationsWithContext(ctx, "Apache2", file.NewLocation(fixture)),
),
PURL: "pkg:npm/pause-stream@0.0.11",
Metadata: pkg.NpmPackageLockEntry{},
Metadata: pkg.NpmPackageLockEntry{Dependencies: map[string]string{"through": "~2.3"}},
},
{
Name: "through",
@ -334,6 +398,19 @@ func TestParsePackageLockLicenseWithArray(t *testing.T) {
for i := range expectedPkgs {
expectedPkgs[i].Locations.Add(file.NewLocation(fixture))
}
expectedRelationships = []artifact.Relationship{
{
From: expectedPkgs[2],
To: expectedPkgs[1],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[1],
To: expectedPkgs[0],
Type: artifact.DependencyOfRelationship,
},
}
adapter := newGenericPackageLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, expectedRelationships)
}

View File

@ -16,12 +16,15 @@ import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)
// pnpmPackage holds the raw name and version extracted from the lockfile.
type pnpmPackage struct {
Name string
Version string
Name string
Version string
Integrity string
Dependencies map[string]string
}
// pnpmLockfileParser defines the interface for parsing different versions of pnpm lockfiles.
@ -29,17 +32,35 @@ type pnpmLockfileParser interface {
Parse(version float64, data []byte) ([]pnpmPackage, error)
}
type pnpmV6PackageEntry struct {
Resolution map[string]string `yaml:"resolution"`
Dependencies map[string]string `yaml:"dependencies"`
}
// pnpmV6LockYaml represents the structure of pnpm lockfiles for versions < 9.0.
type pnpmV6LockYaml struct {
Dependencies map[string]interface{} `yaml:"dependencies"`
Packages map[string]interface{} `yaml:"packages"`
Dependencies map[string]interface{} `yaml:"dependencies"`
Packages map[string]pnpmV6PackageEntry `yaml:"packages"`
}
type pnpmV9SnapshotEntry struct {
Dependencies map[string]string `yaml:"dependencies"`
Optional bool `yaml:"optional"`
OptionalDependencies map[string]string `yaml:"optionalDependencies"`
TransitivePeerDependencies []string `yaml:"transitivePeerDependencies"`
}
type pnpmV9PackageEntry struct {
Resolution map[string]string `yaml:"resolution"`
PeerDependencies map[string]string `yaml:"peerDependencies"`
}
// pnpmV9LockYaml represents the structure of pnpm lockfiles for versions >= 9.0.
type pnpmV9LockYaml struct {
LockfileVersion string `yaml:"lockfileVersion"`
Importers map[string]interface{} `yaml:"importers"` // Using interface{} for forward compatibility
Packages map[string]interface{} `yaml:"packages"`
LockfileVersion string `yaml:"lockfileVersion"`
Importers map[string]interface{} `yaml:"importers"` // Using interface{} for forward compatibility
Packages map[string]pnpmV9PackageEntry `yaml:"packages"`
Snapshots map[string]pnpmV9SnapshotEntry `yaml:"snapshots"`
}
type genericPnpmLockAdapter struct {
@ -77,14 +98,26 @@ func (p *pnpmV6LockYaml) Parse(version float64, data []byte) ([]pnpmPackage, err
}
// All transitive dependencies
for key := range p.Packages {
for key, pkgInfo := range p.Packages {
name, ver, ok := parsePnpmPackageKey(key, splitChar)
if !ok {
log.WithFields("key", key).Trace("unable to parse pnpm package key")
continue
}
pkgKey := name + "@" + ver
packages[pkgKey] = pnpmPackage{Name: name, Version: ver}
integrity := ""
if value, ok := pkgInfo.Resolution["integrity"]; ok {
integrity = value
}
dependencies := make(map[string]string)
for depName, depVersion := range pkgInfo.Dependencies {
var normalizedVersion = strings.SplitN(depVersion, "(", 2)[0]
dependencies[depName] = normalizedVersion
}
packages[pkgKey] = pnpmPackage{Name: name, Version: ver, Integrity: integrity, Dependencies: dependencies}
}
return toSortedSlice(packages), nil
@ -100,7 +133,7 @@ func (p *pnpmV9LockYaml) Parse(_ float64, data []byte) ([]pnpmPackage, error) {
// In v9, all resolved dependencies are listed in the top-level "packages" field.
// The key format is like /<name>@<version> or /<name>@<version>(<peer-deps>).
for key := range p.Packages {
for key, entry := range p.Packages {
// The separator for name and version is consistently '@' in v9+ keys.
name, ver, ok := parsePnpmPackageKey(key, "@")
if !ok {
@ -108,7 +141,26 @@ func (p *pnpmV9LockYaml) Parse(_ float64, data []byte) ([]pnpmPackage, error) {
continue
}
pkgKey := name + "@" + ver
packages[pkgKey] = pnpmPackage{Name: name, Version: ver}
packages[pkgKey] = pnpmPackage{Name: name, Version: ver, Integrity: entry.Resolution["integrity"]}
}
for key, snapshotInfo := range p.Snapshots {
name, ver, ok := parsePnpmPackageKey(key, "@")
if !ok {
log.WithFields("key", key).Trace("unable to parse pnpm v9 package snapshot key")
continue
}
pkgKey := name + "@" + ver
if pkg, ok := packages[pkgKey]; ok {
pkg.Dependencies = make(map[string]string)
for name, versionSpecifier := range snapshotInfo.Dependencies {
var normalizedVersion = strings.SplitN(versionSpecifier, "(", 2)[0]
pkg.Dependencies[name] = normalizedVersion
}
packages[pkgKey] = pkg
} else {
log.WithFields("package", pkgKey).Trace("package not found in packages map")
}
}
return toSortedSlice(packages), nil
@ -149,10 +201,10 @@ func (a genericPnpmLockAdapter) parsePnpmLock(ctx context.Context, resolver file
packages := make([]pkg.Package, len(pnpmPkgs))
for i, p := range pnpmPkgs {
packages[i] = newPnpmPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version)
packages[i] = newPnpmPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Integrity, p.Dependencies)
}
return packages, nil, unknown.IfEmptyf(packages, "unable to determine packages")
return packages, dependency.Resolve(pnpmLockDependencySpecifier, packages), unknown.IfEmptyf(packages, "unable to determine packages")
}
// parseVersionField extracts the version string from a dependency entry.

View File

@ -28,6 +28,7 @@ func TestParsePnpmLock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{}},
},
{
Name: "picocolors",
@ -36,6 +37,7 @@ func TestParsePnpmLock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{}},
},
{
Name: "source-map-js",
@ -44,6 +46,7 @@ func TestParsePnpmLock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{}},
},
{
Name: "@bcoe/v8-coverage",
@ -52,6 +55,10 @@ func TestParsePnpmLock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="},
Dependencies: map[string]string{},
},
},
}
@ -73,6 +80,19 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA=="},
Dependencies: map[string]string{
"@adobe/css-tools": "4.2.0",
"@babel/runtime": "7.21.0",
"@types/testing-library__jest-dom": "5.14.5",
"aria-query": "5.1.3",
"chalk": "3.0.0",
"css.escape": "1.5.1",
"dom-accessibility-api": "0.5.16",
"lodash": "4.17.21",
"redent": "3.0.0",
}},
},
{
Name: "@testing-library/react",
@ -81,6 +101,16 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw=="},
Dependencies: map[string]string{
"@babel/runtime": "7.21.0",
"@testing-library/dom": "8.20.0",
"@types/react-dom": "18.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
},
},
},
{
Name: "@testing-library/user-event",
@ -89,6 +119,13 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg=="},
Dependencies: map[string]string{
"@babel/runtime": "7.21.0",
"@testing-library/dom": "9.2.0",
},
},
},
{
Name: "react",
@ -97,6 +134,12 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="},
Dependencies: map[string]string{
"loose-envify": "1.4.0",
},
},
},
{
Name: "react-dom",
@ -105,6 +148,14 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="},
Dependencies: map[string]string{
"loose-envify": "1.4.0",
"react": "18.2.0",
"scheduler": "0.23.0",
},
},
},
{
Name: "web-vitals",
@ -113,6 +164,10 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="},
Dependencies: map[string]string{},
},
},
{
Name: "@babel/core",
@ -121,6 +176,26 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA=="},
Dependencies: map[string]string{
"@ampproject/remapping": "2.2.1",
"@babel/code-frame": "7.21.4",
"@babel/generator": "7.21.4",
"@babel/helper-compilation-targets": "7.21.4",
"@babel/helper-module-transforms": "7.21.2",
"@babel/helpers": "7.21.0",
"@babel/parser": "7.21.4",
"@babel/template": "7.20.7",
"@babel/traverse": "7.21.4",
"@babel/types": "7.21.4",
"convert-source-map": "1.9.0",
"debug": "4.3.4",
"gensync": "1.0.0-beta.2",
"json5": "2.2.3",
"semver": "6.3.0",
},
},
},
{
Name: "@types/eslint",
@ -129,6 +204,13 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ=="},
Dependencies: map[string]string{
"@types/estree": "1.0.1",
"@types/json-schema": "7.0.11",
},
},
},
{
Name: "read-cache",
@ -137,6 +219,12 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="},
Dependencies: map[string]string{
"pify": "2.3.0",
},
},
},
{
Name: "schema-utils",
@ -145,9 +233,33 @@ func TestParsePnpmV6Lock(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg=="},
Dependencies: map[string]string{
"@types/json-schema": "7.0.11",
"ajv": "6.12.6",
"ajv-keywords": "3.5.2",
},
},
},
}
expectedRelationships = []artifact.Relationship{
{
From: expectedPkgs[3],
To: expectedPkgs[1],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[3],
To: expectedPkgs[4],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[4],
To: expectedPkgs[1],
Type: artifact.DependencyOfRelationship,
},
}
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships)
}
@ -165,6 +277,7 @@ func TestParsePnpmLockV9(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-4RjkiFFI42+268iBv2nC+iMLTJGQW3u9P7YvA3x/6MDrJ9IYZ8I/xx5a2GIhY5gBTOcI4iC5S5in2fGjE+P4Yw=="}},
},
{
Name: "@babel/helper-plugin-utils",
@ -173,6 +286,7 @@ func TestParsePnpmLockV9(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-8A2+zKm53/3w4rwbX11FMW/yFS6c5Vam02P/dw01aK6KbwkKqBaIt3eEATiKtn9I2uS1itk8/aZ2yZ/kURee4Q=="}},
},
{
Name: "is-positive",
@ -181,6 +295,7 @@ func TestParsePnpmLockV9(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-9ffLCf_f5sopimAhg2g91a7b9Rw5A1aA9eI6S391S3VEzYw99I3iKjcZGxLp25s0cRxNBV5aL2mhn7421SSlA=="}},
},
{
Name: "rollup",
@ -189,6 +304,7 @@ func TestParsePnpmLockV9(t *testing.T) {
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-QpQY2Q5i0y0Q3RoAvoChE/R5iN2k05N//bNvQbC2XvRjHFT1qWJ2r3n1bNqE+gGRJaeuQf0BxE42D7CyuLh3ZQ=="}},
},
}
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
@ -196,6 +312,117 @@ func TestParsePnpmLockV9(t *testing.T) {
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expected, expectedRelationships)
}
func TestParsePnpmLockV9WithDependencies(t *testing.T) {
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
fixture := "test-fixtures/pnpm-v9-snapshots/pnpm-lock.yaml"
locationSet := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "cross-spawn",
Version: "7.0.6",
PURL: "pkg:npm/cross-spawn@7.0.6",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="},
Dependencies: map[string]string{
"path-key": "3.1.1",
"shebang-command": "2.0.0",
"which": "2.0.2",
},
},
},
{
Name: "isexe",
Version: "2.0.0",
PURL: "pkg:npm/isexe@2.0.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="},
Dependencies: map[string]string{},
},
},
{
Name: "path-key",
Version: "3.1.1",
PURL: "pkg:npm/path-key@3.1.1",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="},
Dependencies: map[string]string{},
},
},
{
Name: "shebang-command",
Version: "2.0.0",
PURL: "pkg:npm/shebang-command@2.0.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="},
Dependencies: map[string]string{
"shebang-regex": "3.0.0",
},
},
},
{
Name: "shebang-regex",
Version: "3.0.0",
PURL: "pkg:npm/shebang-regex@3.0.0",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="},
Dependencies: map[string]string{},
},
},
{
Name: "which",
Version: "2.0.2",
PURL: "pkg:npm/which@2.0.2",
Locations: locationSet,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="},
Dependencies: map[string]string{
"isexe": "2.0.0",
},
},
},
}
expectedRelationships := []artifact.Relationship{
{
From: expectedPkgs[1],
To: expectedPkgs[5],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[2],
To: expectedPkgs[0],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[3],
To: expectedPkgs[0],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[4],
To: expectedPkgs[3],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[5],
To: expectedPkgs[0],
Type: artifact.DependencyOfRelationship,
},
}
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships)
}
func TestSearchPnpmForLicenses(t *testing.T) {
ctx := context.TODO()
fixture := "test-fixtures/pnpm-remote/pnpm-lock.yaml"
@ -228,6 +455,10 @@ func TestSearchPnpmForLicenses(t *testing.T) {
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.PnpmLockEntry{
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="},
Dependencies: map[string]string{},
},
},
},
},

View File

@ -1,18 +1,25 @@
package javascript
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"maps"
"regexp"
"slices"
"strings"
"github.com/goccy/go-yaml"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)
var (
@ -22,10 +29,6 @@ var (
// "@babel/code-frame@^7.0.0" returns "@babel/code-frame"
packageNameExp = regexp.MustCompile(`^"?((?:@\w[\w-_.]*\/)?\w[\w-_.]*)@`)
// versionExp matches the "version" line of a yarn.lock entry and captures the version value.
// For example: version "4.10.1" (...and the value "4.10.1" is captured)
versionExp = regexp.MustCompile(`^\W+version(?:\W+"|:\W+)([\w-_.]+)"?`)
// packageURLExp matches the name and version of the dependency in yarn.lock
// from the resolved URL, including scope/namespace prefix if any.
// For example:
@ -34,21 +37,30 @@ var (
//
// `resolved "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b"`
// would return "@4lolo/resize-observer-polyfill" and "1.5.2"
packageURLExp = regexp.MustCompile(`^\s+resolved\s+"https://registry\.(?:yarnpkg\.com|npmjs\.org)/(.+?)/-/(?:.+?)-(\d+\..+?)\.tgz`)
packageURLExp = regexp.MustCompile(`^resolved\s+"https://registry\.(?:yarnpkg\.com|npmjs\.org)/(.+?)/-/(?:.+?)-(\d+\..+?)\.tgz`)
// resolvedExp matches the resolved of the dependency in yarn.lock
// For example:
// resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
// would return "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
resolvedExp = regexp.MustCompile(`^\s+resolved\s+"(.+?)"`)
// integrityExp matches the integrity of the dependency in yarn.lock
// For example:
// integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
// would return "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==""
integrityExp = regexp.MustCompile(`^\s+integrity\s+([^\s]+)`)
resolvedExp = regexp.MustCompile(`^resolved\s+"(.+?)"`)
)
type yarnPackage struct {
Name string
Version string
Resolved string
Integrity string
Dependencies map[string]string // We don't currently support dependencies for yarn v1 lock files
}
type yarnV2PackageEntry struct {
Version string `yaml:"version"`
Resolution string `yaml:"resolution"`
Checksum string `yaml:"checksum"`
Dependencies map[string]string `yaml:"dependencies"`
}
type genericYarnLockAdapter struct {
cfg CatalogerConfig
}
@ -59,6 +71,99 @@ func newGenericYarnLockAdapter(cfg CatalogerConfig) genericYarnLockAdapter {
}
}
func parseYarnV1LockFile(reader io.ReadCloser) ([]yarnPackage, error) {
content, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read yarn.lock file: %w", err)
}
re := regexp.MustCompile(`\r?\n`)
lines := re.Split(string(content), -1)
var pkgs []yarnPackage
var pkg = yarnPackage{}
var seenPkgs = strset.New()
dependencies := make(map[string]string)
for _, line := range lines {
if strings.HasPrefix(line, "#") {
continue
}
// Blank lines indicate the end of a package entry, so we add the package
// to the list and reset the dependencies
if len(line) == 0 && len(pkg.Name) > 0 && !seenPkgs.Has(pkg.Name+"@"+pkg.Version) {
pkg.Dependencies = dependencies
pkgs = append(pkgs, pkg)
seenPkgs.Add(pkg.Name + "@" + pkg.Version)
dependencies = make(map[string]string)
pkg = yarnPackage{}
continue
}
// The first line of a package entry is the name of the package with no
// leading spaces
if !strings.HasPrefix(line, " ") {
name := line
pkg.Name = findPackageName(name)
continue
}
if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") {
line = strings.Trim(line, " ")
array := strings.Split(line, " ")
switch array[0] {
case "version":
pkg.Version = strings.Trim(array[1], "\"")
case "resolved":
name, version, resolved := findResolvedPackageAndVersion(line)
if name != "" && version != "" && resolved != "" {
pkg.Name = name
pkg.Version = version
pkg.Resolved = resolved
} else {
pkg.Resolved = strings.Trim(array[1], "\"")
}
case "integrity":
pkg.Integrity = strings.Trim(array[1], "\"")
}
continue
}
if strings.HasPrefix(line, " ") {
line = strings.Trim(line, " ")
array := strings.Split(line, " ")
dependencyName := strings.Trim(array[0], "\"")
dependencyVersion := strings.Trim(array[1], "\"")
dependencies[dependencyName] = dependencyVersion
}
}
// If the last package in the list is not the same as the current package, add the current package
// to the list. In case there was no trailing new line before we hit EOF.
if len(pkg.Name) > 0 && !seenPkgs.Has(pkg.Name+"@"+pkg.Version) {
pkg.Dependencies = dependencies
pkgs = append(pkgs, pkg)
seenPkgs.Add(pkg.Name + "@" + pkg.Version)
}
return pkgs, nil
}
func parseYarnLockYaml(reader io.ReadCloser) ([]yarnPackage, error) {
var lockfile = map[string]yarnV2PackageEntry{}
if err := yaml.NewDecoder(reader, yaml.AllowDuplicateMapKey()).Decode(&lockfile); err != nil {
return nil, fmt.Errorf("failed to unmarshal yarn v2 lockfile: %w", err)
}
packages := make(map[string]yarnPackage)
for key, value := range lockfile {
packageName := findPackageName(key)
if packageName == "" {
log.WithFields("key", key).Error("unable to parse yarn v2 package key")
continue
}
packages[packageName] = yarnPackage{Name: packageName, Version: value.Version, Resolved: value.Resolution, Integrity: value.Checksum, Dependencies: value.Dependencies}
}
return slices.Collect(maps.Values(packages)), nil
}
func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
// in the case we find yarn.lock files in the node_modules directories, skip those
// as the whole purpose of the lock file is for the specific dependencies of the project
@ -66,54 +171,33 @@ func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file
return nil, nil, nil
}
var pkgs []pkg.Package
var currentPackage, currentVersion, currentResolved, currentIntegrity string
scanner := bufio.NewScanner(reader)
parsedPackages := strset.New()
for scanner.Scan() {
line := scanner.Text()
if packageName := findPackageName(line); packageName != "" {
// When we find a new package, check if we have unsaved identifiers
if currentPackage != "" && currentVersion != "" && !parsedPackages.Has(currentPackage+"@"+currentVersion) {
pkgs = append(pkgs, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, currentIntegrity))
parsedPackages.Add(currentPackage + "@" + currentVersion)
}
currentPackage = packageName
} else if version := findPackageVersion(line); version != "" {
currentVersion = version
} else if packageName, version, resolved := findResolvedPackageAndVersion(line); packageName != "" && version != "" && resolved != "" {
currentResolved = resolved
currentPackage = packageName
currentVersion = version
} else if integrity := findIntegrity(line); integrity != "" && !parsedPackages.Has(currentPackage+"@"+currentVersion) {
pkgs = append(pkgs, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, integrity))
parsedPackages.Add(currentPackage + "@" + currentVersion)
// Cleanup to indicate no unsaved identifiers
currentPackage = ""
currentVersion = ""
currentResolved = ""
currentIntegrity = ""
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to load yarn.lock file: %w", err)
}
// Reset the reader to the beginning of the file
reader.ReadCloser = io.NopCloser(bytes.NewBuffer(data))
// check if we have valid unsaved data after end-of-file has reached
if currentPackage != "" && currentVersion != "" && !parsedPackages.Has(currentPackage+"@"+currentVersion) {
pkgs = append(pkgs, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, currentPackage, currentVersion, currentResolved, currentIntegrity))
parsedPackages.Add(currentPackage + "@" + currentVersion)
var yarnPkgs []yarnPackage
// v1 Yarn lockfiles are not YAML, so we need to parse them as a special case. They typically
// include a comment line that indicates the version. I.e. "# yarn lockfile v1"
if strings.Contains(string(data), "# yarn lockfile v1") {
yarnPkgs, err = parseYarnV1LockFile(reader)
} else {
yarnPkgs, err = parseYarnLockYaml(reader)
}
if err := scanner.Err(); err != nil {
if err != nil {
return nil, nil, fmt.Errorf("failed to parse yarn.lock file: %w", err)
}
pkg.Sort(pkgs)
packages := make([]pkg.Package, len(yarnPkgs))
for i, p := range yarnPkgs {
packages[i] = newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Resolved, p.Integrity, p.Dependencies)
}
return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages")
pkg.Sort(packages)
return packages, dependency.Resolve(yarnLockDependencySpecifier, packages), unknown.IfEmptyf(packages, "unable to determine packages")
}
func findPackageName(line string) string {
@ -124,14 +208,6 @@ func findPackageName(line string) string {
return ""
}
func findPackageVersion(line string) string {
if matches := versionExp.FindStringSubmatch(line); len(matches) >= 2 {
return matches[1]
}
return ""
}
func findResolvedPackageAndVersion(line string) (string, string, string) {
var resolved string
if matches := resolvedExp.FindStringSubmatch(line); len(matches) >= 2 {
@ -143,11 +219,3 @@ func findResolvedPackageAndVersion(line string) (string, string, string) {
return "", "", ""
}
func findIntegrity(line string) string {
if matches := integrityExp.FindStringSubmatch(line); len(matches) >= 2 {
return matches[1]
}
return ""
}

View File

@ -17,7 +17,6 @@ import (
)
func TestParseYarnBerry(t *testing.T) {
var expectedRelationships []artifact.Relationship
fixture := "test-fixtures/yarn-berry/yarn.lock"
locations := file.NewLocationSet(file.NewLocation(fixture))
@ -29,7 +28,13 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/%40babel/code-frame@7.10.4",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "@babel/code-frame@npm:7.10.4",
Integrity: "feb4543c8a509fe30f0f6e8d7aa84f82b41148b963b826cd330e34986f649a85cb63b2f13dd4effdf434ac555d16f14940b8ea5f4433297c2f5ff85486ded019",
Dependencies: map[string]string{
"@babel/highlight": "^7.10.4",
},
},
},
{
Name: "@types/minimatch",
@ -38,7 +43,10 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/%40types/minimatch@3.0.3",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "@types/minimatch@npm:3.0.3",
Integrity: "b80259d55b96ef24cb3bb961b6dc18b943f2bb8838b4d8e7bead204f3173e551a416ffa49f9aaf1dc431277fffe36214118628eacf4aea20119df8835229901b",
},
},
{
Name: "@types/qs",
@ -47,7 +55,10 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/%40types/qs@6.9.4",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "@types/qs@npm:6.9.4",
Integrity: "77e509ed213f7694ae35f84a58b88da8744aad019e93556af6aeab4289287abbe71836c051d00649dbac0289ea199e408442590cfb1785009de11c3c8d0cbbea",
},
},
{
Name: "ajv",
@ -56,7 +67,16 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/ajv@6.12.3",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "ajv@npm:6.12.3",
Integrity: "ca559d34710e6969d33bc1316282e1ece4d4d99ff5fdca4bfe31947740f8f90e7824238cdc2954e499cf75b2432e3e6c56b32814ebe04fccf8abcc3fbf36b348",
Dependencies: map[string]string{
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2",
},
},
},
{
Name: "asn1.js",
@ -65,7 +85,15 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/asn1.js@4.10.1",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "asn1.js@npm:4.10.1",
Integrity: "9289a1a55401238755e3142511d7b8f6fc32f08c86ff68bd7100da8b6c186179dd6b14234fba2f7f6099afcd6758a816708485efe44bc5b2a6ec87d9ceeddbb5",
Dependencies: map[string]string{
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
},
},
},
{
Name: "atob",
@ -74,7 +102,10 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/atob@2.1.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "atob@npm:2.1.2",
Integrity: "dfeeeb70090c5ebea7be4b9f787f866686c645d9f39a0d184c817252d0cf08455ed25267d79c03254d3be1f03ac399992a792edcd5ffb9c91e097ab5ef42833a",
},
},
{
Name: "aws-sdk",
@ -83,7 +114,21 @@ func TestParseYarnBerry(t *testing.T) {
Locations: locations,
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "aws-sdk@npm:2.706.0",
Integrity: "bf8ca2fc4f758bdebd04051ec15729affad3eb0e18eed4ae41db5b7d6ff2aed2cf3a12ae082c11b955df0125378c57b8406e1f91006e48f0c162fdbe4ee4e330",
Dependencies: map[string]string{
"buffer": "4.9.2",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19",
},
},
},
{
Name: "c0n-fab_u.laTION",
@ -92,7 +137,16 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/c0n-fab_u.laTION@7.7.7",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "newtest@workspace:.",
Dependencies: map[string]string{
"@babel/code-frame": "7.10.4",
"@types/minimatch": "3.0.3",
"@types/qs": "6.9.4",
"ajv": "6.12.3",
"asn1.js": "4.10.1",
"atob": "2.1.2",
}},
},
{
Name: "jhipster-core",
@ -101,7 +155,48 @@ func TestParseYarnBerry(t *testing.T) {
PURL: "pkg:npm/jhipster-core@7.3.4",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{},
Metadata: pkg.YarnLockEntry{
Resolved: "jhipster-core@npm:7.3.4",
Integrity: "6a97741d574a42a138f98596c668370b41ec8870335bcd758b6b890e279ba30d4d2be447f8cecbf416286f2c53636b406a63a773c7b00709c95af0a9a3f9b397",
Dependencies: map[string]string{
"chevrotain": "7.0.1",
"fs-extra": "8.1.0",
"lodash": "4.17.15",
"winston": "3.2.1",
},
},
},
}
expectedRelationships := []artifact.Relationship{
{
From: expectedPkgs[0],
To: expectedPkgs[7],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[1],
To: expectedPkgs[7],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[2],
To: expectedPkgs[7],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[3],
To: expectedPkgs[7],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[4],
To: expectedPkgs[7],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[5],
To: expectedPkgs[7],
Type: artifact.DependencyOfRelationship,
},
}
@ -125,6 +220,9 @@ func TestParseYarnLock(t *testing.T) {
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a",
Integrity: "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
Dependencies: map[string]string{
"@babel/highlight": "^7.10.4",
},
},
},
{
@ -135,8 +233,9 @@ func TestParseYarnLock(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d",
Integrity: "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
Resolved: "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d",
Integrity: "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
Dependencies: map[string]string{},
},
},
{
@ -147,8 +246,9 @@ func TestParseYarnLock(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a",
Integrity: "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==",
Resolved: "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a",
Integrity: "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==",
Dependencies: map[string]string{},
},
},
{
@ -161,6 +261,12 @@ func TestParseYarnLock(t *testing.T) {
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706",
Integrity: "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
Dependencies: map[string]string{
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2",
},
},
},
{
@ -173,6 +279,11 @@ func TestParseYarnLock(t *testing.T) {
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0",
Integrity: "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
Dependencies: map[string]string{
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
},
},
},
{
@ -184,8 +295,9 @@ func TestParseYarnLock(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9",
Integrity: "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
Resolved: "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9",
Integrity: "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
Dependencies: map[string]string{},
},
},
{
@ -198,6 +310,17 @@ func TestParseYarnLock(t *testing.T) {
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953",
Integrity: "sha512-7GT+yrB5Wb/zOReRdv/Pzkb2Qt+hz6B/8FGMVaoysX3NryHvQUdz7EQWi5yhg9CxOjKxdw5lFwYSs69YlSp1KA==",
Dependencies: map[string]string{
"buffer": "4.9.2",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19",
},
},
},
{
@ -210,6 +333,12 @@ func TestParseYarnLock(t *testing.T) {
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/jhipster-core/-/jhipster-core-7.3.4.tgz#c34b8c97c7f4e8b7518dae015517e2112c73cc80",
Integrity: "sha512-AUhT69kNkqppaJZVfan/xnKG4Gs9Ggj7YLtTZFVe+xg+THrbMb5Ng7PL07PDlDw4KAEA33GMCwuAf65E8EpC4g==",
Dependencies: map[string]string{
"chevrotain": "7.0.1",
"fs-extra": "8.1.0",
"lodash": "4.17.15",
"winston": "3.2.1",
},
},
},
{
@ -220,8 +349,254 @@ func TestParseYarnLock(t *testing.T) {
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/something-i-made-up/-/c0n-fab_u.laTION-7.7.7.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0",
Resolved: "https://registry.yarnpkg.com/something-i-made-up/-/c0n-fab_u.laTION-7.7.7.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0",
Integrity: "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
Dependencies: map[string]string{},
},
},
}
adapter := newGenericYarnLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships)
}
func TestParseYarnLockWithRelationships(t *testing.T) {
fixture := "test-fixtures/yarn-v1-deps/yarn.lock"
locations := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "@babel/code-frame",
Version: "7.10.4",
Locations: locations,
PURL: "pkg:npm/%40babel/code-frame@7.10.4",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a",
Integrity: "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
Dependencies: map[string]string{
"@babel/highlight": "^7.10.4",
},
},
},
{
Name: "@types/minimatch",
Version: "3.0.3",
Locations: locations,
PURL: "pkg:npm/%40types/minimatch@3.0.3",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d",
Integrity: "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
Dependencies: map[string]string{},
},
},
{
Name: "@types/qs",
Version: "6.9.4",
Locations: locations,
PURL: "pkg:npm/%40types/qs@6.9.4",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a",
Integrity: "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==",
Dependencies: map[string]string{},
},
},
{
Name: "ajv",
Version: "6.12.3",
Locations: locations,
PURL: "pkg:npm/ajv@6.12.3",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706",
Integrity: "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
Dependencies: map[string]string{
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2",
},
},
},
{
Name: "asn1.js",
Version: "4.10.1",
Locations: locations,
PURL: "pkg:npm/asn1.js@4.10.1",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0",
Integrity: "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
Dependencies: map[string]string{
"atob": "^2.1.2",
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
},
},
},
{
Name: "atob",
Version: "2.1.2",
Locations: locations,
PURL: "pkg:npm/atob@2.1.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9",
Integrity: "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
Dependencies: map[string]string{},
},
},
{
Name: "aws-sdk",
Version: "2.706.0",
Locations: locations,
PURL: "pkg:npm/aws-sdk@2.706.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953",
Integrity: "sha512-7GT+yrB5Wb/zOReRdv/Pzkb2Qt+hz6B/8FGMVaoysX3NryHvQUdz7EQWi5yhg9CxOjKxdw5lFwYSs69YlSp1KA==",
Dependencies: map[string]string{
"asn1.js": "4.10.1",
"buffer": "4.9.2",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19",
},
},
},
{
Name: "jhipster-core",
Version: "7.3.4",
Locations: locations,
PURL: "pkg:npm/jhipster-core@7.3.4",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/jhipster-core/-/jhipster-core-7.3.4.tgz#c34b8c97c7f4e8b7518dae015517e2112c73cc80",
Integrity: "sha512-AUhT69kNkqppaJZVfan/xnKG4Gs9Ggj7YLtTZFVe+xg+THrbMb5Ng7PL07PDlDw4KAEA33GMCwuAf65E8EpC4g==",
Dependencies: map[string]string{
"chevrotain": "7.0.1",
"fs-extra": "8.1.0",
"lodash": "4.17.15",
"winston": "3.2.1",
},
},
},
{
Name: "something-i-made-up",
Version: "7.7.7",
Locations: locations,
PURL: "pkg:npm/something-i-made-up@7.7.7",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/something-i-made-up/-/c0n-fab_u.laTION-7.7.7.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0",
Integrity: "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
Dependencies: map[string]string{},
},
},
}
expectedRelationships := []artifact.Relationship{
{
From: expectedPkgs[4],
To: expectedPkgs[6],
Type: artifact.DependencyOfRelationship,
},
{
From: expectedPkgs[5],
To: expectedPkgs[4],
Type: artifact.DependencyOfRelationship,
},
}
adapter := newGenericYarnLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships)
}
func TestParseYarnLockWithDuplicates(t *testing.T) {
var expectedRelationships []artifact.Relationship
fixture := "test-fixtures/yarn-dups/yarn.lock"
locations := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "async",
Version: "0.9.2",
Locations: locations,
PURL: "pkg:npm/async@0.9.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d",
Integrity: "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=",
Dependencies: map[string]string{},
},
},
{
Name: "async",
Version: "3.2.3",
Locations: locations,
PURL: "pkg:npm/async@3.2.3",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9",
Integrity: "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==",
Dependencies: map[string]string{},
},
},
{
Name: "merge-objects",
Version: "1.0.5",
Locations: locations,
PURL: "pkg:npm/merge-objects@1.0.5",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/merge-objects/-/merge-objects-1.0.5.tgz#ad923ff3910091acc1438f53eb75b8f37d862a86",
Integrity: "sha1-rZI/85EAkazBQ49T63W4832GKoY=",
Dependencies: map[string]string{},
},
},
{
Name: "@4lolo/resize-observer-polyfill",
Version: "1.5.2",
Locations: locations,
PURL: "pkg:npm/%404lolo/resize-observer-polyfill@1.5.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b",
Integrity: "sha512-HY4JYLITsWBOdeqCF/x3q7Aa2PVl/BmfkPv4H/Qzplc4Lrn9cKmWz6jHyAREH9tFuD0xELjJVgX3JaEmdcXu3g==",
Dependencies: map[string]string{},
},
},
{
Name: "should-type",
Version: "1.3.0",
Locations: locations,
PURL: "pkg:npm/should-type@1.3.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "https://github.com/shouldjs/type.git#31d26945cb3b4ad21d2308776e4442c461666390",
Integrity: "",
Dependencies: map[string]string{},
},
},
}
@ -270,6 +645,9 @@ func TestSearchYarnForLicenses(t *testing.T) {
Metadata: pkg.YarnLockEntry{
Resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a",
Integrity: "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
Dependencies: map[string]string{
"@babel/highlight": "^7.10.4",
},
},
},
},
@ -361,94 +739,6 @@ func TestParseYarnFindPackageNames(t *testing.T) {
}
}
func TestParseYarnFindPackageVersions(t *testing.T) {
tests := []struct {
line string
expected string
}{
{
line: ` version "7.10.4"`,
expected: "7.10.4",
},
{
line: ` version "7.11.5"`,
expected: "7.11.5",
},
{
line: `version "7.12.6"`,
expected: "",
},
{
line: ` version "0.0.0"`,
expected: "0.0.0",
},
{
line: ` version "2" `,
expected: "2",
},
{
line: ` version "9.3"`,
expected: "9.3",
},
{
line: "ajv@^6.10.2, ajv@^6.5.5",
expected: "",
},
{
line: "atob@^2.1.2:",
expected: "",
},
{
line: `"color-convert@npm:^1.9.0":`,
expected: "",
},
{
line: " version: 1.9.3",
expected: "1.9.3",
},
{
line: " version: 2",
expected: "2",
},
{
line: " version: 9.3",
expected: "9.3",
},
{
line: "ajv@^6.10.2, ajv@^6.5.5",
expected: "",
},
{
line: "atob@^2.1.2:",
expected: "",
},
{
line: " version: 1.0.0-alpha+001",
expected: "1.0.0-alpha",
},
{
line: " version: 1.0.0-beta_test+exp.sha.5114f85",
expected: "1.0.0-beta_test",
},
{
line: " version: 1.0.0+21AF26D3-117B344092BD",
expected: "1.0.0",
},
{
line: " version: 0.0.0-use.local",
expected: "0.0.0-use.local",
},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
t.Parallel()
actual := findPackageVersion(test.line)
assert.Equal(t, test.expected, actual)
})
}
}
func generateMockYarnRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)

View File

@ -0,0 +1,61 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
cross-spawn:
specifier: ^7.0.5
version: 7.0.6
packages:
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
snapshots:
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
isexe@2.0.0: {}
path-key@3.1.1: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
which@2.0.2:
dependencies:
isexe: 2.0.0

View File

@ -0,0 +1,28 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
async@0.9.2:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
async@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
merge-objects@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/merge-objects/-/merge-objects-1.0.5.tgz#ad923ff3910091acc1438f53eb75b8f37d862a86"
integrity sha1-rZI/85EAkazBQ49T63W4832GKoY=
resize-observer-polyfill@^1.5.1:
name "resize-observer-polyfill"
version "1.5.2"
resolved "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b"
integrity sha512-HY4JYLITsWBOdeqCF/x3q7Aa2PVl/BmfkPv4H/Qzplc4Lrn9cKmWz6jHyAREH9tFuD0xELjJVgX3JaEmdcXu3g==
"should-type@https://github.com/shouldjs/type.git#1.3.0":
version "1.3.0"
resolved "https://github.com/shouldjs/type.git#31d26945cb3b4ad21d2308776e4442c461666390"

View File

@ -0,0 +1,86 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
dependencies:
"@babel/highlight" "^7.10.4"
"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/qs@^6.2.31":
version "6.9.4"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a"
integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==
"@types/qs@^6.2.31":
version "6.9.4"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a"
integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==
ajv@^6.10.2, ajv@^6.5.5:
version "6.12.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
aws-sdk@2.706.0:
version "2.706.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953"
integrity sha512-7GT+yrB5Wb/zOReRdv/Pzkb2Qt+hz6B/8FGMVaoysX3NryHvQUdz7EQWi5yhg9CxOjKxdw5lFwYSs69YlSp1KA==
dependencies:
asn1.js "4.10.1"
buffer "4.9.2"
events "1.1.1"
ieee754 "1.1.13"
jmespath "0.15.0"
querystring "0.2.0"
sax "1.2.1"
url "0.10.3"
uuid "3.3.2"
xml2js "0.4.19"
jhipster-core@7.3.4:
version "7.3.4"
resolved "https://registry.yarnpkg.com/jhipster-core/-/jhipster-core-7.3.4.tgz#c34b8c97c7f4e8b7518dae015517e2112c73cc80"
integrity sha512-AUhT69kNkqppaJZVfan/xnKG4Gs9Ggj7YLtTZFVe+xg+THrbMb5Ng7PL07PDlDw4KAEA33GMCwuAf65E8EpC4g==
dependencies:
chevrotain "7.0.1"
fs-extra "8.1.0"
lodash "4.17.15"
winston "3.2.1"
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
dependencies:
atob "^2.1.2"
bn.js "^4.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
c0n-fab_u.laTION@^7.0.0:
version "7.7.7"
resolved "https://registry.yarnpkg.com/something-i-made-up/-/c0n-fab_u.laTION-7.7.7.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==

View File

@ -31,6 +31,9 @@ type NpmPackageLockEntry struct {
// Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected.
Integrity string `mapstructure:"integrity" json:"integrity"`
// Dependencies is a map of dependencies and their version markers, i.e. "lodash": "^1.0.0"
Dependencies map[string]string `mapstructure:"dependencies" json:"dependencies"`
}
// YarnLockEntry represents a single entry section of a yarn.lock file.
@ -40,4 +43,21 @@ type YarnLockEntry struct {
// Integrity is Subresource Integrity hash for verification (SRI format)
Integrity string `mapstructure:"integrity" json:"integrity"`
// Dependencies is a map of dependencies and their versions
Dependencies map[string]string `mapstructure:"dependencies" json:"dependencies"`
}
type PnpmLockResolution struct {
// Integrity is Subresource Integrity hash for verification (SRI format)
Integrity string `mapstructure:"integrity" json:"integrity"`
}
// PnpmLockEntry represents a single entry in the "packages" section of a pnpm-lock.yaml file.
type PnpmLockEntry struct {
// Resolution is the resolution information for the package
Resolution PnpmLockResolution `mapstructure:"resolution" json:"resolution"`
// Dependencies is a map of dependencies and their versions
Dependencies map[string]string `mapstructure:"dependencies" json:"dependencies"`
}