@socketsecurity/cli-with-sentry 1.8.4 → 1.9.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.
package/dist/utils.js CHANGED
@@ -17,17 +17,19 @@ var regexps = require('../external/@socketsecurity/registry/lib/regexps');
17
17
  var prompts = require('../external/@socketsecurity/registry/lib/prompts');
18
18
  var env = require('../external/@socketsecurity/registry/lib/env');
19
19
  var spawn = require('../external/@socketsecurity/registry/lib/spawn');
20
- var fs = require('node:fs');
21
- var fs$2 = require('node:fs/promises');
20
+ var require$$0 = require('node:crypto');
21
+ var require$$1$1 = require('node:stream');
22
22
  var require$$9 = require('node:stream/promises');
23
+ var u = require('node:fs');
24
+ var fs$1 = require('node:fs/promises');
23
25
  var require$$11 = require('node:zlib');
24
- var fs$1 = require('../external/@socketsecurity/registry/lib/fs');
25
- var require$$1 = require('node:module');
26
- var os = require('node:os');
26
+ var fs = require('../external/@socketsecurity/registry/lib/fs');
27
27
  var url = require('../external/@socketsecurity/registry/lib/url');
28
+ var require$$1 = require('node:module');
28
29
  var agent = require('../external/@socketsecurity/registry/lib/agent');
29
30
  var bin = require('../external/@socketsecurity/registry/lib/bin');
30
31
  var packages = require('../external/@socketsecurity/registry/lib/packages');
32
+ var os = require('node:os');
31
33
  var process$1 = require('node:process');
32
34
  var require$$7 = require('node:timers/promises');
33
35
  var promises = require('../external/@socketsecurity/registry/lib/promises');
@@ -39,7 +41,6 @@ var streams = require('../external/@socketsecurity/registry/lib/streams');
39
41
  var require$$10 = require('node:util');
40
42
  var require$$12 = require('node:http');
41
43
  var node_tls = require('node:tls');
42
- var require$$0 = require('node:crypto');
43
44
 
44
45
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
45
46
  /**
@@ -297,7 +298,7 @@ async function retryWrite(filepath, content, retries = 3, baseDelay = 10) {
297
298
  for (let attempt = 0; attempt <= retries; attempt++) {
298
299
  try {
299
300
  // eslint-disable-next-line no-await-in-loop
300
- await fs.promises.writeFile(filepath, content);
301
+ await u.promises.writeFile(filepath, content);
301
302
  if (process.platform === 'win32') {
302
303
  // eslint-disable-next-line no-await-in-loop
303
304
  await require$$7.setTimeout(50);
@@ -306,7 +307,7 @@ async function retryWrite(filepath, content, retries = 3, baseDelay = 10) {
306
307
  while (accessRetries < maxAccessRetries) {
307
308
  try {
308
309
  // eslint-disable-next-line no-await-in-loop
309
- await fs.promises.access(filepath);
310
+ await u.promises.access(filepath);
310
311
  // eslint-disable-next-line no-await-in-loop
311
312
  await require$$7.setTimeout(10);
312
313
  break;
@@ -347,7 +348,7 @@ async function readFile(filepath) {
347
348
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
348
349
  try {
349
350
  // eslint-disable-next-line no-await-in-loop
350
- return await fs.promises.readFile(filepath, 'utf8');
351
+ return await u.promises.readFile(filepath, 'utf8');
351
352
  } catch (err) {
352
353
  const isLastAttempt = attempt === maxRetries;
353
354
  const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
@@ -636,7 +637,7 @@ function getConfigValues() {
636
637
  } = constants.default;
637
638
  if (socketAppDataPath) {
638
639
  const configFilePath = path.join(socketAppDataPath, 'config.json');
639
- const raw = fs$1.safeReadFileSync(configFilePath);
640
+ const raw = fs.safeReadFileSync(configFilePath);
640
641
  if (raw) {
641
642
  try {
642
643
  Object.assign(_cachedConfig, JSON.parse(Buffer.from(raw, 'base64').toString()));
@@ -653,7 +654,7 @@ function getConfigValues() {
653
654
  updateConfigValue(constants.CONFIG_KEY_API_TOKEN, token);
654
655
  }
655
656
  } else {
656
- fs.mkdirSync(socketAppDataPath, {
657
+ u.mkdirSync(socketAppDataPath, {
657
658
  recursive: true
658
659
  });
659
660
  }
@@ -682,10 +683,10 @@ function findSocketYmlSync(dir = process.cwd()) {
682
683
  let prevDir = null;
683
684
  while (dir !== prevDir) {
684
685
  let ymlPath = path.join(dir, constants.SOCKET_YML);
685
- let yml = fs$1.safeReadFileSync(ymlPath);
686
+ let yml = fs.safeReadFileSync(ymlPath);
686
687
  if (yml === undefined) {
687
688
  ymlPath = path.join(dir, constants.SOCKET_YAML);
688
- yml = fs$1.safeReadFileSync(ymlPath);
689
+ yml = fs.safeReadFileSync(ymlPath);
689
690
  }
690
691
  if (typeof yml === 'string') {
691
692
  try {
@@ -864,12 +865,12 @@ function updateConfigValue(configKey, value) {
864
865
  socketAppDataPath
865
866
  } = constants.default;
866
867
  if (socketAppDataPath) {
867
- fs.mkdirSync(socketAppDataPath, {
868
+ u.mkdirSync(socketAppDataPath, {
868
869
  recursive: true
869
870
  });
870
871
  const configFilePath = path.join(socketAppDataPath, 'config.json');
871
872
  // Read existing file to preserve formatting, then update with new values.
872
- const existingRaw = fs$1.safeReadFileSync(configFilePath);
873
+ const existingRaw = fs.safeReadFileSync(configFilePath);
873
874
  const EditableJson = getEditableJsonClass();
874
875
  const editor = new EditableJson();
875
876
  if (existingRaw !== undefined) {
@@ -916,7 +917,7 @@ function updateConfigValue(configKey, value) {
916
917
 
917
918
  // Stringify with formatting preserved.
918
919
  const jsonContent = JSON.stringify(contentToSave, undefined, indent).replace(/\n/g, newline);
919
- fs.writeFileSync(configFilePath, Buffer.from(jsonContent + newline).toString('base64'));
920
+ u.writeFileSync(configFilePath, Buffer.from(jsonContent + newline).toString('base64'));
920
921
  }
921
922
  });
922
923
  }
@@ -1892,7 +1893,7 @@ function getExtraCaCerts() {
1892
1893
  return undefined;
1893
1894
  }
1894
1895
  try {
1895
- const extraCerts = fs.readFileSync(certPath, 'utf-8');
1896
+ const extraCerts = u.readFileSync(certPath, 'utf-8');
1896
1897
  // Combine default root certificates with extra certificates. Specifying ca
1897
1898
  // in an agent replaces the default trust store, so both must be included.
1898
1899
  _extraCaCerts = [...node_tls.rootCertificates, extraCerts];
@@ -2225,6 +2226,21 @@ function _httpsRequestFetch(url, init, agent, redirectCount) {
2225
2226
  req.end();
2226
2227
  });
2227
2228
  }
2229
+
2230
+ /**
2231
+ * The cause for a failed request to `path`, with the explanation the Socket
2232
+ * API gives in its `{ error: { message } }` body when there is one.
2233
+ */
2234
+ async function describeApiFailure(response, path) {
2235
+ let serverMessage;
2236
+ try {
2237
+ serverMessage = JSON.parse(await response.text())?.error?.message;
2238
+ } catch {
2239
+ // A body that isn't JSON carries no message.
2240
+ }
2241
+ const reason = await getErrorMessageForHttpStatusCode(response.status);
2242
+ return typeof serverMessage === 'string' && serverMessage ? `${response.statusText}: ${serverMessage} (reason: ${reason}) (path: ${path})` : `${response.statusText} (reason: ${reason}) (path: ${path})`;
2243
+ }
2228
2244
  async function apiFetch(url, init = {}) {
2229
2245
  return await _httpsRequestFetch(url, init, getHttpsAgent(), 0);
2230
2246
  }
@@ -2449,9 +2465,6 @@ async function handleApiCallNoSpinner(value, description) {
2449
2465
  }
