@socketsecurity/cli-with-sentry 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +5 -5
  3. package/dist/cli.js +255 -128
  4. package/dist/cli.js.map +1 -1
  5. package/dist/constants.js +4 -4
  6. package/dist/constants.js.map +1 -1
  7. package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
  8. package/dist/manifest-scripts/socket-facts.init.gradle +241 -54
  9. package/dist/manifest-scripts/socket-facts.plugin.scala +224 -38
  10. package/dist/tsconfig.dts.tsbuildinfo +1 -1
  11. package/dist/types/commands/ci/handle-ci.d.mts.map +1 -1
  12. package/dist/types/commands/fix/handle-fix.d.mts.map +1 -1
  13. package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +2 -1
  14. package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -1
  15. package/dist/types/commands/manifest/convert-maven-to-facts.d.mts +2 -1
  16. package/dist/types/commands/manifest/convert-maven-to-facts.d.mts.map +1 -1
  17. package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts +2 -1
  18. package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -1
  19. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts.map +1 -1
  20. package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
  21. package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -1
  22. package/dist/types/commands/manifest/scripts/assemble.d.mts.map +1 -1
  23. package/dist/types/commands/manifest/scripts/facts.d.mts +3 -2
  24. package/dist/types/commands/manifest/scripts/facts.d.mts.map +1 -1
  25. package/dist/types/commands/manifest/scripts/records.d.mts +7 -1
  26. package/dist/types/commands/manifest/scripts/records.d.mts.map +1 -1
  27. package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts +3 -2
  28. package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts.map +1 -1
  29. package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts.map +1 -1
  30. package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts +9 -4
  31. package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts.map +1 -1
  32. package/dist/types/commands/manifest/scripts/resolution-report.d.mts +7 -2
  33. package/dist/types/commands/manifest/scripts/resolution-report.d.mts.map +1 -1
  34. package/dist/types/commands/manifest/scripts/sidecar.d.mts.map +1 -1
  35. package/dist/types/commands/scan/cmd-scan-create.d.mts.map +1 -1
  36. package/dist/types/commands/scan/cmd-scan-reach.d.mts.map +1 -1
  37. package/dist/types/commands/scan/handle-create-new-scan.d.mts.map +1 -1
  38. package/dist/types/commands/scan/perform-reachability-analysis.d.mts +2 -0
  39. package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
  40. package/dist/types/commands/scan/reach-advisories.d.mts +7 -0
  41. package/dist/types/commands/scan/reach-advisories.d.mts.map +1 -0
  42. package/dist/types/commands/scan/reachability-flags.d.mts.map +1 -1
  43. package/dist/types/constants.d.mts +1 -1
  44. package/dist/types/constants.d.mts.map +1 -1
  45. package/dist/types/utils/advisory-ids.d.mts +14 -0
  46. package/dist/types/utils/advisory-ids.d.mts.map +1 -0
  47. package/dist/types/utils/cve-to-ghsa.d.mts +2 -0
  48. package/dist/types/utils/cve-to-ghsa.d.mts.map +1 -1
  49. package/dist/utils.js +69 -3
  50. package/dist/utils.js.map +1 -1
  51. package/package.json +2 -2
