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 }