syft/syft/pkg/cataloger/dotnet/parse_packages_lock.go
Alex Goodman ad9928cb2a
Merge the .NET deps.json and PE binary catalogers (#3563)
* add combined deps.json + pe binary cataloger

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

* deprecate pe and deps standalone catalogers

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

* parse resource names + add tests

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

* fix integration and CLI tests

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

* add some helpful code comments

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

* allow for dropping Dep packages that are missing DLLs

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

* migrate json schema changes to 24

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

* keep application configuration

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

* correct config help

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

* [wip] detect claims of dlls within deps.json

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

* [wip] fix tests

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

* add assembly repack detection

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

* .net package count is lower due to dll claim requirement

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-03-27 14:38:16 -04:00

164 lines
4.5 KiB
Go

package dotnet
import (
"context"
"encoding/json"
"fmt"
"slices"
"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) { //nolint:funlen
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 {
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
for _, nameVersion := range names {
name, _ := extractNameAndVersion(nameVersion)
dep := allDependencies[nameVersion]
dotnetPkg := newDotnetPackagesLockPackage(name, dep, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
if dotnetPkg != nil {
pkgs = append(pkgs, *dotnetPkg)
pkgMap[nameVersion] = *dotnetPkg
}
}
// fill up relationships
for depNameVersion, dep := range allDependencies {
parentPkg, ok := pkgMap[depNameVersion]
if !ok {
log.Debug("package \"%s\" not found in map of all pacakges", 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.Debug("dependency \"%s\" of package \"%s\" not found in map of all packages", childDepNameVersion, depNameVersion)
continue
}
childPkg = *cpkg
}
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,
}
p := &pkg.Package{
Name: name,
Version: dep.Resolved,
Type: pkg.DotnetPkg,
Metadata: metadata,
Locations: file.NewLocationSet(locations...),
Language: pkg.Dotnet,
PURL: packagesLockPackageURL(name, dep.Resolved),
}
p.SetID()
return p
}
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()
}
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
}