github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/dotnet/deps_json.go (about)

     1  package dotnet
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"regexp"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/scylladb/go-set/strset"
    11  
    12  	"github.com/anchore/syft/syft/file"
    13  	"github.com/anchore/syft/syft/pkg"
    14  )
    15  
    16  type depsJSON struct {
    17  	Location      file.Location
    18  	RuntimeTarget runtimeTarget                    `json:"runtimeTarget"`
    19  	Targets       map[string]map[string]depsTarget `json:"targets"`
    20  	Libraries     map[string]depsLibrary           `json:"libraries"`
    21  }
    22  
    23  type runtimeTarget struct {
    24  	Name string `json:"name"`
    25  }
    26  
    27  type depsTarget struct {
    28  	Dependencies map[string]string            `json:"dependencies"`
    29  	Runtime      map[string]map[string]string `json:"runtime"`
    30  	Resources    map[string]map[string]string `json:"resources"`
    31  	Compile      map[string]map[string]string `json:"compile"`
    32  	Native       map[string]map[string]string `json:"native"`
    33  }
    34  
    35  func (t depsTarget) nativePaths() *strset.Set {
    36  	results := strset.New()
    37  	for path := range t.Native {
    38  		results.Add(path)
    39  	}
    40  	return results
    41  }
    42  
    43  func (t depsTarget) compilePaths() map[string]string {
    44  	result := make(map[string]string)
    45  	for path := range t.Compile {
    46  		trimmedPath := trimLibPrefix(path)
    47  		if _, exists := result[trimmedPath]; exists {
    48  			continue
    49  		}
    50  		result[trimmedPath] = path
    51  	}
    52  	return result
    53  }
    54  
    55  func (t depsTarget) resourcePaths() map[string]string {
    56  	result := make(map[string]string)
    57  	for path := range t.Resources {
    58  		trimmedPath := trimLibPrefix(path)
    59  		if _, exists := result[trimmedPath]; exists {
    60  			continue
    61  		}
    62  		result[trimmedPath] = path
    63  	}
    64  	return result
    65  }
    66  
    67  func (t depsTarget) runtimePaths() map[string]string {
    68  	result := make(map[string]string)
    69  	for path := range t.Runtime {
    70  		trimmedPath := trimLibPrefix(path)
    71  		if _, exists := result[trimmedPath]; exists {
    72  			continue
    73  		}
    74  		result[trimmedPath] = path
    75  	}
    76  	return result
    77  }
    78  
    79  type depsLibrary struct {
    80  	Type     string `json:"type"`
    81  	Path     string `json:"path"`
    82  	Sha512   string `json:"sha512"`
    83  	HashPath string `json:"hashPath"`
    84  }
    85  
    86  // logicalDepsJSONPackage merges target and library information for a given package from all dep.json entries.
    87  // Note: this is not a real construct of the deps.json, just a useful reorganization of the data for downstream processing.
    88  type logicalDepsJSONPackage struct {
    89  	NameVersion string
    90  	Targets     []depsTarget
    91  	Library     *depsLibrary
    92  
    93  	// AnyChildClaimsDLLs is a flag that indicates if any of the children of this package claim a DLL associated with them in the deps.json.
    94  	AnyChildClaimsDLLs bool
    95  
    96  	// AnyChildHasDLLs is a flag that indicates if any of the children of this package have a DLL associated with them (found on disk).
    97  	AnyChildHasDLLs bool
    98  
    99  	// RuntimePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
   100  	// to the target path as described in the deps.json target entry under "runtime".
   101  	RuntimePathsByRelativeDLLPath map[string]string
   102  
   103  	// ResourcePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
   104  	// to the target path as described in the deps.json target entry under "resource".
   105  	ResourcePathsByRelativeDLLPath map[string]string
   106  
   107  	// CompilePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
   108  	// to the target path as described in the deps.json target entry under "compile".
   109  	CompilePathsByRelativeDLLPath map[string]string
   110  
   111  	// NativePaths is a map of the relative path to the DLL relative to the deps.json file
   112  	// to the target path as described in the deps.json target entry under "native". These should not have
   113  	// any runtime references to trim from the front of the path.
   114  	NativePaths *strset.Set
   115  
   116  	// Executables is a list of all the executables that are part of this package. This is populated by the PE cataloger
   117  	// and not something that is found in the deps.json file. This allows us to associate the PE files with this package
   118  	// based on the relative path to the DLL.
   119  	Executables []logicalPE
   120  }
   121  
   122  func (l *logicalDepsJSONPackage) dependencyNameVersions() []string {
   123  	if l.Targets == nil {
   124  		return nil
   125  	}
   126  	results := strset.New()
   127  	for _, t := range l.Targets {
   128  		for name, version := range t.Dependencies {
   129  			results.Add(createNameAndVersion(name, version))
   130  		}
   131  	}
   132  	r := results.List()
   133  	sort.Strings(r)
   134  	return r
   135  }
   136  
   137  // ClaimsDLLs indicates if this package has any DLLs associated with it (directly or indirectly with a dependency).
   138  func (l *logicalDepsJSONPackage) ClaimsDLLs(includeChildren bool) bool {
   139  	selfClaim := len(l.RuntimePathsByRelativeDLLPath) > 0 || len(l.ResourcePathsByRelativeDLLPath) > 0 || len(l.CompilePathsByRelativeDLLPath) > 0 || len(l.NativePaths.List()) > 0
   140  	if !includeChildren {
   141  		return selfClaim
   142  	}
   143  	return selfClaim || l.AnyChildClaimsDLLs
   144  }
   145  
   146  func (l *logicalDepsJSONPackage) FoundDLLs(includeChildren bool) bool {
   147  	selfClaim := len(l.Executables) > 0
   148  	if !includeChildren {
   149  		return selfClaim
   150  	}
   151  	return selfClaim || l.AnyChildHasDLLs
   152  }
   153  
   154  type logicalDepsJSON struct {
   155  	Location              file.Location
   156  	RuntimeTarget         runtimeTarget
   157  	PackagesByNameVersion map[string]logicalDepsJSONPackage
   158  	PackageNameVersions   *strset.Set
   159  	BundlingDetected      bool
   160  	LibmanPackages        []pkg.Package
   161  }
   162  
   163  func (l logicalDepsJSON) RootPackage() (logicalDepsJSONPackage, bool) {
   164  	rootName := getDepsJSONFilePrefix(l.Location.RealPath)
   165  	if rootName == "" {
   166  		return logicalDepsJSONPackage{}, false
   167  	}
   168  
   169  	// iterate over the map to find the root package. If we don't find the root package, that's ok! We still want to
   170  	// get all of the packages that are defined in this deps.json file.
   171  	for _, p := range l.PackagesByNameVersion {
   172  		name, _ := extractNameAndVersion(p.NameVersion)
   173  		// there can be multiple projects defined in a deps.json and only by convention is the root project the same name as the deps.json file
   174  		// however there are other configurations that can lead to differences (e.g. "tool_fsc" vs "fsc.deps.json").
   175  		if p.Library != nil && p.Library.Type == "project" && name == rootName {
   176  			return p, true
   177  		}
   178  	}
   179  	return logicalDepsJSONPackage{}, false
   180  }
   181  
   182  func newDepsJSON(reader file.LocationReadCloser) (*depsJSON, error) {
   183  	var doc depsJSON
   184  	dec := json.NewDecoder(reader)
   185  	if err := dec.Decode(&doc); err != nil {
   186  		return nil, fmt.Errorf("failed to parse deps.json file: %w", err)
   187  	}
   188  	doc.Location = reader.Location
   189  	return &doc, nil
   190  }
   191  
   192  var knownBundlers = strset.New(
   193  	"ILRepack.Lib.MSBuild.Task", // The most official use of ILRepack https://github.com/gluck/il-repack
   194  	"ILRepack.Lib",              // library interface for ILRepack
   195  	"ILRepack.Lib.MSBuild",      // uses Cecil 0.10
   196  	"ILRepack.Lib.NET",          // uses ModuleDefinitions instead of filenames
   197  	"ILRepack.NETStandard",      // .NET Standard compatible version
   198  	"ILRepack.FullAuto",         // https://github.com/kekyo/ILRepack.FullAuto
   199  	"ILMerge",                   // deprecated, but still used in some projects https://github.com/dotnet/ILMerge
   200  	"JetBrains.Build.ILRepack",  // generally from https://www.nuget.org/packages?q=ilrepack&sortBy=relevance
   201  
   202  	// other bundling/modification tools found in results
   203  	"PostSharp.Community.Packer", // Embeds dependencies as resources
   204  	"Brokenevent.ILStrip",        // assembly cleaner (removes unused parts)
   205  	"Brokenevent.ILStrip.CLI",    // command-line/MSBuild variant
   206  	"Costura.Fody",               // referenced in MSBuildRazorCompiler.Lib
   207  	"Fody",                       // IL weaving framework
   208  )
   209  
   210  func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
   211  	packageMap := make(map[string]*logicalDepsJSONPackage)
   212  	nameVersions := strset.New()
   213  
   214  	for _, targets := range deps.Targets {
   215  		for libName, target := range targets {
   216  			_, exists := packageMap[libName]
   217  			if exists {
   218  				// merge this with existing targets (multiple targets can exist for the same library)
   219  				p := packageMap[libName]
   220  				p.Targets = append(p.Targets, target)
   221  				p.RuntimePathsByRelativeDLLPath = mergeMaps(p.RuntimePathsByRelativeDLLPath, target.runtimePaths())
   222  				p.ResourcePathsByRelativeDLLPath = mergeMaps(p.ResourcePathsByRelativeDLLPath, target.resourcePaths())
   223  				p.CompilePathsByRelativeDLLPath = mergeMaps(p.CompilePathsByRelativeDLLPath, target.compilePaths())
   224  				p.NativePaths = mergeSets(p.NativePaths, target.nativePaths())
   225  
   226  				continue
   227  			}
   228  
   229  			var lib *depsLibrary
   230  			l, ok := deps.Libraries[libName]
   231  			if ok {
   232  				lib = &l
   233  			}
   234  
   235  			p := &logicalDepsJSONPackage{
   236  				NameVersion:                    libName,
   237  				Library:                        lib,
   238  				Targets:                        []depsTarget{target},
   239  				RuntimePathsByRelativeDLLPath:  target.runtimePaths(),
   240  				ResourcePathsByRelativeDLLPath: target.resourcePaths(),
   241  				CompilePathsByRelativeDLLPath:  target.compilePaths(),
   242  				NativePaths:                    target.nativePaths(),
   243  			}
   244  			packageMap[libName] = p
   245  			nameVersions.Add(libName)
   246  		}
   247  	}
   248  	packages := make(map[string]logicalDepsJSONPackage)
   249  	var bundlingDetected bool
   250  	for _, p := range packageMap {
   251  		name := strings.Split(p.NameVersion, "/")[0]
   252  		if !bundlingDetected && knownBundlers.Has(name) {
   253  			bundlingDetected = true
   254  		}
   255  		p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, strset.New(), p.dependencyNameVersions()...)
   256  		p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, strset.New(), p.dependencyNameVersions()...)
   257  		packages[p.NameVersion] = *p
   258  	}
   259  
   260  	return logicalDepsJSON{
   261  		Location:              deps.Location,
   262  		RuntimeTarget:         deps.RuntimeTarget,
   263  		PackagesByNameVersion: packages,
   264  		PackageNameVersions:   nameVersions,
   265  		BundlingDetected:      bundlingDetected,
   266  		LibmanPackages:        lm.packages(),
   267  	}
   268  }
   269  
   270  func mergeMaps(m1, m2 map[string]string) map[string]string {
   271  	if m1 == nil {
   272  		m1 = make(map[string]string)
   273  	}
   274  	for k, v := range m2 {
   275  		if _, exists := m1[k]; !exists {
   276  			m1[k] = v
   277  		}
   278  	}
   279  	return m1
   280  }
   281  
   282  func mergeSets(s1, s2 *strset.Set) *strset.Set {
   283  	return strset.Union(s1, s2)
   284  }
   285  
   286  type visitorFunc func(p *logicalDepsJSONPackage) bool
   287  
   288  // searchForDLLEvidence recursively searches for executables found for any of the given nameVersions and children recursively.
   289  func searchForDLLEvidence(packageMap map[string]*logicalDepsJSONPackage, visited *strset.Set, nameVersions ...string) bool {
   290  	return traverseDependencies(packageMap, func(p *logicalDepsJSONPackage) bool {
   291  		return p.FoundDLLs(true)
   292  	}, visited, nameVersions...)
   293  }
   294  
   295  // searchForDLLClaims recursively searches for DLL claims in the deps.json for any of the given nameVersions and children recursively.
   296  func searchForDLLClaims(packageMap map[string]*logicalDepsJSONPackage, visited *strset.Set, nameVersions ...string) bool {
   297  	return traverseDependencies(packageMap, func(p *logicalDepsJSONPackage) bool {
   298  		return p.ClaimsDLLs(true)
   299  	}, visited, nameVersions...)
   300  }
   301  
   302  func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor visitorFunc, visited *strset.Set, nameVersions ...string) bool {
   303  	if len(nameVersions) == 0 {
   304  		return false
   305  	}
   306  
   307  	for _, nameVersion := range nameVersions {
   308  		if visited.Has(nameVersion) {
   309  			continue
   310  		}
   311  		visited.Add(nameVersion)
   312  		if p, ok := packageMap[nameVersion]; ok {
   313  			if visitor(p) {
   314  				return true
   315  			}
   316  
   317  			if traverseDependencies(packageMap, visitor, visited, p.dependencyNameVersions()...) {
   318  				return true
   319  			}
   320  		}
   321  	}
   322  
   323  	return false
   324  }
   325  
   326  var libPathPattern = regexp.MustCompile(`^(?:runtimes/[^/]+/)?lib/net[^/]+/(?P<targetPath>.+)`)
   327  
   328  // trimLibPrefix removes prefixes like "lib/net6.0/" or "runtimes/linux-arm/lib/netcoreapp2.2/" from a path.
   329  // It captures and returns everything after the framework version section using a named capture group.
   330  func trimLibPrefix(s string) string {
   331  	if match := libPathPattern.FindStringSubmatch(s); len(match) > 1 {
   332  		// Get the index of the named capture group
   333  		targetPathIndex := libPathPattern.SubexpIndex("targetPath")
   334  		if targetPathIndex != -1 {
   335  			return match[targetPathIndex]
   336  		}
   337  	}
   338  	return s
   339  }