@@ -53,7 +53,6 @@ gradle.ext.socketFactsState = [
53
53
  // Whole configs whose resolution threw (vs a single unresolved dep in `failures`).
54
54
  unscannable : Collections.synchronizedList([]),
55
55
  scannedConfigs : Collections.synchronizedSet([] as Set),
56
- projectKeys : Collections.synchronizedSet([] as Set),
57
56
  // Only populated with `-Psocket.withFiles=true`: reading `artifact.file` downloads the artifact.
58
57
  paths : Collections.synchronizedMap([:]),
59
58
  perSub : Collections.synchronizedMap([:]),
@@ -63,10 +62,12 @@ gradle.ext.socketFactsState = [
63
62
  projectArtifactExt : Collections.synchronizedMap([:]),
64
63
  // Project path -> "group:name", to tell this build's projects from an included build's.
65
64
  projectGaByPath : Collections.synchronizedMap([:]),
65
+ // Project path -> "group:name:version", the coordinate a failed project dependency is reported by.
66
+ projectGavByPath : Collections.synchronizedMap([:]),
66
67
  ]
67
68
 
68
- // Capture every project's (group:name) before collectors run so they can filter intra-project
69
- // deps without an ordering dependency on other subprojects.
69
+ // Capture every project's identity before collectors run so they can recognize intra-project deps
70
+ // without an ordering dependency on other subprojects.
70
71
  gradle.projectsEvaluated { g ->
71
72
  def rootPath = g.rootProject.projectDir.toPath()
72
73
  // '/'-normalized so values are stable across OSes and portable to another machine.
@@ -75,7 +76,6 @@ gradle.projectsEvaluated { g ->
75
76
  r.isEmpty() ? '.' : r
76
77
  }
77
78
  g.rootProject.allprojects.each { p ->
78
- g.socketFactsState.projectKeys.add("${p.group ?: ''}:${p.name}")
79
79
  // The module's real artifact extension (jar for java; '' for a project that builds no archive,
80
80
  // e.g. a pure aggregator). Used to stamp intra-project deps that resolve without an artifact.
81
81
  def artExt = ''
@@ -90,12 +90,11 @@ gradle.projectsEvaluated { g ->
90
90
  } catch (Exception ignore) {}
91
91
  g.socketFactsState.projectArtifactExt[p.path] = artExt
92
92
  g.socketFactsState.projectGaByPath[p.path] = "${p.group ?: ''}:${p.name}".toString()
93
+ g.socketFactsState.projectGavByPath[p.path] = "${p.group ?: ''}:${p.name}:${p.version ?: ''}".toString()
93
94
  // A wholly excluded subproject emits no project record and its configs are never resolved (see the
94
- // collector). projectKeys/projectArtifactExt above stay populated so a KEPT project depending on it
95
- // still recognizes it as a first-party module.
96
- if (g.ext.socketIsExcluded.call(rel(p.projectDir))) {
97
- return
98
- }
95
+ // collector). The project maps above stay populated so a KEPT project depending on it
96
+ // still recognizes it as a first-party module, and its paths are kept so it is analysed with its code.
97
+ def excluded = g.ext.socketIsExcluded.call(rel(p.projectDir))
99
98
  // Guarded: projects without the java `sourceSets` convention (pure aggregators, AGP-only
100
99
  // Android modules) contribute empty lists rather than failing.
101
100
  // ALL source sets are emitted (not just main/test) so custom ones (integrationTest,
@@ -133,6 +132,7 @@ gradle.projectsEvaluated { g ->
133
132
  sources: (sources as List).sort(),
134
133
  targets: (targets as List).sort(),
135
134
  buildFiles: buildFiles,
135
+ excluded: excluded,
136
136
  ])
137
137
  }
138
138
  }
