@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
package/dist/cli.js CHANGED
@@ -1824,12 +1824,16 @@ function reachMemoryLimitToMb(value) {
1824
1824
  // independent reactors is intentionally duplicated across all of their
1825
1825
  // components[].
1826
1826
 
1827
- // `[]` means resolved and found nothing (e.g. a pom/BOM with no artifact).
1827
+ // `[]` means resolved and found nothing (e.g. a pom/BOM with no artifact); no
1828
+ // `targets` means the artifact did not resolve.
1828
1829
  function attachPaths(entry, artifactPaths) {
1829
1830
  const paths = artifactPaths.pathsById.get(entry.id);
1831
+ const targets = paths ? paths.targets : [];
1830
1832
  return {
1831
1833
  ...entry,
1832
- targets: [...(paths?.targets ?? [])],
1834
+ ...(targets ? {
1835
+ targets: [...targets]
1836
+ } : {}),
1833
1837
  sources: [...(paths?.sources ?? [])]
1834
1838
  };
1835
1839
  }
@@ -2006,7 +2010,7 @@ async function performReachabilityAnalysis(options) {
2006
2010
  // Build Coana arguments.
2007
2011
  const coanaArgs = ['run', analysisTarget, '--output-dir', path.dirname(outputFilePath), '--socket-mode', outputFilePath, '--disable-report-submission', ...(isOmittedReachValue(reachabilityOptions.reachAnalysisTimeout) ? [] : ['--analysis-timeout', reachabilityOptions.reachAnalysisTimeout]), ...(isOmittedReachValue(reachabilityOptions.reachAnalysisMemoryLimit) ? [] : ['--memory-limit', reachabilityOptions.reachAnalysisMemoryLimit]), ...(reachabilityOptions.reachConcurrency ? ['--concurrency', `${reachabilityOptions.reachConcurrency}`] : []), ...(reachabilityOptions.reachContinueOnAnalysisErrors ? ['--reach-continue-on-analysis-errors'] : []), ...(reachabilityOptions.reachContinueOnInstallErrors ? ['--reach-continue-on-install-errors'] : []), ...(reachabilityOptions.reachContinueOnMissingLockFiles ? ['--reach-continue-on-missing-lock-files'] : []), ...(reachabilityOptions.reachContinueOnNoSourceFiles ? ['--reach-continue-on-no-source-files'] : []), ...(reachabilityOptions.reachDebug ? ['--debug'] : []), ...(reachabilityOptions.reachDetailedAnalysisLogFile ? ['--print-analysis-log-file'] : []), ...(reachabilityOptions.reachDisableAnalytics ? ['--disable-analytics-sharing'] : []), ...(reachabilityOptions.reachDisableExternalToolChecks ? ['--disable-external-tool-checks'] : []), ...(reachabilityOptions.reachEnableAnalysisSplitting ? [] : ['--disable-analysis-splitting']), '--run-without-docker', '--manifests-tar-hash', tarHash,
2008
2012
  // Empty reachEcosystems implies scanning all ecosystems.
2009
- ...(reachabilityOptions.reachEcosystems.length ? ['--purl-types', ...reachabilityOptions.reachEcosystems] : []), ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []),
2013
+ ...(reachabilityOptions.reachEcosystems.length ? ['--purl-types', ...reachabilityOptions.reachEcosystems] : []), ...(reachabilityOptions.reachAdvisories.length ? ['--ghsas', ...reachabilityOptions.reachAdvisories] : []), ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []),
2010
2014
  // sidecarPath is always set above when this is true - Coana rejects this
2011
2015
  // flag without one.
2012
2016
  ...(reachabilityOptions.dynamicSbomInference ? ['--maven-use-only-socket-facts'] : []), ...(reachabilityOptions.reachSkipCache ? ['--skip-cache-usage'] : []), ...(reachabilityOptions.reachUseOnlyPregeneratedSboms ? ['--use-only-pregenerated-sboms'] : []), ...(sidecarPath ? ['--compute-artifacts-sidecar', sidecarPath] : [])];
