Skip to content
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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/task/package_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func DefaultPackageTaskFactories() PackageTaskFactories {
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
newSimplePackageTaskFactory(dotnet.NewDotnetPackagesLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
newSimplePackageTaskFactory(erlang.NewOTPCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang", "otp"),
Expand Down
1 change: 1 addition & 0 deletions syft/internal/packagemetadata/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ var jsonTypes = makeJSONTypes(
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry", "DotnetPackagesLockEntry"),
)

func expandLegacyNameVariants(names ...string) []string {
Expand Down
6 changes: 6 additions & 0 deletions syft/internal/packagemetadata/names_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,12 @@ func Test_JSONName_JSONLegacyName(t *testing.T) {
expectedJSONName: "dotnet-deps-entry",
expectedLegacyName: "DotnetDepsMetadata",
},
{
name: "DotnetPackagesLockEntry",
metadata: pkg.DotnetPackagesLockEntry{},
expectedJSONName: "dotnet-packages-lock-entry",
expectedLegacyName: "DotnetPackagesLockEntry",
},
{
name: "DotnetPortableExecutableMetadata",
metadata: pkg.DotnetPortableExecutableEntry{},
Expand Down
4 changes: 4 additions & 0 deletions syft/pkg/cataloger/dotnet/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ func NewDotnetPortableExecutableCataloger() pkg.Cataloger {
return generic.NewCataloger("dotnet-portable-executable-cataloger").
WithParserByGlobs(parseDotnetPortableExecutable, "**/*.dll", "**/*.exe")
}

func NewDotnetPackagesLockCataloger() pkg.Cataloger {
return generic.NewCataloger("dotnet-packages-lock-cataloger").WithParserByGlobs(parseDotnetPackagesLock, "**/packages.lock.json")
}
130 changes: 130 additions & 0 deletions syft/pkg/cataloger/dotnet/parse_dotnet_packages_lock.go
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"`
Copy link
Contributor

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

  "dependencies": {	
    "netcore2.0": {	
      "Contoso.Base": {	
        "type": "direct",	
        "requested": "3.0.0",	
        "resolved": "3.0.0",
        "contentHash":"fVXsnMP2Wq84VA533zj0a/Et+QoLoeNpVXsnMP2Wq84l+hsUxfwunkbqoIHIvpOqwQ/+HIvprVKs+QOihnkbqod=="
        "dependencies": {
             "Contoso.Core": "1.2.3",
             "Fabrikam.Utilities": "[3.1.0]"
         }		
      }	
      "Contoso.Core": {
        "type": "transitive",	
        "requested": "1.2.3",	
        "resolved": "1.2.3",
        "contentHash":"xScnMP2Wq84VA533zj0a/Et+QoLoeNpVXsnMP2Wq84l+hsUxfwunkbqoIHIvpOqwQ/+HIvprVKs+QOihnkbmoq=="
        "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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 allDependencies entry?

}
}

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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 dotNetPkgs or can they be other types?

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:

dependencies[topPackages]map[subPackages]metadata

where topPackages and subPackages can be > 1

Is pkg map just inclusive of topPackages?

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()
}
162 changes: 162 additions & 0 deletions syft/pkg/cataloger/dotnet/parse_dotnet_packages_lock_test.go
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"
55 changes: 55 additions & 0 deletions syft/pkg/cataloger/dotnet/test-fixtures/packages.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"version": 1,
"dependencies": {
"net8.0": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where my understanding goes sideways.
Is the entry at the top of this map (net8.0) always singular, and where (if anywhere) does the metadata come from that describes net8.0

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