Parse donet dependency trees (#2143)

* add dependency information for .NET pkgs

Signed-off-by: Benji Visser <benji@093b.org>

* update pkg coverage directory test

Signed-off-by: Benji Visser <benji@093b.org>

* reverse dependsOn relationship

Signed-off-by: Benji Visser <benji@093b.org>

* update root pkg parsing

Signed-off-by: Benji Visser <benji@093b.org>

* add comments about the test relationships represented

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add docs around relationship sorting functions + update test helpers

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Benji Visser <benji@093b.org>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Benji Visser 2023-10-11 14:01:24 -04:00 committed by GitHub
parent 7732cd3b48
commit fe7a417fb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 494 additions and 201 deletions

View File

@ -1,6 +1,8 @@
package dotnet
import (
"fmt"
"regexp"
"strings"
"github.com/anchore/packageurl-go"
@ -9,13 +11,7 @@ import (
)
func newDotnetDepsPackage(nameVersion string, lib dotnetDepsLibrary, locations ...file.Location) *pkg.Package {
if lib.Type != "package" {
return nil
}
fields := strings.Split(nameVersion, "/")
name := fields[0]
version := fields[1]
name, version := extractNameAndVersion(nameVersion)
m := pkg.DotnetDepsMetadata{
Name: name,
@ -41,6 +37,27 @@ func newDotnetDepsPackage(nameVersion string, lib dotnetDepsLibrary, locations .
return p
}
func getDepsJSONFilePrefix(p string) string {
r := regexp.MustCompile(`([^\/]+)\.deps\.json$`)
match := r.FindStringSubmatch(p)
if len(match) > 1 {
return match[1]
}
return ""
}
func extractNameAndVersion(nameVersion string) (name, version string) {
fields := strings.Split(nameVersion, "/")
name = fields[0]
version = fields[1]
return
}
func createNameAndVersion(name, version string) (nameVersion string) {
nameVersion = fmt.Sprintf("%s/%s", name, version)
return
}
func packageURL(m pkg.DotnetDepsMetadata) string {
var qualifiers packageurl.Qualifiers

View File

@ -5,6 +5,7 @@ import (
"fmt"
"sort"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@ -13,7 +14,17 @@ import (
var _ generic.Parser = parseDotnetDeps
type dotnetRuntimeTarget struct {
Name string `json:"name"`
}
type dotnetDepsTarget struct {
Dependencies map[string]string `json:"dependencies"`
Runtime map[string]struct{} `json:"runtime"`
}
type dotnetDeps struct {
RuntimeTarget dotnetRuntimeTarget `json:"runtimeTarget"`
Targets map[string]map[string]dotnetDepsTarget `json:"targets"`
Libraries map[string]dotnetDepsLibrary `json:"libraries"`
}
@ -24,27 +35,55 @@ type dotnetDepsLibrary struct {
HashPath string `json:"hashPath"`
}
//nolint:funlen
func parseDotnetDeps(_ 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)
var p dotnetDeps
if err := dec.Decode(&p); err != nil {
var depsDoc dotnetDeps
if err := dec.Decode(&depsDoc); err != nil {
return nil, nil, fmt.Errorf("failed to parse deps.json file: %w", err)
}
var names []string
rootName := getDepsJSONFilePrefix(reader.AccessPath())
if rootName == "" {
return nil, nil, fmt.Errorf("unable to determine root package name from deps.json file: %s", reader.AccessPath())
}
var rootPkg *pkg.Package
for nameVersion, lib := range depsDoc.Libraries {
name, _ := extractNameAndVersion(nameVersion)
if lib.Type == "project" && name == rootName {
rootPkg = newDotnetDepsPackage(
nameVersion,
lib,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
}
}
if rootPkg == nil {
return nil, nil, fmt.Errorf("unable to determine root package from deps.json file: %s", reader.AccessPath())
}
pkgs = append(pkgs, *rootPkg)
pkgMap[createNameAndVersion(rootPkg.Name, rootPkg.Version)] = *rootPkg
for nameVersion := range p.Libraries {
var names []string
for nameVersion := range depsDoc.Libraries {
names = append(names, nameVersion)
}
// sort the names so that the order of the packages is deterministic
sort.Strings(names)
for _, nameVersion := range names {
lib := p.Libraries[nameVersion]
// skip the root package
name, version := extractNameAndVersion(nameVersion)
if name == rootPkg.Name && version == rootPkg.Version {
continue
}
lib := depsDoc.Libraries[nameVersion]
dotnetPkg := newDotnetDepsPackage(
nameVersion,
lib,
@ -53,8 +92,36 @@ func parseDotnetDeps(_ file.Resolver, _ *generic.Environment, reader file.Locati
if dotnetPkg != nil {
pkgs = append(pkgs, *dotnetPkg)
pkgMap[nameVersion] = *dotnetPkg
}
}
return pkgs, nil, nil
for pkgNameVersion, target := range depsDoc.Targets[depsDoc.RuntimeTarget.Name] {
for depName, depVersion := range target.Dependencies {
depNameVersion := createNameAndVersion(depName, depVersion)
depPkg, ok := pkgMap[depNameVersion]
if !ok {
log.Debug("unable to find package in map", depNameVersion)
continue
}
p, ok := pkgMap[pkgNameVersion]
if !ok {
log.Debug("unable to find package in map", pkgNameVersion)
continue
}
rel := artifact.Relationship{
From: depPkg,
To: p,
Type: artifact.DependencyOfRelationship,
}
relationships = append(relationships, rel)
}
}
// sort the relationships for deterministic output
// TODO: ideally this would be replaced with artifact.SortRelationships when one exists and is type agnostic.
// this will only consider package-to-package relationships.
pkg.SortRelationships(relationships)
return pkgs, relationships, nil
}

View File

@ -12,8 +12,33 @@ import (
func TestParseDotnetDeps(t *testing.T) {
fixture := "test-fixtures/TestLibrary.deps.json"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
expected := []pkg.Package{
{
rootPkg := pkg.Package{
Name: "TestLibrary",
Version: "1.0.0",
PURL: "pkg:nuget/TestLibrary@1.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
MetadataType: pkg.DotnetDepsMetadataType,
Metadata: pkg.DotnetDepsMetadata{
Name: "TestLibrary",
Version: "1.0.0",
},
}
testCommon := pkg.Package{
Name: "TestCommon",
Version: "1.0.0",
PURL: "pkg:nuget/TestCommon@1.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
MetadataType: pkg.DotnetDepsMetadataType,
Metadata: pkg.DotnetDepsMetadata{
Name: "TestCommon",
Version: "1.0.0",
},
}
awssdkcore := pkg.Package{
Name: "AWSSDK.Core",
Version: "3.7.10.6",
PURL: "pkg:nuget/AWSSDK.Core@3.7.10.6",
@ -28,8 +53,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "awssdk.core/3.7.10.6",
HashPath: "awssdk.core.3.7.10.6.nupkg.sha512",
},
},
{
}
msftDependencyInjectionAbstractions := pkg.Package{
Name: "Microsoft.Extensions.DependencyInjection.Abstractions",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection.Abstractions@6.0.0",
@ -44,8 +69,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "microsoft.extensions.dependencyinjection.abstractions/6.0.0",
HashPath: "microsoft.extensions.dependencyinjection.abstractions.6.0.0.nupkg.sha512",
},
},
{
}
msftDependencyInjection := pkg.Package{
Name: "Microsoft.Extensions.DependencyInjection",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection@6.0.0",
@ -60,8 +85,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "microsoft.extensions.dependencyinjection/6.0.0",
HashPath: "microsoft.extensions.dependencyinjection.6.0.0.nupkg.sha512",
},
},
{
}
msftLoggingAbstractions := pkg.Package{
Name: "Microsoft.Extensions.Logging.Abstractions",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Logging.Abstractions@6.0.0",
@ -76,8 +101,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "microsoft.extensions.logging.abstractions/6.0.0",
HashPath: "microsoft.extensions.logging.abstractions.6.0.0.nupkg.sha512",
},
},
{
}
msftExtensionsLogging := pkg.Package{
Name: "Microsoft.Extensions.Logging",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Logging@6.0.0",
@ -92,9 +117,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "microsoft.extensions.logging/6.0.0",
HashPath: "microsoft.extensions.logging.6.0.0.nupkg.sha512",
},
},
{
}
msftExtensionsOptions := pkg.Package{
Name: "Microsoft.Extensions.Options",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Options@6.0.0",
@ -109,8 +133,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "microsoft.extensions.options/6.0.0",
HashPath: "microsoft.extensions.options.6.0.0.nupkg.sha512",
},
},
{
}
msftExtensionsPrimitives := pkg.Package{
Name: "Microsoft.Extensions.Primitives",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Primitives@6.0.0",
@ -125,8 +149,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "microsoft.extensions.primitives/6.0.0",
HashPath: "microsoft.extensions.primitives.6.0.0.nupkg.sha512",
},
},
{
}
newtonsoftJson := pkg.Package{
Name: "Newtonsoft.Json",
Version: "13.0.1",
PURL: "pkg:nuget/Newtonsoft.Json@13.0.1",
@ -141,8 +165,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "newtonsoft.json/13.0.1",
HashPath: "newtonsoft.json.13.0.1.nupkg.sha512",
},
},
{
}
serilogSinksConsole := pkg.Package{
Name: "Serilog.Sinks.Console",
Version: "4.0.1",
PURL: "pkg:nuget/Serilog.Sinks.Console@4.0.1",
@ -157,8 +181,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "serilog.sinks.console/4.0.1",
HashPath: "serilog.sinks.console.4.0.1.nupkg.sha512",
},
},
{
}
serilog := pkg.Package{
Name: "Serilog",
Version: "2.10.0",
PURL: "pkg:nuget/Serilog@2.10.0",
@ -173,8 +197,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "serilog/2.10.0",
HashPath: "serilog.2.10.0.nupkg.sha512",
},
},
{
}
systemDiagnosticsDiagnosticsource := pkg.Package{
Name: "System.Diagnostics.DiagnosticSource",
Version: "6.0.0",
PURL: "pkg:nuget/System.Diagnostics.DiagnosticSource@6.0.0",
@ -189,8 +213,8 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "system.diagnostics.diagnosticsource/6.0.0",
HashPath: "system.diagnostics.diagnosticsource.6.0.0.nupkg.sha512",
},
},
{
}
systemRuntimeCompilerServicesUnsafe := pkg.Package{
Name: "System.Runtime.CompilerServices.Unsafe",
Version: "6.0.0",
PURL: "pkg:nuget/System.Runtime.CompilerServices.Unsafe@6.0.0",
@ -205,9 +229,146 @@ func TestParseDotnetDeps(t *testing.T) {
Path: "system.runtime.compilerservices.unsafe/6.0.0",
HashPath: "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512",
},
}
expectedPkgs := []pkg.Package{
awssdkcore,
msftDependencyInjection,
msftDependencyInjectionAbstractions,
msftExtensionsLogging,
msftLoggingAbstractions,
msftExtensionsOptions,
msftExtensionsPrimitives,
newtonsoftJson,
serilog,
serilogSinksConsole,
systemDiagnosticsDiagnosticsource,
systemRuntimeCompilerServicesUnsafe,
testCommon,
rootPkg,
}
// ┌── (✓ = is represented in the test)
// ↓
//
// ✓ TestLibrary/1.0.0 (project)
// ✓ ├── [a] Microsoft.Extensions.DependencyInjection/6.0.0 [file version: 6.0.21.52210]
// ✓ │ ├── [b] Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 [file version: 6.0.21.52210]
// ✓ │ └── [c!] System.Runtime.CompilerServices.Unsafe/6.0.0 [NO TARGET INFO]
// ✓ ├── Microsoft.Extensions.Logging/6.0.0 [file version: 6.0.21.52210]
// ✓ │ ├── Microsoft.Extensions.DependencyInjection/6.0.0 ...to [a]
// ✓ │ ├── Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 ...to [b]
// ✓ │ ├── Microsoft.Extensions.Logging.Abstractions/6.0.0 [file version: 6.0.21.52210]
// ✓ │ ├── Microsoft.Extensions.Options/6.0.0 [file version: 6.0.21.52210]
// ✓ │ │ ├── Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 ...to [b]
// ✓ │ │ └── Microsoft.Extensions.Primitives/6.0.0 [file version: 6.0.21.52210]
// ✓ │ │ └── System.Runtime.CompilerServices.Unsafe/6.0.0 ...to [c!]
// ✓ │ └── System.Diagnostics.DiagnosticSource/6.0.0 [NO RUNTIME INFO]
// ✓ │ └── System.Runtime.CompilerServices.Unsafe/6.0.0 ...to [c!]
// ✓ ├── Newtonsoft.Json/13.0.1 [file version: 13.0.1.25517]
// ✓ ├── [d] Serilog/2.10.0 [file version: 2.10.0.0]
// ✓ ├── Serilog.Sinks.Console/4.0.1 [file version: 4.0.1.0]
// ✓ │ └── Serilog/2.10.0 ...to [d]
// ✓ └── [e!] TestCommon/1.0.0 [NOT SERVICEABLE / NO SHA]
// ✓ └── AWSSDK.Core/3.7.10.6 [file version: 3.7.10.6]
expectedRelationships := []artifact.Relationship{
{
From: awssdkcore,
To: testCommon,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjection,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjection,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjectionAbstractions,
To: msftDependencyInjection,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjectionAbstractions,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjectionAbstractions,
To: msftExtensionsOptions,
Type: artifact.DependencyOfRelationship,
},
{
From: msftExtensionsLogging,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: msftLoggingAbstractions,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftExtensionsOptions,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftExtensionsPrimitives,
To: msftExtensionsOptions,
Type: artifact.DependencyOfRelationship,
},
{
From: newtonsoftJson,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: serilog,
To: serilogSinksConsole,
Type: artifact.DependencyOfRelationship,
},
{
From: serilog,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: serilogSinksConsole,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: systemDiagnosticsDiagnosticsource,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: systemRuntimeCompilerServicesUnsafe,
To: msftDependencyInjection,
Type: artifact.DependencyOfRelationship,
},
{
From: systemRuntimeCompilerServicesUnsafe,
To: msftExtensionsPrimitives,
Type: artifact.DependencyOfRelationship,
},
{
From: systemRuntimeCompilerServicesUnsafe,
To: systemDiagnosticsDiagnosticsource,
Type: artifact.DependencyOfRelationship,
},
{
From: testCommon,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
}
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parseDotnetDeps, expected, expectedRelationships)
pkgtest.TestFileParser(t, fixture, parseDotnetDeps, expectedPkgs, expectedRelationships)
}

View File

@ -329,6 +329,10 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi
opts = append(opts, p.compareOptions...)
opts = append(opts, cmp.Reporter(&r))
// order should not matter
pkg.Sort(p.expectedPkgs)
pkg.Sort(pkgs)
if diff := cmp.Diff(p.expectedPkgs, pkgs, opts...); diff != "" {
t.Log("Specific Differences:\n" + r.String())
t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff)
@ -341,6 +345,10 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi
opts = append(opts, p.compareOptions...)
opts = append(opts, cmp.Reporter(&r))
// order should not matter
pkg.SortRelationships(p.expectedRelationships)
pkg.SortRelationships(relationships)
if diff := cmp.Diff(p.expectedRelationships, relationships, opts...); diff != "" {
t.Log("Specific Differences:\n" + r.String())

View File

@ -1,9 +1,47 @@
package pkg
import "github.com/anchore/syft/syft/artifact"
import (
"sort"
"github.com/anchore/syft/syft/artifact"
)
func NewRelationships(catalog *Collection) []artifact.Relationship {
rels := RelationshipsByFileOwnership(catalog)
rels = append(rels, RelationshipsEvidentBy(catalog)...)
return rels
}
// SortRelationships takes a set of package-to-package relationships and sorts them in a stable order by name and version.
// Note: this does not consider package-to-other, other-to-package, or other-to-other relationships.
// TODO: ideally this should be replaced with a more type-agnostic sort function that resides in the artifact package.
func SortRelationships(rels []artifact.Relationship) {
sort.SliceStable(rels, func(i, j int) bool {
return relationshipLess(rels[i], rels[j])
})
}
func relationshipLess(i, j artifact.Relationship) bool {
iFrom, ok1 := i.From.(Package)
iTo, ok2 := i.To.(Package)
jFrom, ok3 := j.From.(Package)
jTo, ok4 := j.To.(Package)
if !(ok1 && ok2 && ok3 && ok4) {
return false
}
if iFrom.Name != jFrom.Name {
return iFrom.Name < jFrom.Name
}
if iFrom.Version != jFrom.Version {
return iFrom.Version < jFrom.Version
}
if iTo.Name != jTo.Name {
return iTo.Name < jTo.Name
}
if iTo.Version != jTo.Version {
return iTo.Version < jTo.Version
}
return i.Type < j.Type
}

View File

@ -247,6 +247,8 @@ var dirOnlyTestCases = []testCase{
"Serilog.Sinks.Console": "4.0.1",
"System.Diagnostics.DiagnosticSource": "6.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
},
},
{