mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 15:36:41 +01:00
fix(dotnet): resolve packages.lock dependencies deterministically (#5210)
* fix(dotnet): resolve packages.lock dependencies within their target framework Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix(dotnet): make packages.lock package metadata deterministic across target frameworks Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: parse packages lock tests Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: update lockfile parse to read into stable structure Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: remove stale comments Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * test: new test for project entry case Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix(dotnet): key packages.lock packages case-insensitively NuGet IDs are case-insensitive, and edge lookup already treated them that way, but package identity used the exact-case name. Two spellings of one ID and version across target frameworks became two packages, and type precedence never ran between them. - packages merge on the lowercased name + version, display name comes from the winning entry - a package listing itself (under any casing) no longer produces a self edge Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> 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:
co-authored by
Alex Goodman
parent
cc326e45a6
commit
c33fa32cf1
@@ -4,8 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/packageurl-go"
|
"github.com/anchore/packageurl-go"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
@@ -18,6 +19,8 @@ import (
|
|||||||
|
|
||||||
var _ generic.Parser = parseDotnetPackagesLock
|
var _ generic.Parser = parseDotnetPackagesLock
|
||||||
|
|
||||||
|
var packagesLockTypePrecedence = []string{"Project", "Direct", "CentralTransitive", "Transitive"}
|
||||||
|
|
||||||
type dotnetPackagesLock struct {
|
type dotnetPackagesLock struct {
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
Dependencies map[string]map[string]dotnetPackagesLockDep `json:"dependencies"`
|
Dependencies map[string]map[string]dotnetPackagesLockDep `json:"dependencies"`
|
||||||
@@ -31,11 +34,38 @@ type dotnetPackagesLockDep struct {
|
|||||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen
|
type packagesLockEntry struct {
|
||||||
var pkgs []pkg.Package
|
name string
|
||||||
var pkgMap = make(map[string]pkg.Package)
|
dep dotnetPackagesLockDep
|
||||||
var relationships []artifact.Relationship
|
}
|
||||||
|
|
||||||
|
func (e packagesLockEntry) nameVersion() string {
|
||||||
|
return createNameAndVersion(e.name, e.dep.Resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// key identifies the package an entry describes. NuGet package IDs are case-insensitive, so two spellings of the
|
||||||
|
// same ID and version are one package.
|
||||||
|
func (e packagesLockEntry) key() string {
|
||||||
|
return createNameAndVersion(strings.ToLower(e.name), e.dep.Resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
type packagesLockFramework struct {
|
||||||
|
name string
|
||||||
|
entries []packagesLockEntry
|
||||||
|
// byName is keyed by the lowercased package name, since NuGet package IDs are case-insensitive.
|
||||||
|
byName map[string]packagesLockEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f packagesLockFramework) find(name string) (packagesLockEntry, bool) {
|
||||||
|
e, ok := f.byName[strings.ToLower(name)]
|
||||||
|
return e, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type packagesLockEdge struct {
|
||||||
|
child, parent artifact.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
dec := json.NewDecoder(reader)
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
// unmarshal file
|
// unmarshal file
|
||||||
@@ -44,70 +74,21 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
|
|||||||
return nil, nil, fmt.Errorf("failed to parse packages.lock.json file: %w", err)
|
return nil, nil, fmt.Errorf("failed to parse packages.lock.json file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect all deps here
|
frameworks := newPackagesLockFrameworks(lockFile)
|
||||||
allDependencies := make(map[string]dotnetPackagesLockDep)
|
|
||||||
|
|
||||||
var names []string
|
|
||||||
for _, dependencies := range lockFile.Dependencies {
|
|
||||||
for name, dep := range dependencies {
|
|
||||||
depNameVersion := createNameAndVersion(name, dep.Resolved)
|
|
||||||
|
|
||||||
if slices.Contains(names, depNameVersion) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
names = append(names, depNameVersion)
|
|
||||||
allDependencies[depNameVersion] = dep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the names so that the order of the packages is deterministic
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
// create artifact for each pkg
|
// create artifact for each pkg
|
||||||
for _, nameVersion := range names {
|
var pkgs []pkg.Package
|
||||||
name, _ := extractNameAndVersion(nameVersion)
|
pkgMap := make(map[string]pkg.Package)
|
||||||
|
|
||||||
dep := allDependencies[nameVersion]
|
for _, entry := range mergePackagesLockEntries(frameworks) {
|
||||||
dotnetPkg := newDotnetPackagesLockPackage(name, dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
dotnetPkg := newDotnetPackagesLockPackage(entry.name, entry.dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||||
if dotnetPkg != nil {
|
if dotnetPkg != nil {
|
||||||
pkgs = append(pkgs, *dotnetPkg)
|
pkgs = append(pkgs, *dotnetPkg)
|
||||||
pkgMap[nameVersion] = *dotnetPkg
|
pkgMap[entry.key()] = *dotnetPkg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill up relationships
|
relationships := packagesLockRelationships(frameworks, pkgMap)
|
||||||
for depNameVersion, dep := range allDependencies {
|
|
||||||
parentPkg, ok := pkgMap[depNameVersion]
|
|
||||||
if !ok {
|
|
||||||
log.Debugf("package \"%s\" not found in map of all packages", depNameVersion)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for childDepName, childDepVersion := range dep.Dependencies {
|
|
||||||
childDepNameVersion := createNameAndVersion(childDepName, childDepVersion)
|
|
||||||
|
|
||||||
// try and find pkg for dependency with exact name and version
|
|
||||||
childPkg, ok := pkgMap[childDepNameVersion]
|
|
||||||
if !ok {
|
|
||||||
// no exact match found, lets match on name only, lockfile will contain other version of pkg
|
|
||||||
cpkg, ok := findPkgByName(childDepName, pkgMap)
|
|
||||||
if !ok {
|
|
||||||
log.Debugf("dependency \"%s\" of package \"%s\" not found in map of all packages", childDepNameVersion, depNameVersion)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
childPkg = *cpkg
|
|
||||||
}
|
|
||||||
|
|
||||||
rel := artifact.Relationship{
|
|
||||||
From: childPkg,
|
|
||||||
To: parentPkg,
|
|
||||||
Type: artifact.DependencyOfRelationship,
|
|
||||||
}
|
|
||||||
relationships = append(relationships, rel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the relationships for deterministic output
|
// sort the relationships for deterministic output
|
||||||
relationship.Sort(relationships)
|
relationship.Sort(relationships)
|
||||||
@@ -115,6 +96,147 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
|
|||||||
return pkgs, relationships, nil
|
return pkgs, relationships, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newPackagesLockFrameworks(lockFile dotnetPackagesLock) []packagesLockFramework {
|
||||||
|
var frameworks []packagesLockFramework
|
||||||
|
|
||||||
|
for _, frameworkName := range slices.Sorted(maps.Keys(lockFile.Dependencies)) {
|
||||||
|
deps := lockFile.Dependencies[frameworkName]
|
||||||
|
framework := packagesLockFramework{
|
||||||
|
name: frameworkName,
|
||||||
|
byName: make(map[string]packagesLockEntry, len(deps)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(deps)) {
|
||||||
|
entry := packagesLockEntry{name: name, dep: deps[name]}
|
||||||
|
framework.entries = append(framework.entries, entry)
|
||||||
|
framework.byName[strings.ToLower(name)] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
frameworks = append(frameworks, framework)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frameworks
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePackagesLockEntries(frameworks []packagesLockFramework) []packagesLockEntry {
|
||||||
|
merged := make(map[string]packagesLockEntry)
|
||||||
|
|
||||||
|
for _, framework := range frameworks {
|
||||||
|
for _, entry := range framework.entries {
|
||||||
|
key := entry.key()
|
||||||
|
if existing, ok := merged[key]; ok && !isMoreDirect(entry.dep.Type, existing.dep.Type) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
merged[key] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []packagesLockEntry
|
||||||
|
for _, key := range slices.Sorted(maps.Keys(merged)) {
|
||||||
|
entries = append(entries, merged[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMoreDirect(a, b string) bool {
|
||||||
|
rankA, rankB := packagesLockTypeRank(a), packagesLockTypeRank(b)
|
||||||
|
if rankA != rankB {
|
||||||
|
return rankA < rankB
|
||||||
|
}
|
||||||
|
return a < b
|
||||||
|
}
|
||||||
|
|
||||||
|
func packagesLockTypeRank(t string) int {
|
||||||
|
if i := slices.Index(packagesLockTypePrecedence, t); i >= 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return len(packagesLockTypePrecedence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func packagesLockRelationships(frameworks []packagesLockFramework, pkgMap map[string]pkg.Package) []artifact.Relationship {
|
||||||
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
|
frameworksByName := make(map[string]packagesLockFramework, len(frameworks))
|
||||||
|
for _, framework := range frameworks {
|
||||||
|
frameworksByName[framework.name] = framework
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[packagesLockEdge]struct{})
|
||||||
|
for _, framework := range frameworks {
|
||||||
|
base, hasBase := frameworksByName[baseFrameworkName(framework.name)]
|
||||||
|
if !hasBase || base.name == framework.name {
|
||||||
|
base = packagesLockFramework{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range framework.entries {
|
||||||
|
parentPkg, ok := pkgMap[entry.key()]
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("package %q not found in map of all packages", entry.nameVersion())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
relationships = append(relationships, packagesLockEntryRelationships(entry, parentPkg, framework, base, pkgMap, seen)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
func packagesLockEntryRelationships(entry packagesLockEntry, parentPkg pkg.Package, framework, base packagesLockFramework, pkgMap map[string]pkg.Package, seen map[packagesLockEdge]struct{}) []artifact.Relationship {
|
||||||
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
|
for _, childName := range slices.Sorted(maps.Keys(entry.dep.Dependencies)) {
|
||||||
|
child, ok := findPackagesLockDependency(childName, framework, base)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("dependency %q of package %q not found under target framework %q", childName, entry.nameVersion(), framework.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
childPkg, ok := pkgMap[child.key()]
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("package %q not found in map of all packages", child.nameVersion())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// a package listing itself (possibly under another casing) is not a dependency
|
||||||
|
if childPkg.ID() == parentPkg.ID() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := packagesLockEdge{child: childPkg.ID(), parent: parentPkg.ID()}
|
||||||
|
if _, exists := seen[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
|
||||||
|
relationships = append(relationships, artifact.Relationship{
|
||||||
|
From: childPkg,
|
||||||
|
To: parentPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPackagesLockDependency finds the entry a dependency edge points at. The version on an edge is only the lower
|
||||||
|
// bound of a version range, so the edge resolves to whatever version its target framework resolved. Runtime-specific
|
||||||
|
// sections such as "net8.0/win-x64" may reference packages listed only in their base framework ("net8.0"). An edge
|
||||||
|
// that resolves nowhere is dropped rather than guessed from another target framework.
|
||||||
|
func findPackagesLockDependency(name string, framework, base packagesLockFramework) (packagesLockEntry, bool) {
|
||||||
|
if entry, ok := framework.find(name); ok {
|
||||||
|
return entry, true
|
||||||
|
}
|
||||||
|
return base.find(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseFrameworkName returns the target framework of a runtime-specific section, such as "net8.0" for "net8.0/win-x64".
|
||||||
|
func baseFrameworkName(frameworkName string) string {
|
||||||
|
name, _, _ := strings.Cut(frameworkName, "/")
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
func newDotnetPackagesLockPackage(name string, dep dotnetPackagesLockDep, locations ...file.Location) *pkg.Package {
|
func newDotnetPackagesLockPackage(name string, dep dotnetPackagesLockDep, locations ...file.Location) *pkg.Package {
|
||||||
metadata := pkg.DotnetPackagesLockEntry{
|
metadata := pkg.DotnetPackagesLockEntry{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -150,14 +272,3 @@ func packagesLockPackageURL(name, version string) string {
|
|||||||
"",
|
"",
|
||||||
).ToString()
|
).ToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPkgByName(pkgName string, pkgMap map[string]pkg.Package) (*pkg.Package, bool) {
|
|
||||||
for pkgNameVersion, pkg := range pkgMap {
|
|
||||||
name, _ := extractNameAndVersion(pkgNameVersion)
|
|
||||||
if name == pkgName {
|
|
||||||
return &pkg, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package dotnet
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
@@ -197,3 +200,304 @@ func TestParseDotnetPackagesLock(t *testing.T) {
|
|||||||
|
|
||||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseDotnetPackagesLock_multipleTargetFrameworks(t *testing.T) {
|
||||||
|
fixture := "testdata/packages.lock-multi-framework.json"
|
||||||
|
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||||
|
|
||||||
|
myLibPkg := pkg.Package{
|
||||||
|
Name: "MyLib",
|
||||||
|
Version: "1.0.0",
|
||||||
|
PURL: "pkg:nuget/MyLib@1.0.0",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "MyLib",
|
||||||
|
Version: "1.0.0",
|
||||||
|
ContentHash: "mylibhash==",
|
||||||
|
Type: "Direct",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
log4net2Pkg := pkg.Package{
|
||||||
|
Name: "log4net",
|
||||||
|
Version: "2.0.5",
|
||||||
|
PURL: "pkg:nuget/log4net@2.0.5",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "log4net",
|
||||||
|
Version: "2.0.5",
|
||||||
|
ContentHash: "log4net205hash==",
|
||||||
|
Type: "Transitive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
log4net1Pkg := pkg.Package{
|
||||||
|
Name: "log4net",
|
||||||
|
Version: "1.2.15",
|
||||||
|
PURL: "pkg:nuget/log4net@1.2.15",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "log4net",
|
||||||
|
Version: "1.2.15",
|
||||||
|
ContentHash: "log4net1215hash==",
|
||||||
|
Type: "Transitive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolved to the same version under both frameworks (spelled "newtonsoft.json" under one), but declared "Direct" by
|
||||||
|
// only one of them
|
||||||
|
newtonsoftPkg := pkg.Package{
|
||||||
|
Name: "Newtonsoft.Json",
|
||||||
|
Version: "13.0.3",
|
||||||
|
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "Newtonsoft.Json",
|
||||||
|
Version: "13.0.3",
|
||||||
|
ContentHash: "newtonsoft1303hash==",
|
||||||
|
Type: "Direct",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// only listed under the runtime-specific "net8.0/win-x64" section, and depends on a package from "net8.0"
|
||||||
|
myLibNativePkg := pkg.Package{
|
||||||
|
Name: "runtime.win-x64.MyLib.Native",
|
||||||
|
Version: "1.0.0",
|
||||||
|
PURL: "pkg:nuget/runtime.win-x64.MyLib.Native@1.0.0",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "runtime.win-x64.MyLib.Native",
|
||||||
|
Version: "1.0.0",
|
||||||
|
ContentHash: "mylibnativehash==",
|
||||||
|
Type: "Transitive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// only listed under "net8.0", so the "netstandard2.0" edge to it must be dropped rather than guessed
|
||||||
|
serilogPkg := pkg.Package{
|
||||||
|
Name: "Serilog",
|
||||||
|
Version: "3.1.1",
|
||||||
|
PURL: "pkg:nuget/Serilog@3.1.1",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "Serilog",
|
||||||
|
Version: "3.1.1",
|
||||||
|
ContentHash: "serilog311hash==",
|
||||||
|
Type: "Transitive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPkgs := []pkg.Package{
|
||||||
|
myLibPkg,
|
||||||
|
newtonsoftPkg,
|
||||||
|
log4net1Pkg,
|
||||||
|
log4net2Pkg,
|
||||||
|
myLibNativePkg,
|
||||||
|
serilogPkg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// the same package is resolved to a different version per target framework, so both edges must be captured, while
|
||||||
|
// the Newtonsoft.Json edge declared by both frameworks must appear only once
|
||||||
|
expectedRelationships := []artifact.Relationship{
|
||||||
|
{
|
||||||
|
From: log4net1Pkg,
|
||||||
|
To: myLibPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: log4net2Pkg,
|
||||||
|
To: myLibPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: log4net2Pkg,
|
||||||
|
To: myLibNativePkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: newtonsoftPkg,
|
||||||
|
To: myLibPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDotnetPackagesLock_projectReference(t *testing.T) {
|
||||||
|
fixture := "testdata/packages.lock-project-reference.json"
|
||||||
|
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||||
|
|
||||||
|
newtonsoftPkg := pkg.Package{
|
||||||
|
Name: "Newtonsoft.Json",
|
||||||
|
Version: "13.0.3",
|
||||||
|
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "Newtonsoft.Json",
|
||||||
|
Version: "13.0.3",
|
||||||
|
ContentHash: "newtonsoft1303hash==",
|
||||||
|
Type: "Transitive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// a project reference has no resolved version or content hash
|
||||||
|
myLibPkg := pkg.Package{
|
||||||
|
Name: "mylib",
|
||||||
|
PURL: "pkg:nuget/mylib",
|
||||||
|
Locations: fixtureLocationSet,
|
||||||
|
Language: pkg.Dotnet,
|
||||||
|
Type: pkg.DotnetPkg,
|
||||||
|
Metadata: pkg.DotnetPackagesLockEntry{
|
||||||
|
Name: "mylib",
|
||||||
|
Type: "Project",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPkgs := []pkg.Package{
|
||||||
|
newtonsoftPkg,
|
||||||
|
myLibPkg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// project references declare their dependencies as version ranges, which resolve to the version the target
|
||||||
|
// framework resolved
|
||||||
|
expectedRelationships := []artifact.Relationship{
|
||||||
|
{
|
||||||
|
From: newtonsoftPkg,
|
||||||
|
To: myLibPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_findPackagesLockDependency(t *testing.T) {
|
||||||
|
frameworks := newPackagesLockFrameworks(dotnetPackagesLock{
|
||||||
|
Dependencies: map[string]map[string]dotnetPackagesLockDep{
|
||||||
|
"net8.0": {
|
||||||
|
"log4net": {Resolved: "2.0.5"},
|
||||||
|
},
|
||||||
|
"net8.0/win-x64": {
|
||||||
|
"runtime.win-x64.Native": {Resolved: "1.0.0"},
|
||||||
|
},
|
||||||
|
"netstandard2.0": {
|
||||||
|
"log4net": {Resolved: "1.2.15"},
|
||||||
|
"Newtonsoft.Json": {Resolved: "13.0.3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
net8, rid, netstandard := frameworks[0], frameworks[1], frameworks[2]
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
depName string
|
||||||
|
framework packagesLockFramework
|
||||||
|
base packagesLockFramework
|
||||||
|
wantVersion string
|
||||||
|
wantFound bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "resolves to the version pinned by its own target framework",
|
||||||
|
depName: "log4net",
|
||||||
|
framework: netstandard,
|
||||||
|
wantVersion: "1.2.15",
|
||||||
|
wantFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package names are matched case-insensitively",
|
||||||
|
depName: "LOG4NET",
|
||||||
|
framework: net8,
|
||||||
|
wantVersion: "2.0.5",
|
||||||
|
wantFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "runtime-specific sections fall back to their base target framework",
|
||||||
|
depName: "log4net",
|
||||||
|
framework: rid,
|
||||||
|
base: net8,
|
||||||
|
wantVersion: "2.0.5",
|
||||||
|
wantFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a package absent from the target framework is not guessed from another one",
|
||||||
|
depName: "Newtonsoft.Json",
|
||||||
|
framework: net8,
|
||||||
|
wantFound: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, found := findPackagesLockDependency(tt.depName, tt.framework, tt.base)
|
||||||
|
require.Equal(t, tt.wantFound, found)
|
||||||
|
if !tt.wantFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.wantVersion, got.dep.Resolved)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mergePackagesLockEntries_typePrecedence(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
types map[string]string // target framework -> type
|
||||||
|
wantType string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "direct wins over transitive",
|
||||||
|
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "Direct"},
|
||||||
|
wantType: "Direct",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "central transitive wins over transitive regardless of framework order",
|
||||||
|
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "CentralTransitive"},
|
||||||
|
wantType: "CentralTransitive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "central transitive wins over transitive in the reverse framework order",
|
||||||
|
types: map[string]string{"net8.0": "CentralTransitive", "netstandard2.0": "Transitive"},
|
||||||
|
wantType: "CentralTransitive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct wins over central transitive",
|
||||||
|
types: map[string]string{"net472": "CentralTransitive", "net8.0": "Direct", "netstandard2.0": "Transitive"},
|
||||||
|
wantType: "Direct",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "known types win over unknown ones",
|
||||||
|
types: map[string]string{"net8.0": "Unknown", "netstandard2.0": "Transitive"},
|
||||||
|
wantType: "Transitive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
lockFile := dotnetPackagesLock{Dependencies: map[string]map[string]dotnetPackagesLockDep{}}
|
||||||
|
for framework, depType := range tt.types {
|
||||||
|
lockFile.Dependencies[framework] = map[string]dotnetPackagesLockDep{
|
||||||
|
"Newtonsoft.Json": {Type: depType, Resolved: "13.0.3"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := mergePackagesLockEntries(newPackagesLockFrameworks(lockFile))
|
||||||
|
require.Len(t, entries, 1)
|
||||||
|
assert.Equal(t, tt.wantType, entries[0].dep.Type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"net8.0": {
|
||||||
|
"MyLib": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[1.0.0, )",
|
||||||
|
"resolved": "1.0.0",
|
||||||
|
"contentHash": "mylibhash==",
|
||||||
|
"dependencies": {
|
||||||
|
"log4net": "2.0.5",
|
||||||
|
"Newtonsoft.Json": "13.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log4net": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.0.5",
|
||||||
|
"contentHash": "log4net205hash=="
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[13.0.3, )",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "newtonsoft1303hash=="
|
||||||
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.1.1",
|
||||||
|
"contentHash": "serilog311hash=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net8.0/win-x64": {
|
||||||
|
"runtime.win-x64.MyLib.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.0.0",
|
||||||
|
"contentHash": "mylibnativehash==",
|
||||||
|
"dependencies": {
|
||||||
|
"log4net": "2.0.0",
|
||||||
|
"RUNTIME.WIN-X64.MYLIB.NATIVE": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"netstandard2.0": {
|
||||||
|
"MyLib": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[1.0.0, )",
|
||||||
|
"resolved": "1.0.0",
|
||||||
|
"contentHash": "mylibhash==",
|
||||||
|
"dependencies": {
|
||||||
|
"log4net": "1.2.15",
|
||||||
|
"newtonsoft.json": "13.0.3",
|
||||||
|
"Serilog": "3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log4net": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.2.15",
|
||||||
|
"contentHash": "log4net1215hash=="
|
||||||
|
},
|
||||||
|
"newtonsoft.json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "newtonsoft1303hash=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"net8.0": {
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "newtonsoft1303hash=="
|
||||||
|
},
|
||||||
|
"mylib": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Newtonsoft.Json": "[13.0.3, )"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user