@socketsecurity/cli-with-sentry 1.1.131 → 1.1.133

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 (55) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli.js +1302 -139
  3. package/dist/cli.js.map +1 -1
  4. package/dist/constants.js +4 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
  7. package/dist/manifest-scripts/socket-facts.init.gradle +479 -0
  8. package/dist/manifest-scripts/socket-facts.plugin.scala +444 -0
  9. package/dist/tsconfig.dts.tsbuildinfo +1 -1
  10. package/dist/types/commands/manifest/cmd-manifest-gradle.d.mts.map +1 -1
  11. package/dist/types/commands/manifest/cmd-manifest-kotlin.d.mts.map +1 -1
  12. package/dist/types/commands/manifest/cmd-manifest-maven.d.mts.map +1 -1
  13. package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +5 -6
  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 +5 -6
  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 +7 -8
  18. package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -1
  19. package/dist/types/commands/manifest/generate_auto_manifest.d.mts +6 -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 +21 -0
  22. package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -0
  23. package/dist/types/commands/manifest/scripts/assemble.d.mts +16 -0
  24. package/dist/types/commands/manifest/scripts/assemble.d.mts.map +1 -0
  25. package/dist/types/commands/manifest/scripts/build-tool.d.mts +3 -0
  26. package/dist/types/commands/manifest/scripts/build-tool.d.mts.map +1 -0
  27. package/dist/types/commands/manifest/scripts/facts.d.mts +46 -0
  28. package/dist/types/commands/manifest/scripts/facts.d.mts.map +1 -0
  29. package/dist/types/commands/manifest/scripts/records.d.mts +64 -0
  30. package/dist/types/commands/manifest/scripts/records.d.mts.map +1 -0
  31. package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts +10 -0
  32. package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts.map +1 -0
  33. package/dist/types/commands/manifest/scripts/resolution-report-ivy.d.mts +7 -0
  34. package/dist/types/commands/manifest/scripts/resolution-report-ivy.d.mts.map +1 -0
  35. package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts +8 -0
  36. package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts.map +1 -0
  37. package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts +41 -0
  38. package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts.map +1 -0
  39. package/dist/types/commands/manifest/scripts/resolution-report.d.mts +12 -0
  40. package/dist/types/commands/manifest/scripts/resolution-report.d.mts.map +1 -0
  41. package/dist/types/commands/manifest/scripts/run.d.mts +31 -0
  42. package/dist/types/commands/manifest/scripts/run.d.mts.map +1 -0
  43. package/dist/types/commands/manifest/scripts/sidecar.d.mts +27 -0
  44. package/dist/types/commands/manifest/scripts/sidecar.d.mts.map +1 -0
  45. package/dist/types/commands/scan/cmd-scan-create.d.mts.map +1 -1
  46. package/dist/types/commands/scan/handle-create-new-scan.d.mts.map +1 -1
  47. package/dist/types/commands/scan/perform-reachability-analysis.d.mts +4 -2
  48. package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
  49. package/dist/utils.js +1 -78
  50. package/dist/utils.js.map +1 -1
  51. package/package.json +3 -2
  52. package/dist/types/commands/manifest/coana-manifest-facts.d.mts +0 -27
  53. package/dist/types/commands/manifest/coana-manifest-facts.d.mts.map +0 -1
  54. package/dist/types/utils/auto-manifest-config.d.mts +0 -55
  55. package/dist/types/utils/auto-manifest-config.d.mts.map +0 -1
package/dist/cli.js CHANGED
@@ -1752,6 +1752,7 @@ async function performReachabilityAnalysis(options) {
1752
1752
  packagePaths,
1753
1753
  reachabilityOptions,
1754
1754
  repoName,
1755
+ resolvedPathsSidecar,
1755
1756
  spinner,
1756
1757
  target,
1757
1758
  uploadManifests = true
@@ -1840,25 +1841,18 @@ async function performReachabilityAnalysis(options) {
1840
1841
  spinner?.infoAndStop('Running reachability analysis with Coana...');
1841
1842
  const outputFilePath = outputPath || constants.default.DOT_SOCKET_DOT_FACTS_JSON;
1842
1843
 
1843
- // Coana reads `--auto-manifest-config` from a JSON file, so write the resolved
1844
- // per-ecosystem build-tool config (mapped from socket.json) to a temp file and
1845
- // pass its absolute path. Cleaned up in the finally below.
1846
- let autoManifestConfigPath;
1847
- const {
1848
- autoManifestConfig
1849
- } = reachabilityOptions;
1850
- if (autoManifestConfig && !utils.isAutoManifestConfigEmpty(autoManifestConfig)) {
1851
- autoManifestConfigPath = path.join(os.tmpdir(), `socket-auto-manifest-config-${require$$0.randomUUID()}.json`);
1852
- await fs$1.promises.writeFile(autoManifestConfigPath, JSON.stringify(autoManifestConfig), 'utf8');
1844
+ // Write the sidecar to a temp file for `--compute-artifacts-sidecar`; cleaned
1845
+ // up in the finally below.
1846
+ let sidecarPath;
1847
+ if (resolvedPathsSidecar?.length) {
1848
+ sidecarPath = path.join(os.tmpdir(), `socket-compute-artifacts-sidecar-${require$$0.randomUUID()}.json`);
1849
+ await fs$1.promises.writeFile(sidecarPath, JSON.stringify(resolvedPathsSidecar), 'utf8');
1853
1850
  }
1854
1851
 
1855
1852
  // Build Coana arguments.
1856
1853
  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']), ...(tarHash ? ['--run-without-docker', '--manifests-tar-hash', tarHash] : []),
1857
1854
  // Empty reachEcosystems implies scanning all ecosystems.
1858
- ...(reachabilityOptions.reachEcosystems.length ? ['--purl-types', ...reachabilityOptions.reachEcosystems] : []), ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []), ...(reachabilityOptions.reachLazyMode ? ['--lazy-mode'] : []), ...(reachabilityOptions.reachSkipCache ? ['--skip-cache-usage'] : []), ...(reachabilityOptions.reachUseOnlyPregeneratedSboms ? ['--use-only-pregenerated-sboms'] : []),
1859
- // Hand the per-ecosystem build-tool config (mapped from socket.json) to
1860
- // Coana's reach-time resolution, as a temp JSON file path.
1861
- ...(autoManifestConfigPath ? ['--auto-manifest-config', autoManifestConfigPath] : [])];
1855
+ ...(reachabilityOptions.reachEcosystems.length ? ['--purl-types', ...reachabilityOptions.reachEcosystems] : []), ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []), ...(reachabilityOptions.reachLazyMode ? ['--lazy-mode'] : []), ...(reachabilityOptions.reachSkipCache ? ['--skip-cache-usage'] : []), ...(reachabilityOptions.reachUseOnlyPregeneratedSboms ? ['--use-only-pregenerated-sboms'] : []), ...(sidecarPath ? ['--compute-artifacts-sidecar', sidecarPath] : [])];
1862
1856
 
1863
1857
  // Build environment variables.
1864
1858
  const coanaEnv = {};
@@ -1915,10 +1909,10 @@ async function performReachabilityAnalysis(options) {
1915
1909
  }
1916
1910
  };