@@ -2351,9 +2355,17 @@ function classifyGradleFailure(detail) {
2351
2355
  if (t.includes('no matching variant') || t.includes('no variants of') || t.includes('unable to find a matching variant') || t.includes('no compatible variant')) {
2352
2356
  return 'no-matching-variant';
2353
2357
  }
2354
- if (t.includes('cannot choose between')) {
2358
+ // The artifact-level form: several transforms produce the requested variant
2359
+ // ("transformation chains" from Gradle 8.12, "transforms" before it).
2360
+ if (t.includes('cannot choose between') || t.includes('found multiple transformation chains') || t.includes('found multiple transforms that can produce a variant')) {
2355
2361
  return 'variant-ambiguity';
2356
2362
  }
2363
+ // One line per problem of the artifact; a checksum missing from the
2364
+ // metadata is no evidence against it, unlike any other problem.
2365
+ const problems = t.split('\n').filter(l => l.startsWith('on artifact '));
2366
+ if (problems.length && problems.every(l => l.endsWith(': checksum is missing from verification metadata.'))) {
2367
+ return 'checksum-missing';
2368
+ }
2357
2369
  if (t.includes('could not get') || t.includes('could not head') || t.includes('status code 401') || t.includes('status code 403') || t.includes('connection refused') || t.includes('connection timed out') || t.includes('read timed out') || t.includes('certification path') || t.includes('peer not authenticated')) {
2358
2370
  return 'repository-or-network';
2359
2371
  }
@@ -2365,8 +2377,9 @@ function classifyGradleFailure(detail) {
2365
2377
 
2366
2378
  // Every kind blocks except variant-ambiguity: the module demonstrably exists
2367
2379
  // and is almost always captured via another configuration, so it's benign for
2368
- // the SBOM (non-blocking, count only). A module that resolves in NO config
2369
- // surfaces as not-found / no-matching-variant, which stay blocking.
2380
+ // the SBOM (non-blocking, count only). The renderer blocks it anyway when the
2381
+ // module resolved in no configuration of its project. Nor does checksum-missing:
2382
+ // teams often record checksums only for what their own tasks resolve.
2370
2383
  const GRADLE_DIALECT = {
2371
2384
  label: 'Gradle',
2372
2385
  classify: classifyGradleFailure,
@@ -2399,8 +2412,13 @@ const GRADLE_DIALECT = {
2399
2412
  blocking: true
2400
2413
  }, {
2401
2414
  key: 'variant-ambiguity',
2415
+ header: n => ` Ambiguous variant — ${n} could not choose a variant, and the module resolved in no other configuration:`,
2402
2416
  blocking: false,
2403
2417
  notice: (n, depCount, configCount) => `Skipped ${depCount} dependency(ies) with ambiguous variant selection in ${configCount} configuration(s) — re-run with --verbose for ${n}'s messages.`
2418
+ }, {
2419
+ key: 'checksum-missing',
2420
+ blocking: false,
2421
+ notice: (n, depCount, configCount) => `Left ${depCount} dependency(ies) uninstalled in ${configCount} configuration(s): ${n}'s verification metadata has no checksum for their artifacts — re-run with --verbose for ${n}'s messages.`
2404
2422
  }]
2405
2423
  };
2406
2424
 
@@ -2450,14 +2468,24 @@ const SBT_DIALECT = {
2450
2468
  // classify off the root-cause message.
2451
2469
  function classifyMavenFailure(detail) {
2452
2470
  const t = (detail || '').toLowerCase();
2453
- if (t.includes('could not transfer') || t.includes('connection refused') || t.includes('connect timed out') || t.includes('connection timed out') || t.includes('read timed out') || t.includes('status code: 401') || t.includes('status code: 403') || t.includes('unauthorized') || t.includes('forbidden') || t.includes('peer not authenticated') || t.includes('certpathbuilderexception')) {
2471
+ if (t.includes('could not transfer') || t.includes('connection refused') || t.includes('connect timed out') || t.includes('connection timed out') || t.includes('read timed out') || t.includes('status code: 401') || t.includes('status code: 403') || t.includes('unauthorized') || t.includes('forbidden') || t.includes('peer not authenticated') || t.includes('certpathbuilderexception') ||
2472
+ // Maven 3.8.1+ blocks plain-HTTP repositories behind its
2473
+ // maven-default-http-blocker mirror.
2474
+ t.includes('blocked mirror for repositories') ||
2475
+ // Offline (-o): no repository was asked, so the artifact may well exist.
2476
+ t.includes('in offline mode')) {
2454
2477
  return 'repository-or-network';
2455
2478
  }
2456
- if (t.includes('could not find artifact') || t.includes('failure to find') || t.includes('could not resolve') || t.includes('no versions available') || t.includes('not found')) {
2479
+ if (t.includes('could not find artifact') || t.includes('failure to find') || t.includes('could not resolve') || t.includes('no versions available') || t.includes('not found') ||
2480
+ // maven-resolver's remote repository filters (prefixes, groupId): the
2481
+ // repository is known not to serve that path or group.
2482
+ t.includes('not allowed from')) {
2457
2483
  return 'not-found';
2458
2484
  }
2459
2485
  // POM exists but can't be read/parsed.
2460
- if (t.includes('failed to read artifact descriptor') || t.includes('invalid pom') || t.includes('could not parse pom')) {
2486
+ if (t.includes('failed to read artifact descriptor') || t.includes('invalid pom') || t.includes('could not parse pom') ||
2487
+ // Maven's warning for a POM its descriptor policy tolerated as invalid.
2488
+ t.includes('is invalid, transitive dependencies')) {
2461
2489
  return 'config-problem';
2462
2490
  }
2463
2491
  return 'other';
@@ -2488,6 +2516,14 @@ const MAVEN_DIALECT = {
2488
2516
  }]
2489
2517
  };
2490
2518
 
2519
+ // A resolution that threw without naming a dependency: one configuration, or
2520
+ // with an empty `config` the whole project.
2521
+
2522
+ // A module (`group:name`, from any longer coordinate) within one project.
2523
+ function moduleKey(project, coord) {
2524
+ return `${project}\t${coord.split(':').slice(0, 2).join(':')}`;
2525
+ }
2526
+
2491
2527
  // Recognized from the build tool's message; drives wording AND whether the kind
2492
2528
  // is blocking. An unrecognized message degrades to 'other' (blocking) — safe.
2493
2529
 
@@ -2497,6 +2533,7 @@ const MAVEN_DIALECT = {
2497
2533
  // names something they can actually set.
2498
2534
 
2499
2535
  const DSI_SETUP_COMMAND = 'socket manifest setup --dynamic-sbom-inference';
2536
+ const REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG = '--reach-continue-on-install-errors';
2500
2537
 
2501
2538
  // The single-build manifest commands take every option as a flag.
2502
2539
  const FLAG_HINTS = {
@@ -2508,17 +2545,19 @@ const RESOLUTION_REPORT_ARTIFACT_LIMIT = 15;
2508
2545
  const RESOLUTION_REPORT_CONFIG_LIMIT = 20;
2509
2546
 
2510
2547
  // Drop a bare "group:name" when a versioned "group:name:v" of the same module
2511
- // is also present: the lenient resolver reports both forms for one failure.
2512
- function dedupCoords(coords) {
2513
- const set = new Set(coords);
2548
+ // failed in the same projects: the lenient resolver reports both forms for one
2549
+ // failure.
2550
+ function dedupInfos(infos) {
2514
2551
  const versioned = new Set();
2515
- for (const c of set) {
2516
- const p = c.split(':');
2552
+ for (const i of infos) {
2553
+ const p = i.coord.split(':');
2517
2554
  if (p.length >= 3) {
2518
- versioned.add(`${p[0]}:${p[1]}`);
2555
+ for (const project of i.projects) {
2556
+ versioned.add(moduleKey(project, i.coord));
2557
+ }
2519
2558
  }
2520
2559
  }
2521
- return [...set].filter(c => c.split(':').length >= 3 || !versioned.has(c)).sort();
2560
+ return infos.filter(i => i.coord.split(':').length >= 3 || ![...i.projects].every(p => versioned.has(moduleKey(p, i.coord)))).sort((a, b) => a.coord < b.coord ? -1 : a.coord > b.coord ? 1 : 0);
2522
2561
  }
2523
2562
  function firstLine(s) {
2524
2563
  return (s || '').split('\n').map(l => l.trim()).find(Boolean) ?? '';
@@ -2537,6 +2576,13 @@ function inProjects(projects) {
2537
2576
  const list = [...(typeof projects === 'string' ? [projects] : projects)].filter(Boolean).sort().map(p => `\`${p}\``);
2538
2577
  return list.length ? ` (in ${fmtList(list, 3)})` : '';
2539
2578
  }
2579
+ function installErrorsHint(hints, ignored) {
2580
+ const flag = hints.continueOnInstallErrorsFlag;
2581
+ if (!flag) {
2582
+ return [];
2583
+ }
2584
+ return [ignored ? `Reachability analysis cannot install the ignored dependencies; pass ${flag} unless you already do.` : `If you ignore unresolved dependencies, also pass ${flag}: reachability analysis cannot install them.`];
2585
+ }
2540
2586
 
2541
2587
  // `excludableConfigs` is empty when excluding configurations cannot clear
2542
2588
  // every blocking failure.
@@ -2557,9 +2603,8 @@ function proceedHint(hints, tool, excludableConfigs) {
2557
2603
  return ['To proceed, either:', ...asFlags.map(o => ` re-run with ${flagText(...o)}`), ...inSocketJson.map(([option, value]) => ` set defaults.manifest.${tool}.${option} to ${JSON.stringify(value)} in ${hints.socketJson} (${setup})`)];
2558
2604
  }
2559
2605
 
2560
- // Severity is per-kind; the exit-code decision lives in the caller. We do NOT
2561
- // cross-reference what resolved elsewhere: the failed selector carries no
2562
- // classifier/type, so relating a failed and a succeeded dep is unsound.
2606
+ // What resolved elsewhere is consulted only by module (group:name): a failed
2607
+ // selector carries no classifier/type to relate it to one artifact.
2563
2608
  function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2564
2609
  const dialect = dialectFor(tool);
2565
2610
  const name = dialect.label;
@@ -2569,19 +2614,27 @@ function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2569
2614
  const specOf = new Map(dialect.categories.map(c => [c.key, c]));
2570
2615
  const isBlocking = cat => specOf.get(cat)?.blocking ?? true;
2571
2616
 
2572
- // Aggregate by (coord, category): one module can fail with different causes
2573
- // across configs. Keep first-seen detail, union the configs and projects.
2617
+ // A non-blocking kind is only benign when its module resolved anyway: one
2618
+ // that resolved in no configuration of its project is missing from that
2619
+ // project's SBOM.
2620
+ const resolved = opts.resolvedModules;
2621
+ const isFailureBlocking = (category, f) => isBlocking(category) || !!resolved && !resolved.has(moduleKey(f.project, f.coord));
2622
+
2623
+ // Aggregate by (coord, category, blocking): one module can fail with
2624
+ // different causes across configs, and block in only some projects. Keep
2625
+ // first-seen detail, union the configs and projects.
2574
2626
 
2575
2627
  const byKey = new Map();
2576
- const keyOf = (coord, category) => `${coord} ${category}`;
2577
2628
  for (const f of failures) {
2578
2629
  const category = dialect.classify(f.detail);
2579
- const key = keyOf(f.coord, category);
2630
+ const blocking = isFailureBlocking(category, f);
2631
+ const key = `${f.coord} ${category} ${blocking}`;
2580
2632
  let info = byKey.get(key);
2581
2633
  if (!info) {
2582
2634
  info = {
2583
2635
  coord: f.coord,
2584
2636
  category,
2637
+ blocking,
2585
2638
  detail: f.detail,
2586
2639
  configs: new Set(),
2587
2640
  projects: new Set(),
@@ -2599,6 +2652,7 @@ function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2599
2652
  }
2600
2653
  }
2601
2654
  const allInfos = [...byKey.values()];
2655
+ const isInfoBlocking = info => info.blocking;
2602
2656
 
2603
2657
  // A whole-config throw is classified by the same cause rules as a per-dep
2604
2658
  // failure: ambiguity stays lenient, every other cause is fail-closed.
@@ -2616,7 +2670,7 @@ function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2616
2670
  const blockingUnscannableProjects = blockingUnscannable.filter(u => !u.config);
2617
2671
  const perDepBlockingConfigs = new Set();
2618
2672
  for (const info of allInfos) {
2619
- if (isBlocking(info.category)) {
2673
+ if (isInfoBlocking(info)) {
2620
2674
  for (const c of info.configs) {
2621
2675
  perDepBlockingConfigs.add(c);
2622
2676
  }
@@ -2626,15 +2680,21 @@ function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2626
2680
  const blockingFailed = [...blockingConfigs].sort();
2627
2681
  // A blocking failure outside any configuration leaves every configuration of
2628
2682
  // its project incomplete, so none can be reported as succeeded.
2629
- const hasBlockingConfiglessFailure = blockingUnscannableProjects.length > 0 || allInfos.some(i => i.configless && isBlocking(i.category));
2683
+ const hasBlockingConfiglessFailure = blockingUnscannableProjects.length > 0 || allInfos.some(i => i.configless && isInfoBlocking(i));
2630
2684
  // An un-scannable config was attempted but resolved nothing, so it didn't succeed.
2631
2685
  const succeeded = hasBlockingConfiglessFailure ? [] : scannedConfigs.filter(c => !blockingConfigs.has(c) && !unscannableConfigs.has(c)).sort();
2632
2686
  const groups = dialect.categories.map(spec => ({
2633
2687
  spec,
2634
- infos: dedupCoords(allInfos.filter(i => i.category === spec.key).map(i => i.coord)).map(c => byKey.get(keyOf(c, spec.key)))
2688
+ infos: dedupInfos(allInfos.filter(i => i.category === spec.key))
2689
+ }));
2690
+ const blockingGroups = groups.map(g => ({
2691
+ ...g,
2692
+ infos: g.infos.filter(isInfoBlocking)
2693
+ })).filter(g => g.infos.length);
2694
+ const nonBlockingGroups = groups.map(g => ({
2695
+ ...g,
2696
+ infos: g.infos.filter(i => !isInfoBlocking(i))
2635
2697
  })).filter(g => g.infos.length);
2636
- const blockingGroups = groups.filter(g => g.spec.blocking);
2637
- const nonBlockingGroups = groups.filter(g => !g.spec.blocking);
2638
2698
  const blockingCount = blockingGroups.reduce((n, g) => n + g.infos.length, 0);
2639
2699
  const hasBlockingFailures = blockingCount > 0 || blockingUnscannable.length > 0;
2640
2700
  const willFail = hasBlockingFailures && !opts.ignoreUnresolved;
@@ -2690,7 +2750,9 @@ function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2690
2750
  }
2691
2751
  if (willFail) {
2692
2752
  out.push('');
2693
- out.push(...proceedHint(hints, tool, hasBlockingConfiglessFailure ? [] : blockingFailed));
2753
+ out.push(...proceedHint(hints, tool, hasBlockingConfiglessFailure ? [] : blockingFailed), ...installErrorsHint(hints, false));
2754
+ } else if (hints.continueOnInstallErrorsFlag) {
2755
+ out.push('', ...installErrorsHint(hints, true));
2694
2756
  }
2695
2757
  out.push('');
2696
2758
  out.push(hints.flags.has('verbose') ? `Re-run with --verbose for ${name}'s full messages.` : `For ${name}'s full messages, ${verboseHint(hints, tool)}.`);
@@ -2706,8 +2768,8 @@ function renderResolutionReport(failures, scannedConfigs, tool, opts = {}) {
2706
2768
  const configCount = new Set(infos.flatMap(i => [...i.configs])).size;
2707
2769
  notices.push(spec.notice(name, infos.length, configCount));
2708
2770
  }
2709
- // A config-level throw whose cause classifies as variant ambiguity is surfaced, not failed —
2710
- // matching the deliberately-lenient per-dep variant-ambiguity policy.
2771
+ // A config-level throw whose cause classifies as variant ambiguity is surfaced, not failed: many
2772
+ // configurations are marked resolvable yet too underspecified to resolve.
2711
2773
  if (nonBlockingUnscannable.length) {
2712
2774
  const n = new Set(nonBlockingUnscannable.map(u => u.config)).size;
2713
2775
  notices.push(hints.flags.has('verbose') ? `Could not scan ${n} configuration(s) — re-run with --verbose for ${name}'s messages.` : `Could not scan ${n} configuration(s); for ${name}'s messages, ${verboseHint(hints, tool)}.`);
@@ -2787,7 +2849,7 @@ function assembleFacts(parsed, opts = {}) {
2787
2849
  return {
2788
2850
  facts,
2789
2851
  report: buildReport(parsed),
2790
- artifactPaths: buildArtifactPaths(finalNodes, [...parsed.projects.values()], graphs, fileExists)
2852
+ artifactPaths: buildArtifactPaths(finalNodes, [...parsed.projects.values()], [...parsed.excludedProjects.values()], graphs, fileExists)
2791
2853
  };
2792
2854
  }
2793
2855
 
@@ -2814,11 +2876,13 @@ function buildPerRoot(parsed) {
2814
2876
  children: [],
2815
2877
  direct: false,
2816
2878
  project: n.project,
2817
- targets: []
2879
+ targets: [],
2880
+ unfetched: false
2818
2881
  };
2819
2882
  nodes.set(id, node);
2820
2883
  }
2821
2884
  node.direct ||= n.direct;
2885
+ node.unfetched ||= n.unfetched;
2822
2886
  node.targets.push(...n.targets);
2823
2887
  }
2824
2888
  for (const [p, c] of r.edges) {
@@ -2858,7 +2922,7 @@ function mergeById(perRoot) {
2858
2922
  prod: false,
2859
2923
  direct: false,
2860
2924
  project: node.project,
2861
- targets: new Set()
2925
+ resolutions: []
2862
2926
  };
2863
2927
  finalNodes.set(coordId, fn);
2864
2928
  }
@@ -2868,8 +2932,11 @@ function mergeById(perRoot) {
2868
2932
  if (node.direct) {
2869
2933
  fn.direct = true;
2870
2934
  }
2871
- for (const t of node.targets) {
2872
- fn.targets.add(t);
2935
+ if (node.unfetched || node.targets.length) {
2936
+ fn.resolutions.push({
2937
+ targets: node.targets,
2938
+ unfetched: node.unfetched
2939
+ });
2873
2940
  }
2874
2941
  }
2875
2942
  }
@@ -3090,19 +3157,34 @@ function byProject(projects, graphs, ids) {
3090
3157
  return [p.projectKey, g ? [...ids(g)].sort() : []];
3091
3158
  }));
3092
3159
  }
3093
- function buildArtifactPaths(finalNodes, projects, graphs, fileExists) {
3160
+ function buildArtifactPaths(finalNodes, projects, excludedProjects, graphs, fileExists) {
3094
3161
  const pathsById = new Map();
3162
+ // Every root that fetched an artifact lists its file, so check each path once.
3163
+ const existing = new Map();
3164
+ const exists = p => {
3165
+ let e = existing.get(p);
3166
+ if (e === undefined) {
3167
+ e = fileExists(p);
3168
+ existing.set(p, e);
3169
+ }
3170
+ return e;
3171
+ };
3095
3172
  for (const [id, fn] of finalNodes) {
3096
3173
  if (!fn.project) {
3097
- pathsById.set(id, {
3174
+ // Only a root that fetched every file of the artifact, all still on
3175
+ // disk, resolved it: a partial list would claim it resolved whole.
3176
+ const complete = fn.resolutions.filter(r => !r.unfetched && r.targets.every(exists));
3177
+ pathsById.set(id, fn.resolutions.length && !complete.length ? {
3178
+ sources: []
3179
+ } : {
3098
3180
  sources: [],
3099
- targets: [...fn.targets].filter(fileExists).sort()
3181
+ targets: [...new Set(complete.flatMap(r => r.targets))].sort()
3100
3182
  });
3101
3183
  }
3102
3184
  }
3103
3185
  // A project's own component shares its id, so this also covers dependency
3104
3186
  // edges onto a sibling project.
3105
- for (const p of projects) {
3187
+ for (const p of [...projects, ...excludedProjects]) {
3106
3188
  pathsById.set(p.projectKey, {
3107
3189
  sources: [...new Set(p.sources)].filter(fileExists).sort(),
3108
3190
  targets: [...new Set(p.targets)].filter(fileExists).sort()
@@ -3133,8 +3215,17 @@ function buildReport(parsed) {
3133
3215
  seenUnscannable.add(key);
3134
3216
  return true;
3135
3217
  });
3218
+ const resolvedModules = new Set();
3219
+ if (failures.length) {
3220
+ for (const r of parsed.roots.values()) {
3221
+ for (const n of r.nodes.values()) {
3222
+ resolvedModules.add(moduleKey(r.projectKey, `${n.coord.group}:${n.coord.name}`));
3223
+ }
3224
+ }
3225
+ }
3136
3226
  return {
3137
3227
  failures,
3228
+ resolvedModules,
3138
3229
  scannedConfigs: parsed.scannedConfigs,
3139
3230
  unscannable
3140
3231
  };
@@ -3288,12 +3379,14 @@ function serializeExcludePathPatterns(paths) {
3288
3379
  // projectSrc projectKey path (--with-files only)
3289
3380
  // projectTgt projectKey path (--with-files only)
3290
3381
  // projectBuild projectKey path (build-root-relative; may be absent on disk)
3382
+ // excludedProject projectKey (--with-files only; paths without a project)
3291
3383
  // root rootId projectKey config prod(0|1)
3292
3384
  // node rootId coordId group name version ext classifier direct(0|1) project
3293
3385
  // edge rootId parentCoordId childCoordId
3294
3386
  // file rootId coordId path (--with-files only)
3387
+ // unfetched rootId coordId (--with-files only; requested artifact not fetched)
3295
3388
  // scanned config
3296
- // failure coord detail config projectKey (config empty: no configuration)
3389
+ // failure coord detail config projectKey (config empty: not placeable in one)
3297
3390
  // unscannable config detail projectKey (config empty: whole project)
3298
3391
  //
3299
3392
  // `projectKey` is the tool's unique project identity (Maven: GAV; Gradle:
@@ -3330,12 +3423,14 @@ function parseRecords(text) {
3330
3423
  buildRoot: '',
3331
3424
  entry: '',
3332
3425
  projects: new Map(),
3426
+ excludedProjects: new Map(),
3333
3427
  roots: new Map(),
3334
3428
  scannedConfigs: [],
3335
3429
  failures: [],
3336
3430
  unscannable: []
3337
3431
  };
3338
3432
  const scanned = new Set();
3433
+ const excluded = new Set();
3339
3434
  const root = id => {
3340
3435
  let r = result.roots.get(id);
3341
3436
  if (!r) {
@@ -3404,6 +3499,9 @@ function parseRecords(text) {
3404
3499
  project(f[1] ?? '').targets.push(f[2]);
3405
3500
  }
3406
3501
  break;
3502
+ case 'excludedProject':
3503
+ excluded.add(f[1] ?? '');
3504
+ break;
3407
3505
  case 'projectBuild':
3408
3506
  if (f[2]) {
3409
3507
  project(f[1] ?? '').buildFiles.push(f[2]);
@@ -3432,7 +3530,8 @@ function parseRecords(text) {
3432
3530
  },
3433
3531
  direct: bool(f[8]),
3434
3532
  project: f[9] ?? '',
3435
- targets: []
3533
+ targets: [],
3534
+ unfetched: false
3436
3535
  });
3437
3536
  break;
3438
3537
  }
@@ -3445,6 +3544,14 @@ function parseRecords(text) {
3445
3544
  }
3446
3545
  break;
3447
3546
  }
3547
+ case 'unfetched':
3548
+ {
3549
+ const node = root(f[1] ?? '').nodes.get(f[2] ?? '');
3550
+ if (node) {
3551
+ node.unfetched = true;
3552
+ }
3553
+ break;
3554
+ }
3448
3555
  case 'file':
3449
3556
  {
3450
3557
  const node = root(f[1] ?? '').nodes.get(f[2] ?? '');
@@ -3479,6 +3586,13 @@ function parseRecords(text) {
3479
3586
  break;
3480
3587
  }
3481
3588
  }
3589
+ for (const key of excluded) {
3590
+ const p = result.projects.get(key);
3591
+ if (p) {
3592
+ result.projects.delete(key);
3593
+ result.excludedProjects.set(key, p);
3594
+ }
3595
+ }
3482
3596
  result.scannedConfigs = [...scanned].sort();
3483
3597
  return result;
3484
3598
  }
@@ -3630,7 +3744,10 @@ async function invokeGradle(opts, initScriptFilename, task, tmpDirPrefix) {
3630
3744
  const bin = resolveBuildToolBin('gradle', opts.projectDir, opts.bin);
3631
3745
  // Disable the configuration cache: the init script's legacy
3632
3746
  // resolvedConfiguration API and shared accumulator aren't cache-safe.
3633
- const args = ['--init-script', initScript, '-Dorg.gradle.configuration-cache=false', `-Psocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-P'), ...(opts.toolOpts ?? []), task, '--no-daemon', '--console=plain'];
3747
+ const args = ['--init-script', initScript, '-Dorg.gradle.configuration-cache=false',
3748
+ // Names each artifact's own verification problem, so a checksum missing
3749
+ // from the metadata can be told apart from a mismatch.
3750
+ '-Porg.gradle.dependency.verification.console=verbose', `-Psocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-P'), ...(opts.toolOpts ?? []), task, '--no-daemon', '--console=plain'];
3634
3751
  const out = await runNeverThrow(bin, args, opts);
3635
3752
  return await assembleFromRecords('gradle', out, recordsFile);
3636
3753
  });
@@ -3644,6 +3761,11 @@ async function enumerateGradleWorkspaces(opts) {
3644
3761
  async function invokeSbtIn(globalBase, opts, pluginSourceFilename, pluginDestFilename, task) {
3645
3762
  await writeSbtPlugin(globalBase, pluginSourceFilename, pluginDestFilename);
3646
3763
  const recordsFile = path.join(globalBase, 'records.tsv');
3764
+ // The global base is shared across sbt roots: a build that dies before
3765
+ // writing must not be read as the previous root's records.
3766
+ await u.promises.rm(recordsFile, {
3767
+ force: true
3768
+ });
3647
3769
  const bin = resolveBuildToolBin('sbt', opts.projectDir, opts.bin);
3648
3770
  // Fresh per-run global base (not ~/.sbt): sbt executes everything under
3649
3771
  // plugins/, so a shared path is a code-injection surface. BSP off for this
@@ -3805,6 +3927,7 @@ async function runManifestFacts({
3805
3927
  const rendered = renderResolutionErrorReport(report.failures, report.scannedConfigs, ecosystem, {
3806
3928
  hints,
3807
3929
  ignoreUnresolved,
3930
+ resolvedModules: report.resolvedModules,
3808
3931
  unscannable: report.unscannable
3809
3932
  });
3810
3933
  if (rendered.hasBlockingFailures) {
@@ -3993,6 +4116,7 @@ async function runEcosystemCandidates({
3993
4116
  excludeConfigs,
3994
4117
  excludePaths: excludePathsForRoot,
3995
4118
  hints: {
4119
+ continueOnInstallErrorsFlag: withFiles ? REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG : undefined,
3996
4120
  flags: optionFlags,
3997
4121
  setupCommand: DSI_SETUP_COMMAND,
3998
4122
  socketJson: displayPath(path.join(dir, constants.SOCKET_JSON))
@@ -4088,10 +4212,10 @@ async function generateRecursiveManifests({
4088
4212
  // A shared global base across every sbt root in this run lets sbt's own
4089
4213
  // Scala-toolchain cache under <base>/boot survive between invocations
4090
4214
  // instead of being reprovisioned per root (the plugin file is rewritten and
4091
- // records.tsv is fully overwritten - not appended - on every invocation, so
4092
- // reuse is safe). A caller-supplied sbtTmpDir is reused as-is (the caller
4093
- // owns its lifetime); otherwise one is allocated and cleaned up here, but
4094
- // only when there's an sbt root to benefit from sharing it at all.
4215
+ // records.tsv is removed before every invocation, so reuse is safe). A
4216
+ // caller-supplied sbtTmpDir is reused as-is (the caller owns its lifetime);
4217
+ // otherwise one is allocated and cleaned up here, but only when there's an
4218
+ // sbt root to benefit from sharing it at all.
4095
4219
  const outcomes = sbtTmpDir ? await runAll(sbtTmpDir) : candidatesByTool.get('sbt')?.length ? await utils.withTmpDir('socket-sbt-facts-shared-', runAll) : await runAll(undefined);
4096
4220
  if (verbose) {
4097
4221
  logger.logger.info(`Discovered ${outcomes.length} build-tool candidate(s).`);
@@ -6564,7 +6688,7 @@ async function convertGradleToFacts({
6564
6688
  verbose,
6565
6689
  withFiles
6566
6690
  }) {
6567
- await runManifestFacts({
6691
+ return await runManifestFacts({
6568
6692
  bin,
6569
6693
  buildOpts: gradleOpts,
6570
6694
  cwd,
@@ -6597,7 +6721,7 @@ async function convertMavenToFacts({
6597
6721
  verbose,
6598
6722
  withFiles
6599
6723
  }) {
6600
- await runManifestFacts({
6724
+ return await runManifestFacts({
6601
6725
  bin,
6602
6726
  buildOpts: mavenOpts,
6603
6727
  cwd,
@@ -6632,7 +6756,7 @@ async function convertSbtToFacts({
6632
6756
  verbose,
6633
6757
  withFiles
6634
6758
  }) {
6635
- await runManifestFacts({
6759
+ return await runManifestFacts({
6636
6760
  bin,
6637
6761
  buildOpts: sbtOpts,
6638
6762
  cwd,
@@ -7109,12 +7233,14 @@ const AUTO_MANIFEST_HINTS = {
7109
7233
  setupCommand: 'socket manifest setup',
7110
7234
  socketJson: constants.SOCKET_JSON
7111
7235
  };
7112
- // Under --auto-manifest, a manifest generator that failed — raising the exit
7113
- // code above the value captured before it ran — aborts the whole run: a partial
7114
- // or empty SBOM silently under-reports dependencies. The generator has already
7115
- // logged the specifics. A tolerated resolution failure (ignoreUnresolved) warns
7116
- // without touching the exit code, so it passes through here and the run
7117
- // continues.
7236
+ // A failed generator aborts --auto-manifest: a partial SBOM silently
7237
+ // under-reports dependencies. Facts generators return null on failure; the
7238
+ // others only change the exit code.
7239
+ function abortFactsRunIfFailed(ecosystem, outcome) {
7240
+ if (outcome === null) {
7241
+ throw new utils.InputError(`Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`);
7242
+ }
7243
+ }
7118
7244
  function abortManifestRunIfFailed(ecosystem, beforeExitCode) {
7119
7245
  if (process.exitCode && process.exitCode !== beforeExitCode) {
7120
7246
  throw new utils.InputError(`Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`);
@@ -7131,6 +7257,10 @@ async function generateAutoManifest({
7131
7257
  }) {
7132
7258
  const sockJson = utils.readOrDefaultSocketJson(cwd);
7133
7259
  const generatedFiles = [];
7260
+ const hints = computeArtifactsSidecar ? {
7261
+ ...AUTO_MANIFEST_HINTS,
7262
+ continueOnInstallErrorsFlag: REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG
7263
+ } : AUTO_MANIFEST_HINTS;
7134
7264
 
7135
7265
  // Resolved paths across all JVM roots, serialized to one sidecar at the end.
7136
7266
  const sidecarAcc = computeArtifactsSidecar ? new Map() : undefined;
@@ -7152,20 +7282,19 @@ async function generateAutoManifest({
7152
7282
  // `defaults.manifest.sbt.facts: false` in socket.json.
7153
7283
  if (sockJson.defaults?.manifest?.sbt?.facts !== false) {
7154
7284
  logger.logger.log('Detected a Scala sbt build, generating Socket facts...');
7155
- const beforeExitCode = process.exitCode;
7156
- await convertSbtToFacts({
7285
+ const outcome = await convertSbtToFacts({
7157
7286
  ...sbtArgs,
7158
7287
  excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '',
7159
7288
  excludePaths,
7160
7289
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.sbt?.ignoreUnresolved),
7161
7290
  includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '',
7162
7291
  javaHome: sockJson.defaults?.manifest?.sbt?.javaHome ?? undefined,
7163
- hints: AUTO_MANIFEST_HINTS,
7292
+ hints,
7164
7293
  sidecarAcc,
7165
7294
  tmpDir,
7166
7295
  withFiles: computeArtifactsSidecar
7167
7296
  });
7168
- abortManifestRunIfFailed('sbt', beforeExitCode);
7297
+ abortFactsRunIfFailed('sbt', outcome);
7169
7298
  } else {
7170
7299
  logger.logger.log('Detected a Scala sbt build, generating pom files with sbt...');
7171
7300
  const beforeExitCode = process.exitCode;
@@ -7188,19 +7317,18 @@ async function generateAutoManifest({
7188
7317
  // `defaults.manifest.gradle.facts: false` in socket.json.
7189
7318
  if (sockJson.defaults?.manifest?.gradle?.facts !== false) {
7190
7319
  logger.logger.log('Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...');
7191
- const beforeExitCode = process.exitCode;
7192
- await convertGradleToFacts({
7320
+ const outcome = await convertGradleToFacts({
7193
7321
  ...gradleArgs,
7194
7322
  excludeConfigs: sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '',
7195
7323
  excludePaths,
7196
7324
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.gradle?.ignoreUnresolved),
7197
7325
  includeConfigs: sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '',
7198
7326
  javaHome: sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined,
7199
- hints: AUTO_MANIFEST_HINTS,
7327
+ hints,
7200
7328
  sidecarAcc,
7201
7329
  withFiles: computeArtifactsSidecar
7202
7330
  });
7203
- abortManifestRunIfFailed('gradle', beforeExitCode);
7331
+ abortFactsRunIfFailed('gradle', outcome);
7204
7332
  } else {
7205
7333
  logger.logger.log('Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...');
7206
7334
  const beforeExitCode = process.exitCode;
@@ -7210,8 +7338,7 @@ async function generateAutoManifest({
7210
7338
  }
7211
7339
  if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) {
7212
7340
  logger.logger.log('Detected a Maven pom.xml build, generating Socket facts...');
7213
- const beforeExitCode = process.exitCode;
7214
- await convertMavenToFacts({
7341
+ const outcome = await convertMavenToFacts({
7215
7342
  // Configured bin wins; else prefer ./mvnw, else mvn on PATH.
7216
7343
  bin: sockJson.defaults?.manifest?.maven?.bin ?? resolveBuildToolBin('maven', cwd),
7217
7344
  cwd,
@@ -7221,12 +7348,12 @@ async function generateAutoManifest({
7221
7348
  includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '',
7222
7349
  javaHome: sockJson.defaults?.manifest?.maven?.javaHome ?? undefined,
7223
7350
  mavenOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.maven?.mavenOpts ?? undefined),
7224
- hints: AUTO_MANIFEST_HINTS,
7351
+ hints,
7225
7352
  sidecarAcc,
7226
7353
  verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose),
7227
7354
  withFiles: computeArtifactsSidecar
7228
7355
  });
7229
- abortManifestRunIfFailed('maven', beforeExitCode);
7356
+ abortFactsRunIfFailed('maven', outcome);
7230
7357
  }
7231
7358
  if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) {
7232
7359
  const condaFile = sockJson.defaults?.manifest?.conda?.infile || detected.condaFile || constants.ENVIRONMENT_YML;
@@ -7522,17 +7649,13 @@ async function createNewScan({
7522
7649
  logger.logger.success('Reachability analysis completed successfully');
7523
7650
  reachabilityReport = reachResult.data?.reachabilityReport;
7524
7651
 
7525
- // Mirror the SBOM inputs Coana analyzed; otherwise its fresh report
7526
- // (appended below) supersedes every facts file.
7527
- const pathsForScan = reach.reachUseOnlyPregeneratedSboms ? filterToPregeneratedSboms(packagePaths, supportedFiles).filter(p => !utils.isReachabilityReportPath(p, {
7652
+ // Upload Coana's inputs next to its report: depscan drops the report's
7653
+ // components without public metadata, such as private libraries.
7654
+ const pathsForScan = (reach.reachUseOnlyPregeneratedSboms ? filterToPregeneratedSboms(packagePaths, supportedFiles) : packagePaths).filter(p => !utils.isReachabilityReportPath(p, {
7528
7655
  cwd,
7529
7656
  outputPath: constants.default.DOT_SOCKET_DOT_FACTS_JSON
7530
- })) : packagePaths.filter(p => !utils.isSocketFactsFile(p));
7531
-
7532
- // Append coana's reachability report, but not twice: a pre-generated facts
7533
- // input can resolve to the same path coana wrote its report to.
7534
- const reportPath = reachabilityReport ? path.resolve(cwd, reachabilityReport) : undefined;
7535
- scanPaths = [...pathsForScan.filter(p => path.resolve(cwd, p) !== reportPath), ...(reachabilityReport ? [reachabilityReport] : [])];
7657
+ }));
7658
+ scanPaths = [...pathsForScan, ...(reachabilityReport ? [reachabilityReport] : [])];
7536
7659
  tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId;
7537
7660
  }
7538
7661
  }
@@ -7683,6 +7806,7 @@ async function handleCi(autoManifest) {
7683
7806
  reach: {
7684
7807
  dynamicSbomInference: false,
7685
7808
  excludePaths: [],
7809
+ reachAdvisories: [],
7686
7810
  reachAnalysisMemoryLimit: '',
7687
7811
  reachAnalysisTimeout: '',
7688
7812
  reachConcurrency: 1,
@@ -9811,8 +9935,6 @@ async function outputFixResult(result, outputKind) {
9811
9935
  logger.logger.success('Finished!');
9812
9936
  }
9813
9937
 
9814
- const GHSA_FORMAT_REGEXP = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/;
9815
- const CVE_FORMAT_REGEXP = /^CVE-\d{4}-\d{4,}$/;
9816
9938
  /**
9817
9939
  * Converts mixed CVE/GHSA/PURL IDs to GHSA IDs only.
9818
9940
  * Filters out invalid IDs and logs conversion results.
@@ -9828,51 +9950,12 @@ async function convertIdsToGhsas(ids, options) {
9828
9950
  debug.debugDir('inspect', {
9829
9951
  ids
9830
9952
  });
9831
- const validGhsas = [];
9832
- const errors = [];
9833
- for (const id of ids) {
9834
- const trimmedId = id.trim();
9835
- if (trimmedId.startsWith('GHSA-')) {
9836
- // Already a GHSA ID, validate format
9837
- if (GHSA_FORMAT_REGEXP.test(trimmedId)) {
9838
- validGhsas.push(trimmedId);
9839
- } else {
9840
- errors.push(`Invalid GHSA format: ${trimmedId}`);
9841
- }
9842
- } else if (trimmedId.startsWith('CVE-')) {
9843
- // Convert CVE to GHSA
9844
- if (!CVE_FORMAT_REGEXP.test(trimmedId)) {
9845
- errors.push(`Invalid CVE format: ${trimmedId}`);
9846
- continue;
9847
- }
9848
-
9849
- // eslint-disable-next-line no-await-in-loop
9850
- const conversionResult = await utils.convertCveToGhsa(trimmedId);
9851
- if (conversionResult.ok) {
9852
- validGhsas.push(conversionResult.data);
9853
- if (!silence) {
9854
- logger.logger.info(`Converted ${trimmedId} to ${conversionResult.data}`);
9855
- }
9856
- } else {
9857
- errors.push(`${trimmedId}: ${conversionResult.message}`);
9858
- }
9859
- } else if (trimmedId.startsWith('pkg:')) {
9860
- // Convert PURL to GHSAs.
9861
- // eslint-disable-next-line no-await-in-loop
9862
- const conversionResult = await utils.convertPurlToGhsas(trimmedId);
9863
- if (conversionResult.ok && conversionResult.data.length) {
9864
- validGhsas.push(...conversionResult.data);
9865
- if (!silence) {
9866
- logger.logger.info(`Converted ${trimmedId} to ${conversionResult.data.length} GHSA(s): ${arrays.joinAnd(conversionResult.data)}`);
9867
- }
9868
- } else {
9869
- errors.push(`${trimmedId}: ${conversionResult.message || 'No GHSAs found'}`);
9870
- }
9871
- } else {
9872
- // Neither CVE, GHSA, nor PURL, skip
9873
- errors.push(`Unsupported ID format (expected CVE, GHSA, or PURL): ${trimmedId}`);
9874
- }
9875
- }
9953
+ const {
9954
+ errors,
9955
+ ghsas: validGhsas
9956
+ } = await utils.resolveAdvisoryIds(ids, {
9957
+ silence
9958
+ });
9876
9959
  if (errors.length) {
9877
9960
  if (!silence) {
9878
9961
  logger.logger.warn(`Skipped ${errors.length} invalid IDs:\n${errors.map(e => ` - ${e}`).join('\n')}`);
@@ -9989,6 +10072,11 @@ const reachabilityFlags = {
9989
10072
  type: 'string',
9990
10073
  description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.COANA_CLI_VERSION}.`
9991
10074
  },
10075
+ reachAdvisories: {
10076
+ type: 'string',
10077
+ isMultiple: true,
10078
+ description: 'List of advisories to run reachability analysis for, as GHSA or CVE IDs, either as a comma separated value or as multiple flags. CVE IDs are converted to GHSA IDs through the GitHub API. Vulnerabilities from other advisories get unknown reachability. Defaults to all advisories.'
10079
+ },
9992
10080
  reachAnalysisMemoryLimit: {
9993
10081
  type: 'string',
9994
10082
  default: '8192',
@@ -10097,7 +10185,7 @@ const excludePathsFlag$1 = {
10097
10185
  excludePaths: {
10098
10186
  type: 'string',
10099
10187
  isMultiple: true,
10100
- description: 'List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (`--cwd` if set), not the reachability target: `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.'
10188
+ description: 'List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (`--cwd` if set), not the reachability target: `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module is not analysed as a project of its own, but where a project that is not excluded depends on it, its code is analysed as part of that project.'
10101
10189
  }
10102
10190
  };
10103
10191
 
@@ -12742,7 +12830,7 @@ const excludePathsFlag = {
12742
12830
  excludePaths: {
12743
12831
  type: 'string',
12744
12832
  isMultiple: true,
12745
- description: 'List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched relative to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.'
12833
+ description: 'List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched relative to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags. An excluded JVM build module is not analysed as a project of its own, but where a project that is not excluded depends on it, its code is analysed as part of that project.'
12746
12834
  }
12747
12835
  };
12748
12836
 
@@ -19935,6 +20023,37 @@ function resolveUvMemberDirs(targets, cwd) {
19935
20023
  return Array.from(memberDirs);
19936
20024
  }
19937
20025
 
20026
+ // GitHub publishes GHSA IDs with a lowercase body and CVE IDs uppercase.
20027
+ function normalizeAdvisoryId(id) {
20028
+ const trimmed = id.trim();
20029
+ return /^ghsa-/i.test(trimmed) ? `GHSA-${trimmed.slice(5).toLowerCase()}` : trimmed.toUpperCase();
20030
+ }
20031
+
20032
+ /**
20033
+ * Resolves the --reach-advisories IDs to GHSA IDs, converting CVE IDs through
20034
+ * the GitHub API. Any ID that cannot be resolved fails the command, since
20035
+ * dropping it would leave that advisory without reachability analysis.
20036
+ */
20037
+ async function resolveReachAdvisories(ids) {
20038
+ const normalizedIds = arrays.arrayUnique(ids.map(normalizeAdvisoryId));
20039
+ // Checked up front so PURLs never reach resolveAdvisoryIds and a typo fails
20040
+ // before any GitHub lookup.
20041
+ const invalidIds = normalizedIds.filter(id => !utils.GHSA_FORMAT_REGEXP.test(id) && !utils.CVE_FORMAT_REGEXP.test(id));
20042
+ if (invalidIds.length) {
20043
+ throw new utils.InputError(`Invalid --reach-advisories IDs: ${invalidIds.join(', ')}. Use GHSA IDs (GHSA-xxxx-xxxx-xxxx) or CVE IDs (CVE-YYYY-NNNN).`);
20044
+ }
20045
+ const {
20046
+ errors,
20047
+ ghsas
20048
+ } = await utils.resolveAdvisoryIds(normalizedIds, {
20049
+ silence: true
20050
+ });
20051
+ if (errors.length) {
20052
+ throw new utils.InputError(`Could not resolve --reach-advisories IDs to GHSA IDs:\n${errors.map(e => ` - ${e}`).join('\n')}`);
20053
+ }
20054
+ return arrays.arrayUnique(ghsas);
20055
+ }
20056
+
19938
20057
  async function suggestTarget() {
19939
20058
  // We could prefill this with sub-dirs of the current
19940
20059
  // dir ... but is that going to be useful?
@@ -20364,8 +20483,10 @@ async function run$d(argv, importMeta, {
20364
20483
  const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
20365
20484
  assertValidExcludePaths(excludePaths);
20366
20485
  const reachExcludePaths = utils.cmdFlagValueToArray(cli.flags['reachExcludePaths']);
20486
+ const reachAdvisoriesRaw = utils.cmdFlagValueToArray(cli.flags['reachAdvisories']);
20367
20487
 
20368
20488
  // Validation helpers for better readability.
20489
+ const hasReachAdvisories = reachAdvisoriesRaw.length > 0;
20369
20490
  const hasReachEcosystems = reachEcosystems.length > 0;
20370
20491
  const hasReachExcludePaths = reachExcludePaths.length > 0;
20371
20492
 
@@ -20383,7 +20504,7 @@ async function run$d(argv, importMeta, {
20383
20504
  // added flags require --reach automatically instead of relying on a
20384
20505
  // hand-maintained list. Deprecated no-ops are excluded on purpose.
20385
20506
  const isUsingAnyBooleanReachFlag = Object.entries(reachabilityFlags).some(([name, flag]) => flag.type === 'boolean' && !DEPRECATED_NO_OP_REACH_FLAGS.has(name) && cli.flags[name] !== flag.default);
20386
- const isUsingAnyReachabilityFlags = hasReachEcosystems || hasReachExcludePaths || isUsingAnyBooleanReachFlag || isUsingNonDefaultConcurrency || isUsingNonDefaultMemoryLimit || isUsingNonDefaultTimeout || isUsingNonDefaultVersion;
20507
+ const isUsingAnyReachabilityFlags = hasReachAdvisories || hasReachEcosystems || hasReachExcludePaths || isUsingAnyBooleanReachFlag || isUsingNonDefaultConcurrency || isUsingNonDefaultMemoryLimit || isUsingNonDefaultTimeout || isUsingNonDefaultVersion;
20387
20508
 
20388
20509
  // Validate target constraints when --reach is enabled.
20389
20510
  const reachTargetValidation = reach ? await validateReachabilityTarget(targets, cwd) : {
@@ -20460,6 +20581,7 @@ async function run$d(argv, importMeta, {
20460
20581
  logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
20461
20582
  return;
20462
20583
  }
20584
+ const reachAdvisories = await resolveReachAdvisories(reachAdvisoriesRaw);
20463
20585
  await handleCreateNewScan({
20464
20586
  autoManifest: Boolean(autoManifest),
20465
20587
  branchName: branchName,
@@ -20477,6 +20599,7 @@ async function run$d(argv, importMeta, {
20477
20599
  reach: {
20478
20600
  dynamicSbomInference: Boolean(dynamicSbomInference),
20479
20601
  excludePaths,
20602
+ reachAdvisories,
20480
20603
  reachAnalysisMemoryLimit,
20481
20604
  reachAnalysisTimeout,
20482
20605
  reachConcurrency: Number(reachConcurrency),
@@ -21195,6 +21318,7 @@ async function scanOneRepo(repoSlug, {
21195
21318
  reach: {
21196
21319
  dynamicSbomInference: false,
21197
21320
  excludePaths: [],
21321
+ reachAdvisories: [],
21198
21322
  reachAnalysisMemoryLimit: '',
21199
21323
  reachAnalysisTimeout: '',
21200
21324
  reachConcurrency: 1,
@@ -22623,6 +22747,7 @@ async function run$7(argv, importMeta, {
22623
22747
 
22624
22748
  // Process comma-separated values for isMultiple flags.
22625
22749
  const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
22750
+ const reachAdvisoriesRaw = utils.cmdFlagValueToArray(cli.flags['reachAdvisories']);
22626
22751
  const reachEcosystemsRaw = utils.cmdFlagValueToArray(cli.flags['reachEcosystems']);
22627
22752
  const reachExcludePaths = utils.cmdFlagValueToArray(cli.flags['reachExcludePaths']);
22628
22753
  assertValidExcludePaths(excludePaths);
@@ -22695,6 +22820,7 @@ async function run$7(argv, importMeta, {
22695
22820
  logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
22696
22821
  return;
22697
22822
  }
22823
+ const reachAdvisories = await resolveReachAdvisories(reachAdvisoriesRaw);
22698
22824
  await handleScanReach({
22699
22825
  cwd,
22700
22826
  interactive,
@@ -22704,6 +22830,7 @@ async function run$7(argv, importMeta, {
22704
22830
  reachabilityOptions: {
22705
22831
  dynamicSbomInference: Boolean(dynamicSbomInference),
22706
22832
  excludePaths,
22833
+ reachAdvisories,
22707
22834
  reachAnalysisMemoryLimit,
22708
22835
  reachAnalysisTimeout,
22709
22836
  reachConcurrency: Number(reachConcurrency),
@@ -24668,5 +24795,5 @@ process.on('unhandledRejection', async (reason, promise) => {
24668
24795
  // eslint-disable-next-line n/no-process-exit
24669
24796
  process.exit(1);
24670
24797
  });
24671
- //# debugId=ccb7632d-f5e4-4589-8d85-1814c416c744
24798
+ //# debugId=5d7a85dd-501f-4419-b550-bef90180bd6a
24672
24799
  //# sourceMappingURL=cli.js.map