@@ -157,13 +157,15 @@ allprojects { project ->
157
157
  // visit/upsertNode closures capture them); `failures`/`scannedConfigs` stay shared.
158
158
  def nodes = [:]
159
159
  def directIds = [] as Set
160
+ def configName = ''
160
161
  // "group:name:version" -> path of the build project this config resolved it to.
161
162
  def projectByGav = [:]
162
163
  def failures = state.failures
163
164
  def scannedConfigs = state.scannedConfigs
164
- def projectKeys = state.projectKeys
165
165
  def projectArtifactExt = state.projectArtifactExt
166
166
  def paths = [:]
167
+ // Ids of nodes whose requested artifact yielded no file.
168
+ def unfetched = [] as Set
167
169
  def withFiles = gradle.socketProp.call(project, 'socket.withFiles')?.toString()?.toLowerCase() == 'true'
168
170
  // Optional `-Psocket.populateFilesFor=<file>` (newline-delimited GAVs): scope `--with-files`
169
171
  // materialization to those GAVs. This run walks EVERY resolvable config (not told the SBOM's
@@ -203,10 +205,6 @@ allprojects { project ->
203
205
  [coord.groupId, coord.artifactId, coord.ext, coord.classifier, coord.version].findAll { it }.join(':')
204
206
  }
205
207
 
206
- def isIntraProject = { String group, String name ->
207
- projectKeys.contains("${group ?: ''}:${name}")
208
- }
209
-
210
208
  // A first-party module dep can resolve with no published artifact (classes-dir variant, or
211
209
  // variant-ambiguous moduleArtifacts), leaving it ext-less. Stamp the module's real artifact
212
210
  // extension so it keeps a full, stable coordinate instead of an ext-less one.
@@ -234,6 +232,24 @@ allprojects { project ->
234
232
  id
235
233
  }
236
234
 
235
+ def causeMessages = { problem ->
236
+ def out = []
237
+ def t = problem
238
+ int guard = 0
239
+ while (t != null && guard++ < 12) { out << t.message; t = t.cause }
240
+ out
241
+ }
242
+
243
+ // The deepest cause's full message: the TS report classifies it, so nothing is categorized here.
244
+ def rawDetail = { problem ->
245
+ def messages = causeMessages(problem).findAll { it }
246
+ return (messages ? messages[-1] : 'unknown resolution failure').toString().trim()
247
+ }
248
+
249
+ def recordFailure = { String coord, String detail ->
250
+ failures << [coord: coord, detail: detail, config: configName, project: project.path]
251
+ }
252
+
237
253
  // Walk a resolved dependency and its transitive closure. We never touch `artifact.file` here —
238
254
  // that forces Gradle to download the file (catastrophic on builds declaring distribution
239
255
  // archives as deps); `artifact.extension`/`artifact.classifier` read already-resolved metadata.
@@ -251,15 +267,23 @@ allprojects { project ->
251
267
 
252
268
  // `moduleArtifacts` forces ARTIFACT-level variant selection, which can throw on a
253
269
  // variant-ambiguous dep even when the dependency GRAPH resolves fine. Guard per-dep: on
254
- // failure emit at GAV level and keep walking so one ambiguous artifact can't sink the config.
270
+ // failure keep walking so one ambiguous artifact can't sink the config.
271
+ def gav = "${dep.moduleGroup ?: ''}:${dep.moduleName}:${dep.moduleVersion ?: ''}".toString()
272
+ def shouldMaterialize = withFiles && (populateGavs == null || populateGavs.contains(gav))
273
+ def projPath = projectByGav[gav]
255
274
  def artifacts
275
+ def artifactsError = null
256
276
  try {
257
277
  artifacts = dep.moduleArtifacts
258
278
  } catch (Exception e) {
259
279
  artifacts = [] as Set
280
+ artifactsError = e
260
281
  }
261
- def projPath = projectByGav["${dep.moduleGroup ?: ''}:${dep.moduleName}:${dep.moduleVersion ?: ''}".toString()]
262
- if (artifacts.isEmpty()) {
282
+ if (artifactsError != null && projPath == null) {
283
+ // No artifact was selected, so there is no coordinate to emit: report the module and
284
+ // bubble its children up (below), as for a skipped first-party module.
285
+ recordFailure(gav, rawDetail(artifactsError))
286
+ } else if (artifacts.isEmpty()) {
263
287
  def ext = effExt(projPath, '')
264
288
  // Skip a no-artifact first-party module (aggregator / build root): it builds no archive, so
265
289
  // it's fully described by `projects` and is never a real artifact dependency.
@@ -273,17 +297,6 @@ allprojects { project ->
273
297
  ], isProd, projPath)
274
298
  }
275
299
  } else {
276
- // Build the GAV scope key only when a scope is set: the no-scope path never reads it and
277
- // this is the hottest line under --parallel.
278
- def shouldMaterialize
279
- if (!withFiles) {
280
- shouldMaterialize = false
281
- } else if (populateGavs == null) {
282
- shouldMaterialize = true
283
- } else {
284
- def gav = "${dep.moduleGroup ?: ''}:${dep.moduleName}:${dep.moduleVersion ?: ''}".toString()
285
- shouldMaterialize = populateGavs.contains(gav)
286
- }
287
300
  artifacts.each { a ->
288
301
  // Directory variants (java-classes-directory etc.) carry no extension; for a first-party
289
302
  // module fall back to its real artifact ext (never artifact.type, a Gradle variant attr).
@@ -310,7 +323,13 @@ allprojects { project ->
310
323
  set << path
311
324
  }
312
325
  } catch (Exception e) {
313
- println "[socket-facts] could not materialize ${aid}: ${e.message?.readLines()?.first()}"
326
+ println "[socket-facts] could not materialize ${aid}: ${rawDetail(e).readLines().find()}"
327
+ // A requested artifact that yields no file is a failure: without one, the dependency
328
+ // would read as resolved with nothing to analyse.
329
+ if (projPath == null) {
330
+ recordFailure(gav, rawDetail(e))
331
+ unfetched << aid
332
+ }
314
333
  }
315
334
  }
316
335
  }