2450
2466
  async function queryApi(path, apiToken) {
2451
2467
  const baseUrl = getDefaultApiBaseUrl();
2452
- if (!baseUrl) {
2453
- throw new Error('Socket API base URL is not configured.');
2454
- }
2455
2468
  const url = `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`;
2456
2469
  const result = await apiFetch(url, {
2457
2470
  method: 'GET',
@@ -2519,7 +2532,7 @@ async function queryApiSafeTextWithStatus(path, description, commandPath) {
2519
2532
  return {
2520
2533
  ok: false,
2521
2534
  message: 'Socket API error',
2522
- cause: `${result.statusText} (reason: ${await getErrorMessageForHttpStatusCode(status)}) (path: ${path})`,
2535
+ cause: await describeApiFailure(result, path),
2523
2536
  data: {
2524
2537
  code: status
2525
2538
  }
@@ -2590,13 +2603,6 @@ async function sendApiRequest(path, options) {
2590
2603
  };
2591
2604
  }
2592
2605
  const baseUrl = getDefaultApiBaseUrl();
2593
- if (!baseUrl) {
2594
- return {
2595
- ok: false,
2596
- message: 'Configuration Error',
2597
- cause: 'Socket API endpoint is not configured. Please check your environment configuration.'
2598
- };
2599
- }
2600
2606
  const {
2601
2607
  body,
2602
2608
  commandPath,
@@ -2657,7 +2663,7 @@ async function sendApiRequest(path, options) {
2657
2663
  return {
2658
2664
  ok: false,
2659
2665
  message: 'Socket API error',
2660
- cause: `${result.statusText} (reason: ${await getErrorMessageForHttpStatusCode(status)}) (path: ${path})`,
2666
+ cause: await describeApiFailure(result, path),
2661
2667
  data: {
2662
2668
  code: status
2663
2669
  }
@@ -4540,7 +4546,7 @@ async function getWorkspaceGlobs(agent, cwd = process.cwd()) {
4540
4546
  let workspacePatterns;
4541
4547
  if (agent === constants.PNPM) {
4542
4548
  const workspacePath = path.join(cwd, 'pnpm-workspace.yaml');
4543
- const yml = await fs$1.safeReadFile(workspacePath);
4549
+ const yml = await fs.safeReadFile(workspacePath);
4544
4550
  if (yml) {
4545
4551
  try {
4546
4552
  workspacePatterns = vendor.distExports$1.parse(yml)?.packages;
@@ -4766,7 +4772,7 @@ async function globWithGitIgnore(patterns, options) {
4766
4772
  venv: true
4767
4773
  };
4768
4774
  }
4769
- const content = (await fs$1.safeReadFile(filepath)) ?? '';
4775
+ const content = (await fs.safeReadFile(filepath)) ?? '';
4770
4776
  return {
4771
4777
  content,
4772
4778
  dir: dirRel,
@@ -4898,7 +4904,7 @@ function pathsToGlobPatterns(paths, cwd) {
4898
4904
  }
4899
4905
  const absolutePath = path.isAbsolute(p) ? p : path.resolve(cwd ?? process.cwd(), p);
4900
4906
  // If the path is a directory, scan it recursively for all files.
4901
- if (fs$1.isDirSync(absolutePath)) {
4907
+ if (fs.isDirSync(absolutePath)) {
4902
4908
  return `${p}/**/*`;
4903
4909
  }
4904
4910
  return p;
@@ -5054,237 +5060,6 @@ function* walkNestedMap(map, keys = []) {
5054
5060
  }
5055
5061
  }
5056
5062
 
5057
- /**
5058
- * Coana integration utilities for Socket CLI.
5059
- * Manages reachability analysis via Coana tech CLI.
5060
- *
5061
- * Key Functions:
5062
- * - compressSocketFactsForUpload: Brotli-compress any .socket.facts.json
5063
- * entries in scanPaths just before upload, returning swapped paths plus a
5064
- * cleanup callback. Coana keeps writing plain JSON; the on-the-wire form
5065
- * to depscan is brotli (api-v0 decodes at the multipart boundary).
5066
- * - extractReachabilityErrors: Extract per-component reachability errors
5067
- * - extractTier1ReachabilityScanId: Extract scan ID from socket facts file
5068
- * - getFullWorkspacePath: Label a build-root/workspace pair the way Coana does
5069
- * - snapshotSocketFacts: Copy aside the socket facts files Coana overwrites
5070
- * in place, so a failed analysis can be rolled back
5071
- *
5072
- * Integration:
5073
- * - Works with @coana-tech/cli for reachability analysis
5074
- * - Processes socket facts JSON files
5075
- * - Extracts full application reachability scan identifiers
5076
- */
5077
-
5078
- const {
5079
- DOT_SOCKET_DOT_FACTS_JSON
5080
- } = constants.default;
5081
- /**
5082
- * For each `.socket.facts.json` in `scanPaths`, stream-brotli-compress a
5083
- * sibling `.socket.facts.json.br` next to the original file and swap its
5084
- * path in. Other paths pass through unchanged. Missing files also pass
5085
- * through unchanged (the upload will fail downstream with the same error
5086
- * it would have).
5087
- *
5088
- * Streaming + worker-thread compression keeps the event loop responsive:
5089
- * default brotli quality (11) on a 60+MB facts file takes multiple seconds
5090
- * of CPU, which would otherwise freeze the spinner / signal handlers /
5091
- * any concurrent work.
5092
- *
5093
- * The `.br` lives next to the source rather than under the OS temp dir
5094
- * because depscan's multipart ingest (`addStreamEntry`) rejects entries
5095
- * whose names contain `..` traversal segments. The SDK computes the
5096
- * multipart entry name via `path.relative(cwd, brPath)`, so an OS-tmpdir
5097
- * temp path turns into `../../../var/folders/...` and gets dropped as
5098
- * `unmatchedFiles`. Sibling-write keeps the relative path inside cwd, and
5099
- * keeps the directory shape symmetric with the plain `.socket.facts.json`
5100
- * upload (depscan strips only the `.br` suffix at ingest, so
5101
- * `<dir>/.socket.facts.json.br` and `<dir>/.socket.facts.json` resolve to
5102
- * the same storage path).
5103
- *
5104
- * Concurrent scans against the same source directory are already racy on
5105
- * `.socket.facts.json` itself (coana writes to a single path), so the
5106
- * sibling `.br` doesn't introduce a new race.
5107
- *
5108
- * Caller MUST `await cleanup()` (typically in a `finally` block) once the
5109
- * upload completes — successful or not — to remove the sibling files.
5110
- */
5111
- async function compressSocketFactsForUpload(scanPaths) {
5112
- const brPaths = [];
5113
- const cleanup = async () => {
5114
- const targets = brPaths.splice(0);
5115
- // `recursive: true` defends against the (defensive) case where a sibling
5116
- // path was somehow created as a directory — `rm` would otherwise throw
5117
- // on the first such entry and skip the rest. `force: true` no-ops on
5118
- // missing paths so the function stays idempotent.
5119
- await Promise.all(targets.map(t => fs$2.rm(t, {
5120
- recursive: true,
5121
- force: true
5122
- })));
5123
- };
5124
- // Use `allSettled` (not `all`) so a failure in one entry doesn't leak the
5125
- // others' in-flight pipelines past our `catch`. If we used `all`, the
5126
- // first rejection would bubble out while sibling pipelines were still
5127
- // writing bytes — `cleanup()` would race with those writes and could
5128
- // remove a `.br` only to have it re-created after we returned.
5129
- const results = await Promise.allSettled(scanPaths.map(async p => {
5130
- // depscan decodes only the bare `.socket.facts.json.br`; a named facts
5131
- // file is uploaded as plain JSON.
5132
- if (path.basename(p) !== DOT_SOCKET_DOT_FACTS_JSON) {
5133
- return p;
5134
- }
5135
- if (!fs.existsSync(p)) {
5136
- return p;
5137
- }
5138
- const brPath = `${p}.br`;
5139
- // Track the sibling path BEFORE the pipeline starts so a
5140
- // partially-written `.br` is removed even if the pipeline rejects.
5141
- // `rm({ force: true })` no-ops on missing files, so tracking before
5142
- // creation is safe.
5143
- brPaths.push(brPath);
5144
- await require$$9.pipeline(fs.createReadStream(p), require$$11.createBrotliCompress(), fs.createWriteStream(brPath));
5145
- return brPath;
5146
- }));
5147
- const failure = results.find(r => r.status === 'rejected');
5148
- if (failure) {
5149
- // All pipelines have settled, so cleanup() can safely remove every
5150
- // `.br` we tracked (succeeded or partial) without racing live writes.
5151
- await cleanup();
5152
- throw failure.reason;
5153
- }
5154
- const paths = results.map(r => r.value);
5155
- return {
5156
- paths,
5157
- cleanup
5158
- };
5159
- }
5160
-
5161
- // `.socket.facts.json` or a named `<entry>.socket.facts.json`, matching
5162
- // depscan's case-insensitive `*.socket.facts.json`.
5163
- function isSocketFactsFile(filepath) {
5164
- return path.basename(filepath).toLowerCase().endsWith(DOT_SOCKET_DOT_FACTS_JSON);
5165
- }
5166
-
5167
- // A Coana reachability report, never input to a new analysis: the bare
5168
- // `.socket.facts.json` (Coana's default name; producers name theirs after the
5169
- // build) or the path this run tells Coana to write to.
5170
- function isReachabilityReportPath(filepath, options) {
5171
- const {
5172
- cwd,
5173
- outputPath
5174
- } = {
5175
- __proto__: null,
5176
- ...options
5177
- };
5178
- return path.basename(filepath).toLowerCase() === DOT_SOCKET_DOT_FACTS_JSON || path.resolve(cwd, filepath) === path.resolve(cwd, outputPath);
5179
- }
5180
- function extractReachabilityErrors(socketFactsFile) {
5181
- const json = fs$1.readJsonSync(socketFactsFile, {
5182
- throws: false
5183
- });
5184
- if (!json || !Array.isArray(json.components)) {
5185
- return [];
5186
- }
5187
- const errors = [];
5188
- for (const component of json.components) {
5189
- if (!Array.isArray(component.reachability)) {
5190
- continue;
5191
- }
5192
- for (const ghsaEntry of component.reachability) {
5193
- if (!Array.isArray(ghsaEntry.reachability)) {
5194
- continue;
5195
- }
5196
- for (const entry of ghsaEntry.reachability) {
5197
- if (entry.type === 'error') {
5198
- errors.push({
5199
- componentName: String(component.name ?? ''),
5200
- componentVersion: String(component.version ?? ''),
5201
- ghsaId: String(ghsaEntry.ghsa_id ?? ''),
5202
- subprojectPath: String(entry.subprojectPath ?? ''),
5203
- workspacePath: String(entry.workspacePath ?? '')
5204
- });
5205
- }
5206
- }
5207
- }
5208
- }
5209
- return errors;
5210
- }
5211
- function extractTier1ReachabilityScanId(socketFactsFile) {
5212
- const json = fs$1.readJsonSync(socketFactsFile, {
5213
- throws: false
5214
- });
5215
- const tier1ReachabilityScanId = String(json?.['tier1ReachabilityScanId'] ?? '').trim();
5216
- return tier1ReachabilityScanId.length > 0 ? tier1ReachabilityScanId : undefined;
5217
- }
5218
-
5219
- // Label a reachability location the way Coana's own `getFullWorkspacePath`
5220
- // does. Since @coana-tech/cli v15.10.8 `subprojectPath` names the build root
5221
- // and `workspacePath` the workspace within it, so a single-root repo reports
5222
- // `.` as its subproject and must be labelled by its workspace alone.
5223
- function getFullWorkspacePath(subprojectPath, workspacePath) {
5224
- const subproject = subprojectPath && subprojectPath !== '.' ? subprojectPath : '';
5225
- const workspace = workspacePath && workspacePath !== '.' ? workspacePath : '';
5226
- if (subproject && workspace) {
5227
- return `${subproject}/${workspace}`;
5228
- }
5229
- return subproject || workspace || '.';
5230
- }
5231
- // Coana overwrites `.socket.facts.json` in place, so an analysis that fails
5232
- // part way leaves partial output where the scan's inputs used to be.
5233
- async function snapshotSocketFacts(scanPaths, options) {
5234
- const {
5235
- cwd,
5236
- outputPath,
5237
- tmpDir
5238
- } = {
5239
- __proto__: null,
5240
- ...options
5241
- };
5242
- const sources = new Set(scanPaths.filter(p => path.basename(p) === DOT_SOCKET_DOT_FACTS_JSON).map(p => path.resolve(cwd, p)));
5243
- sources.add(path.resolve(cwd, outputPath));
5244
- const snapshotDir = path.join(tmpDir, 'socket-facts-snapshot');
5245
- await fs$2.mkdir(snapshotDir, {
5246
- recursive: true
5247
- });
5248
- const captured = [];
5249
- const created = [];
5250
- await Promise.all(Array.from(sources, async (source, index) => {
5251
- if (!fs.existsSync(source)) {
5252
- created.push(source);
5253
- return;
5254
- }
5255
- const backup = path.join(snapshotDir, `${index}.json`);
5256
- try {
5257
- await fs$2.copyFile(source, backup);
5258
- captured.push({
5259
- backup,
5260
- source
5261
- });
5262
- } catch (e) {
5263
- // A path we cannot copy is left out of both lists so restore() leaves
5264
- // it untouched rather than deleting a file it never captured.
5265
- debug$2.debugFn('warn', `[socket-facts] snapshot failed: ${source}`);
5266
- debug$2.debugDir('error', e);
5267
- }
5268
- }));
5269
- return {
5270
- async restore() {
5271
- await Promise.all([...captured.map(async ({
5272
- backup,
5273
- source
5274
- }) => {
5275
- try {
5276
- await fs$2.copyFile(backup, source);
5277
- } catch (e) {
5278
- debug$2.debugFn('warn', `[socket-facts] restore failed: ${source}`);
5279
- debug$2.debugDir('error', e);
5280
- }
5281
- }), ...created.map(source => fs$2.rm(source, {
5282
- force: true
5283
- }).catch(() => {}))]);
5284
- }
5285
- };
5286
- }
5287
-
5288
5063
  /**
5289
5064
  * File system utilities for Socket CLI.
5290
5065
  * Provides file and directory search functionality.
@@ -5336,7 +5111,7 @@ async function findUp(name, options) {
5336
5111
  const thePath = path.join(dir, name);
5337
5112
  try {
5338
5113
  // eslint-disable-next-line no-await-in-loop
5339
- const stats = await fs.promises.stat(thePath);
5114
+ const stats = await u.promises.stat(thePath);
5340
5115
  if (!onlyDirectories && stats.isFile()) {
5341
5116
  return thePath;
5342
5117
  }
@@ -5350,11 +5125,11 @@ async function findUp(name, options) {
5350
5125
  return undefined;
5351
5126
  }
5352
5127
  async function withTmpDir(prefix, fn) {
5353
- const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
5128
+ const tmpDir = await u.promises.mkdtemp(path.join(os.tmpdir(), prefix));
5354
5129
  try {
5355
5130
  return await fn(tmpDir);
5356
5131
  } finally {
5357
- await fs.promises.rm(tmpDir, {
5132
+ await u.promises.rm(tmpDir, {
5358
5133
  recursive: true,
5359
5134
  force: true
5360
5135
  }).catch(() => {});
@@ -5366,771 +5141,605 @@ async function withTmpDir(prefix, fn) {
5366
5141
  // path still gets a usable absolute value instead of throwing.
5367
5142
  async function realpathOrResolved(target) {
5368
5143
  try {
5369
- return await fs.promises.realpath(target);
5144
+ return await u.promises.realpath(target);
5370
5145
  } catch {
5371
5146
  return path.resolve(target);
5372
5147
  }
5373
5148
  }
5374
5149
 
5375
- function findBinPathDetailsSync(binName) {
5376
- const rawBinPaths = bin.whichBinSync(binName, {
5377
- all: true,
5378
- nothrow: true
5379
- }) ?? [];
5380
- // whichBinSync may return a string when only one result is found, even with all: true.
5381
- // This handles both the current published version and future versions.
5382
- const binPaths = Array.isArray(rawBinPaths) ? rawBinPaths : typeof rawBinPaths === 'string' ? [rawBinPaths] : [];
5383
- const {
5384
- shadowBinPath
5385
- } = constants.default;
5386
- let shadowIndex = -1;
5387
- let theBinPath;
5388
- for (let i = 0, {
5389
- length
5390
- } = binPaths; i < length; i += 1) {
5391
- const binPath = binPaths[i];
5392
- // Skip our bin directory if it's in the front.
5393
- if (path.dirname(binPath) === shadowBinPath) {
5394
- shadowIndex = i;
5395
- } else {
5396
- theBinPath = bin.resolveBinPathSync(binPath);
5397
- break;
5398
- }
5150
+ /**
5151
+ * Coana CLI releases for Socket CLI.
5152
+ * Runs the Coana CLI from the cache that Coana and the Socket CLIs share, and
5153
+ * downloads it from the Socket API when the cache lacks it. "Launching Coana"
5154
+ * in coana-package-manager's packages/cli/docs/gcs-releases.md specifies the
5155
+ * steps.
5156
+ *
5157
+ * Key Functions:
5158
+ * - spawnCoana: Run the Coana CLI for a version spec
5159
+ *
5160
+ * Cache:
5161
+ * - Only the vendored coana-cache.mjs reads or writes it
5162
+ */
5163
+
5164
+ const CACHE_NOT_WRITABLE_EXIT_CODE = 3;
5165
+ const COANA_CACHE_SCRIPT = 'coana-cache.mjs';
5166
+ const COMPONENT = 'cli';
5167
+ const NOT_CACHED_EXIT_CODE = 4;
5168
+ const SHA256_REGEXP = /^[0-9a-f]{64}$/;
5169
+ function cacheResultToCResult(result) {
5170
+ if (result.exitCode === 0) {
5171
+ return {
5172
+ ok: true,
5173
+ data: result.stdout
5174
+ };
5399
5175
  }
5400
5176
  return {
5401
- name: binName,
5402
- path: theBinPath,
5403
- shadowed: shadowIndex !== -1
5177
+ ok: false,
5178
+ message: result.exitCode === CACHE_NOT_WRITABLE_EXIT_CODE ? 'Cannot write to the Coana cache' : 'The Coana cache failed',
5179
+ cause: result.stderr || `${COANA_CACHE_SCRIPT} exited with ${result.exitCode}`
5404
5180
  };
5405
5181
  }
5406
- function findNpmDirPathSync(npmBinPath) {
5407
- const {
5408
- WIN32
5409
- } = constants.default;
5410
- let thePath = npmBinPath;
5411
- while (true) {
5412
- const libNmNpmPath = path.join(thePath, `lib/${constants.NODE_MODULES}/${constants.NPM}`);
5413
- // mise, which uses opaque binaries, puts its npm bin in a path like:
5414
- // /Users/SomeUsername/.local/share/mise/installs/node/vX.X.X/bin/npm.
5415
- // HOWEVER, the location of the npm install is:
5416
- // /Users/SomeUsername/.local/share/mise/installs/node/vX.X.X/lib/node_modules/npm.
5417
- if (
5418
- // Use existsSync here because statsSync, even with { throwIfNoEntry: false },
5419
- // will throw an ENOTDIR error for paths like ./a-file-that-exists/a-directory-that-does-not.
5420
- // See https://github.com/nodejs/node/issues/56993.
5421
- fs$1.isDirSync(libNmNpmPath)) {
5422
- thePath = libNmNpmPath;
5423
- }
5424
- const hasNmInCurrPath = fs$1.isDirSync(path.join(thePath, constants.NODE_MODULES));
5425
- const hasNmInParentPath = !hasNmInCurrPath && fs$1.isDirSync(path.join(thePath, `../${constants.NODE_MODULES}`));
5426
- if (
5427
- // npm bin paths may look like:
5428
- // /usr/local/share/npm/bin/npm
5429
- // /Users/SomeUsername/.nvm/versions/node/vX.X.X/bin/npm
5430
- // C:\Users\SomeUsername\AppData\Roaming\npm\bin\npm.cmd
5431
- // OR
5432
- // C:\Program Files\nodejs\npm.cmd
5433
- //
5434
- // In practically all cases the npm path contains a node_modules folder:
5435
- // /usr/local/share/npm/bin/npm/node_modules
5436
- // C:\Program Files\nodejs\node_modules
5437
- (hasNmInCurrPath ||
5438
- // In some bespoke cases the node_modules folder is in the parent directory.
5439
- hasNmInParentPath) && (
5440
- // Optimistically look for the default location.
5441
- path.basename(thePath) === constants.NPM ||
5442
- // Chocolatey installs npm bins in the same directory as node bins.
5443
- WIN32 && fs.existsSync(path.join(thePath, `${constants.NPM}.cmd`)))) {
5444
- return hasNmInParentPath ? path.dirname(thePath) : thePath;
5445
- }
5446
- const parent = path.dirname(thePath);
5447
- if (parent === thePath) {
5448
- return undefined;
5449
- }
5450
- thePath = parent;
5451
- }
5452
- }
5182
+
5453
5183
  /**
5454
- * Converts absolute scan targets inside cwd back to cwd-relative paths before
5455
- * glob expansion. SCA excludes passed through `additionalIgnores` are anchored
5456
- * to cwd, so package discovery needs target globs in the same coordinate
5457
- * system for fast-glob to apply those ignores consistently.
5184
+ * Build a CResult error from a thrown spawn error, preserving exit code,
5185
+ * signal, and stderr context.
5458
5186
  */
5459
- function normalizeScanInputPath(pathToNormalize, cwd) {
5460
- if (!path.isAbsolute(pathToNormalize)) {
5461
- return pathToNormalize;
5187
+ function coanaSpawnErrorResult(e) {
5188
+ const stderr = e?.stderr;
5189
+ const stdout = e?.stdout;
5190
+ const exitCode = e?.code;
5191
+ const signal = e?.signal;
5192
+ const details = [];
5193
+ if (typeof exitCode === 'number') {
5194
+ details.push(`exit code ${exitCode}`);
5462
5195
  }
5463
- const relativePath = path.relative(cwd, pathToNormalize);
5464
- const isInsideCwd = relativePath === '' || !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
5465
- if (!isInsideCwd) {
5466
- return pathToNormalize;
5196
+ if (signal) {
5197
+ details.push(`signal ${signal}`);
5467
5198
  }
5468
- return stripTrailingSlash(relativePath.replaceAll('\\', '/')) || '.';
5469
- }
5470
- async function getPackageFilesForScan(inputPaths, supportedFiles, options) {
5471
- const {
5472
- additionalIgnores,
5473
- config: socketConfig,
5474
- cwd = process.cwd()
5475
- } = {
5476
- __proto__: null,
5477
- ...options
5199
+ const detailSuffix = details.length ? ` (${details.join(', ')})` : '';
5200
+ // Coana logs some failures, e.g. unresolved Gradle dependencies, to stdout.
5201
+ const detail = stderr || stdout || getErrorCause(e);
5202
+ const message = typeof exitCode === 'string' ? `Failed to launch Coana${detailSuffix}: ${detail}` : `Coana command failed${detailSuffix}: ${detail}`;
5203
+ return {
5204
+ ok: false,
5205
+ data: e,
5206
+ message
5478
5207
  };
5479
-
5480
- // Apply the supported files filter during streaming to avoid accumulating
5481
- // all files in memory. This is critical for large monorepos with 100k+ files
5482
- // where accumulating all paths before filtering causes OOM errors.
5483
- const filter = createSupportedFilesFilter(supportedFiles);
5484
- const normalizedInputPaths = inputPaths.map(p => normalizeScanInputPath(p, cwd));
5485
- return await globWithGitIgnore(pathsToGlobPatterns(normalizedInputPaths, cwd), {
5486
- additionalIgnores,
5487
- cwd,
5488
- filter,
5489
- socketConfig
5490
- });
5491
5208
  }
5492
5209
 
5493
- function exitWithBinPathError$2(binName) {
5494
- logger.logger.fail(`Socket unable to locate ${binName}; ensure it is available in the PATH environment variable`);
5495
- // The exit code 127 indicates that the command or binary being executed
5496
- // could not be found.
5497
- // eslint-disable-next-line n/no-process-exit
5498
- process.exit(127);
5499
- // This line is never reached in production, but helps tests.
5500
- throw new Error('process.exit called');
5501
- }
5502
- let _yarnBinPath;
5503
- function getYarnBinPath() {
5504
- if (_yarnBinPath === undefined) {
5505
- _yarnBinPath = getYarnBinPathDetails().path;
5506
- if (!_yarnBinPath) {
5507
- exitWithBinPathError$2(constants.YARN);
5210
+ /**
5211
+ * Stream `component`'s tarball into `destDir`, checking its size and sha256 on
5212
+ * the way. The tarball holds only regular files, so anything else is dropped.
5213
+ */
5214
+ async function downloadComponent(component, destDir) {
5215
+ const response = await apiFetch(component.url, {
5216
+ headers: {
5217
+ 'User-Agent': getCliUserAgent()
5508
5218
  }
5219
+ });
5220
+ if (!response.ok || !response.body) {
5221
+ await response.body?.cancel();
5222
+ debug$2.debugFn('error', `Coana CLI download failed with ${response.status}`);
5223
+ return {
5224
+ ok: false,
5225
+ status: response.status
5226
+ };
5509
5227
  }
5510
- return _yarnBinPath;
5511
- }
5512
- let _yarnBinPathDetails;
5513
- function getYarnBinPathDetails() {
5514
- if (_yarnBinPathDetails === undefined) {
5515
- _yarnBinPathDetails = findBinPathDetailsSync(constants.YARN);
5516
- }
5517
- return _yarnBinPathDetails;
5518
- }
5519
- function isYarnBinPathShadowed() {
5520
- return getYarnBinPathDetails().shadowed;
5521
- }
5522
-
5523
- let _isYarnBerry;
5524
- function isYarnBerry() {
5525
- if (_isYarnBerry === undefined) {
5526
- try {
5527
- const yarnBinPath = getYarnBinPath();
5528
- const result = spawn.spawnSync(yarnBinPath, [constants.FLAG_VERSION], {
5529
- encoding: constants.UTF8,
5530
- // On Windows, yarn is often a .cmd file that requires shell execution.
5531
- // The spawn function from @socketsecurity/registry will handle this properly
5532
- // when shell is true.
5533
- shell: constants.default.WIN32
5534
- });
5535
- if (result.status === 0 && result.stdout) {
5536
- const version = result.stdout;
5537
- // Yarn Berry starts from version 2.x
5538
- const majorVersion = parseInt(version.split('.')[0], 10);
5539
- _isYarnBerry = majorVersion >= 2;
5540
- } else {
5541
- _isYarnBerry = false;
5228
+ const hash = require$$0.createHash('sha256');
5229
+ let received = 0;
5230
+ await require$$9.pipeline(require$$1$1.Readable.from(response.body), new require$$1$1.Transform({
5231
+ transform(chunk, _encoding, callback) {
5232
+ received += chunk.length;
5233
+ if (received > component.size) {
5234
+ callback(new Error(`The Coana CLI tarball is larger than the expected ${component.size} bytes`));
5235
+ return;
5542
5236
  }
5543
- } catch {
5544
- _isYarnBerry = false;
5545
- }
5237
+ hash.update(chunk);
5238
+ callback(null, chunk);
5239
+ }
5240
+ }), vendor.uo({
5241
+ cwd: destDir,
5242
+ filter: (_path, entry) => 'type' in entry && (entry.type === 'Directory' || entry.type === 'File'),
5243
+ // Run as root, tar would otherwise give the shared cache the archive's
5244
+ // owners.
5245
+ preserveOwner: false,
5246
+ strict: true
5247
+ }));
5248
+ const sha256 = hash.digest('hex');
5249
+ if (received !== component.size || sha256 !== component.sha256) {
5250
+ throw new Error(`The Coana CLI tarball does not match its checksum (expected ${component.size} bytes with sha256 ${component.sha256}, got ${received} bytes with sha256 ${sha256})`);
5546
5251
  }
5547
- return _isYarnBerry;
5252
+ return {
5253
+ ok: true
5254
+ };
5548
5255
  }
5549
5256
 
5550
5257
  /**
5551
- * DLX execution utilities for Socket CLI.
5552
- * Manages package execution via npx/pnpm dlx/yarn dlx commands.
5553
- *
5554
- * Key Functions:
5555
- * - spawnCdxgenDlx: Execute CycloneDX generator via dlx
5556
- * - spawnCoanaDlx: Execute Coana CLI tool via dlx
5557
- * - spawnDlx: Execute packages using dlx-style commands
5558
- * - spawnSynpDlx: Execute Synp converter via dlx
5559
- *
5560
- * Package Manager Detection:
5561
- * - Auto-detects npm, pnpm, or yarn based on lockfiles
5562
- * - Supports force-refresh and silent execution modes
5563
- *
5564
- * Integration:
5565
- * - Works with shadow binaries for security scanning
5566
- * - Handles version pinning and cache management
5567
- * - Configures environment for third-party tools
5258
+ * The directory of the Coana CLI's `cli` component for `versionSpec`, an exact
5259
+ * version or a range, downloading it into the cache when the cache lacks it.
5260
+ * `env` reaches the cache script, which reads `XDG_CACHE_HOME` from it.
5568
5261
  */
5569
-
5570
- const require$2 = require$$1.createRequire((typeof document === 'undefined' ? require$$0$1.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
5571
- const {
5572
- PACKAGE_LOCK_JSON,
5573
- PNPM_LOCK_YAML,
5574
- YARN_LOCK
5575
- } = constants.default;
5576
- /**
5577
- * Regex to check if a version string contains range operators.
5578
- * Matches any version with range operators: ~, ^, >, <, =, x, X, *, spaces, or ||.
5579
- */
5580
- const rangeOperatorsRegExp = /[~^><=xX* ]|\|\|/;
5581
-
5582
- /**
5583
- * Spawns a package using dlx-style execution (npx/pnpm dlx/yarn dlx).
5584
- * Automatically detects the appropriate package manager if not specified.
5585
- * Uses force/update flags to ensure the latest version within the range is fetched.
5586
- */
5587
- async function spawnDlx(packageSpec, args, options, spawnExtra) {
5588
- // If version is not pinned exactly, default to force and silent for better UX.
5589
- const isNotPinned = rangeOperatorsRegExp.test(packageSpec.version);
5590
- const {
5591
- agent,
5592
- force = false,
5593
- silent = isNotPinned,
5594
- ...shadowOptions
5595
- } = options ?? {};
5596
- let finalShadowOptions = shadowOptions;
5597
- let pm = agent;
5598
-
5599
- // Auto-detect package manager if not specified.
5600
- if (!pm) {
5601
- const pnpmLockPath = await findUp(PNPM_LOCK_YAML, {
5602
- onlyFiles: true
5603
- });
5604
- const yarnLockPath = pnpmLockPath ? undefined : await findUp(YARN_LOCK, {
5605
- onlyFiles: true
5606
- });
5607
- const npmLockPath = pnpmLockPath || yarnLockPath ? undefined : await findUp(PACKAGE_LOCK_JSON, {
5608
- onlyFiles: true
5609
- });
5610
- if (pnpmLockPath) {
5611
- pm = constants.PNPM;
5612
- } else if (yarnLockPath) {
5613
- pm = constants.YARN;
5614
- } else if (npmLockPath) {
5615
- pm = constants.NPM;
5616
- } else {
5617
- // Default to npm if no lockfile found.
5618
- pm = constants.NPM;
5619
- }
5620
- }
5621
- const packageString = `${packageSpec.name}@${packageSpec.version}`;
5622
-
5623
- // Build command args based on package manager.
5624
- let spawnArgs;
5625
- if (pm === constants.PNPM) {
5626
- spawnArgs = [];
5627
- // The --silent flag must come before dlx, not after.
5628
- if (silent) {
5629
- spawnArgs.push(constants.FLAG_SILENT);
5262
+ async function getCoanaCliDir(versionSpec, org, env, spinner) {
5263
+ try {
5264
+ const exactVersion = vendor.semverExports.valid(versionSpec);
5265
+ if (exactVersion) {
5266
+ const cached = await lookup(exactVersion);
5267
+ if (cached) {
5268
+ return cached;
5269
+ }
5630
5270
  }
5631
- spawnArgs.push('dlx');
5632
- // Never let the target project's `.pnpmfile.cjs` hooks run while we launch
5633
- // a third-party tool via `pnpm dlx`. In a pnpm workspace root, `pnpm dlx`
5634
- // loads the cwd's `.pnpmfile.cjs`, so a broken hook there (e.g. a `require`
5635
- // of an unresolved Git LFS pointer) crashes the launcher with a bare exit
5636
- // code before our tool ever starts. The dlx tool installs into an isolated
5637
- // store, so the project's install hooks are irrelevant to it. pnpm honors
5638
- // this only as a config setting, not as a `dlx` CLI flag, so it must be set
5639
- // via the npm_config_ env var. See: https://pnpm.io/npmrc#settings
5640
- const pnpmEnv = {
5641
- ...objects.getOwn(finalShadowOptions, 'env'),
5642
- npm_config_ignore_pnpmfile: 'true'
5643
- };
5644
- if (force) {
5645
- // Set dlx-cache-max-age to 0 minutes to bypass cache and force a fresh
5646
- // download. This ensures we always get the latest version within the range.
5647
- pnpmEnv['npm_config_dlx_cache_max_age'] = '0';
5271
+ if (!org.ok) {
5272
+ // getDefaultOrgSlug puts some explanations in `data`.
5273
+ const detail = org.cause ?? (typeof org.data === 'string' ? org.data : undefined);
5274
+ return {
5275
+ ok: false,
5276
+ message: constants.ERROR_UNABLE_RESOLVE_ORG,
5277
+ cause: `Downloading the Coana CLI ${versionSpec} needs one: ${org.message}${detail ? ` (${detail})` : ''}`
5278
+ };
5648
5279
  }
5649
- finalShadowOptions = {
5650
- ...finalShadowOptions,
5651
- env: pnpmEnv
5652
- };
5653
- spawnArgs.push(packageString, ...args);
5654
- const shadowPnpmBin = /*@__PURE__*/require$2(constants.default.shadowPnpmBinPath);
5655
- return await shadowPnpmBin(spawnArgs, finalShadowOptions, spawnExtra);
5656
- } else if (pm === constants.YARN && isYarnBerry()) {
5657
- spawnArgs = ['dlx'];
5658
- // Yarn dlx runs in a temporary environment by design and should always fetch fresh.
5659
- if (silent) {
5660
- spawnArgs.push(constants.FLAG_QUIET);
5280
+ const orgSlug = org.data;
5281
+ const artifacts = await requestArtifacts(exactVersion ?? versionSpec, orgSlug);
5282
+ if (!artifacts.ok) {
5283
+ return artifacts;
5661
5284
  }
5662
- spawnArgs.push(packageString, ...args);
5663
-
5664
- // Use node-modules linker instead of PnP to avoid issues with packages
5665
- // that have undeclared dependencies (e.g. @coana-tech/cli -> @babel/types).
5666
- finalShadowOptions = {
5667
- ...finalShadowOptions,
5668
- env: {
5669
- ...objects.getOwn(finalShadowOptions, 'env'),
5670
- YARN_NODE_LINKER: 'node-modules'
5285
+ if (artifacts.data.version !== exactVersion) {
5286
+ const cached = await lookup(artifacts.data.version);
5287
+ if (cached) {
5288
+ return cached;
5671
5289
  }
5672
- };
5673
- const shadowYarnBin = /*@__PURE__*/require$2(constants.default.shadowYarnBinPath);
5674
- return await shadowYarnBin(spawnArgs, finalShadowOptions, spawnExtra);
5675
- } else {
5676
- // Use npm exec/npx.
5677
- // For consistency, we'll use npx which is more commonly used for one-off execution.
5678
- spawnArgs = ['--yes'];
5679
- if (force) {
5680
- // Use --force to bypass cache and get latest within range.
5681
- spawnArgs.push('--force');
5682
5290
  }
5683
- if (silent) {
5684
- spawnArgs.push(constants.FLAG_SILENT);
5291
+ const downloading = `Downloading the Coana CLI ${artifacts.data.version}...`;
5292
+ const spinning = spinner?.isSpinning ? spinner : undefined;
5293
+ const previousText = spinning?.text;
5294
+ if (spinning) {
5295
+ spinning.setText(downloading);
5296
+ } else {
5297
+ logger.logger.info(downloading);
5685
5298
  }
5686
- spawnArgs.push(packageString, ...args);
5687
- const shadowNpxBin = /*@__PURE__*/require$2(constants.default.shadowNpxBinPath);
5688
- return await shadowNpxBin(spawnArgs, finalShadowOptions, spawnExtra);
5299
+ try {
5300
+ return await installComponent(artifacts.data, orgSlug, env);
5301
+ } finally {
5302
+ spinning?.setText(previousText);
5303
+ }
5304
+ } catch (e) {
5305
+ return {
5306
+ ok: false,
5307
+ message: `Failed to get the Coana CLI ${versionSpec}`,
5308
+ cause: getErrorCause(e)
5309
+ };
5310
+ }
5311
+
5312
+ /** The cached directory or a failure; `undefined` when the cache lacks it. */
5313
+ async function lookup(version) {
5314
+ const result = await runCoanaCache(['lookup', version, COMPONENT], env);
5315
+ return result.exitCode === NOT_CACHED_EXIT_CODE ? undefined : cacheResultToCResult(result);
5689
5316
  }
5690
5317
  }
5691
- /**
5692
- * Cache of resolved Coana CLI script paths from the npm-install fallback,
5693
- * keyed by version string. Lives for the lifetime of the Socket CLI process so
5694
- * repeated invocations (e.g. socket fix --pr looping per GHSA) only install
5695
- * once.
5696
- */
5697
- const installedCoanaScriptPathsByVersion = new Map();
5698
-
5699
- /**
5700
- * Strip npm-injected `npm_package_*` env vars before spawning a Coana
5701
- * subprocess. npm (and pnpm/yarn classic) populate one env var per leaf in
5702
- * the cwd's package.json — `npm_package_dependencies_*`, `npm_package_scripts_*`,
5703
- * etc. In big monorepos with hundreds of deps this can easily account for
5704
- * 50KB+ of environment, pushing combined argv + env past Linux ARG_MAX
5705
- * (~128KB) and causing `spawn` to fail with E2BIG before Coana even starts.
5706
- *
5707
- * Coana does not read `npm_package_*` itself, so dropping them is safe. We
5708
- * intentionally keep `npm_config_*` (registry, cache, proxy settings sourced
5709
- * from .npmrc), `npm_lifecycle_*`, and everything else untouched — those can
5710
- * matter for outbound network behavior of nested `npm install` calls.
5711
- */
5712
- function sanitizeEnvForCoanaSubprocess(env) {
5713
- const out = {};
5714
- for (const key of Object.keys(env)) {
5715
- if (key.startsWith('npm_package_')) {
5716
- continue;
5318
+ async function installComponent(artifacts, orgSlug, env) {
5319
+ return await withTmpDir('socket-coana-cli-', async extractDir => {
5320
+ let current = artifacts;
5321
+ let download = await downloadComponent(current.cli, extractDir);
5322
+ // A 4xx means the signed URL expired or was otherwise refused.
5323
+ if (!download.ok && download.status >= 400 && download.status < 500) {
5324
+ // Pin the new request to the resolved version, which a range is not.
5325
+ const renewed = await requestArtifacts(current.version, orgSlug);
5326
+ if (!renewed.ok) {
5327
+ return renewed;
5328
+ }
5329
+ current = renewed.data;
5330
+ download = await downloadComponent(current.cli, extractDir);
5331
+ }
5332
+ if (!download.ok) {
5333
+ return {
5334
+ ok: false,
5335
+ message: `Failed to download the Coana CLI ${current.version}`,
5336
+ cause: `The download host responded with status ${download.status}.`
5337
+ };
5717
5338
  }
5718
- out[key] = env[key];
5339
+ return cacheResultToCResult(await runCoanaCache(['install', current.version, COMPONENT, current.cli.sha256, extractDir], env));
5340
+ });
5341
+ }
5342
+ function parseCoanaArtifacts(value) {
5343
+ const {
5344
+ components,
5345
+ version
5346
+ } = value ?? {};
5347
+ const {
5348
+ sha256,
5349
+ size,
5350
+ url
5351
+ } = components?.[COMPONENT] ?? {};
5352
+ if (typeof version !== 'string' || !vendor.semverExports.valid(version) || typeof sha256 !== 'string' || !SHA256_REGEXP.test(sha256) || typeof size !== 'number' || !Number.isSafeInteger(size) || size <= 0 || typeof url !== 'string' ||
5353
+ // apiFetch speaks only https.
5354
+ !url.startsWith('https://') || !URL.canParse(url)) {
5355
+ return undefined;
5719
5356
  }
5720
- return out;
5357
+ return {
5358
+ cli: {
5359
+ sha256,
5360
+ size,
5361
+ url
5362
+ },
5363
+ version
5364
+ };
5721
5365
  }
5722
-
5723
- /**
5724
- * Spawn an installed Coana entry point via `node` (or directly, if it's a
5725
- * native binary). Shared by the SOCKET_CLI_COANA_LOCAL_PATH branch and the
5726
- * npm-install fallback.
5727
- */
5728
- async function spawnCoanaScriptViaNode(scriptPath, args, finalEnv, options, spawnExtra) {
5729
- const isBinary = !scriptPath.endsWith('.js') && !scriptPath.endsWith('.mjs');
5730
- const spawnArgs = isBinary ? args : [scriptPath, ...args];
5731
- const spawnResult = await spawn.spawn(isBinary ? scriptPath : 'node', spawnArgs, {
5732
- cwd: options.cwd,
5733
- env: sanitizeEnvForCoanaSubprocess(finalEnv),
5734
- stdio: options.stdio ?? spawnExtra?.['stdio'] ?? 'inherit'
5366
+ async function requestArtifacts(versionSpec, orgSlug) {
5367
+ const result = await sendApiRequest(`orgs/${encodeURIComponent(orgSlug)}/coana-cli/artifacts`, {
5368
+ body: {
5369
+ components: [COMPONENT],
5370
+ version: versionSpec
5371
+ },
5372
+ method: 'POST'
5735
5373
  });
5374
+ if (!result.ok) {
5375
+ return result;
5376
+ }
5377
+ const artifacts = parseCoanaArtifacts(result.data);
5378
+ if (!artifacts) {
5379
+ debug$2.debugDir('inspect', {
5380
+ body: result.data
5381
+ });
5382
+ return {
5383
+ ok: false,
5384
+ message: 'Socket API error',
5385
+ cause: `Unexpected response to a request for the Coana CLI ${versionSpec}`
5386
+ };
5387
+ }
5736
5388
  return {
5737
5389
  ok: true,
5738
- data: spawnResult.stdout
5390
+ data: artifacts
5739
5391
  };
5740
5392
  }
5741
5393
 
5742
5394
  /**
5743
- * Resolve the executable JS file inside an installed @coana-tech/cli package
5744
- * by reading its package.json `bin` field. Returns an absolute path suitable
5745
- * for passing to `node`.
5395
+ * Run the vendored cache script, which prints only a directory to stdout.
5396
+ * Throws only when Node itself cannot be spawned.
5746
5397
  */
5747
- async function resolveCoanaBinFromInstallDir(installDir) {
5748
- const packageJsonPath = path.join(installDir, 'node_modules', '@coana-tech', 'cli', 'package.json');
5749
- const pkg = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
5750
- const {
5751
- bin
5752
- } = pkg;
5753
- let relativeBin;
5754
- if (typeof bin === 'string') {
5755
- relativeBin = bin;
5756
- } else if (bin && typeof bin === 'object') {
5757
- // Prefer an entry named "coana" if present; otherwise take the first.
5758
- relativeBin = bin['coana'] ?? Object.values(bin)[0];
5759
- }
5760
- if (!relativeBin) {
5761
- throw new Error(`@coana-tech/cli package.json at ${packageJsonPath} is missing a usable bin entry`);
5398
+ async function runCoanaCache(args, env) {
5399
+ try {
5400
+ const result = await spawn.spawn(constants.default.execPath, [path.join(constants.default.distPath, COANA_CACHE_SCRIPT), ...args], {
5401
+ env,
5402
+ stdio: 'pipe'
5403
+ });
5404
+ return {
5405
+ exitCode: 0,
5406
+ stderr: String(result.stderr).trim(),
5407
+ stdout: String(result.stdout).trim()
5408
+ };
5409
+ } catch (e) {
5410
+ const exitCode = e?.code;
5411
+ if (typeof exitCode !== 'number') {
5412
+ throw e;
5413
+ }
5414
+ return {
5415
+ exitCode,
5416
+ stderr: String(e?.stderr ?? '').trim(),
5417
+ stdout: String(e?.stdout ?? '').trim()
5418
+ };
5762
5419
  }
5763
- return path.resolve(path.dirname(packageJsonPath), relativeBin);
5764
5420
  }
5765
5421
 
5766
5422
  /**
5767
- * Install @coana-tech/cli into a fresh temp directory via `npm install` and
5768
- * return its executable JS path. Caches the result per version for the
5769
- * lifetime of the process.
5423
+ * `env` without the `npm_package_*` variables that npm, pnpm and yarn classic
5424
+ * set for each leaf of the cwd's package.json. Coana doesn't read them, and in
5425
+ * a large monorepo they can push argv and env past ARG_MAX, so that the spawn
5426
+ * fails with E2BIG.
5427
+ *
5428
+ * The registry's `spawn` spreads `process.env` under the env it is given, so
5429
+ * they are set to `undefined`, which Node drops.
5770
5430
  */
5771
- async function installCoanaToTmpdir(version, finalEnv) {
5772
- const cached = installedCoanaScriptPathsByVersion.get(version);
5773
- if (cached) {
5774
- return cached;
5431
+ function withoutNpmPackageEnv(env) {
5432
+ const out = {};
5433
+ for (const key of Object.keys(env)) {
5434
+ out[key] = key.startsWith('npm_package_') ? undefined : env[key];
5775
5435
  }
5776
- const installDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'socket-coana-'));
5777
- await spawn.spawn('npm', ['install', '--no-save', '--no-package-lock', '--no-audit', '--no-fund', '--prefix', installDir, `@coana-tech/cli@${version}`], {
5778
- env: sanitizeEnvForCoanaSubprocess(finalEnv),
5779
- stdio: 'inherit'
5780
- });
5781
- const scriptPath = await resolveCoanaBinFromInstallDir(installDir);
5782
- installedCoanaScriptPathsByVersion.set(version, scriptPath);
5783
- return scriptPath;
5436
+ return out;
5784
5437
  }
5785
5438
 
5786
5439
  /**
5787
- * Fallback path used when the dlx (npx / pnpm dlx / yarn dlx) invocation
5788
- * fails. Installs @coana-tech/cli into a temp directory via `npm install`
5789
- * and spawns it directly via `node`.
5440
+ * Run the Coana CLI, downloading it into the cache that Coana and the Socket
5441
+ * CLIs share when the cache lacks the version. Returns a CResult with Coana's
5442
+ * stdout.
5443
+ *
5444
+ * If SOCKET_CLI_COANA_LOCAL_PATH environment variable is set, uses the local
5445
+ * Coana CLI at that path instead.
5790
5446
  */
5791
- async function spawnCoanaViaNpmInstall(args, version, finalEnv, options, spawnExtra) {
5792
- let scriptPath;
5447
+ async function spawnCoana(args, orgSlug, options) {
5448
+ const {
5449
+ coanaVersion,
5450
+ cwd,
5451
+ env: spawnEnv,
5452
+ spinner,
5453
+ stdio = 'inherit'
5454
+ } = {
5455
+ __proto__: null,
5456
+ ...options
5457
+ };
5458
+ const mixinsEnv = {
5459
+ SOCKET_CLI_VERSION: constants.default.ENV.INLINED_SOCKET_CLI_VERSION,
5460
+ // Forwarded to the Coana CLI so it can append our product token to its
5461
+ // outbound axios User-Agent header.
5462
+ SOCKET_CALLER_USER_AGENT: getCliUserAgent()
5463
+ };
5464
+ const defaultApiToken = getDefaultApiToken();
5465
+ if (defaultApiToken) {
5466
+ mixinsEnv['SOCKET_CLI_API_TOKEN'] = defaultApiToken;
5467
+ }
5468
+ const org = orgSlug ? {
5469
+ ok: true,
5470
+ data: orgSlug
5471
+ } : await getDefaultOrgSlug();
5472
+ if (org.ok) {
5473
+ mixinsEnv['SOCKET_ORG_SLUG'] = org.data;
5474
+ }
5475
+ const proxyUrl = getDefaultProxyUrl();
5476
+ if (proxyUrl) {
5477
+ mixinsEnv['SOCKET_CLI_API_PROXY'] = proxyUrl;
5478
+ }
5479
+ const finalEnv = withoutNpmPackageEnv({
5480
+ ...process.env,
5481
+ ...constants.default.processEnv,
5482
+ ...mixinsEnv,
5483
+ ...spawnEnv
5484
+ });
5485
+ let scriptPath = process.env['SOCKET_CLI_COANA_LOCAL_PATH'];
5486
+ if (!scriptPath) {
5487
+ const coanaDirCResult = await getCoanaCliDir(coanaVersion || constants.COANA_CLI_VERSION, org, finalEnv, spinner);
5488
+ if (!coanaDirCResult.ok) {
5489
+ return coanaDirCResult;
5490
+ }
5491
+ scriptPath = path.join(coanaDirCResult.data, 'cli-wrapper.mjs');
5492
+ }
5493
+
5494
+ // A local path may name a native binary.
5495
+ const isBinary = !scriptPath.endsWith('.js') && !scriptPath.endsWith('.mjs');
5793
5496
  try {
5794
- scriptPath = await installCoanaToTmpdir(version, finalEnv);
5795
- } catch (e) {
5796
- const stderr = e?.stderr;
5797
- const cause = getErrorCause(e);
5497
+ const spawnResult = await spawn.spawn(isBinary ? scriptPath : constants.default.execPath, isBinary ? args : [scriptPath, ...args], {
5498
+ cwd,
5499
+ env: finalEnv,
5500
+ spinner,
5501
+ stdio
5502
+ });
5798
5503
  return {
5799
- ok: false,
5800
- data: e,
5801
- message: `npm install fallback failed: ${stderr || cause}`
5504
+ ok: true,
5505
+ data: String(spawnResult.stdout)
5802
5506
  };
5803
- }
5804
- try {
5805
- return await spawnCoanaScriptViaNode(scriptPath, args, finalEnv, options, spawnExtra);
5806
5507
  } catch (e) {
5807
- return buildDlxErrorResult(e);
5508
+ return coanaSpawnErrorResult(e);
5808
5509
  }
5809
5510
  }
5511
+
5512
+ /**
5513
+ * Coana integration utilities for Socket CLI.
5514
+ * Manages reachability analysis via Coana tech CLI.
5515
+ *
5516
+ * Key Functions:
5517
+ * - compressSocketFactsForUpload: Brotli-compress any .socket.facts.json
5518
+ * entries in scanPaths just before upload, returning swapped paths plus a
5519
+ * cleanup callback. Coana keeps writing plain JSON; the on-the-wire form
5520
+ * to depscan is brotli (api-v0 decodes at the multipart boundary).
5521
+ * - extractReachabilityErrors: Extract per-component reachability errors
5522
+ * - extractTier1ReachabilityScanId: Extract scan ID from socket facts file
5523
+ * - getFullWorkspacePath: Label a build-root/workspace pair the way Coana does
5524
+ * - snapshotSocketFacts: Copy aside the socket facts files Coana overwrites
5525
+ * in place, so a failed analysis can be rolled back
5526
+ *
5527
+ * Integration:
5528
+ * - Works with @coana-tech/cli for reachability analysis
5529
+ * - Processes socket facts JSON files
5530
+ * - Extracts full application reachability scan identifiers
5531
+ */
5532
+
5533
+ const {
5534
+ DOT_SOCKET_DOT_FACTS_JSON
5535
+ } = constants.default;
5810
5536
  /**
5811
- * Resolve how the Coana engine should be launched.
5537
+ * For each `.socket.facts.json` in `scanPaths`, stream-brotli-compress a
5538
+ * sibling `.socket.facts.json.br` next to the original file and swap its
5539
+ * path in. Other paths pass through unchanged. Missing files also pass
5540
+ * through unchanged (the upload will fail downstream with the same error
5541
+ * it would have).
5542
+ *
5543
+ * Streaming + worker-thread compression keeps the event loop responsive:
5544
+ * default brotli quality (11) on a 60+MB facts file takes multiple seconds
5545
+ * of CPU, which would otherwise freeze the spinner / signal handlers /
5546
+ * any concurrent work.
5547
+ *
5548
+ * The `.br` lives next to the source rather than under the OS temp dir
5549
+ * because depscan's multipart ingest (`addStreamEntry`) rejects entries
5550
+ * whose names contain `..` traversal segments. The SDK computes the
5551
+ * multipart entry name via `path.relative(cwd, brPath)`, so an OS-tmpdir
5552
+ * temp path turns into `../../../var/folders/...` and gets dropped as
5553
+ * `unmatchedFiles`. Sibling-write keeps the relative path inside cwd, and
5554
+ * keeps the directory shape symmetric with the plain `.socket.facts.json`
5555
+ * upload (depscan strips only the `.br` suffix at ingest, so
5556
+ * `<dir>/.socket.facts.json.br` and `<dir>/.socket.facts.json` resolve to
5557
+ * the same storage path).
5812
5558
  *
5813
- * SOCKET_CLI_COANA_LAUNCHER wins when set:
5814
- * - 'auto' (default): try dlx first, fall back to `npm install` + `node` on
5815
- * launcher-level failures.
5816
- * - 'npm-install': skip dlx entirely; always `npm install` + `node`.
5817
- * - 'npx': dlx only; never fall back.
5818
- * Unrecognized values warn and behave as 'auto'.
5559
+ * Concurrent scans against the same source directory are already racy on
5560
+ * `.socket.facts.json` itself (coana writes to a single path), so the
5561
+ * sibling `.br` doesn't introduce a new race.
5819
5562
  *
5820
- * The legacy boolean variables SOCKET_CLI_COANA_FORCE_NPM_INSTALL
5821
- * ('npm-install') and SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK ('npx') are still
5822
- * honored when the new variable is unset, but are intentionally undocumented.
5563
+ * Caller MUST `await cleanup()` (typically in a `finally` block) once the
5564
+ * upload completes — successful or not — to remove the sibling files.
5823
5565
  */
5824
- function getCoanaLauncherMode() {
5825
- const rawMode = process.env['SOCKET_CLI_COANA_LAUNCHER'];
5826
- const mode = rawMode?.trim().toLowerCase();
5827
- if (mode) {
5828
- if (mode === 'auto' || mode === 'npm-install' || mode === 'npx') {
5829
- return mode;
5566
+ async function compressSocketFactsForUpload(scanPaths) {
5567
+ const brPaths = [];
5568
+ const cleanup = async () => {
5569
+ const targets = brPaths.splice(0);
5570
+ // `recursive: true` defends against the (defensive) case where a sibling
5571
+ // path was somehow created as a directory — `rm` would otherwise throw
5572
+ // on the first such entry and skip the rest. `force: true` no-ops on
5573
+ // missing paths so the function stays idempotent.
5574
+ await Promise.all(targets.map(t => fs$1.rm(t, {
5575
+ recursive: true,
5576
+ force: true
5577
+ })));
5578
+ };
5579
+ // Use `allSettled` (not `all`) so a failure in one entry doesn't leak the
5580
+ // others' in-flight pipelines past our `catch`. If we used `all`, the
5581
+ // first rejection would bubble out while sibling pipelines were still
5582
+ // writing bytes — `cleanup()` would race with those writes and could
5583
+ // remove a `.br` only to have it re-created after we returned.
5584
+ const results = await Promise.allSettled(scanPaths.map(async p => {
5585
+ // depscan decodes only the bare `.socket.facts.json.br`; a named facts
5586
+ // file is uploaded as plain JSON.
5587
+ if (path.basename(p) !== DOT_SOCKET_DOT_FACTS_JSON) {
5588
+ return p;
5589
+ }
5590
+ if (!u.existsSync(p)) {
5591
+ return p;
5830
5592
  }
5831
- logger.logger.warn(`Ignoring unrecognized SOCKET_CLI_COANA_LAUNCHER value "${rawMode}"; expected "auto", "npm-install", or "npx".`);
5832
- return 'auto';
5593
+ const brPath = `${p}.br`;
5594
+ // Track the sibling path BEFORE the pipeline starts so a
5595
+ // partially-written `.br` is removed even if the pipeline rejects.
5596
+ // `rm({ force: true })` no-ops on missing files, so tracking before
5597
+ // creation is safe.
5598
+ brPaths.push(brPath);
5599
+ await require$$9.pipeline(u.createReadStream(p), require$$11.createBrotliCompress(), u.createWriteStream(brPath));
5600
+ return brPath;
5601
+ }));
5602
+ const failure = results.find(r => r.status === 'rejected');
5603
+ if (failure) {
5604
+ // All pipelines have settled, so cleanup() can safely remove every
5605
+ // `.br` we tracked (succeeded or partial) without racing live writes.
5606
+ await cleanup();
5607
+ throw failure.reason;
5608
+ }
5609
+ const paths = results.map(r => r.value);
5610
+ return {
5611
+ paths,
5612
+ cleanup
5613
+ };
5614
+ }
5615
+
5616
+ // `.socket.facts.json` or a named `<entry>.socket.facts.json`, matching
5617
+ // depscan's case-insensitive `*.socket.facts.json`.
5618
+ function isSocketFactsFile(filepath) {
5619
+ return path.basename(filepath).toLowerCase().endsWith(DOT_SOCKET_DOT_FACTS_JSON);
5620
+ }
5621
+
5622
+ // A Coana reachability report, never input to a new analysis: the bare
5623
+ // `.socket.facts.json` (Coana's default name; producers name theirs after the
5624
+ // build) or the path this run tells Coana to write to.
5625
+ function isReachabilityReportPath(filepath, options) {
5626
+ const {
5627
+ cwd,
5628
+ outputPath
5629
+ } = {
5630
+ __proto__: null,
5631
+ ...options
5632
+ };
5633
+ return path.basename(filepath).toLowerCase() === DOT_SOCKET_DOT_FACTS_JSON || path.resolve(cwd, filepath) === path.resolve(cwd, outputPath);
5634
+ }
5635
+ function extractReachabilityErrors(socketFactsFile) {
5636
+ const json = fs.readJsonSync(socketFactsFile, {
5637
+ throws: false
5638
+ });
5639
+ if (!json || !Array.isArray(json.components)) {
5640
+ return [];
5833
5641
  }
5834
- if (process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL']) {
5835
- return 'npm-install';
5642
+ const errors = [];
5643
+ for (const component of json.components) {
5644
+ if (!Array.isArray(component.reachability)) {
5645
+ continue;
5646
+ }
5647
+ for (const ghsaEntry of component.reachability) {
5648
+ if (!Array.isArray(ghsaEntry.reachability)) {
5649
+ continue;
5650
+ }
5651
+ for (const entry of ghsaEntry.reachability) {
5652
+ if (entry.type === 'error') {
5653
+ errors.push({
5654
+ componentName: String(component.name ?? ''),
5655
+ componentVersion: String(component.version ?? ''),
5656
+ ghsaId: String(ghsaEntry.ghsa_id ?? ''),
5657
+ subprojectPath: String(entry.subprojectPath ?? ''),
5658
+ workspacePath: String(entry.workspacePath ?? '')
5659
+ });
5660
+ }
5661
+ }
5662
+ }
5836
5663
  }
5837
- if (process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK']) {
5838
- return 'npx';
5664
+ return errors;
5665
+ }
5666
+ function extractTier1ReachabilityScanId(socketFactsFile) {
5667
+ const json = fs.readJsonSync(socketFactsFile, {
5668
+ throws: false
5669
+ });
5670
+ const tier1ReachabilityScanId = String(json?.['tier1ReachabilityScanId'] ?? '').trim();
5671
+ return tier1ReachabilityScanId.length > 0 ? tier1ReachabilityScanId : undefined;
5672
+ }
5673
+
5674
+ // Label a reachability location the way Coana's own `getFullWorkspacePath`
5675
+ // does. Since @coana-tech/cli v15.10.8 `subprojectPath` names the build root
5676
+ // and `workspacePath` the workspace within it, so a single-root repo reports
5677
+ // `.` as its subproject and must be labelled by its workspace alone.
5678
+ function getFullWorkspacePath(subprojectPath, workspacePath) {
5679
+ const subproject = subprojectPath && subprojectPath !== '.' ? subprojectPath : '';
5680
+ const workspace = workspacePath && workspacePath !== '.' ? workspacePath : '';
5681
+ if (subproject && workspace) {
5682
+ return `${subproject}/${workspace}`;
5839
5683
  }
5840
- return 'auto';
5684
+ return subproject || workspace || '.';
5841
5685
  }
5842
-
5843
- /**
5844
- * Helper to spawn coana with dlx.
5845
- * Automatically uses force and silent when version is not pinned exactly.
5846
- * Returns a CResult with stdout extraction for backward compatibility.
5847
- *
5848
- * If SOCKET_CLI_COANA_LOCAL_PATH environment variable is set, uses the local
5849
- * Coana CLI at that path instead of downloading from npm.
5850
- *
5851
- * If the dlx path fails (e.g. broken `npx` on the host), falls back to
5852
- * `npm install`-ing @coana-tech/cli into a temp directory and invoking it
5853
- * directly via `node`. The launcher strategy can be overridden with
5854
- * SOCKET_CLI_COANA_LAUNCHER: 'auto' (the default) tries dlx with the
5855
- * npm-install fallback, 'npm-install' skips dlx entirely, and 'npx' never
5856
- * falls back.
5857
- */
5858
- async function spawnCoanaDlx(args, orgSlug, options, spawnExtra) {
5686
+ // Coana overwrites `.socket.facts.json` in place, so an analysis that fails
5687
+ // part way leaves partial output where the scan's inputs used to be.
5688
+ async function snapshotSocketFacts(scanPaths, options) {
5859
5689
  const {
5860
- coanaVersion,
5861
- env: spawnEnv,
5862
- ipc,
5863
- ...dlxOptions
5690
+ cwd,
5691
+ outputPath,
5692
+ tmpDir
5864
5693
  } = {
5865
5694
  __proto__: null,
5866
5695
  ...options
5867
5696
  };
5868
- const mixinsEnv = {
5869
- SOCKET_CLI_VERSION: constants.default.ENV.INLINED_SOCKET_CLI_VERSION,
5870
- // Forwarded to the Coana CLI so it can append our product token to its
5871
- // outbound axios User-Agent header.
5872
- SOCKET_CALLER_USER_AGENT: getCliUserAgent()
5873
- };
5874
- const defaultApiToken = getDefaultApiToken();
5875
- if (defaultApiToken) {
5876
- mixinsEnv['SOCKET_CLI_API_TOKEN'] = defaultApiToken;
5877
- }
5878
- if (orgSlug) {
5879
- mixinsEnv['SOCKET_ORG_SLUG'] = orgSlug;
5880
- } else {
5881
- const orgSlugCResult = await getDefaultOrgSlug();
5882
- if (orgSlugCResult.ok) {
5883
- mixinsEnv['SOCKET_ORG_SLUG'] = orgSlugCResult.data;
5697
+ const sources = new Set(scanPaths.filter(p => path.basename(p) === DOT_SOCKET_DOT_FACTS_JSON).map(p => path.resolve(cwd, p)));
5698
+ sources.add(path.resolve(cwd, outputPath));
5699
+ const snapshotDir = path.join(tmpDir, 'socket-facts-snapshot');
5700
+ await fs$1.mkdir(snapshotDir, {
5701
+ recursive: true
5702
+ });
5703
+ const captured = [];
5704
+ const created = [];
5705
+ await Promise.all(Array.from(sources, async (source, index) => {
5706
+ if (!u.existsSync(source)) {
5707
+ created.push(source);
5708
+ return;
5884
5709
  }
5885
- }
5886
- const proxyUrl = getDefaultProxyUrl();
5887
- if (proxyUrl) {
5888
- mixinsEnv['SOCKET_CLI_API_PROXY'] = proxyUrl;
5889
- }
5890
- const finalEnv = {
5891
- ...process.env,
5892
- ...constants.default.processEnv,
5893
- ...mixinsEnv,
5894
- ...spawnEnv
5895
- };
5896
- const resolvedVersion = coanaVersion || constants.default.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION;
5897
-
5898
- // `shadowNpmBase` (the dlx launcher) configures the child's stdio from its
5899
- // `options` arg, NOT from the registry-spawn `extra` arg — the latter only
5900
- // attaches metadata to the result. Callers that requested streaming via
5901
- // `spawnExtra` (the 4th arg), e.g. `{ stdio: 'inherit' }` from
5902
- // `socket manifest gradle`, were therefore silently ignored on this path:
5903
- // Coana ran piped and its output — including the real failure reason — never
5904
- // reached the user, leaving only an unhelpful "command failed". Resolve the
5905
- // requested stdio from either argument and honor it on every launch path:
5906
- // dlx, local-path, and npm-install (e.g. `socket fix --silence` requests
5907
- // `stdio: 'pipe'` via options).
5908
- const requestedStdio = spawnExtra?.['stdio'] ?? objects.getOwn(dlxOptions, 'stdio');
5909
- const localCoanaPath = process.env['SOCKET_CLI_COANA_LOCAL_PATH'];
5910
- // Use local Coana CLI if path is provided.
5911
- if (localCoanaPath) {
5710
+ const backup = path.join(snapshotDir, `${index}.json`);
5912
5711
  try {
5913
- return await spawnCoanaScriptViaNode(localCoanaPath, args, finalEnv, {
5914
- cwd: dlxOptions.cwd,
5915
- stdio: requestedStdio
5916
- }, spawnExtra);
5712
+ await fs$1.copyFile(source, backup);
5713
+ captured.push({
5714
+ backup,
5715
+ source
5716
+ });
5917
5717
  } catch (e) {
5918
- return buildDlxErrorResult(e);
5718
+ // A path we cannot copy is left out of both lists so restore() leaves
5719
+ // it untouched rather than deleting a file it never captured.
5720
+ debug$2.debugFn('warn', `[socket-facts] snapshot failed: ${source}`);
5721
+ debug$2.debugDir('error', e);
5919
5722
  }
5920
- }
5921
- const launcherMode = getCoanaLauncherMode();
5922
-
5923
- // Allow forcing the npm-install path for debugging or for environments
5924
- // where dlx is known-broken.
5925
- if (launcherMode === 'npm-install') {
5926
- return await spawnCoanaViaNpmInstall(args, resolvedVersion, finalEnv, {
5927
- cwd: dlxOptions.cwd,
5928
- stdio: requestedStdio
5929
- }, spawnExtra);
5930
- }
5931
- try {
5932
- // Use npm/dlx version.
5933
- const result = await spawnDlx({
5934
- name: '@coana-tech/cli',
5935
- version: resolvedVersion
5936
- }, args, {
5937
- force: true,
5938
- // Do NOT silence the launcher. `--silent` (npm loglevel silent) hides
5939
- // npm's own download/registry/launch errors, so when npx/pnpm-dlx fails
5940
- // to fetch @coana-tech/cli the user is left with a bare exit code and no
5941
- // cause. shadowNpmBase defaults to `--loglevel error`, which keeps real
5942
- // launcher errors visible while staying quiet on success.
5943
- silent: false,
5944
- ...dlxOptions,
5945
- ...(requestedStdio === undefined ? {} : {
5946
- stdio: requestedStdio
5947
- }),
5948
- env: finalEnv,
5949
- ipc: {
5950
- [constants.default.SOCKET_CLI_SHADOW_ACCEPT_RISKS]: true,
5951
- [constants.default.SOCKET_CLI_SHADOW_API_TOKEN]: constants.default.SOCKET_PUBLIC_API_TOKEN,
5952
- [constants.default.SOCKET_CLI_SHADOW_SILENT]: true,
5953
- ...ipc
5954
- }
5955
- }, spawnExtra);
5956
- const output = await result.spawnPromise;
5957
- return {
5958
- ok: true,
5959
- data: output.stdout
5960
- };
5961
- } catch (e) {
5962
- const dlxError = buildDlxErrorResult(e);
5963
- if (launcherMode === 'npx') {
5964
- return dlxError;
5965
- }
5966
-
5967
- // Only retry via `npm install` when the failure looks like the launcher
5968
- // never got Coana running. A real Coana process that booted and exited
5969
- // with an error would just hit the same failure on retry.
5970
- if (!shouldFallbackOnDlxError(e)) {
5971
- return dlxError;
5972
- }
5973
- logger.logger.warn('Coana dlx invocation failed; retrying via `npm install` + `node`.');
5974
- const fallbackResult = await spawnCoanaViaNpmInstall(args, resolvedVersion, finalEnv, {
5975
- cwd: dlxOptions.cwd,
5976
- stdio: requestedStdio
5977
- }, spawnExtra);
5978
- if (fallbackResult.ok) {
5979
- return fallbackResult;
5980
- }
5981
- // Surface both errors so support has full context.
5982
- return {
5983
- ok: false,
5984
- data: e,
5985
- message: `${dlxError.message}. npm-install fallback also failed: ${fallbackResult.message}`
5986
- };
5987
- }
5988
- }
5989
-
5990
- /**
5991
- * Decide whether a thrown dlx error should trigger the npm-install fallback.
5992
- *
5993
- * The goal is to retry only when the dlx launcher (npx / pnpm dlx / yarn dlx)
5994
- * failed before Coana itself ran. If Coana actually booted, any subsequent
5995
- * non-zero exit is a real Coana failure and retrying would hit the same one.
5996
- *
5997
- * Signals we use, in priority order:
5998
- * 1. Captured stderr containing Coana's startup banner — definitive proof
5999
- * Coana ran, so do NOT retry. Only available when the caller passed
6000
- * `stdio: 'pipe'` (or the spawn defaulted to it).
6001
- * 2. Spawn-level errors (`e.code` is a string like 'ENOENT'): the binary
6002
- * wasn't found / couldn't start — retry.
6003
- * 3. Signal kills (`e.signal` set, or numeric `e.code >= 128`): conventionally
6004
- * not a clean exit; the customer-observed exit code 249 falls here. Retry.
6005
- * 4. Small integer exit codes with no banner in captured stderr: ambiguous,
6006
- * but Coana's own exit codes are small integers, so default to NOT retrying
6007
- * rather than blindly re-running Coana.
6008
- */
6009
- function shouldFallbackOnDlxError(e) {
6010
- // Coana clearly ran (its banner is in the captured stderr) → any later
6011
- // non-zero exit is a real Coana failure and retrying would hit it again.
6012
- if (coanaBannerSeen(e)) {
6013
- return false;
6014
- }
6015
- return dlxLauncherFailedBeforeCoana(e);
6016
- }
6017
-
6018
- /**
6019
- * Heuristic: did the dlx launcher (npx / pnpm dlx / yarn dlx) fail BEFORE the
6020
- * Coana process itself started? True for spawn-level errors (a string `code`
6021
- * like ENOENT), signal kills, and exit codes >= 128 (conventionally
6022
- * signal-derived) — all cases where the launcher, not Coana, is the culprit
6023
- * (e.g. npx missing from PATH, or @coana-tech/cli failing to download). A small
6024
- * integer exit code is deliberately NOT treated as a launch failure: Coana's
6025
- * own exit codes are small integers too, so it is genuinely ambiguous.
6026
- *
6027
- * Caveat: a launcher that fails to download the package can also exit with a
6028
- * small integer (npm/npx often exit 1), which lands in the ambiguous bucket.
6029
- * We cannot disambiguate those from a real Coana exit without inspecting the
6030
- * launcher's output, so the npm-install fallback does not fire for them.
6031
- */
6032
- function dlxLauncherFailedBeforeCoana(e) {
6033
- const code = e?.code;
6034
- // Spawn-level failure (e.g. ENOENT when npx is missing from PATH).
6035
- if (typeof code === 'string') {
6036
- return true;
6037
- }
6038
- // Killed by signal — almost never a clean Coana exit.
6039
- if (e?.signal) {
6040
- return true;
6041
- }
6042
- // Exit codes >= 128 are conventionally signal-derived, and the observed
6043
- // npx-launcher failures in the wild fall into this range (e.g. 249, 254).
6044
- return typeof code === 'number' && code >= 128;
6045
- }
6046
-
6047
- /**
6048
- * Definitive proof Coana actually booted: its startup banner appears in the
6049
- * captured stderr. Only available when the launcher's output was piped
6050
- * (captured); with inherited stdio there is nothing to inspect, so this
6051
- * returns false (the failure is then classified by exit code / signal alone).
6052
- */
6053
- function coanaBannerSeen(e) {
6054
- const capturedStderr = String(e?.stderr ?? '');
6055
- return !!capturedStderr && /Coana CLI version/i.test(capturedStderr);
6056
- }
6057
-
6058
- /**
6059
- * Build a CResult error from a thrown spawn error, preserving exit code,
6060
- * signal, and stderr context.
6061
- */
6062
- function buildDlxErrorResult(e) {
6063
- const stderr = e?.stderr;
6064
- const stdout = e?.stdout;
6065
- const exitCode = e?.code;
6066
- const signal = e?.signal;
6067
- const cause = getErrorCause(e);
6068
- const details = [];
6069
- if (typeof exitCode === 'number') {
6070
- details.push(`exit code ${exitCode}`);
6071
- }
6072
- if (signal) {
6073
- details.push(`signal ${signal}`);
6074
- }
6075
- const detailSuffix = details.length ? ` (${details.join(', ')})` : '';
6076
- // Prefer captured stderr, then stdout, then the generic spawn error. Coana
6077
- // logs some failures (e.g. unresolved Gradle dependencies) to stdout, so
6078
- // without the stdout fallback a piped failure collapsed to an unhelpful
6079
- // "command failed" even when the real reason was captured.
6080
- const detail = stderr || stdout || cause;
6081
- // Be honest about WHERE the failure happened. On the dlx path the spawned
6082
- // process is the package-manager launcher (npx / pnpm dlx / yarn dlx), which
6083
- // downloads @coana-tech/cli and only then runs it — so a failure may be the
6084
- // launcher dying before Coana ever started, not Coana itself. We can only be
6085
- // CERTAIN of that for a spawn-level error (a string `code` like ENOENT: the
6086
- // launcher binary could not start, so Coana provably never ran). A non-zero
6087
- // exit or signal is genuinely ambiguous — Coana may have started, streamed
6088
- // output, and then died (e.g. OOM), or the launcher may have failed to fetch
6089
- // the package — and with inherited stdio there is no captured output to tell
6090
- // them apart, so we must not assert either way.
6091
- let message;
6092
- if (coanaBannerSeen(e)) {
6093
- message = `Coana command failed${detailSuffix}: ${detail}`;
6094
- } else if (typeof e?.code === 'string') {
6095
- message = `Failed to launch Coana via the package manager${detailSuffix} — the npx/pnpm-dlx/yarn-dlx launcher could not start (e.g. it is missing from PATH): ${detail}`;
6096
- } else {
6097
- message = `Coana failed to run via the package manager${detailSuffix}: ${detail}`;
6098
- }
5723
+ }));
6099
5724
  return {
6100
- ok: false,
6101
- data: e,
6102
- message
5725
+ async restore() {
5726
+ await Promise.all([...captured.map(async ({
5727
+ backup,
5728
+ source
5729
+ }) => {
5730
+ try {
5731
+ await fs$1.copyFile(backup, source);
5732
+ } catch (e) {
5733
+ debug$2.debugFn('warn', `[socket-facts] restore failed: ${source}`);
5734
+ debug$2.debugDir('error', e);
5735
+ }
5736
+ }), ...created.map(source => fs$1.rm(source, {
5737
+ force: true
5738
+ }).catch(() => {}))]);
5739
+ }
6103
5740
  };
6104
5741
  }
6105
5742
 
6106
- /**
6107
- * Helper to spawn cdxgen with dlx.
6108
- */
6109
- async function spawnCdxgenDlx(args, options, spawnExtra) {
6110
- return await spawnDlx({
6111
- name: '@cyclonedx/cdxgen',
6112
- version: constants.default.ENV.INLINED_SOCKET_CLI_CYCLONEDX_CDXGEN_VERSION
6113
- }, args, {
6114
- force: false,
6115
- silent: true,
6116
- ...options
6117
- }, spawnExtra);
6118
- }
6119
-
6120
- /**
6121
- * Helper to spawn synp with dlx.
6122
- */
6123
- async function spawnSynpDlx(args, options, spawnExtra) {
6124
- return await spawnDlx({
6125
- name: 'synp',
6126
- version: `${constants.default.ENV.INLINED_SOCKET_CLI_SYNP_VERSION}`
6127
- }, args, {
6128
- force: false,
6129
- silent: true,
6130
- ...options
6131
- }, spawnExtra);
6132
- }
6133
-
6134
5743
  function getEnterpriseOrgs(orgs) {
6135
5744
  return orgs.filter(o => o.plan.includes('enterprise'));
6136
5745
  }
@@ -6244,7 +5853,7 @@ function readSocketJsonCascade(dir, boundaryDir, rootSockJson) {
6244
5853
  // increasing precedence and the nearest-to-dir file wins last.
6245
5854
  const ancestorsFarToNear = [];
6246
5855
  while (current !== boundary) {
6247
- if (fs.existsSync(path.join(current, constants.SOCKET_JSON))) {
5856
+ if (u.existsSync(path.join(current, constants.SOCKET_JSON))) {
6248
5857
  const jsonCResult = readSocketJsonSync(current, true);
6249
5858
  if (jsonCResult.ok) {
6250
5859
  ancestorsFarToNear.unshift(jsonCResult.data);
@@ -6269,7 +5878,7 @@ function getDefaultSocketJson() {
6269
5878
  }
6270
5879
  function readSocketJsonSync(cwd, defaultOnError = false) {
6271
5880
  const sockJsonPath = path.join(cwd, constants.SOCKET_JSON);
6272
- if (!fs.existsSync(sockJsonPath)) {
5881
+ if (!u.existsSync(sockJsonPath)) {
6273
5882
  debug$2.debugFn('notice', `miss: ${constants.SOCKET_JSON} not found at ${cwd}`);
6274
5883
  return {
6275
5884
  ok: true,
@@ -6278,7 +5887,7 @@ function readSocketJsonSync(cwd, defaultOnError = false) {
6278
5887
  }
6279
5888
  let jsonContent = null;
6280
5889
  try {
6281
- jsonContent = fs.readFileSync(sockJsonPath, 'utf8');
5890
+ jsonContent = u.readFileSync(sockJsonPath, 'utf8');
6282
5891
  } catch (e) {
6283
5892
  if (defaultOnError) {
6284
5893
  logger.logger.warn(`Failed to read ${constants.SOCKET_JSON}, using default`);
@@ -6335,28 +5944,146 @@ function readSocketJsonSync(cwd, defaultOnError = false) {
6335
5944
  data: jsonObj
6336
5945
  };
6337
5946
  }
6338
- async function writeSocketJson(cwd, sockJson) {
6339
- let jsonContent = '';
6340
- try {
6341
- jsonContent = JSON.stringify(sockJson, null, 2);
6342
- } catch (e) {
6343
- debug$2.debugFn('error', `Failed to serialize ${constants.SOCKET_JSON} to JSON`);
6344
- debug$2.debugDir('inspect', {
6345
- sockJson
6346
- });
6347
- debug$2.debugDir('error', e);
6348
- return {
6349
- ok: false,
6350
- message: 'Failed to serialize to JSON',
6351
- cause: `There was an unexpected problem converting the ${constants.SOCKET_JSON} object to a JSON string. Unable to store it.`
6352
- };
5947
+ async function writeSocketJson(cwd, sockJson) {
5948
+ let jsonContent = '';
5949
+ try {
5950
+ jsonContent = JSON.stringify(sockJson, null, 2);
5951
+ } catch (e) {
5952
+ debug$2.debugFn('error', `Failed to serialize ${constants.SOCKET_JSON} to JSON`);
5953
+ debug$2.debugDir('inspect', {
5954
+ sockJson
5955
+ });
5956
+ debug$2.debugDir('error', e);
5957
+ return {
5958
+ ok: false,
5959
+ message: 'Failed to serialize to JSON',
5960
+ cause: `There was an unexpected problem converting the ${constants.SOCKET_JSON} object to a JSON string. Unable to store it.`
5961
+ };
5962
+ }
5963
+ const filepath = path.join(cwd, constants.SOCKET_JSON);
5964
+ await u.promises.writeFile(filepath, `${jsonContent}\n`, 'utf8');
5965
+ return {
5966
+ ok: true,
5967
+ data: undefined
5968
+ };
5969
+ }
5970
+
5971
+ function findBinPathDetailsSync(binName) {
5972
+ const rawBinPaths = bin.whichBinSync(binName, {
5973
+ all: true,
5974
+ nothrow: true
5975
+ }) ?? [];
5976
+ // whichBinSync may return a string when only one result is found, even with all: true.
5977
+ // This handles both the current published version and future versions.
5978
+ const binPaths = Array.isArray(rawBinPaths) ? rawBinPaths : typeof rawBinPaths === 'string' ? [rawBinPaths] : [];
5979
+ const {
5980
+ shadowBinPath
5981
+ } = constants.default;
5982
+ let shadowIndex = -1;
5983
+ let theBinPath;
5984
+ for (let i = 0, {
5985
+ length
5986
+ } = binPaths; i < length; i += 1) {
5987
+ const binPath = binPaths[i];
5988
+ // Skip our bin directory if it's in the front.
5989
+ if (path.dirname(binPath) === shadowBinPath) {
5990
+ shadowIndex = i;
5991
+ } else {
5992
+ theBinPath = bin.resolveBinPathSync(binPath);
5993
+ break;
5994
+ }
5995
+ }
5996
+ return {
5997
+ name: binName,
5998
+ path: theBinPath,
5999
+ shadowed: shadowIndex !== -1
6000
+ };
6001
+ }
6002
+ function findNpmDirPathSync(npmBinPath) {
6003
+ const {
6004
+ WIN32
6005
+ } = constants.default;
6006
+ let thePath = npmBinPath;
6007
+ while (true) {
6008
+ const libNmNpmPath = path.join(thePath, `lib/${constants.NODE_MODULES}/${constants.NPM}`);
6009
+ // mise, which uses opaque binaries, puts its npm bin in a path like:
6010
+ // /Users/SomeUsername/.local/share/mise/installs/node/vX.X.X/bin/npm.
6011
+ // HOWEVER, the location of the npm install is:
6012
+ // /Users/SomeUsername/.local/share/mise/installs/node/vX.X.X/lib/node_modules/npm.
6013
+ if (
6014
+ // Use existsSync here because statsSync, even with { throwIfNoEntry: false },
6015
+ // will throw an ENOTDIR error for paths like ./a-file-that-exists/a-directory-that-does-not.
6016
+ // See https://github.com/nodejs/node/issues/56993.
6017
+ fs.isDirSync(libNmNpmPath)) {
6018
+ thePath = libNmNpmPath;
6019
+ }
6020
+ const hasNmInCurrPath = fs.isDirSync(path.join(thePath, constants.NODE_MODULES));
6021
+ const hasNmInParentPath = !hasNmInCurrPath && fs.isDirSync(path.join(thePath, `../${constants.NODE_MODULES}`));
6022
+ if (
6023
+ // npm bin paths may look like:
6024
+ // /usr/local/share/npm/bin/npm
6025
+ // /Users/SomeUsername/.nvm/versions/node/vX.X.X/bin/npm
6026
+ // C:\Users\SomeUsername\AppData\Roaming\npm\bin\npm.cmd
6027
+ // OR
6028
+ // C:\Program Files\nodejs\npm.cmd
6029
+ //
6030
+ // In practically all cases the npm path contains a node_modules folder:
6031
+ // /usr/local/share/npm/bin/npm/node_modules
6032
+ // C:\Program Files\nodejs\node_modules
6033
+ (hasNmInCurrPath ||
6034
+ // In some bespoke cases the node_modules folder is in the parent directory.
6035
+ hasNmInParentPath) && (
6036
+ // Optimistically look for the default location.
6037
+ path.basename(thePath) === constants.NPM ||
6038
+ // Chocolatey installs npm bins in the same directory as node bins.
6039
+ WIN32 && u.existsSync(path.join(thePath, `${constants.NPM}.cmd`)))) {
6040
+ return hasNmInParentPath ? path.dirname(thePath) : thePath;
6041
+ }
6042
+ const parent = path.dirname(thePath);
6043
+ if (parent === thePath) {
6044
+ return undefined;
6045
+ }
6046
+ thePath = parent;
6047
+ }
6048
+ }
6049
+ /**
6050
+ * Converts absolute scan targets inside cwd back to cwd-relative paths before
6051
+ * glob expansion. SCA excludes passed through `additionalIgnores` are anchored
6052
+ * to cwd, so package discovery needs target globs in the same coordinate
6053
+ * system for fast-glob to apply those ignores consistently.
6054
+ */
6055
+ function normalizeScanInputPath(pathToNormalize, cwd) {
6056
+ if (!path.isAbsolute(pathToNormalize)) {
6057
+ return pathToNormalize;
6353
6058
  }
6354
- const filepath = path.join(cwd, constants.SOCKET_JSON);
6355
- await fs.promises.writeFile(filepath, `${jsonContent}\n`, 'utf8');
6356
- return {
6357
- ok: true,
6358
- data: undefined
6059
+ const relativePath = path.relative(cwd, pathToNormalize);
6060
+ const isInsideCwd = relativePath === '' || !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
6061
+ if (!isInsideCwd) {
6062
+ return pathToNormalize;
6063
+ }
6064
+ return stripTrailingSlash(relativePath.replaceAll('\\', '/')) || '.';
6065
+ }
6066
+ async function getPackageFilesForScan(inputPaths, supportedFiles, options) {
6067
+ const {
6068
+ additionalIgnores,
6069
+ config: socketConfig,
6070
+ cwd = process.cwd()
6071
+ } = {
6072
+ __proto__: null,
6073
+ ...options
6359
6074
  };
6075
+
6076
+ // Apply the supported files filter during streaming to avoid accumulating
6077
+ // all files in memory. This is critical for large monorepos with 100k+ files
6078
+ // where accumulating all paths before filtering causes OOM errors.
6079
+ const filter = createSupportedFilesFilter(supportedFiles);
6080
+ const normalizedInputPaths = inputPaths.map(p => normalizeScanInputPath(p, cwd));
6081
+ return await globWithGitIgnore(pathsToGlobPatterns(normalizedInputPaths, cwd), {
6082
+ additionalIgnores,
6083
+ cwd,
6084
+ filter,
6085
+ socketConfig
6086
+ });
6360
6087
  }
6361
6088
 
6362
6089
  /**
@@ -6389,11 +6116,11 @@ async function readCache(key,
6389
6116
  // 5 minute in milliseconds time to live (TTL).
6390
6117
  ttlMs = 5 * 60 * 1000) {
6391
6118
  const cacheJsonPath = path.join(constants.default.githubCachePath, `${key}.json`);
6392
- const stat = fs$1.safeStatsSync(cacheJsonPath);
6119
+ const stat = fs.safeStatsSync(cacheJsonPath);
6393
6120
  if (stat) {
6394
6121
  const isExpired = Date.now() - stat.mtimeMs > ttlMs;
6395
6122
  if (!isExpired) {
6396
- return await fs$1.readJson(cacheJsonPath);
6123
+ return await fs.readJson(cacheJsonPath);
6397
6124
  }
6398
6125
  }
6399
6126
  return undefined;
@@ -6403,12 +6130,12 @@ async function writeCache(key, data) {
6403
6130
  githubCachePath
6404
6131
  } = constants.default;
6405
6132
  const cacheJsonPath = path.join(githubCachePath, `${key}.json`);
6406
- if (!fs.existsSync(githubCachePath)) {
6407
- await fs.promises.mkdir(githubCachePath, {
6133
+ if (!u.existsSync(githubCachePath)) {
6134
+ await u.promises.mkdir(githubCachePath, {
6408
6135
  recursive: true
6409
6136
  });
6410
6137
  }
6411
- await fs$1.writeJson(cacheJsonPath, data);
6138
+ await fs.writeJson(cacheJsonPath, data);
6412
6139
  }
6413
6140
  async function cacheFetch(key, fetcher, ttlMs) {
6414
6141
  // Optionally disable cache.
@@ -6920,7 +6647,7 @@ const COMPLETION_CMD_PREFIX = 'complete -F _socket_completion';
6920
6647
  function getCompletionSourcingCommand() {
6921
6648
  // Note: this is exported to distPath in .config/rollup.dist.config.mjs
6922
6649
  const completionScriptExportPath = path.join(constants.default.distPath, 'socket-completion.bash');
6923
- if (!fs.existsSync(completionScriptExportPath)) {
6650
+ if (!u.existsSync(completionScriptExportPath)) {
6924
6651
  return {
6925
6652
  ok: false,
6926
6653
  message: 'Tab Completion script not found',
@@ -6973,6 +6700,210 @@ fi
6973
6700
  };
6974
6701
  }
6975
6702
 
6703
+ function exitWithBinPathError$2(binName) {
6704
+ logger.logger.fail(`Socket unable to locate ${binName}; ensure it is available in the PATH environment variable`);
6705
+ // The exit code 127 indicates that the command or binary being executed
6706
+ // could not be found.
6707
+ // eslint-disable-next-line n/no-process-exit
6708
+ process.exit(127);
6709
+ // This line is never reached in production, but helps tests.
6710
+ throw new Error('process.exit called');
6711
+ }
6712
+ let _yarnBinPath;
6713
+ function getYarnBinPath() {
6714
+ if (_yarnBinPath === undefined) {
6715
+ _yarnBinPath = getYarnBinPathDetails().path;
6716
+ if (!_yarnBinPath) {
6717
+ exitWithBinPathError$2(constants.YARN);
6718
+ }
6719
+ }
6720
+ return _yarnBinPath;
6721
+ }
6722
+ let _yarnBinPathDetails;
6723
+ function getYarnBinPathDetails() {
6724
+ if (_yarnBinPathDetails === undefined) {
6725
+ _yarnBinPathDetails = findBinPathDetailsSync(constants.YARN);
6726
+ }
6727
+ return _yarnBinPathDetails;
6728
+ }
6729
+ function isYarnBinPathShadowed() {
6730
+ return getYarnBinPathDetails().shadowed;
6731
+ }
6732
+
6733
+ let _isYarnBerry;
6734
+ function isYarnBerry() {
6735
+ if (_isYarnBerry === undefined) {
6736
+ try {
6737
+ const yarnBinPath = getYarnBinPath();
6738
+ const result = spawn.spawnSync(yarnBinPath, [constants.FLAG_VERSION], {
6739
+ encoding: constants.UTF8,
6740
+ // On Windows, yarn is often a .cmd file that requires shell execution.
6741
+ // The spawn function from @socketsecurity/registry will handle this properly
6742
+ // when shell is true.
6743
+ shell: constants.default.WIN32
6744
+ });
6745
+ if (result.status === 0 && result.stdout) {
6746
+ const version = result.stdout;
6747
+ // Yarn Berry starts from version 2.x
6748
+ const majorVersion = parseInt(version.split('.')[0], 10);
6749
+ _isYarnBerry = majorVersion >= 2;
6750
+ } else {
6751
+ _isYarnBerry = false;
6752
+ }
6753
+ } catch {
6754
+ _isYarnBerry = false;
6755
+ }
6756
+ }
6757
+ return _isYarnBerry;
6758
+ }
6759
+
6760
+ /**
6761
+ * DLX execution utilities for Socket CLI.
6762
+ * Manages package execution via npx/pnpm dlx/yarn dlx commands.
6763
+ *
6764
+ * Key Functions:
6765
+ * - spawnCdxgenDlx: Execute CycloneDX generator via dlx
6766
+ * - spawnDlx: Execute packages using dlx-style commands
6767
+ * - spawnSynpDlx: Execute Synp converter via dlx
6768
+ *
6769
+ * Package Manager Detection:
6770
+ * - Auto-detects npm, pnpm, or yarn based on lockfiles
6771
+ * - Supports silent execution mode
6772
+ *
6773
+ * Integration:
6774
+ * - Works with shadow binaries for security scanning
6775
+ * - Handles version pinning and cache management
6776
+ * - Configures environment for third-party tools
6777
+ */
6778
+
6779
+ const require$2 = require$$1.createRequire((typeof document === 'undefined' ? require$$0$1.pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('utils.js', document.baseURI).href)));
6780
+ const {
6781
+ PACKAGE_LOCK_JSON,
6782
+ PNPM_LOCK_YAML,
6783
+ YARN_LOCK
6784
+ } = constants.default;
6785
+ /**
6786
+ * Spawns a package using dlx-style execution (npx/pnpm dlx/yarn dlx).
6787
+ * Automatically detects the appropriate package manager if not specified.
6788
+ */
6789
+ async function spawnDlx(packageSpec, args, options, spawnExtra) {
6790
+ const {
6791
+ agent,
6792
+ silent = false,
6793
+ ...shadowOptions
6794
+ } = options ?? {};
6795
+ let finalShadowOptions = shadowOptions;
6796
+ let pm = agent;
6797
+
6798
+ // Auto-detect package manager if not specified.
6799
+ if (!pm) {
6800
+ const pnpmLockPath = await findUp(PNPM_LOCK_YAML, {
6801
+ onlyFiles: true
6802
+ });
6803
+ const yarnLockPath = pnpmLockPath ? undefined : await findUp(YARN_LOCK, {
6804
+ onlyFiles: true
6805
+ });
6806
+ const npmLockPath = pnpmLockPath || yarnLockPath ? undefined : await findUp(PACKAGE_LOCK_JSON, {
6807
+ onlyFiles: true
6808
+ });
6809
+ if (pnpmLockPath) {
6810
+ pm = constants.PNPM;
6811
+ } else if (yarnLockPath) {
6812
+ pm = constants.YARN;
6813
+ } else if (npmLockPath) {
6814
+ pm = constants.NPM;
6815
+ } else {
6816
+ // Default to npm if no lockfile found.
6817
+ pm = constants.NPM;
6818
+ }
6819
+ }
6820
+ const packageString = `${packageSpec.name}@${packageSpec.version}`;
6821
+
6822
+ // Build command args based on package manager.
6823
+ let spawnArgs;
6824
+ if (pm === constants.PNPM) {
6825
+ spawnArgs = [];
6826
+ // The --silent flag must come before dlx, not after.
6827
+ if (silent) {
6828
+ spawnArgs.push(constants.FLAG_SILENT);
6829
+ }
6830
+ spawnArgs.push('dlx');
6831
+ // Never let the target project's `.pnpmfile.cjs` hooks run while we launch
6832
+ // a third-party tool via `pnpm dlx`. In a pnpm workspace root, `pnpm dlx`
6833
+ // loads the cwd's `.pnpmfile.cjs`, so a broken hook there (e.g. a `require`
6834
+ // of an unresolved Git LFS pointer) crashes the launcher with a bare exit
6835
+ // code before our tool ever starts. The dlx tool installs into an isolated
6836
+ // store, so the project's install hooks are irrelevant to it. pnpm honors
6837
+ // this only as a config setting, not as a `dlx` CLI flag, so it must be set
6838
+ // via the npm_config_ env var. See: https://pnpm.io/npmrc#settings
6839
+ finalShadowOptions = {
6840
+ ...finalShadowOptions,
6841
+ env: {
6842
+ ...objects.getOwn(finalShadowOptions, 'env'),
6843
+ npm_config_ignore_pnpmfile: 'true'
6844
+ }
6845
+ };
6846
+ spawnArgs.push(packageString, ...args);
6847
+ const shadowPnpmBin = /*@__PURE__*/require$2(constants.default.shadowPnpmBinPath);
6848
+ return await shadowPnpmBin(spawnArgs, finalShadowOptions, spawnExtra);
6849
+ } else if (pm === constants.YARN && isYarnBerry()) {
6850
+ spawnArgs = ['dlx'];
6851
+ // Yarn dlx runs in a temporary environment by design and should always fetch fresh.
6852
+ if (silent) {
6853
+ spawnArgs.push(constants.FLAG_QUIET);
6854
+ }
6855
+ spawnArgs.push(packageString, ...args);
6856
+
6857
+ // Use node-modules linker instead of PnP to avoid issues with packages
6858
+ // that have undeclared dependencies (e.g. @coana-tech/cli -> @babel/types).
6859
+ finalShadowOptions = {
6860
+ ...finalShadowOptions,
6861
+ env: {
6862
+ ...objects.getOwn(finalShadowOptions, 'env'),
6863
+ YARN_NODE_LINKER: 'node-modules'
6864
+ }
6865
+ };
6866
+ const shadowYarnBin = /*@__PURE__*/require$2(constants.default.shadowYarnBinPath);
6867
+ return await shadowYarnBin(spawnArgs, finalShadowOptions, spawnExtra);
6868
+ } else {
6869
+ // Use npm exec/npx.
6870
+ // For consistency, we'll use npx which is more commonly used for one-off execution.
6871
+ spawnArgs = ['--yes'];
6872
+ if (silent) {
6873
+ spawnArgs.push(constants.FLAG_SILENT);
6874
+ }
6875
+ spawnArgs.push(packageString, ...args);
6876
+ const shadowNpxBin = /*@__PURE__*/require$2(constants.default.shadowNpxBinPath);
6877
+ return await shadowNpxBin(spawnArgs, finalShadowOptions, spawnExtra);
6878
+ }
6879
+ }
6880
+
6881
+ /**
6882
+ * Helper to spawn cdxgen with dlx.
6883
+ */
6884
+ async function spawnCdxgenDlx(args, options, spawnExtra) {
6885
+ return await spawnDlx({
6886
+ name: '@cyclonedx/cdxgen',
6887
+ version: constants.default.ENV.INLINED_SOCKET_CLI_CYCLONEDX_CDXGEN_VERSION
6888
+ }, args, {
6889
+ silent: true,
6890
+ ...options
6891
+ }, spawnExtra);
6892
+ }
6893
+
6894
+ /**
6895
+ * Helper to spawn synp with dlx.
6896
+ */
6897
+ async function spawnSynpDlx(args, options, spawnExtra) {
6898
+ return await spawnDlx({
6899
+ name: 'synp',
6900
+ version: `${constants.default.ENV.INLINED_SOCKET_CLI_SYNP_VERSION}`
6901
+ }, args, {
6902
+ silent: true,
6903
+ ...options
6904
+ }, spawnExtra);
6905
+ }
6906
+
6976
6907
  /**
6977
6908
  * Safe wrapper for npm-package-arg that doesn't throw.
6978
6909
  * Returns undefined if parsing fails.
@@ -7001,7 +6932,7 @@ function exitWithBinPathError$1(binName) {
7001
6932
  function findBinNextToNode(binName) {
7002
6933
  const nodeDir = path.dirname(process.execPath);
7003
6934
  const binPath = path.join(nodeDir, binName);
7004
- if (fs.existsSync(binPath)) {
6935
+ if (u.existsSync(binPath)) {
7005
6936
  try {
7006
6937
  return bin.resolveBinPathSync(binPath);
7007
6938
  } catch {
@@ -7037,7 +6968,7 @@ function resolveBinPathToJsEntry(binPath, cliEntryName) {
7037
6968
  return binPath;
7038
6969
  }
7039
6970
  const jsEntryPath = path.join(npmDirPath, 'bin', cliEntryName);
7040
- return fs.existsSync(jsEntryPath) ? jsEntryPath : binPath;
6971
+ return u.existsSync(jsEntryPath) ? jsEntryPath : binPath;
7041
6972
  }
7042
6973
  let _npmBinPath;
7043
6974
  function getNpmBinPath() {
@@ -7098,7 +7029,7 @@ function getNpmRequire() {
7098
7029
  if (_npmRequire === undefined) {
7099
7030
  const npmDirPath = getNpmDirPath();
7100
7031
  const npmNmPath = path.join(npmDirPath, `${constants.NODE_MODULES}/npm`);
7101
- _npmRequire = require$$1.createRequire(path.join(fs.existsSync(npmNmPath) ? npmNmPath : npmDirPath, '<dummy-basename>'));
7032
+ _npmRequire = require$$1.createRequire(path.join(u.existsSync(npmNmPath) ? npmNmPath : npmDirPath, '<dummy-basename>'));
7102
7033
  }
7103
7034
  return _npmRequire;
7104
7035
  }
@@ -7327,8 +7258,8 @@ const readLockFileByAgent = (() => {
7327
7258
  return undefined;
7328
7259
  };
7329
7260
  }
7330
- const binaryReader = wrapReader(fs$1.readFileBinary);
7331
- const defaultReader = wrapReader(async lockPath => await fs$1.readFileUtf8(lockPath));
7261
+ const binaryReader = wrapReader(fs.readFileBinary);
7262
+ const defaultReader = wrapReader(async lockPath => await fs.readFileUtf8(lockPath));
7332
7263
  return new Map([[BUN, wrapReader(async (lockPath, agentExecPath, cwd = process.cwd()) => {
7333
7264
  const ext = path.extname(lockPath);
7334
7265
  if (ext === EXT_LOCK) {
@@ -7402,14 +7333,14 @@ function preferWindowsCmdShim(binPath, binName) {
7402
7333
  const cmdShim = path.join(path.dirname(binPath), `${binName}.cmd`);
7403
7334
 
7404
7335
  // Ensure shim exists, otherwise failback to binPath
7405
- return fs.existsSync(cmdShim) ? cmdShim : binPath;
7336
+ return u.existsSync(cmdShim) ? cmdShim : binPath;
7406
7337
  }
7407
7338
  async function getAgentExecPath(agent) {
7408
7339
  const binName = binByAgent.get(agent);
7409
7340
  if (binName === NPM) {
7410
7341
  // Try to use constants.npmExecPath first, but verify it exists.
7411
7342
  const npmPath = preferWindowsCmdShim(constants.default.npmExecPath, NPM);
7412
- if (fs.existsSync(npmPath)) {
7343
+ if (u.existsSync(npmPath)) {
7413
7344
  return npmPath;
7414
7345
  }
7415
7346
  // If npmExecPath doesn't exist, try common locations.
@@ -7417,12 +7348,12 @@ async function getAgentExecPath(agent) {
7417
7348
  const nodeDir = path.dirname(process.execPath);
7418
7349
  if (constants.default.WIN32) {
7419
7350
  const npmCmdInNodeDir = path.join(nodeDir, `${NPM}.cmd`);
7420
- if (fs.existsSync(npmCmdInNodeDir)) {
7351
+ if (u.existsSync(npmCmdInNodeDir)) {
7421
7352
  return npmCmdInNodeDir;
7422
7353
  }
7423
7354
  }
7424
7355
  const npmInNodeDir = path.join(nodeDir, NPM);
7425
- if (fs.existsSync(npmInNodeDir)) {
7356
+ if (u.existsSync(npmInNodeDir)) {
7426
7357
  return preferWindowsCmdShim(npmInNodeDir, NPM);
7427
7358
  }
7428
7359
  // Fall back to whichBin.
@@ -7433,7 +7364,7 @@ async function getAgentExecPath(agent) {
7433
7364
  if (binName === PNPM) {
7434
7365
  // Try to use constants.pnpmExecPath first, but verify it exists.
7435
7366
  const pnpmPath = constants.default.pnpmExecPath;
7436
- if (fs.existsSync(pnpmPath)) {
7367
+ if (u.existsSync(pnpmPath)) {
7437
7368
  return pnpmPath;
7438
7369
  }
7439
7370
  // Fall back to whichBin.
@@ -7507,7 +7438,7 @@ async function detectPackageEnvironment({
7507
7438
  const pkgJsonPath = lockPath ? path.resolve(lockPath, `${isHiddenLockFile ? '../' : ''}../${PACKAGE_JSON}`) : await findUp(PACKAGE_JSON, {
7508
7439
  cwd
7509
7440
  });
7510
- const pkgPath = pkgJsonPath && fs.existsSync(pkgJsonPath) ? path.dirname(pkgJsonPath) : undefined;
7441
+ const pkgPath = pkgJsonPath && u.existsSync(pkgJsonPath) ? path.dirname(pkgJsonPath) : undefined;
7511
7442
  const editablePkgJson = pkgPath ? await packages.readPackageJson(pkgPath, {
7512
7443
  editable: true
7513
7444
  }) : undefined;
@@ -8337,7 +8268,7 @@ function parsePnpmLockfile(lockfileContent) {
8337
8268
  return objects.isObjectObject(result) ? result : null;
8338
8269
  }
8339
8270
  async function readPnpmLockfile(lockfilePath) {
8340
- return fs.existsSync(lockfilePath) ? await fs$1.readFileUtf8(lockfilePath) : undefined;
8271
+ return u.existsSync(lockfilePath) ? await fs.readFileUtf8(lockfilePath) : undefined;
8341
8272
  }
8342
8273
  function stripLeadingPnpmDepPathSlash(depPath) {
8343
8274
  return isPnpmDepPath(depPath) ? depPath.slice(1) : depPath;
@@ -9204,7 +9135,7 @@ exports.socketDashboardLink = socketDashboardLink;
9204
9135
  exports.socketDevLink = socketDevLink;
9205
9136
  exports.socketDocsLink = socketDocsLink;
9206
9137
  exports.spawnCdxgenDlx = spawnCdxgenDlx;
9207
- exports.spawnCoanaDlx = spawnCoanaDlx;
9138
+ exports.spawnCoana = spawnCoana;
9208
9139
  exports.spawnSynpDlx = spawnSynpDlx;
9209
9140
  exports.stripTrailingSlash = stripTrailingSlash;
9210
9141
  exports.suggestOrgSlug = suggestOrgSlug;
@@ -9220,5 +9151,5 @@ exports.walkNestedMap = walkNestedMap;
9220
9151
  exports.webLink = webLink;
9221
9152
  exports.withTmpDir = withTmpDir;
9222
9153
  exports.writeSocketJson = writeSocketJson;
9223
- //# debugId=788830e0-d926-4fb1-9858-2d1428ea1492
9154
+ //# debugId=c2092386-922b-4fd3-808d-771e3469061f
9224
9155
  //# sourceMappingURL=utils.js.map