@socketsecurity/cli-with-sentry 1.8.4 → 1.9.1
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/CHANGELOG.md +14 -0
- package/README.md +5 -6
- package/dist/cli.js +136 -137
- package/dist/cli.js.map +1 -1
- package/dist/coana-cache.mjs +203 -0
- package/dist/constants.js +9 -8
- package/dist/constants.js.map +1 -1
- package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
- package/dist/shadow-pnpm-bin2.js +3 -3
- package/dist/shadow-pnpm-bin2.js.map +1 -1
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/fix/cmd-fix.d.mts.map +1 -1
- package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
- package/dist/types/constants.d.mts +4 -2
- package/dist/types/constants.d.mts.map +1 -1
- package/dist/types/utils/api.d.mts +1 -1
- package/dist/types/utils/api.d.mts.map +1 -1
- package/dist/types/utils/coana-release.d.mts +21 -0
- package/dist/types/utils/coana-release.d.mts.map +1 -0
- package/dist/types/utils/dlx.d.mts +0 -22
- package/dist/types/utils/dlx.d.mts.map +1 -1
- package/dist/utils.js +924 -993
- package/dist/utils.js.map +1 -1
- package/dist/vendor.js +9000 -5623
- package/package.json +2 -2
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
|
|
21
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
686
|
+
let yml = fs.safeReadFileSync(ymlPath);
|
|
686
687
|
if (yml === undefined) {
|
|
687
688
|
ymlPath = path.join(dir, constants.SOCKET_YAML);
|
|
688
|
-
yml = fs
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5144
|
+
return await u.promises.realpath(target);
|
|
5370
5145
|
} catch {
|
|
5371
5146
|
return path.resolve(target);
|
|
5372
5147
|
}
|
|
5373
5148
|
}
|
|
5374
5149
|
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
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
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5455
|
-
*
|
|
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
|
|
5460
|
-
|
|
5461
|
-
|
|
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
|
-
|
|
5464
|
-
|
|
5465
|
-
if (!isInsideCwd) {
|
|
5466
|
-
return pathToNormalize;
|
|
5196
|
+
if (signal) {
|
|
5197
|
+
details.push(`signal ${signal}`);
|
|
5467
5198
|
}
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
const {
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
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
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
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
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
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
|
-
|
|
5544
|
-
|
|
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
|
|
5252
|
+
return {
|
|
5253
|
+
ok: true
|
|
5254
|
+
};
|
|
5548
5255
|
}
|
|
5549
5256
|
|
|
5550
5257
|
/**
|
|
5551
|
-
*
|
|
5552
|
-
*
|
|
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
|
-
|
|
5571
|
-
const
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
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
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
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
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
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
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
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
|
-
|
|
5684
|
-
|
|
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
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
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
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
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
|
-
|
|
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
|
|
5357
|
+
return {
|
|
5358
|
+
cli: {
|
|
5359
|
+
sha256,
|
|
5360
|
+
size,
|
|
5361
|
+
url
|
|
5362
|
+
},
|
|
5363
|
+
version
|
|
5364
|
+
};
|
|
5721
5365
|
}
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
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:
|
|
5390
|
+
data: artifacts
|
|
5739
5391
|
};
|
|
5740
5392
|
}
|
|
5741
5393
|
|
|
5742
5394
|
/**
|
|
5743
|
-
*
|
|
5744
|
-
*
|
|
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
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
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
|
-
*
|
|
5768
|
-
*
|
|
5769
|
-
*
|
|
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
|
-
|
|
5772
|
-
const
|
|
5773
|
-
|
|
5774
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5788
|
-
*
|
|
5789
|
-
*
|
|
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
|
|
5792
|
-
|
|
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
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
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:
|
|
5800
|
-
data:
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
5814
|
-
*
|
|
5815
|
-
*
|
|
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
|
-
*
|
|
5821
|
-
*
|
|
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
|
|
5825
|
-
const
|
|
5826
|
-
const
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
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
|
-
|
|
5832
|
-
|
|
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
|
-
|
|
5835
|
-
|
|
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
|
-
|
|
5838
|
-
|
|
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 '
|
|
5684
|
+
return subproject || workspace || '.';
|
|
5841
5685
|
}
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
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
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
...dlxOptions
|
|
5690
|
+
cwd,
|
|
5691
|
+
outputPath,
|
|
5692
|
+
tmpDir
|
|
5864
5693
|
} = {
|
|
5865
5694
|
__proto__: null,
|
|
5866
5695
|
...options
|
|
5867
5696
|
};
|
|
5868
|
-
const
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
};
|
|
5874
|
-
const
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
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
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5712
|
+
await fs$1.copyFile(source, backup);
|
|
5713
|
+
captured.push({
|
|
5714
|
+
backup,
|
|
5715
|
+
source
|
|
5716
|
+
});
|
|
5917
5717
|
} catch (e) {
|
|
5918
|
-
|
|
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
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
6407
|
-
await
|
|
6133
|
+
if (!u.existsSync(githubCachePath)) {
|
|
6134
|
+
await u.promises.mkdir(githubCachePath, {
|
|
6408
6135
|
recursive: true
|
|
6409
6136
|
});
|
|
6410
6137
|
}
|
|
6411
|
-
await fs
|
|
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 (!
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
7331
|
-
const defaultReader = wrapReader(async lockPath => await fs
|
|
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
|
|
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 (
|
|
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 (
|
|
7351
|
+
if (u.existsSync(npmCmdInNodeDir)) {
|
|
7421
7352
|
return npmCmdInNodeDir;
|
|
7422
7353
|
}
|
|
7423
7354
|
}
|
|
7424
7355
|
const npmInNodeDir = path.join(nodeDir, NPM);
|
|
7425
|
-
if (
|
|
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 (
|
|
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 &&
|
|
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
|
|
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.
|
|
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=
|
|
9154
|
+
//# debugId=c2092386-922b-4fd3-808d-771e3469061f
|
|
9224
9155
|
//# sourceMappingURL=utils.js.map
|