-
Notifications
You must be signed in to change notification settings - Fork 581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add cataloger for NuGet packages #3484
base: main
Are you sure you want to change the base?
Changes from all commits
4ba3e5e
252523e
dcbe1bf
39e374f
bf60621
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package dotnet | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"sort" | ||
|
||
"github.com/anchore/packageurl-go" | ||
"github.com/anchore/syft/internal/log" | ||
"github.com/anchore/syft/internal/relationship" | ||
"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" | ||
) | ||
|
||
var _ generic.Parser = parseDotnetPackagesLock | ||
|
||
type dotnetPackagesLock struct { | ||
Version int `json:"version"` | ||
Dependencies map[string]map[string]dotnetPackagesLockDep `json:"dependencies"` | ||
} | ||
|
||
type dotnetPackagesLockDep struct { | ||
Type string `json:"type"` | ||
Requested string `json:"requested"` | ||
Resolved string `json:"resolved"` | ||
ContentHash string `json:"contentHash"` | ||
Dependencies map[string]string `json:"dependencies,omitempty"` | ||
} | ||
|
||
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { | ||
var pkgs []pkg.Package | ||
var pkgMap = make(map[string]pkg.Package) | ||
var relationships []artifact.Relationship | ||
|
||
dec := json.NewDecoder(reader) | ||
|
||
// unmarshal file | ||
var lockFile dotnetPackagesLock | ||
if err := dec.Decode(&lockFile); err != nil { | ||
return nil, nil, fmt.Errorf("failed to parse packages.lock.json file: %w", err) | ||
} | ||
|
||
// collect all deps here | ||
allDependencies := make(map[string]dotnetPackagesLockDep) | ||
|
||
var names []string | ||
for _, dependencies := range lockFile.Dependencies { | ||
for name, dep := range dependencies { | ||
names = append(names, name) | ||
allDependencies[name] = dep | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the map has multiple entries here for the same name key? Are we overriding the |
||
} | ||
} | ||
|
||
// sort the names so that the order of the packages is deterministic | ||
sort.Strings(names) | ||
|
||
// create artifact for each pkg | ||
for _, name := range names { | ||
dep := allDependencies[name] | ||
dotnetPkg := newDotnetPackagesLockPackage(name, dep, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) | ||
if dotnetPkg != nil { | ||
pkgs = append(pkgs, *dotnetPkg) | ||
pkgMap[name] = *dotnetPkg | ||
} | ||
} | ||
|
||
// fill up relationships | ||
for name, dep := range allDependencies { | ||
parentPkg, ok := pkgMap[name] | ||
if !ok { | ||
log.Debugf("unable to find package in map: %s", name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this might need a better abstraction for this debug message to be helpful. Which map? What does the pkgMap represent? Are allDependencies only inclusive of Apologies for all the question on this review. Is there a good reference or specification document I can look at for the lockfile so it's easier to maybe build a mental model of what we're cataloging here? That might aid in the review and help me understand if the relationships being created are correct. From what I am reading the lockfile is:
Is pkg map just inclusive of |
||
continue | ||
} | ||
|
||
for childName := range dep.Dependencies { | ||
childPkg, ok := pkgMap[childName] | ||
if !ok { | ||
log.Debugf("unable to find dependency package in map: %s", childName) | ||
continue | ||
} | ||
|
||
rel := artifact.Relationship{ | ||
From: parentPkg, | ||
To: childPkg, | ||
Type: artifact.DependencyOfRelationship, | ||
} | ||
relationships = append(relationships, rel) | ||
} | ||
} | ||
|
||
// sort the relationships for deterministic output | ||
relationship.Sort(relationships) | ||
|
||
return pkgs, relationships, nil | ||
} | ||
|
||
func newDotnetPackagesLockPackage(name string, dep dotnetPackagesLockDep, locations ...file.Location) *pkg.Package { | ||
metadata := pkg.DotnetPackagesLockEntry{ | ||
Name: name, | ||
Version: dep.Resolved, | ||
ContentHash: dep.ContentHash, | ||
Type: dep.Type, | ||
} | ||
|
||
return &pkg.Package{ | ||
Name: name, | ||
Version: dep.Resolved, | ||
Type: pkg.DotnetPkg, | ||
Metadata: metadata, | ||
Locations: file.NewLocationSet(locations...), | ||
Language: pkg.Dotnet, | ||
PURL: packagesLockPackageURL(name, dep.Resolved), | ||
} | ||
} | ||
|
||
func packagesLockPackageURL(name, version string) string { | ||
var qualifiers packageurl.Qualifiers | ||
|
||
return packageurl.NewPackageURL( | ||
packageurl.TypeNuget, // See explanation in syft/pkg/cataloger/dotnet/package.go as to why this was chosen. | ||
"", | ||
name, | ||
version, | ||
qualifiers, | ||
"", | ||
).ToString() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
package dotnet | ||
|
||
import ( | ||
"testing" | ||
|
||
"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/internal/pkgtest" | ||
) | ||
|
||
func Test_corruptDotnetPackagesLock(t *testing.T) { | ||
pkgtest.NewCatalogTester(). | ||
FromFile(t, "test-fixtures/glob-paths/src/packages.lock.json"). | ||
WithError(). | ||
TestParser(t, parseDotnetDeps) | ||
} | ||
|
||
func TestParseDotnetPackagesLock(t *testing.T) { | ||
fixture := "test-fixtures/packages.lock.json" | ||
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture)) | ||
|
||
autoMapperPkg := pkg.Package{ | ||
Name: "AutoMapper", | ||
Version: "13.0.1", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "AutoMapper", | ||
Version: "13.0.1", | ||
ContentHash: "/Fx1SbJ16qS7dU4i604Sle+U9VLX+WSNVJggk6MupKVkYvvBm4XqYaeFuf67diHefHKHs50uQIS2YEDFhPCakQ==", | ||
Type: "Direct", | ||
}, | ||
} | ||
|
||
bootstrapPkg := pkg.Package{ | ||
Name: "bootstrap", | ||
Version: "5.0.0", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "bootstrap", | ||
Version: "5.0.0", | ||
ContentHash: "NKQFzFwrfWOMjTwr+X/2iJyCveuAGF+fNzkxyB0YW45+InVhcE9PUxoL1a8Vmc/Lq9E/CQd4DjO8kU32P4w/Gg==", | ||
Type: "Direct", | ||
}, | ||
} | ||
|
||
log4netPkg := pkg.Package{ | ||
Name: "log4net", | ||
Version: "2.0.5", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "log4net", | ||
Version: "2.0.5", | ||
ContentHash: "AEqPZz+v+OikfnR2SqRVdQPnSaLq5y9Iz1CfRQZ9kTKPYCXHG6zYmDHb7wJotICpDLMr/JqokyjiqKAjUKp0ng==", | ||
Type: "Direct", | ||
}, | ||
} | ||
|
||
dependencyInjectionAbstractionsPkg := pkg.Package{ | ||
Name: "Microsoft.Extensions.DependencyInjection.Abstractions", | ||
Version: "6.0.0", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "Microsoft.Extensions.DependencyInjection.Abstractions", | ||
Version: "6.0.0", | ||
ContentHash: "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==", | ||
Type: "Transitive", | ||
}, | ||
} | ||
|
||
extensionOptionsPkg := pkg.Package{ | ||
Name: "Microsoft.Extensions.Options", | ||
Version: "6.0.0", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "Microsoft.Extensions.Options", | ||
Version: "6.0.0", | ||
ContentHash: "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", | ||
Type: "Transitive", | ||
}, | ||
} | ||
|
||
extensionPrimitivesPkg := pkg.Package{ | ||
Name: "Microsoft.Extensions.Primitives", | ||
Version: "6.0.0", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "Microsoft.Extensions.Primitives", | ||
Version: "6.0.0", | ||
ContentHash: "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", | ||
Type: "Transitive", | ||
}, | ||
} | ||
|
||
compilerServicesUnsafePkg := pkg.Package{ | ||
Name: "System.Runtime.CompilerServices.Unsafe", | ||
Version: "6.0.0", | ||
PURL: "pkg:nuget/[email protected]", | ||
Locations: fixtureLocationSet, | ||
Language: pkg.Dotnet, | ||
Type: pkg.DotnetPkg, | ||
Metadata: pkg.DotnetPackagesLockEntry{ | ||
Name: "System.Runtime.CompilerServices.Unsafe", | ||
Version: "6.0.0", | ||
ContentHash: "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", | ||
Type: "Transitive", | ||
}, | ||
} | ||
|
||
expectedPkgs := []pkg.Package{ | ||
autoMapperPkg, | ||
bootstrapPkg, | ||
log4netPkg, | ||
dependencyInjectionAbstractionsPkg, | ||
extensionOptionsPkg, | ||
extensionPrimitivesPkg, | ||
compilerServicesUnsafePkg, | ||
} | ||
|
||
expectedRelationships := []artifact.Relationship{ | ||
{ | ||
From: autoMapperPkg, | ||
To: extensionOptionsPkg, | ||
Type: artifact.DependencyOfRelationship, | ||
}, | ||
{ | ||
From: extensionOptionsPkg, | ||
To: dependencyInjectionAbstractionsPkg, | ||
Type: artifact.DependencyOfRelationship, | ||
}, | ||
{ | ||
From: extensionOptionsPkg, | ||
To: extensionPrimitivesPkg, | ||
Type: artifact.DependencyOfRelationship, | ||
}, | ||
{ | ||
From: extensionPrimitivesPkg, | ||
To: compilerServicesUnsafePkg, | ||
Type: artifact.DependencyOfRelationship, | ||
}, | ||
} | ||
|
||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"i am bogus" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"version": 1, | ||
"dependencies": { | ||
"net8.0": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is where my understanding goes sideways. |
||
"AutoMapper": { | ||
"type": "Direct", | ||
"requested": "[13.0.1, )", | ||
"resolved": "13.0.1", | ||
"contentHash": "/Fx1SbJ16qS7dU4i604Sle+U9VLX+WSNVJggk6MupKVkYvvBm4XqYaeFuf67diHefHKHs50uQIS2YEDFhPCakQ==", | ||
"dependencies": { | ||
"Microsoft.Extensions.Options": "6.0.0" | ||
} | ||
}, | ||
"bootstrap": { | ||
"type": "Direct", | ||
"requested": "[5.0.0, )", | ||
"resolved": "5.0.0", | ||
"contentHash": "NKQFzFwrfWOMjTwr+X/2iJyCveuAGF+fNzkxyB0YW45+InVhcE9PUxoL1a8Vmc/Lq9E/CQd4DjO8kU32P4w/Gg==" | ||
}, | ||
"log4net": { | ||
"type": "Direct", | ||
"requested": "[2.0.5, )", | ||
"resolved": "2.0.5", | ||
"contentHash": "AEqPZz+v+OikfnR2SqRVdQPnSaLq5y9Iz1CfRQZ9kTKPYCXHG6zYmDHb7wJotICpDLMr/JqokyjiqKAjUKp0ng==" | ||
}, | ||
"Microsoft.Extensions.DependencyInjection.Abstractions": { | ||
"type": "Transitive", | ||
"resolved": "6.0.0", | ||
"contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" | ||
}, | ||
"Microsoft.Extensions.Options": { | ||
"type": "Transitive", | ||
"resolved": "6.0.0", | ||
"contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", | ||
"dependencies": { | ||
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", | ||
"Microsoft.Extensions.Primitives": "6.0.0" | ||
} | ||
}, | ||
"Microsoft.Extensions.Primitives": { | ||
"type": "Transitive", | ||
"resolved": "6.0.0", | ||
"contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", | ||
"dependencies": { | ||
"System.Runtime.CompilerServices.Unsafe": "6.0.0" | ||
} | ||
}, | ||
"System.Runtime.CompilerServices.Unsafe": { | ||
"type": "Transitive", | ||
"resolved": "6.0.0", | ||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment for other reviewers - the reason this looks this way is that the dependencies have this nested map quality where there are sub fields. @Kemosabert do you have an example where we can see how a sibling to the top level package would behave? In the bottom example it would be a sibling package to
netcore2.0
- I pulled this from here