@@ -320,9 +339,9 @@ allprojects { project ->
320
339
  dep.children.each { child ->
321
340
  childIds.addAll(visit(child, isProd, cache))
322
341
  }
323
- // A skipped no-artifact first-party module produced no node, so bubble its resolved children
324
- // up to the parent (return childIds) — otherwise the module's transitives orphan (attached to
325
- // nothing, not bubbled). The module itself is described by `projects`.
342
+ // A skipped no-artifact first-party module, or a module whose artifacts could not be
343
+ // selected, produced no node, so bubble its resolved children up to the parent (return
344
+ // childIds); otherwise the module's transitives would be attached to nothing.
326
345
  if (producedIds.isEmpty()) {
327
346
  cache[dep] = childIds
328
347
  return childIds
@@ -408,13 +427,168 @@ allprojects { project ->
408
427
  return true
409
428
  }
410
429
 
411
- // Deepest cause's message, in FULL — Coana classifies/templates it, so we don't categorize here.
412
- def rawDetail = { problem ->
413
- def deepest = null
414
- def t = problem
415
- int guard = 0
416
- while (t != null && guard++ < 12) { if (t.message) { deepest = t.message }; t = t.cause }
417
- return (deepest ?: 'unknown resolution failure').toString().trim()
430
+ // A failed edge is reported by the selector Gradle ATTEMPTED (after forces and substitutions).
431
+ // A project of this build is named by its own coordinate, as its resolved node would be.
432
+ def selectorCoord = { sel ->
433
+ if (sel instanceof org.gradle.api.artifacts.component.ModuleComponentSelector) {
434
+ return "${sel.group}:${sel.module}${sel.version ? ':' + sel.version : ''}".toString()
435
+ }
436
+ if (sel instanceof org.gradle.api.artifacts.component.ProjectComponentSelector) {
437
+ def build = sel.metaClass.respondsTo(sel, 'getBuildPath') ? sel.buildPath :
438
+ sel.metaClass.respondsTo(sel, 'getBuildName') ? sel.buildName : ':'
439
+ def gav = build == ':' ? state.projectGavByPath[sel.projectPath] : null
440
+ if (gav != null) {
441
+ return gav
442
+ }
443
+ }
444
+ sel.displayName.toString()
445
+ }
446
+
447
+ // Gradle verifies only artifact-set reads, never the walk's `ResolvedArtifact.file`, and fails
448
+ // them with every fatal failure so far; only STRICT throws, from 6.2, with the root build's metadata.
449
+ def rootBuild = gradle
450
+ while (rootBuild.parent != null) {
451
+ rootBuild = rootBuild.parent
452
+ }
453
+ def startParameter = gradle.startParameter
454
+ def verifyArtifacts = withFiles &&
455
+ new File(rootBuild.rootProject.rootDir, 'gradle/verification-metadata.xml').isFile() &&
456
+ startParameter.metaClass.respondsTo(startParameter, 'getDependencyVerificationMode') &&
457
+ startParameter.dependencyVerificationMode.toString() == 'STRICT'
458
+ def gavOf = { Map c -> "${c.groupId}:${c.artifactId}:${c.version}".toString() }
459
+ def verificationMessage = { Throwable e ->
460
+ causeMessages(e).find { it?.startsWith('Dependency verification failed') }
461
+ }
462
+ // Old Gradle splits a module into one ResolvedDependency per target configuration, of which only
463
+ // some carry its artifacts: an artifact-less one is the same component, so fold it into them.
464
+ def foldArtifactless = {
465
+ if (!nodes.values().any { n -> !n.coord.ext && n.project == null }) {
466
+ return
467
+ }
468
+ def withExt = [:]
469
+ nodes.each { id, n ->
470
+ if (n.coord.ext) {
471
+ withExt.get(gavOf(n.coord), []) << id
472
+ }
473
+ }
474
+ def folds = [:]
475
+ nodes.each { id, n ->
476
+ def siblings = (!n.coord.ext && n.project == null) ? withExt[gavOf(n.coord)] : null
477
+ if (siblings) {
478
+ folds[id] = siblings
479
+ }
480
+ }
481
+ if (folds.isEmpty()) {
482
+ return
483
+ }
484
+ def replaced = { Collection ids -> ids.collectMany { c -> folds[c] ?: [c] } }
485
+ folds.each { id, siblings ->
486
+ def folded = nodes.remove(id)
487
+ siblings.each { sid ->
488
+ nodes[sid].children.addAll(replaced(folded.children).findAll { it != sid })
489
+ if (folded.prod) {
490
+ nodes[sid].prod = true
491
+ }
492
+ }
493
+ }
494
+ nodes.each { nid, n ->
495
+ if (n.children.any { folds.containsKey(it) }) {
496
+ def children = replaced(n.children).findAll { it != nid }
497
+ n.children.clear()
498
+ n.children.addAll(children)
499
+ }
500
+ }
501
+ def direct = replaced(directIds)
502
+ directIds.clear()
503
+ directIds.addAll(direct)
504
+ }
505
+
506
+ // Drops the files of this config's artifacts that failed verification and reports them.
507
+ def checkVerification = { cfg ->
508
+ def external = paths.keySet().findAll { id -> nodes[id].project == null }
509
+ if (external.isEmpty()) {
510
+ return
511
+ }
512
+ def fetched = external.collect { id -> gavOf(nodes[id].coord) } as Set
513
+ def message = null
514
+ try {
515
+ cfg.incoming.artifactView {
516
+ lenient(true)
517
+ componentFilter { id ->
518
+ id instanceof org.gradle.api.artifacts.component.ModuleComponentIdentifier &&
519
+ fetched.contains("${id.group}:${id.module}:${id.version}".toString())
520
+ }
521
+ }.artifacts.each { it.file }
522
+ } catch (Exception e) {
523
+ message = verificationMessage(e)
524
+ if (message == null) {
525
+ // Any other failure concerns files the walk already read without it.
526
+ println "[socket-facts] could not check artifact verification for ${cfg.name}: ${rawDetail(e).readLines().find()}"
527
+ }
528
+ }
529
+ if (message == null) {
530
+ return
531
+ }
532
+ // Each failed artifact is named by its identifier's display name, "<file> (<group>:<module>:<version>)",
533
+ // whose file part is `<name>-<version>[-<classifier>].<ext>` rather than the cached file's own name.
534
+ def artifactRef = /(\S+) \(([^\s():]+):([^\s():]+):([^\s()]+)\)/
535
+ def lines = message.readLines()
536
+ def header = lines ? lines[0] : ''
537
+ // The verbose report (the Socket CLI sets org.gradle.dependency.verification.console=verbose) gives
538
+ // each artifact's problems: "On artifact <ref> in repository '<repo>': <problem>", or "On artifact
539
+ // <ref> multiple problems reported:" followed by one "in repository '<repo>': <problem>" line each.
540
+ def failed = []
541
+ def current = null
542
+ lines.each { raw ->
543
+ def line = raw.trim().replaceFirst(/^- /, '')
544
+ def m = line =~ ('^On artifact ' + artifactRef + ' (.*)$')
545
+ if (m.matches()) {
546
+ current = [gav: "${m.group(2)}:${m.group(3)}:${m.group(4)}".toString(), file: m.group(1), problems: []]
547
+ failed << current
548
+ if (m.group(5) != 'multiple problems reported:') {
549
+ current.problems << m.group(5)
550
+ }
551
+ } else if (current != null && line.startsWith("in repository '")) {
552
+ current.problems << line
553
+ } else {
554
+ current = null
555
+ }
556
+ }
557
+ failed.each { f ->
558
+ f.detail = ([header] + f.problems.collect { "On artifact ${f.file} (${f.gav}) ${it}" }).join('\n')
559
+ }
560
+ if (failed.isEmpty()) {
561
+ // The summary report names only the artifacts.
562
+ (message =~ artifactRef).each { m ->
563
+ failed << [gav: "${m[2]}:${m[3]}:${m[4]}".toString(), file: m[1], detail: message]
564
+ }
565
+ }
566
+ def markUnfetched = { Collection ids ->
567
+ ids.each { id ->
568
+ paths.remove(id)
569
+ unfetched << id
570
+ }
571
+ }
572
+ if (failed.isEmpty()) {
573
+ // No artifact can be told apart: the walked graph stands, without any of its external files.
574
+ state.unscannable << [config: cfg.name, detail: message, project: project.path]
575
+ markUnfetched(external)
576
+ return
577
+ }
578
+ failed.each { f ->
579
+ def ids = external.findAll { id -> gavOf(nodes[id].coord) == f.gav }
580
+ // A failed metadata file (pom, module) names none of the module's artifacts: all are suspect.
581
+ // A failed artifact this config did not fetch leaves it untouched.
582
+ def hit = (f.file.endsWith('.pom') || f.file.endsWith('.module')) ? ids : ids.findAll { id ->
583
+ def c = nodes[id].coord
584
+ def named = "${c.artifactId}-${c.version}${c.classifier ? '-' + c.classifier : ''}${c.ext ? '.' + c.ext : ''}"
585
+ f.file == named.toString() || paths[id]?.any { p -> f.file == new File(p).name }
586
+ }
587
+ if (!hit.isEmpty()) {
588
+ recordFailure(f.gav, f.detail)
589
+ markUnfetched(hit)
590
+ }
591
+ }
418
592
  }
419
593
 
420
594
  targetConfigs.each { cfg ->
@@ -426,10 +600,13 @@ allprojects { project ->
426
600
  // Fresh tree for THIS resolution root — captured by visit/upsertNode, so no union with siblings.
427
601
  nodes = [:]
428
602
  directIds = [] as Set
603
+ configName = cfg.name
429
604
  paths = [:]
605
+ unfetched = [] as Set
430
606
  projectByGav = [:]
431
607
  // Per-config try/catch: AGP-style configs can fail variant ambiguity from an init-script
432
608
  // context lacking the consumer attributes AGP sets internally.
609
+ def walked = false
433
610
  try {
434
611
  def lenient = cfg.resolvedConfiguration.lenientConfiguration
435
612
  // The resolution graph names a project dependency by project path; within one graph a
@@ -453,20 +630,25 @@ allprojects { project ->
453
630
  firstLevel.each { dep ->
454
631
  directIds.addAll(visit(dep, isProd, cache))
455
632
  }
633
+ foldArtifactless()
456
634
  // Unresolved deps drive the root aggregator's abort/warn decision but are NOT emitted as
457
635
  // nodes — their selector-only coordinates would surface as half-formed phantom artifacts.
458
- lenient.unresolvedModuleDependencies.each { dep ->
459
- def sel = dep.selector
460
- if (isIntraProject(sel.group, sel.name)) {
461
- return
636
+ // hasError() covers every unresolved dependency, project ones included.
637
+ if (cfg.resolvedConfiguration.hasError()) {
638
+ def reported = [] as Set
639
+ cfg.incoming.resolutionResult.allDependencies.each { d ->
640
+ if (d instanceof org.gradle.api.artifacts.result.UnresolvedDependencyResult) {
641
+ def coord = selectorCoord(d.attempted ?: d.requested)
642
+ if (reported.add(coord)) {
643
+ recordFailure(coord, rawDetail(d.failure))
644
+ }
645
+ }
462
646
  }
463
- failures << [
464
- coord : "${sel.group}:${sel.name}${sel.version ? ':' + sel.version : ''}".toString(),
465
- detail: rawDetail(dep.problem),
466
- config: cfg.name,
467
- project: project.path,
468
- ]
469
647
  }
648
+ if (verifyArtifacts) {
649
+ checkVerification(cfg)
650
+ }
651
+ walked = true
470
652
  } catch (Exception e) {
471
653
  // A config-LEVEL resolution failure (a resolutionStrategy/beforeResolve rule that throws,
472
654
  // an artifact-transform failure, a broken repository, ...) rather than a per-dep failure.
@@ -474,11 +656,11 @@ allprojects { project ->
474
656
  state.unscannable << [config: cfg.name, detail: rawDetail(e), project: project.path]
475
657
  }
476
658
  // Stash this resolution root's tree; the key only has to be unique per root. Skip empty
477
- // configs — they'd contribute nothing.
478
- if (!nodes.isEmpty()) {
659
+ // configs, which contribute nothing, and a partial walk, which can orphan components.
660
+ if (walked && !nodes.isEmpty()) {
479
661
  synchronized (state.perSub) {
480
662
  state.perSub["${project.path}::${cfg.name}".toString()] =
481
- [nodes: nodes, direct: directIds, paths: paths, projectPath: project.path, config: cfg.name, prod: isProd]
663
+ [nodes: nodes, direct: directIds, paths: paths, unfetched: unfetched, projectPath: project.path, config: cfg.name, prod: isProd]
482
664
  }
483
665
  }
484
666
  }
@@ -518,8 +700,12 @@ rootProject { rp ->
518
700
  def projectsInfo
519
701
  synchronized (state.projectsInfo) { projectsInfo = new ArrayList(state.projectsInfo) }
520
702
  projectsInfo.each { pi ->
521
- rec(['project', pi.path, pi.group ?: '', pi.name, pi.version ?: '', pi.dir])
522
- pi.buildFiles.each { f -> rec(['projectBuild', pi.path, f]) }
703
+ if (pi.excluded) {
704
+ if (withFilesProjects) rec(['excludedProject', pi.path])
705
+ } else {
706
+ rec(['project', pi.path, pi.group ?: '', pi.name, pi.version ?: '', pi.dir])
707
+ pi.buildFiles.each { f -> rec(['projectBuild', pi.path, f]) }
708
+ }
523
709
  if (withFilesProjects) {
524
710
  pi.sources.each { s -> rec(['projectSrc', pi.path, s]) }
525
711
  pi.targets.each { t -> rec(['projectTgt', pi.path, t]) }
@@ -541,6 +727,7 @@ rootProject { rp ->
541
727
  node.children.each { childId -> rec(['edge', rootId, coordId, childId]) }
542
728
  def fs = tree.paths[coordId]
543
729
  if (fs) { (fs as List).sort().each { p -> rec(['file', rootId, coordId, p]) } }
730
+ if (tree.unfetched.contains(coordId)) rec(['unfetched', rootId, coordId])
544
731
  }
545
732
  }
546
733