1917
1911
  } finally {
1918
- // The run no longer needs the temp config file; best-effort cleanup.
1919
- if (autoManifestConfigPath) {
1912
+ // Best-effort cleanup of the temp sidecar.
1913
+ if (sidecarPath) {
1920
1914
  try {
1921
- await fs$1.promises.unlink(autoManifestConfigPath);
1915
+ await fs$1.promises.unlink(sidecarPath);
1922
1916
  } catch {
1923
1917
  // File may already be gone or unwritable.
1924
1918
  }
@@ -4282,102 +4276,1238 @@ async function extractBazelToMaven(opts) {
4282
4276
  }
4283
4277
  }
4284
4278
 
4285
- // Delegates Socket facts generation for a JVM build tool to the Coana CLI's
4286
- // `manifest <ecosystem>` command. The build-tool resolution scripts (the Gradle
4287
- // init script and the sbt plugin) live in Coana now, so socket-cli no longer
4288
- // runs them itself; it only asks Coana for the uploadable `.socket.facts.json`.
4279
+ // Gradle's variant-aware resolver: distinct exceptions give mutually-exclusive
4280
+ // phrasing, so most-specific-first substring checks classify reliably.
4281
+ function classifyGradleFailure(detail) {
4282
+ const t = (detail || '').toLowerCase();
4283
+ // Check before variant ambiguity.
4284
+ if (t.includes('conflict on capability')) {
4285
+ return 'capability-conflict';
4286
+ }
4287
+ // Zero compatible variants — the opposite of ambiguity below.
4288
+ if (t.includes('no matching variant') || t.includes('no variants of')) {
4289
+ return 'no-matching-variant';
4290
+ }
4291
+ if (t.includes('cannot choose between')) {
4292
+ return 'variant-ambiguity';
4293
+ }
4294
+ 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')) {
4295
+ return 'repository-or-network';
4296
+ }
4297
+ if (t.includes('could not find')) {
4298
+ return 'not-found';
4299
+ }
4300
+ return 'other';
4301
+ }
4302
+
4303
+ // Every kind blocks except variant-ambiguity: the module demonstrably exists
4304
+ // and is almost always captured via another configuration, so it's benign for
4305
+ // the SBOM (non-blocking, count only). A module that resolves in NO config
4306
+ // surfaces as not-found / no-matching-variant, which stay blocking.
4307
+ const GRADLE_DIALECT = {
4308
+ label: 'Gradle',
4309
+ classify: classifyGradleFailure,
4310
+ categories: [{
4311
+ key: 'not-found',
4312
+ header: () => ` Not found in any repository:`,
4313
+ blocking: true
4314
+ }, {
4315
+ key: 'no-matching-variant',
4316
+ header: n => ` No compatible variant — ${n} found the module but no variant matched the requested attributes:`,
4317
+ blocking: true
4318
+ }, {
4319
+ key: 'capability-conflict',
4320
+ header: n => ` Capability conflict — ${n} found two modules providing the same capability and cannot use both (add a capability-resolution or module-replacement rule):`,
4321
+ showReason: true,
4322
+ blocking: true
4323
+ }, {
4324
+ key: 'repository-or-network',
4325
+ header: n => ` Repository or network error — ${n} could not reach or authenticate to a repository:`,
4326
+ blocking: true
4327
+ }, {
4328
+ key: 'config-problem',
4329
+ header: n => ` Resolver/configuration problem (reason from ${n}):`,
4330
+ showReason: true,
4331
+ blocking: true
4332
+ }, {
4333
+ key: 'other',
4334
+ header: n => ` Other resolution failures (reason from ${n}):`,
4335
+ showReason: true,
4336
+ blocking: true
4337
+ }, {
4338
+ key: 'variant-ambiguity',
4339
+ blocking: false,
4340
+ notice: (n, depCount, configCount) => `Skipped ${depCount} dependency(ies) with ambiguous variant selection in ${configCount} configuration(s) — re-run with --verbose for ${n}'s messages.`
4341
+ }]
4342
+ };
4343
+
4344
+ // Ivy/sbt resolver: no attribute-based variants, so no variant categories
4345
+ // (Gradle-only). Ivy degrades transport failures to "not found".
4346
+ function classifyIvyFailure(detail) {
4347
+ const t = (detail || '').toLowerCase();
4348
+ if (t.includes('server access error') || t.includes('download failed') || t.includes('connection timed out') || t.includes('unauthorized') || t.includes('forbidden')) {
4349
+ return 'repository-or-network';
4350
+ }
4351
+ if (t.includes('no resolver found') || t.includes('configuration not found') || t.includes('configuration not public')) {
4352
+ return 'config-problem';
4353
+ }
4354
+ if (t.includes('not found') || t.includes('unresolved dependency')) {
4355
+ return 'not-found';
4356
+ }
4357
+ return 'other';
4358
+ }
4359
+
4360
+ // Every kind blocks; no non-blocking kind because Ivy has no variant ambiguity.
4361
+ const SBT_DIALECT = {
4362
+ label: 'sbt',
4363
+ classify: classifyIvyFailure,
4364
+ categories: [{
4365
+ key: 'not-found',
4366
+ header: () => ` Not found in any repository:`,
4367
+ blocking: true
4368
+ }, {
4369
+ key: 'repository-or-network',
4370
+ header: n => ` Repository or network error — ${n} could not reach or authenticate to a repository:`,
4371
+ blocking: true
4372
+ }, {
4373
+ key: 'config-problem',
4374
+ header: n => ` Resolver/configuration problem (reason from ${n}):`,
4375
+ showReason: true,
4376
+ blocking: true
4377
+ }, {
4378
+ key: 'other',
4379
+ header: n => ` Other resolution failures (reason from ${n}):`,
4380
+ showReason: true,
4381
+ blocking: true
4382
+ }]
4383
+ };
4384
+
4385
+ // Maven's resolver (Aether/maven-resolver): no attribute-based variants. Two
4386
+ // failure shapes (artifact-resolution miss with config = scope, dependency-graph
4387
+ // build failure with config = "graph") both classify off the root-cause message.
4388
+ function classifyMavenFailure(detail) {
4389
+ const t = (detail || '').toLowerCase();
4390
+ 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')) {
4391
+ return 'repository-or-network';
4392
+ }
4393
+ 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')) {
4394
+ return 'not-found';
4395
+ }
4396
+ // POM exists but can't be read/parsed.
4397
+ if (t.includes('failed to read artifact descriptor') || t.includes('invalid pom') || t.includes('could not parse pom')) {
4398
+ return 'config-problem';
4399
+ }
4400
+ return 'other';
4401
+ }
4402
+
4403
+ // Every kind blocks; no non-blocking kind because Maven has no variant ambiguity.
4404
+ const MAVEN_DIALECT = {
4405
+ label: 'Maven',
4406
+ classify: classifyMavenFailure,
4407
+ categories: [{
4408
+ key: 'not-found',
4409
+ header: () => ` Not found in any repository:`,
4410
+ blocking: true
4411
+ }, {
4412
+ key: 'repository-or-network',
4413
+ header: n => ` Repository or network error — ${n} could not reach or authenticate to a repository:`,
4414
+ blocking: true
4415
+ }, {
4416
+ key: 'config-problem',
4417
+ header: n => ` POM/descriptor problem (reason from ${n}):`,
4418
+ showReason: true,
4419
+ blocking: true
4420
+ }, {
4421
+ key: 'other',
4422
+ header: n => ` Other resolution failures (reason from ${n}):`,
4423
+ showReason: true,
4424
+ blocking: true
4425
+ }]
4426
+ };
4427
+
4428
+ // Recognized from the build tool's message; drives wording AND whether the kind
4429
+ // is blocking. An unrecognized message degrades to 'other' (blocking) — safe.
4430
+
4431
+ // Per-resolver classify + render/score policy.
4432
+
4433
+ const RESOLUTION_REPORT_ARTIFACT_LIMIT = 15;
4434
+ const RESOLUTION_REPORT_CONFIG_LIMIT = 20;
4435
+
4436
+ // Drop a bare "group:name" when a versioned "group:name:v" of the same module
4437
+ // is also present: the lenient resolver reports both forms for one failure.
4438
+ function dedupCoords(coords) {
4439
+ const set = new Set(coords);
4440
+ const versioned = new Set();
4441
+ for (const c of set) {
4442
+ const p = c.split(':');
4443
+ if (p.length >= 3) {
4444
+ versioned.add(`${p[0]}:${p[1]}`);
4445
+ }
4446
+ }
4447
+ return [...set].filter(c => c.split(':').length >= 3 || !versioned.has(c)).sort();
4448
+ }
4449
+ function fmtList(list, limit) {
4450
+ const shown = list.slice(0, limit).join(', ');
4451
+ return list.length > limit ? `${shown} (+${list.length - limit} more)` : shown;
4452
+ }
4453
+ function firstLine(s) {
4454
+ return (s || '').split('\n').map(l => l.trim()).find(Boolean) ?? '';
4455
+ }
4456
+
4457
+ // Severity is per-kind; the exit-code decision lives in the caller. We do NOT
4458
+ // cross-reference what resolved elsewhere: the failed selector carries no
4459
+ // classifier/type, so relating a failed and a succeeded dep is unsound.
4460
+ function renderResolutionReport(failures, scannedConfigs, dialect, opts = {}) {
4461
+ const name = dialect.label;
4462
+ const specOf = new Map(dialect.categories.map(c => [c.key, c]));
4463
+ const isBlocking = cat => specOf.get(cat)?.blocking ?? true;
4464
+
4465
+ // Aggregate by (coord, category): one module can fail with different causes
4466
+ // across configs. Keep first-seen detail, union the configs.
4467
+
4468
+ const byKey = new Map();
4469
+ const keyOf = (coord, category) => `${coord} ${category}`;
4470
+ for (const f of failures) {
4471
+ const category = dialect.classify(f.detail);
4472
+ const key = keyOf(f.coord, category);
4473
+ let info = byKey.get(key);
4474
+ if (!info) {
4475
+ info = {
4476
+ coord: f.coord,
4477
+ category,
4478
+ detail: f.detail,
4479
+ configs: new Set()
4480
+ };
4481
+ byKey.set(key, info);
4482
+ }
4483
+ if (f.config) {
4484
+ info.configs.add(f.config);
4485
+ }
4486
+ }
4487
+ const allInfos = [...byKey.values()];
4488
+ const blockingConfigs = new Set();
4489
+ for (const info of allInfos) {
4490
+ if (isBlocking(info.category)) {
4491
+ for (const c of info.configs) {
4492
+ blockingConfigs.add(c);
4493
+ }
4494
+ }
4495
+ }
4496
+ const blockingFailed = [...blockingConfigs].sort();
4497
+ const succeeded = scannedConfigs.filter(c => !blockingConfigs.has(c)).sort();
4498
+ const groups = dialect.categories.map(spec => ({
4499
+ spec,
4500
+ infos: dedupCoords(allInfos.filter(i => i.category === spec.key).map(i => i.coord)).map(c => byKey.get(keyOf(c, spec.key)))
4501
+ })).filter(g => g.infos.length);
4502
+ const blockingGroups = groups.filter(g => g.spec.blocking);
4503
+ const nonBlockingGroups = groups.filter(g => !g.spec.blocking);
4504
+ const blockingCount = blockingGroups.reduce((n, g) => n + g.infos.length, 0);
4505
+ const hasBlockingFailures = blockingCount > 0;
4506
+ const willFail = hasBlockingFailures && !opts.ignoreUnresolved;
4507
+ const out = [];
4508
+ if (hasBlockingFailures) {
4509
+ out.push(opts.ignoreUnresolved ? `Ignored ${blockingCount} unresolved dependency(ies) in ${blockingFailed.length} configuration(s):` : `Could not resolve ${blockingCount} dependency(ies) in ${blockingFailed.length} configuration(s):`);
4510
+ for (const {
4511
+ infos,
4512
+ spec
4513
+ } of blockingGroups) {
4514
+ out.push('');
4515
+ out.push(spec.header ? spec.header(name) : '');
4516
+ for (const info of infos.slice(0, RESOLUTION_REPORT_ARTIFACT_LIMIT)) {
4517
+ const fl = firstLine(info.detail);
4518
+ const reasonSuffix = spec.showReason && fl ? ` [${fl}]` : '';
4519
+ out.push(` - ${info.coord}${reasonSuffix}`);
4520
+ }
4521
+ if (infos.length > RESOLUTION_REPORT_ARTIFACT_LIMIT) {
4522
+ out.push(` … and ${infos.length - RESOLUTION_REPORT_ARTIFACT_LIMIT} more`);
4523
+ }
4524
+ }
4525
+ out.push('');
4526
+ if (succeeded.length) {
4527
+ out.push(`Resolution succeeded in: ${fmtList(succeeded, RESOLUTION_REPORT_CONFIG_LIMIT)}`);
4528
+ }
4529
+ if (blockingFailed.length) {
4530
+ out.push(`Resolution failed in: ${fmtList(blockingFailed, RESOLUTION_REPORT_CONFIG_LIMIT)}`);
4531
+ }
4532
+ if (willFail) {
4533
+ out.push('');
4534
+ out.push(`To proceed, re-run with either:`);
4535
+ out.push(` --ignore-unresolved`);
4536
+ if (blockingFailed.length) {
4537
+ out.push(` --exclude-configs '${blockingFailed.join(',')}'`);
4538
+ }
4539
+ }
4540
+ out.push('');
4541
+ out.push(`Re-run with --verbose for ${name}'s full messages.`);
4542
+ }
4543
+ const notices = [];
4544
+ for (const {
4545
+ infos,
4546
+ spec
4547
+ } of nonBlockingGroups) {
4548
+ if (!spec.notice) {
4549
+ continue;
4550
+ }
4551
+ const configCount = new Set(infos.flatMap(i => [...i.configs])).size;
4552
+ notices.push(spec.notice(name, infos.length, configCount));
4553
+ }
4554
+ const detailLines = [`${name}'s full message for each unresolved dependency:`];
4555
+ for (const info of allInfos) {
4556
+ detailLines.push('');
4557
+ detailLines.push(` ${info.coord}:`);
4558
+ for (const line of (info.detail || '(no message)').split('\n')) {
4559
+ detailLines.push(` ${line}`);
4560
+ }
4561
+ }
4562
+ return {
4563
+ summary: out.join('\n'),
4564
+ details: detailLines.join('\n'),
4565
+ hasBlockingFailures,
4566
+ nonBlockingNotice: notices.join('\n')
4567
+ };
4568
+ }
4569
+ function dialectFor(tool) {
4570
+ switch (tool) {
4571
+ case 'gradle':
4572
+ return GRADLE_DIALECT;
4573
+ case 'sbt':
4574
+ return SBT_DIALECT;
4575
+ case 'maven':
4576
+ return MAVEN_DIALECT;
4577
+ }
4578
+ }
4579
+ function renderResolutionErrorReport(failures, scannedConfigs = [], tool = 'gradle', opts = {}) {
4580
+ return renderResolutionReport(failures, scannedConfigs, dialectFor(tool), opts);
4581
+ }
4582
+
4583
+ // No sources/targets here: those are local absolute paths, returned in-memory
4584
+ // as ResolvedArtifactPaths, never serialized into the SBOM.
4585
+
4586
+ // Resolved on-disk paths for a --with-files run, keyed by coordinate. `targets`
4587
+ // = classpath entries (jars / module output dirs); `sources` = module source
4588
+ // roots.
4589
+
4590
+ // Coordinate-based (not `id`-based) so it also matches foreign SBOMs like
4591
+ // CycloneDX. Empty segments dropped.
4592
+ function mavenCoordinateKey(groupId, artifactId, type, classifier, version) {
4593
+ return [groupId, artifactId, type, classifier, version].filter(Boolean).join(':');
4594
+ }
4595
+
4596
+ const PURL_TYPE_MAVEN = 'maven';
4597
+ function assembleFacts(parsed, opts = {}) {
4598
+ const fileExists = opts.fileExists ?? fs$1.existsSync;
4599
+ const perRoot = buildPerRoot(parsed);
4600
+ const {
4601
+ directByRoot,
4602
+ finalNodes
4603
+ } = mergePathSensitive(perRoot);
4604
+ const tool = parsed.tool || 'gradle';
4605
+ const components = buildComponents(finalNodes);
4606
+ const projects = opts.emitProjects === false ? [] : buildProjects(parsed, finalNodes, directByRoot, perRoot);
4607
+ const metadata = {
4608
+ format: 'socket-facts-sbom',
4609
+ tool,
4610
+ toolVersion: parsed.toolVersion,
4611
+ ...(parsed.javaVersion ? {
4612
+ javaVersion: parsed.javaVersion
4613
+ } : {})
4614
+ };
4615
+ const facts = projects.length ? {
4616
+ metadata,
4617
+ projects,
4618
+ components
4619
+ } : {
4620
+ metadata,
4621
+ components
4622
+ };
4623
+ return {
4624
+ facts,
4625
+ report: buildReport(parsed),
4626
+ artifactPaths: buildArtifactPaths(finalNodes, [...parsed.projects.values()], fileExists)
4627
+ };
4628
+ }
4629
+ function gav(group, name, version) {
4630
+ return `${group}:${name}:${version}`;
4631
+ }
4632
+ function shortHash(s) {
4633
+ return require$$0.createHash('sha256').update(s, 'utf8').digest('hex').slice(0, 12);
4634
+ }
4635
+ function buildPerRoot(parsed) {
4636
+ const out = new Map();
4637
+ for (const [rootId, r] of parsed.roots) {
4638
+ const childrenByParent = new Map();
4639
+ for (const [p, c] of r.edges) {
4640
+ if (!r.nodes.has(p) || !r.nodes.has(c)) {
4641
+ continue;
4642
+ }
4643
+ let set = childrenByParent.get(p);
4644
+ if (!set) {
4645
+ set = new Set();
4646
+ childrenByParent.set(p, set);
4647
+ }
4648
+ set.add(c);
4649
+ }
4650
+ const nodes = new Map();
4651
+ for (const [coordId, n] of r.nodes) {
4652
+ nodes.set(coordId, {
4653
+ coord: n.coord,
4654
+ children: [...(childrenByParent.get(coordId) ?? [])].sort(),
4655
+ direct: n.direct,
4656
+ targets: n.targets
4657
+ });
4658
+ }
4659
+ out.set(rootId, {
4660
+ projectKey: r.projectKey,
4661
+ prod: r.prod,
4662
+ nodes
4663
+ });
4664
+ }
4665
+ return out;
4666
+ }
4667
+
4668
+ // A coordinate with identical subtrees everywhere collapses to one node (id =
4669
+ // coordId); divergent subtrees each get a content-addressed id
4670
+ // (`<coordId>#<subtree-hash>`) so per-subproject overrides stay distinct.
4671
+ function mergePathSensitive(perRoot) {
4672
+ const memo = new Map();
4673
+ const nodesOf = rootId => perRoot.get(rootId)?.nodes;
4674
+ function computeSig(rootId, coordId, onPath) {
4675
+ const memoKey = rootId + ' ' + coordId;
4676
+ const cached = memo.get(memoKey);
4677
+ if (cached !== undefined) {
4678
+ return cached;
4679
+ }
4680
+ if (onPath.has(coordId)) {
4681
+ // Cycle: back-edge as leaf.
4682
+ return coordId;
4683
+ }
4684
+ const node = nodesOf(rootId)?.get(coordId);
4685
+ if (!node) {
4686
+ return coordId;
4687
+ }
4688
+ onPath.add(coordId);
4689
+ const childSigs = node.children.map(c => computeSig(rootId, c, onPath));
4690
+ onPath.delete(coordId);
4691
+ // Digest, not the raw string: caching expanded subtree strings OOMs on
4692
+ // reconverging DAGs; a fixed-size digest keeps the pass O(V+E).
4693
+ const sig = coordId + '{' + childSigs.join(',') + '}';
4694
+ const digest = require$$0.createHash('sha256').update(sig, 'utf8').digest('hex').slice(0, 16);
4695
+ memo.set(memoKey, digest);
4696
+ return digest;
4697
+ }
4698
+
4699
+ // Sorted iteration keeps cyclic-graph signatures stable run-to-run.
4700
+ const sigsByCoord = new Map();
4701
+ for (const rootId of [...perRoot.keys()].sort()) {
4702
+ const nodes = perRoot.get(rootId).nodes;
4703
+ for (const coordId of [...nodes.keys()].sort()) {
4704
+ const sig = computeSig(rootId, coordId, new Set());
4705
+ let set = sigsByCoord.get(coordId);
4706
+ if (!set) {
4707
+ set = new Set();
4708
+ sigsByCoord.set(coordId, set);
4709
+ }
4710
+ set.add(sig);
4711
+ }
4712
+ }
4713
+ const divergent = coordId => (sigsByCoord.get(coordId)?.size ?? 0) > 1;
4714
+ const emittedIdMemo = new Map();
4715
+ const emittedIdFor = (rootId, coordId) => {
4716
+ const k = rootId + ' ' + coordId;
4717
+ let v = emittedIdMemo.get(k);
4718
+ if (v === undefined) {
4719
+ v = divergent(coordId) ? coordId + '#' + shortHash(computeSig(rootId, coordId, new Set())) : coordId;
4720
+ emittedIdMemo.set(k, v);
4721
+ }
4722
+ return v;
4723
+ };
4724
+ const finalNodes = new Map();
4725
+ const directByRoot = new Map();
4726
+ for (const [rootId, {
4727
+ nodes,
4728
+ prod
4729
+ }] of perRoot) {
4730
+ for (const [coordId, node] of nodes) {
4731
+ const eid = emittedIdFor(rootId, coordId);
4732
+ let fn = finalNodes.get(eid);
4733
+ if (!fn) {
4734
+ fn = {
4735
+ coord: node.coord,
4736
+ children: new Set(),
4737
+ prod: false,
4738
+ direct: false,
4739
+ targets: new Set()
4740
+ };
4741
+ finalNodes.set(eid, fn);
4742
+ }
4743
+ if (prod) {
4744
+ fn.prod = true;
4745
+ }
4746
+ if (node.direct) {
4747
+ fn.direct = true;
4748
+ }
4749
+ for (const c of node.children) {
4750
+ fn.children.add(emittedIdFor(rootId, c));
4751
+ }
4752
+ for (const t of node.targets) {
4753
+ fn.targets.add(t);
4754
+ }
4755
+ if (node.direct) {
4756
+ let d = directByRoot.get(rootId);
4757
+ if (!d) {
4758
+ d = new Set();
4759
+ directByRoot.set(rootId, d);
4760
+ }
4761
+ d.add(eid);
4762
+ }
4763
+ }
4764
+ }
4765
+ return {
4766
+ finalNodes,
4767
+ directByRoot
4768
+ };
4769
+ }
4770
+ function buildComponents(finalNodes) {
4771
+ return [...finalNodes.keys()].sort().map(id => {
4772
+ const fn = finalNodes.get(id);
4773
+ const c = fn.coord;
4774
+ const qualifiers = {
4775
+ __proto__: null
4776
+ };
4777
+ if (c.classifier) {
4778
+ qualifiers['classifier'] = c.classifier;
4779
+ }
4780
+ if (c.ext) {
4781
+ qualifiers['ext'] = c.ext;
4782
+ }
4783
+ const comp = {
4784
+ type: PURL_TYPE_MAVEN,
4785
+ namespace: c.group,
4786
+ name: c.name,
4787
+ ...(c.version ? {
4788
+ version: c.version
4789
+ } : {}),
4790
+ ...(Object.keys(qualifiers).length ? {
4791
+ qualifiers
4792
+ } : {}),
4793
+ id
4794
+ };
4795
+ if (fn.direct) {
4796
+ comp.direct = true;
4797
+ }
4798
+ if (!fn.prod) {
4799
+ comp.dev = true;
4800
+ }
4801
+ if (fn.children.size) {
4802
+ comp.dependencies = [...fn.children].sort();
4803
+ }
4804
+ return comp;
4805
+ });
4806
+ }
4807
+ function buildProjects(parsed, finalNodes, directByRoot, perRoot) {
4808
+ const idsByGav = new Map();
4809
+ for (const [id, fn] of finalNodes) {
4810
+ const key = gav(fn.coord.group, fn.coord.name, fn.coord.version ?? '');
4811
+ let set = idsByGav.get(key);
4812
+ if (!set) {
4813
+ set = new Set();
4814
+ idsByGav.set(key, set);
4815
+ }
4816
+ set.add(id);
4817
+ }
4818
+ const directByProject = new Map();
4819
+ for (const [rootId, ids] of directByRoot) {
4820
+ const pk = perRoot.get(rootId)?.projectKey ?? '';
4821
+ let set = directByProject.get(pk);
4822
+ if (!set) {
4823
+ set = new Set();
4824
+ directByProject.set(pk, set);
4825
+ }
4826
+ for (const id of ids) {
4827
+ set.add(id);
4828
+ }
4829
+ }
4830
+ const projects = [...parsed.projects.values()].map(p => {
4831
+ const entry = {
4832
+ type: PURL_TYPE_MAVEN,
4833
+ namespace: p.group,
4834
+ name: p.name,
4835
+ ...(p.version ? {
4836
+ version: p.version
4837
+ } : {}),
4838
+ subprojectDir: p.dir,
4839
+ dependencies: [...(directByProject.get(p.projectKey) ?? [])].sort(),
4840
+ resolvedAs: [...(idsByGav.get(gav(p.group, p.name, p.version)) ?? [])].sort()
4841
+ };
4842
+ return entry;
4843
+ });
4844
+ projects.sort((a, b) => {
4845
+ const ka = `${a.subprojectDir} ${a.namespace}:${a.name}`;
4846
+ const kb = `${b.subprojectDir} ${b.namespace}:${b.name}`;
4847
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
4848
+ });
4849
+ return projects;
4850
+ }
4851
+ function unionInto(map, key, add) {
4852
+ if (!add.length) {
4853
+ return;
4854
+ }
4855
+ const acc = map.get(key);
4856
+ if (acc) {
4857
+ for (const f of add) {
4858
+ if (!acc.includes(f)) {
4859
+ acc.push(f);
4860
+ }
4861
+ }
4862
+ } else {
4863
+ map.set(key, [...add]);
4864
+ }
4865
+ }
4866
+ function buildArtifactPaths(finalNodes, projects, fileExists) {
4867
+ const projectsByGav = new Map();
4868
+ for (const p of projects) {
4869
+ projectsByGav.set(gav(p.group, p.name, p.version), {
4870
+ sources: p.sources,
4871
+ targets: p.targets
4872
+ });
4873
+ }
4874
+ const targetsByCoord = new Map();
4875
+ const targetsByGav = new Map();
4876
+ const sourcesByCoord = new Map();
4877
+ const coords = new Set();
4878
+ for (const fn of finalNodes.values()) {
4879
+ const c = fn.coord;
4880
+ const coordKey = mavenCoordinateKey(c.group, c.name, c.ext, c.classifier, c.version);
4881
+ if (!coordKey) {
4882
+ continue;
4883
+ }
4884
+ coords.add(coordKey);
4885
+ const pi = projectsByGav.get(gav(c.group, c.name, c.version ?? ''));
4886
+ const sources = (pi?.sources ?? []).filter(fileExists);
4887
+ const targets = [...new Set([...fn.targets, ...(pi?.targets ?? [])])].filter(fileExists).sort();
4888
+ if (sources.length) {
4889
+ sourcesByCoord.set(coordKey, sources);
4890
+ }
4891
+ if (!targets.length) {
4892
+ continue;
4893
+ }
4894
+ targetsByCoord.set(coordKey, targets);
4895
+ const gavKey = mavenCoordinateKey(c.group, c.name, undefined, undefined, c.version);
4896
+ if (gavKey) {
4897
+ const acc = targetsByGav.get(gavKey);
4898
+ if (acc) {
4899
+ for (const f of targets) {
4900
+ if (!acc.includes(f)) {
4901
+ acc.push(f);
4902
+ }
4903
+ }
4904
+ } else {
4905
+ targetsByGav.set(gavKey, [...targets]);
4906
+ }
4907
+ }
4908
+ }
4909
+ // A top-level module is a `project` but usually not a dependency node, so its
4910
+ // source roots (where reachability starts) are missed by the node loop above;
4911
+ // emit first-party module paths here.
4912
+ for (const p of projects) {
4913
+ const coordKey = mavenCoordinateKey(p.group, p.name, undefined, undefined, p.version);
4914
+ if (!coordKey) {
4915
+ continue;
4916
+ }
4917
+ coords.add(coordKey);
4918
+ unionInto(sourcesByCoord, coordKey, p.sources.filter(fileExists));
4919
+ const targets = p.targets.filter(fileExists);
4920
+ unionInto(targetsByCoord, coordKey, targets);
4921
+ unionInto(targetsByGav, coordKey, targets);
4922
+ }
4923
+ return {
4924
+ targetsByCoord,
4925
+ targetsByGav,
4926
+ sourcesByCoord,
4927
+ coords
4928
+ };
4929
+ }
4930
+ function buildReport(parsed) {
4931
+ const seen = new Set();
4932
+ const failures = parsed.failures.filter(f => {
4933
+ const key = `${f.coord}|${f.detail}|${f.config}`;
4934
+ if (seen.has(key)) {
4935
+ return false;
4936
+ }
4937
+ seen.add(key);
4938
+ return true;
4939
+ });
4940
+ return {
4941
+ failures,
4942
+ scannedConfigs: parsed.scannedConfigs
4943
+ };
4944
+ }
4945
+
4946
+ // PATH fallback when no `bin` and no project wrapper.
4947
+ const DEFAULT_BUILD_TOOL_BIN = {
4948
+ __proto__: null,
4949
+ gradle: 'gradle',
4950
+ maven: 'mvn',
4951
+ sbt: 'sbt'
4952
+ };
4953
+
4954
+ // Project-local wrapper, preferred because it pins the expected build-tool
4955
+ // version. sbt has no wrapper convention. POSIX names only (no win32 target).
4956
+ const BUILD_TOOL_WRAPPER = {
4957
+ __proto__: null,
4958
+ gradle: 'gradlew',
4959
+ maven: 'mvnw'
4960
+ };
4961
+ function resolveBuildToolBin(tool, projectDir, bin) {
4962
+ if (bin) {
4963
+ return bin;
4964
+ }
4965
+ const wrapperName = BUILD_TOOL_WRAPPER[tool];
4966
+ if (wrapperName && fs$1.existsSync(path.resolve(projectDir, wrapperName))) {
4967
+ return `./${wrapperName}`;
4968
+ }
4969
+ return DEFAULT_BUILD_TOOL_BIN[tool];
4970
+ }
4971
+
4972
+ // Line-protocol the build-tool scripts emit to a records file (NOT stdout — sbt
4973
+ // prints unsilenceable resolution noise there). One record per line, fields
4974
+ // backslash-escaped (\\, \t, \n, \r) so a value can't break framing:
4289
4975
  //
4290
- // The resolved artifact-paths sidecar is intentionally NOT requested here: it
4291
- // only matters for reachability analysis, which is internal to Coana, so Coana
4292
- // emits it itself when it runs reachability. `socket manifest` only needs the
4293
- // facts file.
4976
+ // <tag>\t<field>\t<field>...
4294
4977
  //
4295
- // `spawnCoanaDlx` resolves the Coana CLI via dlx (or a local build when
4296
- // `SOCKET_CLI_COANA_LOCAL_PATH` is set). `bin` (the gradle/maven/sbt executable)
4297
- // is always resolved by the caller to a concrete default (`<cwd>/gradlew`, or
4298
- // `mvn`/`sbt` on PATH) before we get here, so it is forwarded verbatim; the
4299
- // empty guard below is just a cheap safeguard against passing `--bin ''`.
4300
- async function runCoanaManifestFacts({
4978
+ // meta tool toolVersion javaVersion
4979
+ // project projectKey group name version dir
4980
+ // projectSrc projectKey path (--with-files only)
4981
+ // projectTgt projectKey path (--with-files only)
4982
+ // root rootId projectKey config prod(0|1)
4983
+ // node rootId coordId group name version ext classifier direct(0|1)
4984
+ // edge rootId parentCoordId childCoordId
4985
+ // file rootId coordId path (--with-files only)
4986
+ // scanned config
4987
+ // failure coord detail config
4988
+ //
4989
+ // A `root` is one (subproject, configuration) resolution root; `coordId` is the
4990
+ // coordinate key (`group:name:ext:classifier:version`, empty segments dropped),
4991
+ // used opaquely as the per-root node key. Unknown tags are ignored.
4992
+
4993
+ function unescapeField(s) {
4994
+ if (!s.includes('\\')) {
4995
+ return s;
4996
+ }
4997
+ let out = '';
4998
+ for (let i = 0; i < s.length; i += 1) {
4999
+ const c = s[i];
5000
+ if (c === '\\' && i + 1 < s.length) {
5001
+ const n = s[++i];
5002
+ out += n === 't' ? '\t' : n === 'n' ? '\n' : n === 'r' ? '\r' : n;
5003
+ } else {
5004
+ out += c;
5005
+ }
5006
+ }
5007
+ return out;
5008
+ }
5009
+ function bool(s) {
5010
+ return s === '1' || s === 'true';
5011
+ }
5012
+ function parseRecords(text) {
5013
+ const result = {
5014
+ tool: '',
5015
+ toolVersion: '',
5016
+ javaVersion: '',
5017
+ projects: new Map(),
5018
+ roots: new Map(),
5019
+ scannedConfigs: [],
5020
+ failures: []
5021
+ };
5022
+ const scanned = new Set();
5023
+ const root = id => {
5024
+ let r = result.roots.get(id);
5025
+ if (!r) {
5026
+ r = {
5027
+ rootId: id,
5028
+ projectKey: '',
5029
+ config: '',
5030
+ prod: false,
5031
+ nodes: new Map(),
5032
+ edges: []
5033
+ };
5034
+ result.roots.set(id, r);
5035
+ }
5036
+ return r;
5037
+ };
5038
+ const project = key => {
5039
+ let p = result.projects.get(key);
5040
+ if (!p) {
5041
+ p = {
5042
+ projectKey: key,
5043
+ group: '',
5044
+ name: '',
5045
+ version: '',
5046
+ dir: '',
5047
+ sources: [],
5048
+ targets: []
5049
+ };
5050
+ result.projects.set(key, p);
5051
+ }
5052
+ return p;
5053
+ };
5054
+ for (const rawLine of text.split('\n')) {
5055
+ if (!rawLine) {
5056
+ continue;
5057
+ }
5058
+ const f = rawLine.split('\t').map(unescapeField);
5059
+ switch (f[0]) {
5060
+ case 'meta':
5061
+ result.tool = f[1] ?? '';
5062
+ result.toolVersion = f[2] ?? '';
5063
+ result.javaVersion = f[3] ?? '';
5064
+ break;
5065
+ case 'project':
5066
+ {
5067
+ const p = project(f[1] ?? '');
5068
+ p.group = f[2] ?? '';
5069
+ p.name = f[3] ?? '';
5070
+ p.version = f[4] ?? '';
5071
+ p.dir = f[5] ?? '';
5072
+ break;
5073
+ }
5074
+ case 'projectSrc':
5075
+ if (f[2]) {
5076
+ project(f[1] ?? '').sources.push(f[2]);
5077
+ }
5078
+ break;
5079
+ case 'projectTgt':
5080
+ if (f[2]) {
5081
+ project(f[1] ?? '').targets.push(f[2]);
5082
+ }
5083
+ break;
5084
+ case 'root':
5085
+ {
5086
+ const r = root(f[1] ?? '');
5087
+ r.projectKey = f[2] ?? '';
5088
+ r.config = f[3] ?? '';
5089
+ r.prod = bool(f[4]);
5090
+ break;
5091
+ }
5092
+ case 'node':
5093
+ {
5094
+ const r = root(f[1] ?? '');
5095
+ const coordId = f[2] ?? '';
5096
+ r.nodes.set(coordId, {
5097
+ coordId,
5098
+ coord: {
5099
+ group: f[3] ?? '',
5100
+ name: f[4] ?? '',
5101
+ version: f[5] ?? '',
5102
+ ext: f[6] ?? '',
5103
+ classifier: f[7] ?? ''
5104
+ },
5105
+ direct: bool(f[8]),
5106
+ targets: []
5107
+ });
5108
+ break;
5109
+ }
5110
+ case 'edge':
5111
+ {
5112
+ const parent = f[2] ?? '';
5113
+ const child = f[3] ?? '';
5114
+ if (parent !== child) {
5115
+ root(f[1] ?? '').edges.push([parent, child]);
5116
+ }
5117
+ break;
5118
+ }
5119
+ case 'file':
5120
+ {
5121
+ const node = root(f[1] ?? '').nodes.get(f[2] ?? '');
5122
+ if (node && f[3]) {
5123
+ node.targets.push(f[3]);
5124
+ }
5125
+ break;
5126
+ }
5127
+ case 'scanned':
5128
+ if (f[1]) {
5129
+ scanned.add(f[1]);
5130
+ }
5131
+ break;
5132
+ case 'failure':
5133
+ if (f[1]) {
5134
+ result.failures.push({
5135
+ coord: f[1],
5136
+ detail: f[2] ?? '',
5137
+ config: f[3] ?? ''
5138
+ });
5139
+ }
5140
+ break;
5141
+ }
5142
+ }
5143
+ result.scannedConfigs = [...scanned].sort();
5144
+ return result;
5145
+ }
5146
+
5147
+ const FACTS_TASK = 'socketFacts';
5148
+ const SBT_PLUGIN_FILENAME = 'SocketFactsPlugin.scala';
5149
+
5150
+ // Bundled emitter assets, copied into dist by the rollup build.
5151
+ function manifestScriptsPath(...parts) {
5152
+ return path.join(constants.default.distPath, 'manifest-scripts', ...parts);
5153
+ }
5154
+
5155
+ // Don't throw on a non-zero exit: the script emits failure records, so a usable
5156
+ // records file still exists. A non-exit spawn error (e.g. missing executable)
5157
+ // propagates.
5158
+ async function runNeverThrow(bin, args, opts) {
5159
+ try {
5160
+ const result = await spawn.spawn(bin, args, {
5161
+ cwd: opts.projectDir,
5162
+ stdio: opts.stdio ?? 'inherit',
5163
+ ...(opts.env ? {
5164
+ env: opts.env
5165
+ } : {}),
5166
+ ...(opts.signal ? {
5167
+ signal: opts.signal
5168
+ } : {})
5169
+ });
5170
+ return {
5171
+ code: result.code,
5172
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
5173
+ stderr: typeof result.stderr === 'string' ? result.stderr : ''
5174
+ };
5175
+ } catch (e) {
5176
+ // A build tool that exits non-zero rejects with the spawn-result shape: a
5177
+ // numeric exit `code` plus captured stdout/stderr. Return it so the caller
5178
+ // can assemble failure records / surface the output. Anything else — e.g. a
5179
+ // missing executable, whose `code` is a string like 'ENOENT' — propagates.
5180
+ // This mirrors how utils/dlx.mts classifies spawn failures (a numeric `code`
5181
+ // is a real process exit); the registry's isSpawnError is avoided here
5182
+ // because it is currently broken and never matches.
5183
+ if (e !== null && typeof e === 'object' && typeof e.code === 'number') {
5184
+ const err = e;
5185
+ return {
5186
+ code: err.code,
5187
+ stdout: typeof err.stdout === 'string' ? err.stdout : '',
5188
+ stderr: typeof err.stderr === 'string' ? err.stderr : ''
5189
+ };
5190
+ }
5191
+ throw e;
5192
+ }
5193
+ }
5194
+ async function withTmpDir(prefix, fn) {
5195
+ const tmpDir = await fs$1.promises.mkdtemp(path.join(os.tmpdir(), prefix));
5196
+ try {
5197
+ return await fn(tmpDir);
5198
+ } finally {
5199
+ await fs$1.promises.rm(tmpDir, {
5200
+ recursive: true,
5201
+ force: true
5202
+ }).catch(() => {});
5203
+ }
5204
+ }
5205
+ async function writeSbtPlugin(globalBase) {
5206
+ const src = await fs$1.promises.readFile(manifestScriptsPath('socket-facts.plugin.scala'), 'utf8');
5207
+ const pluginsDir = path.join(globalBase, 'plugins');
5208
+ await fs$1.promises.mkdir(pluginsDir, {
5209
+ recursive: true
5210
+ });
5211
+ await fs$1.promises.writeFile(path.join(pluginsDir, SBT_PLUGIN_FILENAME), src);
5212
+ }
5213
+ async function assembleFromRecords(out, recordsFile) {
5214
+ const text = fs$1.existsSync(recordsFile) ? await fs$1.promises.readFile(recordsFile, 'utf8') : '';
5215
+ const {
5216
+ artifactPaths,
5217
+ facts,
5218
+ report
5219
+ } = assembleFacts(parseRecords(text));
5220
+ return {
5221
+ code: out.code,
5222
+ facts,
5223
+ report,
5224
+ artifactPaths,
5225
+ stderr: out.stderr,
5226
+ stdout: out.stdout
5227
+ };
5228
+ }
5229
+
5230
+ // Missing only in an unbuilt local checkout. Fail loudly: without the extension,
5231
+ // Maven silently emits an empty SBOM.
5232
+ function assertMavenExtensionBuilt(jarPath) {
5233
+ if (fs$1.existsSync(jarPath)) {
5234
+ return;
5235
+ }
5236
+ throw new Error(`Maven manifest extension jar not found at ${jarPath}. It is bundled in the published CLI; for local dev build it with: bash src/commands/manifest/scripts/maven-extension/build-jar.sh`);
5237
+ }
5238
+
5239
+ // Runs the build-tool script (which emits a records file) and assembles it.
5240
+ // Writes no files; the caller persists facts or consumes artifactPaths.
5241
+ async function runManifestScript(tool, opts) {
5242
+ switch (tool) {
5243
+ case 'gradle':
5244
+ return await runGradle(opts);
5245
+ case 'sbt':
5246
+ return await runSbt(opts);
5247
+ case 'maven':
5248
+ return await runMaven(opts);
5249
+ }
5250
+ }
5251
+ function commonProps(opts, prefix) {
5252
+ const props = [];
5253
+ if (opts.withFiles) {
5254
+ props.push(`${prefix}socket.withFiles=true`);
5255
+ }
5256
+ if (opts.populateFilesFor) {
5257
+ props.push(`${prefix}socket.populateFilesFor=${opts.populateFilesFor}`);
5258
+ }
5259
+ if (opts.includeConfigs) {
5260
+ props.push(`${prefix}socket.includeConfigs=${opts.includeConfigs}`);
5261
+ }
5262
+ if (opts.excludeConfigs) {
5263
+ props.push(`${prefix}socket.excludeConfigs=${opts.excludeConfigs}`);
5264
+ }
5265
+ return props;
5266
+ }
5267
+ async function runGradle(opts) {
5268
+ const initScript = manifestScriptsPath('socket-facts.init.gradle');
5269
+ return await withTmpDir('socket-gradle-facts-', async tmp => {
5270
+ const recordsFile = path.join(tmp, 'records.tsv');
5271
+ const bin = resolveBuildToolBin('gradle', opts.projectDir, opts.bin);
5272
+ // Disable the configuration cache: the init script's legacy
5273
+ // resolvedConfiguration API and shared accumulator aren't cache-safe.
5274
+ const args = ['--init-script', initScript, '-Dorg.gradle.configuration-cache=false', `-Psocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-P'), ...(opts.toolOpts ?? []), FACTS_TASK, '--no-daemon', '--console=plain'];
5275
+ const out = await runNeverThrow(bin, args, opts);
5276
+ return await assembleFromRecords(out, recordsFile);
5277
+ });
5278
+ }
5279
+ async function runSbt(opts) {
5280
+ return await withTmpDir('socket-sbt-facts-', async globalBase => {
5281
+ await writeSbtPlugin(globalBase);
5282
+ const recordsFile = path.join(globalBase, 'records.tsv');
5283
+ const bin = resolveBuildToolBin('sbt', opts.projectDir, opts.bin);
5284
+ // Fresh per-run global base (not ~/.sbt): sbt executes everything under
5285
+ // plugins/, so a shared path is a code-injection surface. BSP off for this run.
5286
+ const props = [`-Dsbt.global.base=${globalBase}`, '-Dsbt.server.autostart=false', `-Dsocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-D')];
5287
+ // sbt's launcher doesn't always honor JAVA_HOME; never override a
5288
+ // caller-supplied --java-home.
5289
+ const javaHome = opts.env?.['JAVA_HOME'] ?? process.env['JAVA_HOME'];
5290
+ const javaHomeOpt = javaHome && !(opts.toolOpts ?? []).includes('--java-home') ? ['--java-home', javaHome] : [];
5291
+ const args = [...javaHomeOpt, ...props, ...(opts.toolOpts ?? []), '--batch', FACTS_TASK];
5292
+ const out = await runNeverThrow(bin, args, opts);
5293
+ return await assembleFromRecords(out, recordsFile);
5294
+ });
5295
+ }
5296
+ async function runMaven(opts) {
5297
+ const jarPath = manifestScriptsPath('maven-extension', 'coana-maven-extension.jar');
5298
+ assertMavenExtensionBuilt(jarPath);
5299
+ return await withTmpDir('socket-maven-facts-', async tmp => {
5300
+ const recordsFile = path.join(tmp, 'records.tsv');
5301
+ const bin = resolveBuildToolBin('maven', opts.projectDir, opts.bin);
5302
+ // `validate` is the cheapest phase that triggers the afterSessionEnd
5303
+ // extension; no compile needed (analysis uses configured paths, not classes).
5304
+ const props = [`-Dmaven.ext.class.path=${jarPath}`, '-Dcoana.task=socket-facts', `-Dsocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-D')];
5305
+ const args = [...props, ...(opts.toolOpts ?? []), '--batch-mode', 'validate'];
5306
+ const out = await runNeverThrow(bin, args, opts);
5307
+ return await assembleFromRecords(out, recordsFile);
5308
+ });
5309
+ }
5310
+
5311
+ // Frozen contract with `coana run --compute-artifacts-sidecar`; change only in
5312
+ // sync with the coana consumer. Per coordinate: targets/sources present →
5313
+ // resolved (coana uses the paths); both empty → resolved with no artifact
5314
+ // (pom/BOM), not a failure; absent → coana degrades that vuln to precomputed.
5315
+
5316
+ // Bare array, no schema version: socket-cli pins the coana version, so producer
5317
+ // and consumer never drift.
5318
+
5319
+ // Keyed by full coordinate; unions paths so multiple build roots merge into one.
5320
+
5321
+ function pushUnique(into, from) {
5322
+ for (const f of from) {
5323
+ if (!into.includes(f)) {
5324
+ into.push(f);
5325
+ }
5326
+ }
5327
+ }
5328
+ function addEntry(acc, artifactPaths, group, name, version, ext, classifier) {
5329
+ const coordKey = mavenCoordinateKey(group, name, ext || undefined, classifier ?? undefined, version || undefined);
5330
+ if (!coordKey) {
5331
+ return;
5332
+ }
5333
+ let entry = acc.get(coordKey);
5334
+ if (!entry) {
5335
+ entry = {
5336
+ group,
5337
+ name,
5338
+ version,
5339
+ ext,
5340
+ classifier,
5341
+ targets: [],
5342
+ sources: []
5343
+ };
5344
+ acc.set(coordKey, entry);
5345
+ }
5346
+ pushUnique(entry.targets, artifactPaths.targetsByCoord.get(coordKey) ?? []);
5347
+ pushUnique(entry.sources, artifactPaths.sourcesByCoord.get(coordKey) ?? []);
5348
+ }
5349
+
5350
+ // Emit an entry for every SBOM component AND every first-party project: a
5351
+ // top-level module is a project, not a dependency component, yet its source
5352
+ // roots are where reachability starts, so the sidecar must carry them.
5353
+ function accumulateSidecar(acc, facts, artifactPaths) {
5354
+ for (const comp of facts.components) {
5355
+ addEntry(acc, artifactPaths, comp.namespace ?? '', comp.name, comp.version ?? '', comp.qualifiers?.['ext'] ?? '', comp.qualifiers?.['classifier'] ?? null);
5356
+ }
5357
+ // First-party modules have no ext/classifier.
5358
+ for (const proj of facts.projects ?? []) {
5359
+ addEntry(acc, artifactPaths, proj.namespace ?? '', proj.name, proj.version ?? '', '', null);
5360
+ }
5361
+ }
5362
+ function serializeSidecar(acc) {
5363
+ const resolved = [...acc.values()];
5364
+ for (const entry of resolved) {
5365
+ entry.targets.sort();
5366
+ entry.sources.sort();
5367
+ }
5368
+ resolved.sort((a, b) => {
5369
+ const ka = `${a.group}:${a.name}:${a.ext}:${a.classifier ?? ''}:${a.version}`;
5370
+ const kb = `${b.group}:${b.name}:${b.ext}:${b.classifier ?? ''}:${b.version}`;
5371
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
5372
+ });
5373
+ return resolved;
5374
+ }
5375
+
5376
+ // Last N non-empty lines of the captured build output, for diagnosing a crash
5377
+ // without forcing a --verbose rebuild.
5378
+ function tailBuildOutput(stdout, stderr) {
5379
+ const combined = [stdout, stderr].map(s => s.trimEnd()).filter(Boolean).join('\n');
5380
+ return combined.split('\n').slice(-40).join('\n');
5381
+ }
5382
+
5383
+ // Runs the bundled build-tool resolution script for a JVM project and writes
5384
+ // `.socket.facts.json`. `withFiles` (reachability only) additionally folds
5385
+ // resolved artifact paths into `sidecarAcc`. A blocking resolution failure sets
5386
+ // a non-zero exit code and returns (matching the `--pom` generator) unless
5387
+ // `ignoreUnresolved`; a crashed build — a process failure, not an unresolved
5388
+ // dependency — always fails.
5389
+ async function runManifestFacts({
4301
5390
  bin,
4302
5391
  buildOpts,
4303
- buildOptsFlag,
4304
5392
  cwd,
4305
5393
  ecosystem,
4306
5394
  excludeConfigs,
4307
5395
  ignoreUnresolved,
4308
5396
  includeConfigs,
4309
- verbose
5397
+ sidecarAcc,
5398
+ verbose,
5399
+ withFiles
4310
5400
  }) {
4311
- // Pin the facts output location explicitly rather than relying on Coana's
4312
- // "project root" default. `factsPath` is then the single source of truth for
4313
- // both what we tell Coana to write and what we verify exists below, so the
4314
- // two can't drift apart if Coana's default ever changes. This is deliberately
4315
- // NOT user-configurable: Socket facts always land in the project root so that
4316
- // `socket scan create <project>` finds them (see cmd-manifest-scala.mts, which
4317
- // rejects --out/--stdout in facts mode).
4318
- const factsDir = cwd;
4319
- const factsFile = constants.default.DOT_SOCKET_DOT_FACTS_JSON;
4320
- const factsPath = path.join(factsDir, factsFile);
4321
- // `coana manifest <ecosystem> <path>` emits `.socket.facts.json` by default;
4322
- // there is no `--facts` flag (the artifact-paths sidecar is reachability-
4323
- // internal and not requested here).
4324
- const coanaArgs = ['manifest', ecosystem, cwd, '--output-dir', factsDir, '--output-file', factsFile];
4325
- if (bin) {
4326
- coanaArgs.push('--bin', bin);
4327
- }
4328
- if (includeConfigs) {
4329
- coanaArgs.push('--include-configs', includeConfigs);
4330
- }
4331
- if (excludeConfigs) {
4332
- coanaArgs.push('--exclude-configs', excludeConfigs);
4333
- }
4334
- if (ignoreUnresolved) {
4335
- coanaArgs.push('--ignore-unresolved');
5401
+ const factsPath = path.join(cwd, constants.default.DOT_SOCKET_DOT_FACTS_JSON);
5402
+ logger.logger.log(`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`);
5403
+ const scriptOpts = {
5404
+ bin: bin || undefined,
5405
+ excludeConfigs: excludeConfigs || undefined,
5406
+ includeConfigs: includeConfigs || undefined,
5407
+ projectDir: cwd,
5408
+ // Stream the build tool's output only when asked; otherwise capture it and
5409
+ // show a spinner, surfacing the output only if the build crashes.
5410
+ stdio: verbose ? 'inherit' : 'pipe',
5411
+ toolOpts: buildOpts,
5412
+ withFiles
5413
+ };
5414
+ const {
5415
+ spinner
5416
+ } = constants.default;
5417
+ let result;
5418
+ try {
5419
+ if (verbose) {
5420
+ logger.logger.info(`(Running ${ecosystem} with output streaming; this can take a while.)`);
5421
+ result = await runManifestScript(ecosystem, scriptOpts);
5422
+ } else {
5423
+ logger.logger.info(`(No live output; pass --verbose to stream the ${ecosystem} build output.)`);
5424
+ spinner.start(`Resolving ${ecosystem} dependencies ...`);
5425
+ result = await runManifestScript(ecosystem, scriptOpts);
5426
+ if (result.code === 0) {
5427
+ spinner.successAndStop(`Resolved ${ecosystem} dependencies.`);
5428
+ } else {
5429
+ spinner.failAndStop(`${ecosystem} build exited with code ${result.code}.`);
5430
+ }
5431
+ }
5432
+ } catch (e) {
5433
+ // Only a spawn-level failure (e.g. the build tool missing from PATH) reaches
5434
+ // here; runNeverThrow returns non-zero build exits rather than throwing.
5435
+ if (!verbose) {
5436
+ spinner.failAndStop(`Failed to run ${ecosystem}.`);
5437
+ }
5438
+ process.exitCode = 1;
5439
+ logger.logger.fail(`Could not run the ${ecosystem} build tool` + (verbose ? `: ${e}` : ' (run with --verbose for details).'));
5440
+ return;
4336
5441
  }
4337
- if (verbose) {
4338
- coanaArgs.push('--debug');
5442
+ const {
5443
+ artifactPaths,
5444
+ code,
5445
+ facts,
5446
+ report,
5447
+ stderr,
5448
+ stdout
5449
+ } = result;
5450
+ const rendered = renderResolutionErrorReport(report.failures, report.scannedConfigs, ecosystem, {
5451
+ ignoreUnresolved
5452
+ });
5453
+ if (rendered.hasBlockingFailures) {
5454
+ if (ignoreUnresolved) {
5455
+ logger.logger.warn(rendered.summary);
5456
+ } else {
5457
+ process.exitCode = 1;
5458
+ logger.logger.fail(rendered.summary);
5459
+ if (verbose && rendered.details) {
5460
+ logger.logger.log(rendered.details);
5461
+ }
5462
+ return;
5463
+ }
4339
5464
  }
4340
- // `--gradle-opts` / `--sbt-opts` are variadic on the Coana side; keep them
4341
- // last so the pass-through values don't swallow any following flags.
4342
- if (buildOpts.length) {
4343
- coanaArgs.push(buildOptsFlag, ...buildOpts);
5465
+ if (rendered.nonBlockingNotice) {
5466
+ logger.logger.info(rendered.nonBlockingNotice);
4344
5467
  }
4345
- logger.logger.log(`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`);
4346
- if (verbose) {
4347
- logger.logger.log('[VERBOSE] coana args:', coanaArgs);
5468
+ if (verbose && rendered.details) {
5469
+ logger.logger.log(rendered.details);
4348
5470
  }
4349
5471
 
4350
- // Stream Coana's output so the user sees build-tool progress and Coana's own
4351
- // "Socket facts file written to: ..." line.
4352
- const result = await utils.spawnCoanaDlx(coanaArgs, undefined, {
4353
- cwd
4354
- }, {
4355
- stdio: 'inherit'
4356
- });
4357
- if (!result.ok) {
5472
+ // A non-zero build exit with no usable output (no graph, no first-party
5473
+ // modules, no failure records) means the build died before the socketFacts
5474
+ // task emitted anything — a script/plugin compile error, OOM, or an unchecked
5475
+ // exception in the extension. The build tool's own exit is the only signal, so
5476
+ // fail closed rather than silently dropping the ecosystem with an empty SBOM
5477
+ // (the empty-facts branch below would otherwise just log "nothing to upload").
5478
+ if (code !== 0 && !facts.components.length && !facts.projects?.length && !report.failures.length) {
5479
+ if (!verbose) {
5480
+ const tail = tailBuildOutput(stdout, stderr);
5481
+ if (tail) {
5482
+ logger.logger.group('Build output:');
5483
+ logger.logger.error(tail);
5484
+ logger.logger.groupEnd();
5485
+ }
5486
+ }
5487
+ // A crashed build is a process failure (missing JDK/build tool, unparseable
5488
+ // project, OOM, plugin error), not an unresolved dependency, so it fails
5489
+ // regardless of `ignoreUnresolved` — that flag only tolerates dependencies a
5490
+ // successful run couldn't resolve.
4358
5491
  process.exitCode = 1;
4359
- logger.logger.fail(result.message || 'Coana failed to generate Socket facts');
5492
+ logger.logger.fail(`The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`);
4360
5493
  return;
4361
5494
  }
4362
- // A zero exit code doesn't guarantee a facts file was written: Coana skips
4363
- // emitting it when there are no resolvable dependencies (e.g. with
4364
- // --ignore-unresolved). We pinned the output to `factsPath` above, so confirm
4365
- // it exists before claiming success; otherwise the "next step: socket scan
4366
- // create" line would mislead.
4367
- if (!fs$1.existsSync(factsPath)) {
4368
- logger.logger.warn(`Coana completed but wrote no ${factsFile} (no resolvable dependencies?); nothing to upload.`);
5495
+
5496
+ // Nothing resolved at all no dependencies and no first-party modules. A
5497
+ // project with only first-party modules (empty components, non-empty projects)
5498
+ // still has source roots reachability needs, so it must be written.
5499
+ if (!facts.components.length && !facts.projects?.length) {
5500
+ logger.logger.warn(`No resolvable ${ecosystem} dependencies found; nothing to upload.`);
4369
5501
  return;
4370
5502
  }
5503
+ await fs$1.promises.writeFile(factsPath, JSON.stringify(facts, null, 2), 'utf8');
5504
+ if (withFiles && sidecarAcc) {
5505
+ accumulateSidecar(sidecarAcc, facts, artifactPaths);
5506
+ }
4371
5507
  logger.logger.success('Generated Socket facts');
4372
- logger.logger.log('');
4373
- logger.logger.log('Next step is to generate a Scan by running the `socket scan create` command on the same directory.');
4374
5508
  }
4375
5509
 
4376
- // Generates a `.socket.facts.json` for a Gradle project by delegating to the
4377
- // Coana CLI's `manifest gradle` command (which owns the Gradle init script that
4378
- // resolves the dependency graph). socket-cli no longer runs gradle itself; an
4379
- // explicit `bin` is forwarded as `--bin`, otherwise Coana defaults to
4380
- // `./gradlew`.
5510
+ // Generates `.socket.facts.json` for a Gradle project via the bundled init script.
4381
5511
  async function convertGradleToFacts({
4382
5512
  bin,
4383
5513
  cwd,
@@ -4385,26 +5515,25 @@ async function convertGradleToFacts({
4385
5515
  gradleOpts,
4386
5516
  ignoreUnresolved,
4387
5517
  includeConfigs,
4388
- verbose
5518
+ sidecarAcc,
5519
+ verbose,
5520
+ withFiles
4389
5521
  }) {
4390
- await runCoanaManifestFacts({
5522
+ await runManifestFacts({
4391
5523
  bin,
4392
5524
  buildOpts: gradleOpts,
4393
- buildOptsFlag: '--gradle-opts',
4394
5525
  cwd,
4395
5526
  ecosystem: 'gradle',
4396
5527
  excludeConfigs,
4397
5528
  ignoreUnresolved,
4398
5529
  includeConfigs,
4399
- verbose
5530
+ sidecarAcc,
5531
+ verbose,
5532
+ withFiles
4400
5533
  });
4401
5534
  }
4402
5535
 
4403
- // Generates a `.socket.facts.json` for a Maven project by delegating to the
4404
- // Coana CLI's `manifest maven` command (which owns the Maven plugin that
4405
- // resolves the dependency graph). socket-cli no longer runs maven itself; an
4406
- // explicit `bin` is forwarded as `--bin`, otherwise Coana defaults to `mvn` on
4407
- // PATH.
5536
+ // Generates `.socket.facts.json` for a Maven project via the bundled extension.
4408
5537
  async function convertMavenToFacts({
4409
5538
  bin,
4410
5539
  cwd,
@@ -4412,28 +5541,27 @@ async function convertMavenToFacts({
4412
5541
  ignoreUnresolved,
4413
5542
  includeConfigs,
4414
5543
  mavenOpts,
4415
- verbose
5544
+ sidecarAcc,
5545
+ verbose,
5546
+ withFiles
4416
5547
  }) {
4417
- await runCoanaManifestFacts({
5548
+ await runManifestFacts({
4418
5549
  bin,
4419
5550
  buildOpts: mavenOpts,
4420
- buildOptsFlag: '--maven-opts',
4421
5551
  cwd,
4422
5552
  ecosystem: 'maven',
4423
5553
  excludeConfigs,
4424
5554
  ignoreUnresolved,
4425
5555
  includeConfigs,
4426
- verbose
5556
+ sidecarAcc,
5557
+ verbose,
5558
+ withFiles
4427
5559
  });
4428
5560
  }
4429
5561
 
4430
- // Generates a `.socket.facts.json` for an sbt project by delegating to the
4431
- // Coana CLI's `manifest sbt` command (which owns the sbt plugin that resolves
4432
- // the dependency graph). socket-cli no longer runs sbt itself; an explicit
4433
- // `bin` is forwarded as `--bin`, otherwise Coana defaults to `sbt` on PATH.
4434
- // JDK-compatibility guidance (sbt 0.13/early 1.x cannot run on modern JDKs) is
4435
- // handled by Coana; pass a compatible JDK via `--sbt-opts "--java-home <path>"`
4436
- // or `JAVA_HOME`.
5562
+ // Generates `.socket.facts.json` for an sbt project via the bundled sbt plugin.
5563
+ // sbt 0.13/early 1.x can't run on modern JDKs pass a compatible JDK via
5564
+ // `--sbt-opts "--java-home <path>"` or `JAVA_HOME`.
4437
5565
  async function convertSbtToFacts({
4438
5566
  bin,
4439
5567
  cwd,
@@ -4441,18 +5569,21 @@ async function convertSbtToFacts({
4441
5569
  ignoreUnresolved,
4442
5570
  includeConfigs,
4443
5571
  sbtOpts,
4444
- verbose
5572
+ sidecarAcc,
5573
+ verbose,
5574
+ withFiles
4445
5575
  }) {
4446
- await runCoanaManifestFacts({
5576
+ await runManifestFacts({
4447
5577
  bin,
4448
5578
  buildOpts: sbtOpts,
4449
- buildOptsFlag: '--sbt-opts',
4450
5579
  cwd,
4451
5580
  ecosystem: 'sbt',
4452
5581
  excludeConfigs,
4453
5582
  ignoreUnresolved,
4454
5583
  includeConfigs,
4455
- verbose
5584
+ sidecarAcc,
5585
+ verbose,
5586
+ withFiles
4456
5587
  });
4457
5588
  }
4458
5589
 
@@ -4954,7 +6085,19 @@ function parseBuildToolOpts(opts) {
4954
6085
  return tokens;
4955
6086
  }
4956
6087
 
6088
+ // Under --auto-manifest, a manifest generator that failed — raising the exit
6089
+ // code above the value captured before it ran — aborts the whole run: a partial
6090
+ // or empty SBOM silently under-reports dependencies. The generator has already
6091
+ // logged the specifics. A tolerated resolution failure (ignoreUnresolved) warns
6092
+ // without touching the exit code, so it passes through here and the run
6093
+ // continues.
6094
+ function abortManifestRunIfFailed(ecosystem, beforeExitCode) {
6095
+ if (process.exitCode && process.exitCode !== beforeExitCode) {
6096
+ throw new utils.InputError(`Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`);
6097
+ }
6098
+ }
4957
6099
  async function generateAutoManifest({
6100
+ computeArtifactsSidecar,
4958
6101
  cwd,
4959
6102
  detected,
4960
6103
  outputKind,
@@ -4962,6 +6105,9 @@ async function generateAutoManifest({
4962
6105
  }) {
4963
6106
  const sockJson = utils.readOrDefaultSocketJson(cwd);
4964
6107
  const generatedFiles = [];
6108
+
6109
+ // Resolved paths across all JVM roots, serialized to one sidecar at the end.
6110
+ const sidecarAcc = computeArtifactsSidecar ? new Map() : undefined;
4965
6111
  if (verbose) {
4966
6112
  logger.logger.info(`Using this ${constants.SOCKET_JSON} for defaults:`, sockJson);
4967
6113
  }
@@ -4980,26 +6126,30 @@ async function generateAutoManifest({
4980
6126
  // `defaults.manifest.sbt.facts: false` in socket.json.
4981
6127
  if (sockJson.defaults?.manifest?.sbt?.facts !== false) {
4982
6128
  logger.logger.log('Detected a Scala sbt build, generating Socket facts...');
6129
+ const beforeExitCode = process.exitCode;
4983
6130
  await convertSbtToFacts({
4984
6131
  ...sbtArgs,
4985
6132
  excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '',
4986
6133
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.sbt?.ignoreUnresolved),
4987
- includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? ''
6134
+ includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '',
6135
+ sidecarAcc,
6136
+ withFiles: computeArtifactsSidecar
4988
6137
  });
6138
+ abortManifestRunIfFailed('sbt', beforeExitCode);
4989
6139
  } else {
4990
6140
  logger.logger.log('Detected a Scala sbt build, generating pom files with sbt...');
6141
+ const beforeExitCode = process.exitCode;
4991
6142
  await convertSbtToMaven({
4992
6143
  ...sbtArgs,
4993
6144
  out: sockJson.defaults?.manifest?.sbt?.outfile ?? './pom.xml'
4994
6145
  });
6146
+ abortManifestRunIfFailed('sbt', beforeExitCode);
4995
6147
  }
4996
6148
  }
4997
6149
  if (!sockJson?.defaults?.manifest?.gradle?.disabled && detected.gradle) {
4998
6150
  const gradleArgs = {
4999
- // Note: `gradlew` is more likely to be resolved against cwd.
5000
- // Note: .resolve() won't butcher an absolute path.
5001
- // TODO: `gradlew` (or anything else given) may want to resolve against PATH.
5002
- bin: sockJson.defaults?.manifest?.gradle?.bin ? path.resolve(cwd, sockJson.defaults.manifest.gradle.bin) : path.join(cwd, 'gradlew'),
6151
+ // Configured bin wins; else prefer ./gradlew, else gradle on PATH.
6152
+ bin: sockJson.defaults?.manifest?.gradle?.bin ? path.resolve(cwd, sockJson.defaults.manifest.gradle.bin) : resolveBuildToolBin('gradle', cwd),
5003
6153
  cwd,
5004
6154
  verbose: Boolean(sockJson.defaults?.manifest?.gradle?.verbose),
5005
6155
  gradleOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.gradle?.gradleOpts)
@@ -5008,29 +6158,39 @@ async function generateAutoManifest({
5008
6158
  // `defaults.manifest.gradle.facts: false` in socket.json.
5009
6159
  if (sockJson.defaults?.manifest?.gradle?.facts !== false) {
5010
6160
  logger.logger.log('Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...');
6161
+ const beforeExitCode = process.exitCode;
5011
6162
  await convertGradleToFacts({
5012
6163
  ...gradleArgs,
5013
6164
  excludeConfigs: sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '',
5014
6165
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.gradle?.ignoreUnresolved),
5015
- includeConfigs: sockJson.defaults?.manifest?.gradle?.includeConfigs ?? ''
6166
+ includeConfigs: sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '',
6167
+ sidecarAcc,
6168
+ withFiles: computeArtifactsSidecar
5016
6169
  });
6170
+ abortManifestRunIfFailed('gradle', beforeExitCode);
5017
6171
  } else {
5018
6172
  logger.logger.log('Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...');
6173
+ const beforeExitCode = process.exitCode;
5019
6174
  await convertGradleToMaven(gradleArgs);
6175
+ abortManifestRunIfFailed('gradle', beforeExitCode);
5020
6176
  }
5021
6177
  }
5022
6178
  if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) {
5023
6179
  logger.logger.log('Detected a Maven pom.xml build, generating Socket facts...');
6180
+ const beforeExitCode = process.exitCode;
5024
6181
  await convertMavenToFacts({
5025
- // Note: `mvn` is more likely to be resolved against PATH env.
5026
- bin: sockJson.defaults?.manifest?.maven?.bin ?? 'mvn',
6182
+ // Configured bin wins; else prefer ./mvnw, else mvn on PATH.
6183
+ bin: sockJson.defaults?.manifest?.maven?.bin ?? resolveBuildToolBin('maven', cwd),
5027
6184
  cwd,
5028
6185
  excludeConfigs: sockJson.defaults?.manifest?.maven?.excludeConfigs ?? '',
5029
6186
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.maven?.ignoreUnresolved),
5030
6187
  includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '',
5031
6188
  mavenOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.maven?.mavenOpts),
5032
- verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose)
6189
+ sidecarAcc,
6190
+ verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose),
6191
+ withFiles: computeArtifactsSidecar
5033
6192
  });
6193
+ abortManifestRunIfFailed('maven', beforeExitCode);
5034
6194
  }
5035
6195
  if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) {
5036
6196
  logger.logger.log('Detected an environment.yml file, running default Conda generator...');
@@ -5079,7 +6239,8 @@ async function generateAutoManifest({
5079
6239
  }
5080
6240
  }
5081
6241
  return {
5082
- generatedFiles
6242
+ generatedFiles,
6243
+ resolvedPathsSidecar: sidecarAcc && sidecarAcc.size ? serializeSidecar(sidecarAcc) : undefined
5083
6244
  };
5084
6245
  }
5085
6246
 
@@ -5142,6 +6303,9 @@ async function handleCreateNewScan({
5142
6303
  tmp,
5143
6304
  workspace
5144
6305
  });
6306
+
6307
+ // Sidecar forwarded to reachability; populated only when reach runs.
6308
+ let resolvedPathsSidecar;
5145
6309
  if (autoManifest) {
5146
6310
  logger.logger.info('Auto-generating manifest files ...');
5147
6311
  require$$9.debugFn('notice', 'Auto-manifest mode enabled');
@@ -5151,11 +6315,13 @@ async function handleCreateNewScan({
5151
6315
  detected
5152
6316
  });
5153
6317
  const autoManifestResult = await generateAutoManifest({
5154
- detected,
6318
+ computeArtifactsSidecar: reach.runReachabilityAnalysis,
5155
6319
  cwd,
6320
+ detected,
5156
6321
  outputKind,
5157
6322
  verbose: false
5158
6323
  });
6324
+ resolvedPathsSidecar = autoManifestResult.resolvedPathsSidecar;
5159
6325
  if (autoManifestResult.generatedFiles.length) {
5160
6326
  scanTargets = Array.from(new Set([...targets, ...autoManifestResult.generatedFiles]));
5161
6327
  }
@@ -5239,6 +6405,7 @@ async function handleCreateNewScan({
5239
6405
  packagePaths,
5240
6406
  reachabilityOptions: mergedReachabilityOptions,
5241
6407
  repoName,
6408
+ resolvedPathsSidecar,
5242
6409
  spinner,
5243
6410
  target: targets[0]
5244
6411
  });
@@ -10313,7 +11480,7 @@ const config$c = {
10313
11480
  ...flags.commonFlags,
10314
11481
  bin: {
10315
11482
  type: 'string',
10316
- description: 'Location of gradlew binary to use, default: CWD/gradlew'
11483
+ description: 'Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH'
10317
11484
  },
10318
11485
  facts: {
10319
11486
  type: 'boolean',
@@ -10424,7 +11591,8 @@ async function run$D(argv, importMeta, {
10424
11591
  bin = sockJson.defaults?.manifest?.gradle?.bin;
10425
11592
  logger.logger.info(`Using default --bin from ${constants.SOCKET_JSON}:`, bin);
10426
11593
  } else {
10427
- bin = path.join(cwd, 'gradlew');
11594
+ // Prefer the project's ./gradlew wrapper, else `gradle` on PATH.
11595
+ bin = resolveBuildToolBin('gradle', cwd);
10428
11596
  }
10429
11597
  }
10430
11598
  if (!gradleOpts) {
@@ -10558,7 +11726,7 @@ const config$b = {
10558
11726
  ...flags.commonFlags,
10559
11727
  bin: {
10560
11728
  type: 'string',
10561
- description: 'Location of gradlew binary to use, default: CWD/gradlew'
11729
+ description: 'Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH'
10562
11730
  },
10563
11731
  facts: {
10564
11732
  type: 'boolean',
@@ -10669,7 +11837,8 @@ async function run$C(argv, importMeta, {
10669
11837
  bin = sockJson.defaults?.manifest?.gradle?.bin;
10670
11838
  logger.logger.info(`Using default --bin from ${constants.SOCKET_JSON}:`, bin);
10671
11839
  } else {
10672
- bin = path.join(cwd, 'gradlew');
11840
+ // Prefer the project's ./gradlew wrapper, else `gradle` on PATH.
11841
+ bin = resolveBuildToolBin('gradle', cwd);
10673
11842
  }
10674
11843
  }
10675
11844
  if (!gradleOpts) {
@@ -10796,7 +11965,7 @@ const config$a = {
10796
11965
  ...flags.commonFlags,
10797
11966
  bin: {
10798
11967
  type: 'string',
10799
- description: 'Location of the maven binary to use, default: mvn on PATH'
11968
+ description: 'Location of the maven binary to use, default: ./mvnw if present, else mvn on PATH'
10800
11969
  },
10801
11970
  includeConfigs: {
10802
11971
  type: 'string',
@@ -10890,7 +12059,8 @@ async function run$B(argv, importMeta, {
10890
12059
  bin = sockJson.defaults?.manifest?.maven?.bin;
10891
12060
  logger.logger.info(`Using default --bin from ${constants.SOCKET_JSON}:`, bin);
10892
12061
  } else {
10893
- bin = 'mvn';
12062
+ // Prefer the project's ./mvnw wrapper, else `mvn` on PATH.
12063
+ bin = resolveBuildToolBin('maven', cwd);
10894
12064
  }
10895
12065
  }
10896
12066
  if (!mavenOpts) {
@@ -16596,13 +17766,6 @@ async function run$d(argv, importMeta, {
16596
17766
  pendingHead: Boolean(pendingHead),
16597
17767
  pullRequest: Number(pullRequest),
16598
17768
  reach: {
16599
- // Build-tool config for the reach-time resolution, mapped from socket.json
16600
- // (per-ecosystem). Best-effort on plain --reach; under --auto-manifest the
16601
- // config carries top-level failOnBuildToolError=true (fail-closed). Only
16602
- // built when reachability runs.
16603
- autoManifestConfig: reach ? utils.buildAutoManifestConfig(sockJson, {
16604
- autoManifest: Boolean(autoManifest)
16605
- }) : undefined,
16606
17769
  excludePaths,
16607
17770
  reachAnalysisMemoryLimit,
16608
17771
  reachAnalysisTimeout,
@@ -20667,5 +21830,5 @@ process.on('unhandledRejection', async (reason, promise) => {
20667
21830
  // eslint-disable-next-line n/no-process-exit
20668
21831
  process.exit(1);
20669
21832
  });
20670
- //# debugId=6c799c7a-7beb-4a6d-a648-0638ba473165
21833
+ //# debugId=124ab184-9379-4139-921b-7d32dd1456f3
20671
21834
  //# sourceMappingURL=cli.js.map