socket 1.1.101 → 1.1.103
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 +17 -2
- package/dist/cli.js +1492 -128
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +8 -4
- package/dist/constants.js.map +1 -1
- package/dist/socket-facts.init.gradle +353 -0
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/manifest/bazel/bazel-pypi-discovery.d.mts +31 -0
- package/dist/types/commands/manifest/bazel/bazel-pypi-discovery.d.mts.map +1 -0
- package/dist/types/commands/manifest/bazel/bazel-pypi-parser.d.mts +46 -0
- package/dist/types/commands/manifest/bazel/bazel-pypi-parser.d.mts.map +1 -0
- package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts +16 -2
- package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-repo-discovery.d.mts +3 -3
- package/dist/types/commands/manifest/bazel/bazel-repo-discovery.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/cmd-manifest-bazel.d.mts +19 -0
- package/dist/types/commands/manifest/bazel/cmd-manifest-bazel.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/extract_bazel_to_maven.d.mts +1 -0
- package/dist/types/commands/manifest/bazel/extract_bazel_to_maven.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/extract_bazel_to_pypi.d.mts +20 -0
- package/dist/types/commands/manifest/bazel/extract_bazel_to_pypi.d.mts.map +1 -0
- package/dist/types/commands/manifest/cmd-manifest-gradle.d.mts.map +1 -1
- package/dist/types/commands/manifest/cmd-manifest-kotlin.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +7 -0
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -0
- package/dist/types/commands/manifest/convert_gradle_to_maven.d.mts.map +1 -1
- package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
- package/dist/types/commands/scan/handle-create-new-scan.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 -0
- package/dist/types/constants.d.mts.map +1 -1
- package/dist/types/utils/dlx.d.mts.map +1 -1
- package/dist/types/utils/socket-json.d.mts +1 -0
- package/dist/types/utils/socket-json.d.mts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -3
package/dist/cli.js
CHANGED
|
@@ -1763,14 +1763,16 @@ async function performReachabilityAnalysis(options) {
|
|
|
1763
1763
|
return sockSdkCResult;
|
|
1764
1764
|
}
|
|
1765
1765
|
const sockSdk = sockSdkCResult.data;
|
|
1766
|
-
|
|
1767
|
-
// Exclude any .socket.facts.json files that happen to be in the scan
|
|
1768
|
-
// folder before the analysis was run.
|
|
1769
|
-
const filepathsToUpload = packagePaths.filter(p => path.basename(p).toLowerCase() !== constants.default.DOT_SOCKET_DOT_FACTS_JSON);
|
|
1770
1766
|
spinner?.start('Uploading manifests for reachability analysis...');
|
|
1771
1767
|
|
|
1772
1768
|
// Ensure uploaded manifest files are relative to analysis target as coana resolves SBOM manifest files relative to this path
|
|
1773
|
-
|
|
1769
|
+
// NOTE: previously stripped any `.socket.facts.json` from packagePaths
|
|
1770
|
+
// here to avoid uploading leftover post-reachability output. With the
|
|
1771
|
+
// producer flow (`socket manifest gradle --facts`) those files are
|
|
1772
|
+
// legitimate INPUT to compute-artifacts, so we now upload them. Stale
|
|
1773
|
+
// facts files are cleaned up downstream — see the post-success
|
|
1774
|
+
// deletion in handle-create-new-scan.mts.
|
|
1775
|
+
const uploadCResult = await utils.handleApiCall(sockSdk.uploadManifestFiles(orgSlug, packagePaths, path.resolve(cwd, analysisTarget)), {
|
|
1774
1776
|
description: 'upload manifests',
|
|
1775
1777
|
spinner
|
|
1776
1778
|
});
|
|
@@ -2243,6 +2245,8 @@ async function provisionPythonShim() {
|
|
|
2243
2245
|
// Default per-invocation timeout for bazel queries. Bazel cold-cache starts
|
|
2244
2246
|
// can take several minutes; 10 minutes is generous while still bounding CI hangs.
|
|
2245
2247
|
const BAZEL_QUERY_TIMEOUT_MS = 600_000;
|
|
2248
|
+
const STDERR_TAIL_BYTES = 4_096;
|
|
2249
|
+
const STDOUT_EXCERPT_BYTES = 1_024;
|
|
2246
2250
|
|
|
2247
2251
|
// Splits the user-supplied --bazel-flags string on whitespace.
|
|
2248
2252
|
// Empty / undefined returns []. No shell parsing — quoted args with embedded
|
|
@@ -2263,11 +2267,22 @@ function buildBazelModShowVisibleReposArgv(opts) {
|
|
|
2263
2267
|
startup.push(`--output_base=${opts.bazelOutputBase}`);
|
|
2264
2268
|
}
|
|
2265
2269
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
2266
|
-
return [...startup, 'mod', '
|
|
2270
|
+
return [...startup, 'mod', 'dump_repo_mapping', '', '--output=json', ...userFlags];
|
|
2271
|
+
}
|
|
2272
|
+
function buildBazelModShowPipExtensionArgv(opts) {
|
|
2273
|
+
const startup = [];
|
|
2274
|
+
if (opts.bazelRc) {
|
|
2275
|
+
startup.push(`--bazelrc=${opts.bazelRc}`);
|
|
2276
|
+
}
|
|
2277
|
+
if (opts.bazelOutputBase) {
|
|
2278
|
+
startup.push(`--output_base=${opts.bazelOutputBase}`);
|
|
2279
|
+
}
|
|
2280
|
+
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
2281
|
+
return [...startup, 'mod', 'show_extension', '@rules_python//python/extensions:pip.bzl%pip', '--extension_usages=<root>', ...userFlags];
|
|
2267
2282
|
}
|
|
2268
|
-
function buildBazelArgv(queryStr, opts) {
|
|
2283
|
+
function buildBazelArgv(queryStr, opts, output = 'build') {
|
|
2269
2284
|
// Startup flags MUST precede the `query` subcommand.
|
|
2270
|
-
// Bazel argv shape: <startup> query <queryFlags> <invocationFlags> <queryStr> --output
|
|
2285
|
+
// Bazel argv shape: <startup> query <queryFlags> <invocationFlags> <queryStr> --output=<output> <userFlags>
|
|
2271
2286
|
const startup = [];
|
|
2272
2287
|
if (opts.bazelRc) {
|
|
2273
2288
|
startup.push(`--bazelrc=${opts.bazelRc}`);
|
|
@@ -2278,7 +2293,7 @@ function buildBazelArgv(queryStr, opts) {
|
|
|
2278
2293
|
// Keep query output stable and avoid updating Bazel lockfiles while extracting.
|
|
2279
2294
|
const queryFlags = ['--lockfile_mode=off', '--noshow_progress'];
|
|
2280
2295
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
2281
|
-
return [...startup, 'query', ...queryFlags, ...opts.invocationFlags, queryStr,
|
|
2296
|
+
return [...startup, 'query', ...queryFlags, ...opts.invocationFlags, queryStr, `--output=${output}`, ...userFlags];
|
|
2282
2297
|
}
|
|
2283
2298
|
function stringField(value) {
|
|
2284
2299
|
return typeof value === 'string' ? value : '';
|
|
@@ -2286,6 +2301,46 @@ function stringField(value) {
|
|
|
2286
2301
|
function numericExitCode(value) {
|
|
2287
2302
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
2288
2303
|
}
|
|
2304
|
+
function byteLength(value) {
|
|
2305
|
+
return Buffer.byteLength(value, 'utf8');
|
|
2306
|
+
}
|
|
2307
|
+
function excerpt(value, maxBytes) {
|
|
2308
|
+
if (byteLength(value) <= maxBytes) {
|
|
2309
|
+
return value;
|
|
2310
|
+
}
|
|
2311
|
+
return value.slice(0, maxBytes) + '\n[truncated]';
|
|
2312
|
+
}
|
|
2313
|
+
function logBazelTrace({
|
|
2314
|
+
argv,
|
|
2315
|
+
durationMs,
|
|
2316
|
+
opts,
|
|
2317
|
+
result,
|
|
2318
|
+
step
|
|
2319
|
+
}) {
|
|
2320
|
+
if (!opts.verbose) {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const stderrBytes = byteLength(result.stderr);
|
|
2324
|
+
const stdoutBytes = byteLength(result.stdout);
|
|
2325
|
+
const category = result.code === 0 ? 'ok' : 'bazel-query-failed';
|
|
2326
|
+
logger.logger.log('[VERBOSE] bazel subprocess trace:', `category=${category}`, {
|
|
2327
|
+
argv,
|
|
2328
|
+
category,
|
|
2329
|
+
code: result.code,
|
|
2330
|
+
cwd: opts.cwd,
|
|
2331
|
+
durationMs,
|
|
2332
|
+
stderrBytes,
|
|
2333
|
+
stdoutBytes,
|
|
2334
|
+
step,
|
|
2335
|
+
timedOut: false,
|
|
2336
|
+
timeoutMs: BAZEL_QUERY_TIMEOUT_MS
|
|
2337
|
+
});
|
|
2338
|
+
if (result.code !== 0 && result.stderr) {
|
|
2339
|
+
logger.logger.log('[VERBOSE] bazel stderr tail:', excerpt(result.stderr.slice(-STDERR_TAIL_BYTES), STDERR_TAIL_BYTES));
|
|
2340
|
+
} else if (result.stdout && stdoutBytes <= STDOUT_EXCERPT_BYTES) {
|
|
2341
|
+
logger.logger.log('[VERBOSE] bazel stdout excerpt:', result.stdout);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2289
2344
|
function normalizeSpawnError(error) {
|
|
2290
2345
|
const e = error;
|
|
2291
2346
|
return {
|
|
@@ -2301,11 +2356,12 @@ function normalizeSpawnError(error) {
|
|
|
2301
2356
|
* and fails on non-zero exit. Rejected spawn calls are normalized into a
|
|
2302
2357
|
* BazelQueryResult so retry/skip handling can inspect stderr.
|
|
2303
2358
|
*/
|
|
2304
|
-
async function runBazelQuery(queryStr, opts) {
|
|
2305
|
-
const argv = buildBazelArgv(queryStr, opts);
|
|
2359
|
+
async function runBazelQuery(queryStr, opts, output) {
|
|
2360
|
+
const argv = buildBazelArgv(queryStr, opts, output);
|
|
2306
2361
|
if (opts.verbose) {
|
|
2307
2362
|
logger.logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv);
|
|
2308
2363
|
}
|
|
2364
|
+
const startedAt = Date.now();
|
|
2309
2365
|
const {
|
|
2310
2366
|
spinner
|
|
2311
2367
|
} = constants.default;
|
|
@@ -2340,19 +2396,30 @@ async function runBazelQuery(queryStr, opts) {
|
|
|
2340
2396
|
} else {
|
|
2341
2397
|
spinner.failAndStop(`bazel query failed (${truncated}).`);
|
|
2342
2398
|
}
|
|
2399
|
+
if (result) {
|
|
2400
|
+
logBazelTrace({
|
|
2401
|
+
argv,
|
|
2402
|
+
durationMs: Date.now() - startedAt,
|
|
2403
|
+
opts,
|
|
2404
|
+
result,
|
|
2405
|
+
step: `bazel query ${truncated}`
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2343
2408
|
}
|
|
2344
2409
|
}
|
|
2345
2410
|
|
|
2346
2411
|
/**
|
|
2347
2412
|
* Bzlmod-native visible repository enumeration. This is only a candidate
|
|
2348
2413
|
* source; callers must still validate each returned apparent repo name with a
|
|
2349
|
-
* semantic query for generated
|
|
2414
|
+
* semantic query for generated ecosystem rules.
|
|
2350
2415
|
*/
|
|
2351
2416
|
async function runBazelModShowVisibleRepos(opts) {
|
|
2352
2417
|
const argv = buildBazelModShowVisibleReposArgv(opts);
|
|
2353
2418
|
if (opts.verbose) {
|
|
2354
2419
|
logger.logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv);
|
|
2355
2420
|
}
|
|
2421
|
+
const startedAt = Date.now();
|
|
2422
|
+
let result;
|
|
2356
2423
|
try {
|
|
2357
2424
|
const output = await spawn.spawn(opts.bin, argv, {
|
|
2358
2425
|
cwd: opts.cwd,
|
|
@@ -2366,14 +2433,65 @@ async function runBazelModShowVisibleRepos(opts) {
|
|
|
2366
2433
|
stderr,
|
|
2367
2434
|
stdout
|
|
2368
2435
|
} = output;
|
|
2369
|
-
|
|
2436
|
+
result = {
|
|
2437
|
+
code,
|
|
2438
|
+
stdout,
|
|
2439
|
+
stderr
|
|
2440
|
+
};
|
|
2441
|
+
} catch (e) {
|
|
2442
|
+
result = normalizeSpawnError(e);
|
|
2443
|
+
}
|
|
2444
|
+
logBazelTrace({
|
|
2445
|
+
argv,
|
|
2446
|
+
durationMs: Date.now() - startedAt,
|
|
2447
|
+
opts,
|
|
2448
|
+
result,
|
|
2449
|
+
step: 'bazel mod dump_repo_mapping'
|
|
2450
|
+
});
|
|
2451
|
+
return result;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Bzlmod-native rules_python pip extension usage inspection. This is the
|
|
2456
|
+
* authoritative source for root-module pip.parse metadata when Bazel supports
|
|
2457
|
+
* the command; callers keep bounded static parsing as fallback.
|
|
2458
|
+
*/
|
|
2459
|
+
async function runBazelModShowPipExtension(opts) {
|
|
2460
|
+
const argv = buildBazelModShowPipExtensionArgv(opts);
|
|
2461
|
+
if (opts.verbose) {
|
|
2462
|
+
logger.logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv);
|
|
2463
|
+
}
|
|
2464
|
+
const startedAt = Date.now();
|
|
2465
|
+
let result;
|
|
2466
|
+
try {
|
|
2467
|
+
const output = await spawn.spawn(opts.bin, argv, {
|
|
2468
|
+
cwd: opts.cwd,
|
|
2469
|
+
timeout: BAZEL_QUERY_TIMEOUT_MS,
|
|
2470
|
+
...(opts.env ? {
|
|
2471
|
+
env: opts.env
|
|
2472
|
+
} : {})
|
|
2473
|
+
});
|
|
2474
|
+
const {
|
|
2475
|
+
code,
|
|
2476
|
+
stderr,
|
|
2477
|
+
stdout
|
|
2478
|
+
} = output;
|
|
2479
|
+
result = {
|
|
2370
2480
|
code,
|
|
2371
2481
|
stdout,
|
|
2372
2482
|
stderr
|
|
2373
2483
|
};
|
|
2374
2484
|
} catch (e) {
|
|
2375
|
-
|
|
2485
|
+
result = normalizeSpawnError(e);
|
|
2376
2486
|
}
|
|
2487
|
+
logBazelTrace({
|
|
2488
|
+
argv,
|
|
2489
|
+
durationMs: Date.now() - startedAt,
|
|
2490
|
+
opts,
|
|
2491
|
+
result,
|
|
2492
|
+
step: 'bazel mod show_extension rules_python pip'
|
|
2493
|
+
});
|
|
2494
|
+
return result;
|
|
2377
2495
|
}
|
|
2378
2496
|
|
|
2379
2497
|
/**
|
|
@@ -2392,13 +2510,31 @@ function buildProbeFor(opts) {
|
|
|
2392
2510
|
};
|
|
2393
2511
|
}
|
|
2394
2512
|
|
|
2513
|
+
/**
|
|
2514
|
+
* Build a `RepoProbe` for validating pip hub candidates.
|
|
2515
|
+
* Queries the hub for package targets (e.g. `@<hub>//...`) and returns
|
|
2516
|
+
* stdout so the caller can check for `:pkg` labels or alias rules.
|
|
2517
|
+
* Does NOT require `pypi_name=` tags in the hub output, because those
|
|
2518
|
+
* tags live on spoke repos, not the hub alias layer.
|
|
2519
|
+
*/
|
|
2520
|
+
function buildPypiProbeFor(opts) {
|
|
2521
|
+
return async hubName => {
|
|
2522
|
+
const queryStr = `@${hubName}//...`;
|
|
2523
|
+
const result = await runBazelQuery(queryStr, opts);
|
|
2524
|
+
return {
|
|
2525
|
+
stdout: result.stdout,
|
|
2526
|
+
code: result.code
|
|
2527
|
+
};
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2395
2531
|
// Maximum size (bytes) we will read for any single Bazel workspace file.
|
|
2396
2532
|
// Prevents DoS via maliciously large MODULE.bazel / WORKSPACE / .bzl files.
|
|
2397
|
-
const MAX_WORKSPACE_FILE_BYTES = 5 * 1024 * 1024;
|
|
2533
|
+
const MAX_WORKSPACE_FILE_BYTES$1 = 5 * 1024 * 1024;
|
|
2398
2534
|
|
|
2399
2535
|
// Maximum candidate count we will return (deduped) before truncating.
|
|
2400
2536
|
// Real repos have <20; this is a hard ceiling against pathological inputs.
|
|
2401
|
-
const MAX_CANDIDATES = 256;
|
|
2537
|
+
const MAX_CANDIDATES$1 = 256;
|
|
2402
2538
|
|
|
2403
2539
|
// Regex strategy: anchored, bounded character classes, no nested quantifiers.
|
|
2404
2540
|
// Match `use_repo(maven, "X", "Y", ...)` with a bounded arg-list window to
|
|
@@ -2419,13 +2555,13 @@ const MAVEN_COORDINATES_MARKER_RE = /\bmaven_coordinates\s*=/;
|
|
|
2419
2555
|
|
|
2420
2556
|
// Reads file contents, refusing files that exceed MAX_WORKSPACE_FILE_BYTES.
|
|
2421
2557
|
// Returns null when the file is missing, oversized, or unreadable.
|
|
2422
|
-
function safeReadFile(file) {
|
|
2558
|
+
function safeReadFile$1(file) {
|
|
2423
2559
|
if (!fs$1.existsSync(file)) {
|
|
2424
2560
|
return null;
|
|
2425
2561
|
}
|
|
2426
2562
|
try {
|
|
2427
2563
|
const stat = fs$1.statSync(file);
|
|
2428
|
-
if (stat.size > MAX_WORKSPACE_FILE_BYTES) {
|
|
2564
|
+
if (stat.size > MAX_WORKSPACE_FILE_BYTES$1) {
|
|
2429
2565
|
return null;
|
|
2430
2566
|
}
|
|
2431
2567
|
return fs$1.readFileSync(file, 'utf8');
|
|
@@ -2437,7 +2573,7 @@ function safeReadFile(file) {
|
|
|
2437
2573
|
// Walks workspace root for legacy Starlark sources we can scan: WORKSPACE
|
|
2438
2574
|
// (and WORKSPACE.bazel) plus top-level .bzl files. Non-recursive by design;
|
|
2439
2575
|
// Phase 1 explicitly avoids static Starlark parsing at depth.
|
|
2440
|
-
function listLegacyStarlarkFiles(cwd) {
|
|
2576
|
+
function listLegacyStarlarkFiles$1(cwd) {
|
|
2441
2577
|
const files = [];
|
|
2442
2578
|
const candidates = ['WORKSPACE', 'WORKSPACE.bazel'];
|
|
2443
2579
|
for (const c of candidates) {
|
|
@@ -2467,7 +2603,7 @@ function uniqueSorted(items) {
|
|
|
2467
2603
|
if (!seen.has(item)) {
|
|
2468
2604
|
seen.add(item);
|
|
2469
2605
|
out.push(item);
|
|
2470
|
-
if (out.length >= MAX_CANDIDATES) {
|
|
2606
|
+
if (out.length >= MAX_CANDIDATES$1) {
|
|
2471
2607
|
break;
|
|
2472
2608
|
}
|
|
2473
2609
|
}
|
|
@@ -2491,14 +2627,29 @@ function apparentNameFromJsonValue(value) {
|
|
|
2491
2627
|
}
|
|
2492
2628
|
return undefined;
|
|
2493
2629
|
}
|
|
2630
|
+
function apparentNamesFromRepoMapping(value) {
|
|
2631
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2632
|
+
return [];
|
|
2633
|
+
}
|
|
2634
|
+
const candidates = [];
|
|
2635
|
+
for (const [name, canonicalName] of Object.entries(value)) {
|
|
2636
|
+
if (name.startsWith('@') || typeof canonicalName !== 'string') {
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
if (BAZEL_REPO_NAME_RE.test(name)) {
|
|
2640
|
+
candidates.push(name);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return candidates;
|
|
2644
|
+
}
|
|
2494
2645
|
function normalizeRepoName(name) {
|
|
2495
2646
|
const repo = name.startsWith('@') ? name.slice(1) : name;
|
|
2496
2647
|
return BAZEL_REPO_NAME_RE.test(repo) ? repo : undefined;
|
|
2497
2648
|
}
|
|
2498
2649
|
|
|
2499
|
-
// Parse `bazel mod
|
|
2500
|
-
//
|
|
2501
|
-
//
|
|
2650
|
+
// Parse `bazel mod dump_repo_mapping "" --output=json` output. Also accept the
|
|
2651
|
+
// older streamed jsonproto shape in case older Bazel versions or fixtures still
|
|
2652
|
+
// return repository records with apparentName fields.
|
|
2502
2653
|
function parseVisibleRepoCandidates(output) {
|
|
2503
2654
|
const candidates = [];
|
|
2504
2655
|
for (const line of output.split(/\r?\n/)) {
|
|
@@ -2508,6 +2659,7 @@ function parseVisibleRepoCandidates(output) {
|
|
|
2508
2659
|
}
|
|
2509
2660
|
try {
|
|
2510
2661
|
const parsed = JSON.parse(trimmed);
|
|
2662
|
+
candidates.push(...apparentNamesFromRepoMapping(parsed));
|
|
2511
2663
|
const apparentName = apparentNameFromJsonValue(parsed);
|
|
2512
2664
|
if (apparentName) {
|
|
2513
2665
|
const repo = normalizeRepoName(apparentName);
|
|
@@ -2529,7 +2681,7 @@ function parseMavenRepoCandidates(cwd, verbose) {
|
|
|
2529
2681
|
|
|
2530
2682
|
// Bzlmod path: parse MODULE.bazel for use_repo(maven, ...).
|
|
2531
2683
|
const moduleBazel = path.join(cwd, 'MODULE.bazel');
|
|
2532
|
-
const moduleContent = safeReadFile(moduleBazel);
|
|
2684
|
+
const moduleContent = safeReadFile$1(moduleBazel);
|
|
2533
2685
|
if (moduleContent) {
|
|
2534
2686
|
const bzlmodHits = [];
|
|
2535
2687
|
for (const m of moduleContent.matchAll(USE_REPO_RE)) {
|
|
@@ -2547,12 +2699,12 @@ function parseMavenRepoCandidates(cwd, verbose) {
|
|
|
2547
2699
|
}
|
|
2548
2700
|
|
|
2549
2701
|
// Legacy path: scan WORKSPACE + top-level .bzl files for maven_install(name=...).
|
|
2550
|
-
const legacyFiles = listLegacyStarlarkFiles(cwd);
|
|
2702
|
+
const legacyFiles = listLegacyStarlarkFiles$1(cwd);
|
|
2551
2703
|
if (verbose) {
|
|
2552
2704
|
logger.logger.log('[VERBOSE] discovery: legacy files considered:', legacyFiles.length ? legacyFiles : '(none)');
|
|
2553
2705
|
}
|
|
2554
2706
|
for (const file of legacyFiles) {
|
|
2555
|
-
const content = safeReadFile(file);
|
|
2707
|
+
const content = safeReadFile$1(file);
|
|
2556
2708
|
if (!content) {
|
|
2557
2709
|
continue;
|
|
2558
2710
|
}
|
|
@@ -3023,8 +3175,18 @@ async function extractBazelToMaven(opts) {
|
|
|
3023
3175
|
});
|
|
3024
3176
|
}
|
|
3025
3177
|
if (!allArtifacts.length) {
|
|
3026
|
-
|
|
3027
|
-
|
|
3178
|
+
if (!repos.size) {
|
|
3179
|
+
if (verbose) {
|
|
3180
|
+
logger.logger.info('No Maven artifacts extracted. failureCategory=no-supported-ecosystem');
|
|
3181
|
+
}
|
|
3182
|
+
return {
|
|
3183
|
+
artifactCount: 0,
|
|
3184
|
+
manifestPath,
|
|
3185
|
+
noEcosystemFound: true,
|
|
3186
|
+
ok: false
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
logger.logger.fail(`Discovered Maven repo(s) ${repoNames.join(', ')} but extracted zero artifacts. failureCategory=ecosystem-detected-but-empty`);
|
|
3028
3190
|
return {
|
|
3029
3191
|
artifactCount: 0,
|
|
3030
3192
|
manifestPath,
|
|
@@ -3038,7 +3200,6 @@ async function extractBazelToMaven(opts) {
|
|
|
3038
3200
|
ok: true
|
|
3039
3201
|
};
|
|
3040
3202
|
} catch (e) {
|
|
3041
|
-
process.exitCode = 1;
|
|
3042
3203
|
// Always surface the error message; users should not have to
|
|
3043
3204
|
// re-run a multi-minute bazel build with --verbose just to see whether
|
|
3044
3205
|
// the failure was a missing dependency, permission error, or network blip.
|
|
@@ -3057,6 +3218,130 @@ async function extractBazelToMaven(opts) {
|
|
|
3057
3218
|
}
|
|
3058
3219
|
}
|
|
3059
3220
|
|
|
3221
|
+
async function convertGradleToFacts({
|
|
3222
|
+
bin,
|
|
3223
|
+
cwd,
|
|
3224
|
+
gradleOpts,
|
|
3225
|
+
verbose
|
|
3226
|
+
}) {
|
|
3227
|
+
const rBin = path.resolve(cwd, bin);
|
|
3228
|
+
const binExists = fs$1.existsSync(rBin);
|
|
3229
|
+
const cwdExists = fs$1.existsSync(cwd);
|
|
3230
|
+
logger.logger.group('gradle2facts:');
|
|
3231
|
+
logger.logger.info(`- executing: \`${rBin}\``);
|
|
3232
|
+
if (!binExists) {
|
|
3233
|
+
logger.logger.warn(`Warning: It appears the executable could not be found. An error might be printed later because of that.`);
|
|
3234
|
+
}
|
|
3235
|
+
logger.logger.info(`- src dir: \`${cwd}\``);
|
|
3236
|
+
if (!cwdExists) {
|
|
3237
|
+
logger.logger.warn(`Warning: It appears the src dir could not be found. An error might be printed later because of that.`);
|
|
3238
|
+
}
|
|
3239
|
+
logger.logger.groupEnd();
|
|
3240
|
+
try {
|
|
3241
|
+
// The init script is bundled alongside the existing pom-generating one.
|
|
3242
|
+
// See .config/rollup.dist.config.mjs:copySocketFactsInitGradle.
|
|
3243
|
+
const initLocation = path.join(constants.default.distPath, 'socket-facts.init.gradle');
|
|
3244
|
+
const commandArgs = ['--init-script', initLocation, ...gradleOpts, 'socketFacts'];
|
|
3245
|
+
if (verbose) {
|
|
3246
|
+
logger.logger.log('[VERBOSE] Executing:', [bin], ', args:', commandArgs);
|
|
3247
|
+
}
|
|
3248
|
+
logger.logger.log(`Generating Socket facts from \`${bin}\` on \`${cwd}\` ...`);
|
|
3249
|
+
const output = await execGradle$1(rBin, commandArgs, cwd, verbose);
|
|
3250
|
+
if (output.code) {
|
|
3251
|
+
process.exitCode = 1;
|
|
3252
|
+
logger.logger.fail(`Gradle exited with exit code ${output.code}`);
|
|
3253
|
+
if (!verbose) {
|
|
3254
|
+
logger.logger.group('stderr:');
|
|
3255
|
+
logger.logger.error(output.stderr);
|
|
3256
|
+
logger.logger.groupEnd();
|
|
3257
|
+
}
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
logger.logger.success('Executed gradle successfully');
|
|
3261
|
+
if (verbose) {
|
|
3262
|
+
// Output already streamed; the "Reported exports:" summary lines were
|
|
3263
|
+
// visible inline. No need to repeat them from a captured stdout.
|
|
3264
|
+
logger.logger.log('');
|
|
3265
|
+
logger.logger.log('Next step is to generate a Scan by running the `socket scan create` command on the same directory.');
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
const exports = Array.from(output.stdout.matchAll(/^Socket facts file written to: (.*)/gm), m => m[1]);
|
|
3269
|
+
if (exports.length) {
|
|
3270
|
+
logger.logger.log('Reported exports:');
|
|
3271
|
+
for (const fn of exports) {
|
|
3272
|
+
logger.logger.log('- ', fn);
|
|
3273
|
+
}
|
|
3274
|
+
} else {
|
|
3275
|
+
// Gradle script may have skipped emission when no resolvable
|
|
3276
|
+
// dependencies were found (see the `components.isEmpty()` branch in
|
|
3277
|
+
// socket-facts.init.gradle). Surface the skip reason if present so
|
|
3278
|
+
// the user understands why nothing was written.
|
|
3279
|
+
const skipMatch = output.stdout.match(/^\[socket-facts\] no resolvable dependencies.*/m);
|
|
3280
|
+
if (skipMatch) {
|
|
3281
|
+
logger.logger.warn(skipMatch[0]);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
logger.logger.log('');
|
|
3285
|
+
logger.logger.log('Next step is to generate a Scan by running the `socket scan create` command on the same directory.');
|
|
3286
|
+
} catch (e) {
|
|
3287
|
+
process.exitCode = 1;
|
|
3288
|
+
logger.logger.fail('There was an unexpected error while generating Socket facts' + (verbose ? '' : ' (use --verbose for details)'));
|
|
3289
|
+
if (verbose) {
|
|
3290
|
+
logger.logger.group('[VERBOSE] error:');
|
|
3291
|
+
logger.logger.log(e);
|
|
3292
|
+
logger.logger.groupEnd();
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
async function execGradle$1(bin, commandArgs, cwd, verbose) {
|
|
3297
|
+
// When verbose, stream gradle stdout/stderr directly to the user's
|
|
3298
|
+
// terminal — no spinner, no capture. The trade-off is that the post-run
|
|
3299
|
+
// "Reported exports:" summary is skipped (the lines were already visible
|
|
3300
|
+
// inline). For huge builds where the user wants to see progress, this is
|
|
3301
|
+
// the right default. Non-verbose runs still get the spinner + summary.
|
|
3302
|
+
if (verbose) {
|
|
3303
|
+
logger.logger.info('(Running gradle with output streaming. This can take a while.)');
|
|
3304
|
+
const output = await spawn.spawn(bin, commandArgs, {
|
|
3305
|
+
cwd,
|
|
3306
|
+
stdio: 'inherit'
|
|
3307
|
+
});
|
|
3308
|
+
return {
|
|
3309
|
+
code: output.code,
|
|
3310
|
+
stdout: '',
|
|
3311
|
+
stderr: ''
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
const {
|
|
3315
|
+
spinner
|
|
3316
|
+
} = constants.default;
|
|
3317
|
+
let pass = false;
|
|
3318
|
+
try {
|
|
3319
|
+
logger.logger.info('(Running gradle can take a while, depending on the size of the project)');
|
|
3320
|
+
logger.logger.info('(No live output. Pass --verbose to stream gradle output instead.)');
|
|
3321
|
+
spinner.start(`Running gradlew...`);
|
|
3322
|
+
const output = await spawn.spawn(bin, commandArgs, {
|
|
3323
|
+
cwd
|
|
3324
|
+
});
|
|
3325
|
+
pass = true;
|
|
3326
|
+
const {
|
|
3327
|
+
code,
|
|
3328
|
+
stderr,
|
|
3329
|
+
stdout
|
|
3330
|
+
} = output;
|
|
3331
|
+
return {
|
|
3332
|
+
code,
|
|
3333
|
+
stdout,
|
|
3334
|
+
stderr
|
|
3335
|
+
};
|
|
3336
|
+
} finally {
|
|
3337
|
+
if (pass) {
|
|
3338
|
+
spinner.successAndStop('Gracefully completed gradlew execution.');
|
|
3339
|
+
} else {
|
|
3340
|
+
spinner.failAndStop('There was an error while trying to run gradlew.');
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3060
3345
|
async function convertGradleToMaven({
|
|
3061
3346
|
bin,
|
|
3062
3347
|
cwd,
|
|
@@ -3093,16 +3378,10 @@ async function convertGradleToMaven({
|
|
|
3093
3378
|
logger.logger.log('[VERBOSE] Executing:', [bin], ', args:', commandArgs);
|
|
3094
3379
|
}
|
|
3095
3380
|
logger.logger.log(`Converting gradle to maven from \`${bin}\` on \`${cwd}\` ...`);
|
|
3096
|
-
const output = await
|
|
3097
|
-
if (verbose) {
|
|
3098
|
-
logger.logger.group('[VERBOSE] gradle stdout:');
|
|
3099
|
-
logger.logger.log(output);
|
|
3100
|
-
logger.logger.groupEnd();
|
|
3101
|
-
}
|
|
3381
|
+
const output = await execGradle(rBin, commandArgs, cwd, verbose);
|
|
3102
3382
|
if (output.code) {
|
|
3103
3383
|
process.exitCode = 1;
|
|
3104
3384
|
logger.logger.fail(`Gradle exited with exit code ${output.code}`);
|
|
3105
|
-
// (In verbose mode, stderr was printed above, no need to repeat it)
|
|
3106
3385
|
if (!verbose) {
|
|
3107
3386
|
logger.logger.group('stderr:');
|
|
3108
3387
|
logger.logger.error(output.stderr);
|
|
@@ -3111,6 +3390,13 @@ async function convertGradleToMaven({
|
|
|
3111
3390
|
return;
|
|
3112
3391
|
}
|
|
3113
3392
|
logger.logger.success('Executed gradle successfully');
|
|
3393
|
+
if (verbose) {
|
|
3394
|
+
// Output already streamed; "POM file copied to:" lines were visible
|
|
3395
|
+
// inline. Skip the captured-stdout summary.
|
|
3396
|
+
logger.logger.log('');
|
|
3397
|
+
logger.logger.log('Next step is to generate a Scan by running the `socket scan create` command on the same directory');
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3114
3400
|
logger.logger.log('Reported exports:');
|
|
3115
3401
|
output.stdout.replace(/^POM file copied to: (.*)/gm, (_all, fn) => {
|
|
3116
3402
|
logger.logger.log('- ', fn);
|
|
@@ -3128,20 +3414,32 @@ async function convertGradleToMaven({
|
|
|
3128
3414
|
}
|
|
3129
3415
|
}
|
|
3130
3416
|
}
|
|
3131
|
-
async function
|
|
3417
|
+
async function execGradle(bin, commandArgs, cwd, verbose) {
|
|
3418
|
+
// When verbose, stream gradle stdout/stderr directly to the user's
|
|
3419
|
+
// terminal — no spinner, no capture. The trade-off is that the post-run
|
|
3420
|
+
// "Reported exports:" summary is skipped (the lines were already visible
|
|
3421
|
+
// inline). Non-verbose runs still get the spinner + summary.
|
|
3422
|
+
if (verbose) {
|
|
3423
|
+
logger.logger.info('(Running gradle with output streaming. This can take a while.)');
|
|
3424
|
+
const output = await spawn.spawn(bin, commandArgs, {
|
|
3425
|
+
cwd,
|
|
3426
|
+
stdio: 'inherit'
|
|
3427
|
+
});
|
|
3428
|
+
return {
|
|
3429
|
+
code: output.code,
|
|
3430
|
+
stdout: '',
|
|
3431
|
+
stderr: ''
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3132
3434
|
const {
|
|
3133
3435
|
spinner
|
|
3134
3436
|
} = constants.default;
|
|
3135
3437
|
let pass = false;
|
|
3136
3438
|
try {
|
|
3137
|
-
logger.logger.info('(Running gradle can take a while,
|
|
3138
|
-
logger.logger.info('(
|
|
3439
|
+
logger.logger.info('(Running gradle can take a while, depending on the size of the project)');
|
|
3440
|
+
logger.logger.info('(No live output. Pass --verbose to stream gradle output instead.)');
|
|
3139
3441
|
spinner.start(`Running gradlew...`);
|
|
3140
3442
|
const output = await spawn.spawn(bin, commandArgs, {
|
|
3141
|
-
// We can pipe the output through to have the user see the result
|
|
3142
|
-
// of running gradlew, but then we can't (easily) gather the output
|
|
3143
|
-
// to discover the generated files... probably a flag we should allow?
|
|
3144
|
-
// stdio: isDebug() ? 'inherit' : undefined,
|
|
3145
3443
|
cwd
|
|
3146
3444
|
});
|
|
3147
3445
|
pass = true;
|
|
@@ -3512,8 +3810,7 @@ async function generateAutoManifest({
|
|
|
3512
3810
|
});
|
|
3513
3811
|
}
|
|
3514
3812
|
if (!sockJson?.defaults?.manifest?.gradle?.disabled && detected.gradle) {
|
|
3515
|
-
|
|
3516
|
-
await convertGradleToMaven({
|
|
3813
|
+
const gradleArgs = {
|
|
3517
3814
|
// Note: `gradlew` is more likely to be resolved against cwd.
|
|
3518
3815
|
// Note: .resolve() won't butcher an absolute path.
|
|
3519
3816
|
// TODO: `gradlew` (or anything else given) may want to resolve against PATH.
|
|
@@ -3521,7 +3818,14 @@ async function generateAutoManifest({
|
|
|
3521
3818
|
cwd,
|
|
3522
3819
|
verbose: Boolean(sockJson.defaults?.manifest?.gradle?.verbose),
|
|
3523
3820
|
gradleOpts: sockJson.defaults?.manifest?.gradle?.gradleOpts?.split(' ').map(s => s.trim()).filter(Boolean) ?? []
|
|
3524
|
-
}
|
|
3821
|
+
};
|
|
3822
|
+
if (sockJson.defaults?.manifest?.gradle?.facts) {
|
|
3823
|
+
logger.logger.log('Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...');
|
|
3824
|
+
await convertGradleToFacts(gradleArgs);
|
|
3825
|
+
} else {
|
|
3826
|
+
logger.logger.log('Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...');
|
|
3827
|
+
await convertGradleToMaven(gradleArgs);
|
|
3828
|
+
}
|
|
3525
3829
|
}
|
|
3526
3830
|
if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) {
|
|
3527
3831
|
logger.logger.log('Detected an environment.yml file, running default Conda generator...');
|
|
@@ -3536,24 +3840,23 @@ async function generateAutoManifest({
|
|
|
3536
3840
|
if (!sockJson?.defaults?.manifest?.bazel?.disabled && detected.bazel) {
|
|
3537
3841
|
const bazelConfig = sockJson?.defaults?.manifest?.bazel;
|
|
3538
3842
|
logger.logger.log('Detected a Bazel workspace, extracting Maven dependencies via bazel query...');
|
|
3539
|
-
const
|
|
3843
|
+
const mavenResult = await extractBazelToMaven({
|
|
3540
3844
|
bazelFlags: bazelConfig?.bazelFlags,
|
|
3541
3845
|
bazelOutputBase: bazelConfig?.bazelOutputBase,
|
|
3542
3846
|
bazelRc: bazelConfig?.bazelRc,
|
|
3543
3847
|
bin: bazelConfig?.bazel ?? bazelConfig?.bin,
|
|
3544
3848
|
cwd,
|
|
3545
|
-
// Auto-manifest writes into a sibling directory instead of the repo root
|
|
3546
|
-
// so scan discovery can pick it up without colliding with a checked-in
|
|
3547
|
-
// rules_jvm_external lockfile or repo-root gitignore patterns.
|
|
3548
3849
|
out: bazelConfig?.out ?? cwd,
|
|
3549
3850
|
outLayout: 'flat',
|
|
3550
3851
|
verbose: Boolean(bazelConfig?.verbose) || verbose
|
|
3551
3852
|
});
|
|
3552
|
-
if (!
|
|
3553
|
-
throw new Error('Bazel auto-manifest generation failed');
|
|
3853
|
+
if (!mavenResult.ok && !mavenResult.noEcosystemFound) {
|
|
3854
|
+
throw new Error('Bazel auto-manifest generation failed for ecosystem(s): maven');
|
|
3554
3855
|
}
|
|
3555
|
-
if (
|
|
3556
|
-
generatedFiles.push(
|
|
3856
|
+
if (mavenResult.ok && mavenResult.manifestPath) {
|
|
3857
|
+
generatedFiles.push(mavenResult.manifestPath);
|
|
3858
|
+
} else if (mavenResult.noEcosystemFound) {
|
|
3859
|
+
logger.logger.info('No supported Bazel Maven ecosystem detected.');
|
|
3557
3860
|
}
|
|
3558
3861
|
}
|
|
3559
3862
|
return {
|
|
@@ -3575,19 +3878,11 @@ function getCdxSpdxPatterns(supportedFiles) {
|
|
|
3575
3878
|
}
|
|
3576
3879
|
return patterns;
|
|
3577
3880
|
}
|
|
3578
|
-
function
|
|
3881
|
+
function filterToCdxSpdxOnly(filepaths, supportedFiles) {
|
|
3579
3882
|
const patterns = getCdxSpdxPatterns(supportedFiles);
|
|
3580
|
-
return filepaths.filter(filepath => {
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
if (basename === constants.default.DOT_SOCKET_DOT_FACTS_JSON) {
|
|
3584
|
-
return true;
|
|
3585
|
-
}
|
|
3586
|
-
// Include CDX and SPDX files.
|
|
3587
|
-
return vendor.micromatchExports.some(filepath, patterns, {
|
|
3588
|
-
nocase: true
|
|
3589
|
-
});
|
|
3590
|
-
});
|
|
3883
|
+
return filepaths.filter(filepath => vendor.micromatchExports.some(filepath, patterns, {
|
|
3884
|
+
nocase: true
|
|
3885
|
+
}));
|
|
3591
3886
|
}
|
|
3592
3887
|
async function handleCreateNewScan({
|
|
3593
3888
|
autoManifest,
|
|
@@ -3706,6 +4001,7 @@ async function handleCreateNewScan({
|
|
|
3706
4001
|
}
|
|
3707
4002
|
let scanPaths = packagePaths;
|
|
3708
4003
|
let tier1ReachabilityScanId;
|
|
4004
|
+
let reachabilityReport;
|
|
3709
4005
|
|
|
3710
4006
|
// If reachability is enabled, perform reachability analysis.
|
|
3711
4007
|
if (reach.runReachabilityAnalysis) {
|
|
@@ -3735,14 +4031,14 @@ async function handleCreateNewScan({
|
|
|
3735
4031
|
return;
|
|
3736
4032
|
}
|
|
3737
4033
|
logger.logger.success('Reachability analysis completed successfully');
|
|
3738
|
-
|
|
4034
|
+
reachabilityReport = reachResult.data?.reachabilityReport;
|
|
3739
4035
|
|
|
3740
4036
|
// Ensure the .socket.facts.json isn't duplicated in case it happened
|
|
3741
4037
|
// to be in the scan folder before the analysis was run.
|
|
3742
|
-
const filteredPackagePaths = packagePaths.filter(p => path.basename(p)
|
|
4038
|
+
const filteredPackagePaths = packagePaths.filter(p => path.basename(p) !== constants.default.DOT_SOCKET_DOT_FACTS_JSON);
|
|
3743
4039
|
|
|
3744
4040
|
// When using pregenerated SBOMs only, filter to CDX/SPDX files.
|
|
3745
|
-
const pathsForScan = reach.reachUseOnlyPregeneratedSboms ?
|
|
4041
|
+
const pathsForScan = reach.reachUseOnlyPregeneratedSboms ? filterToCdxSpdxOnly(filteredPackagePaths, supportedFiles) : filteredPackagePaths;
|
|
3746
4042
|
scanPaths = [...pathsForScan, ...(reachabilityReport ? [reachabilityReport] : [])];
|
|
3747
4043
|
tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId;
|
|
3748
4044
|
}
|
|
@@ -3778,6 +4074,22 @@ async function handleCreateNewScan({
|
|
|
3778
4074
|
if (reach && scanId && tier1ReachabilityScanId) {
|
|
3779
4075
|
await finalizeTier1Scan(tier1ReachabilityScanId, scanId);
|
|
3780
4076
|
}
|
|
4077
|
+
|
|
4078
|
+
// On a successful scan, clean up the `.socket.facts.json` coana wrote at
|
|
4079
|
+
// the path we instructed it to write to (via `--socket-mode`). Failed
|
|
4080
|
+
// scans leave the file in place for debugging. Producer-written files
|
|
4081
|
+
// (e.g. from `socket manifest gradle --facts`) are NOT touched here —
|
|
4082
|
+
// those are user-owned input that the user can clean up themselves; in
|
|
4083
|
+
// the --reach path coana overwrites that file with its enriched output
|
|
4084
|
+
// anyway, so it's the same path that gets removed.
|
|
4085
|
+
if (fullScanCResult.ok && scanId && reachabilityReport) {
|
|
4086
|
+
try {
|
|
4087
|
+
await fs.unlink(path.resolve(cwd, reachabilityReport));
|
|
4088
|
+
require$$9.debugFn('notice', `[socket-facts] removed coana output after successful scan: ${reachabilityReport}`);
|
|
4089
|
+
} catch {
|
|
4090
|
+
// Best-effort — file may already be gone or unwritable.
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
3781
4093
|
if (report && fullScanCResult.ok) {
|
|
3782
4094
|
if (scanId) {
|
|
3783
4095
|
await handleScanReport({
|
|
@@ -7148,57 +7460,956 @@ async function run$G(argv, importMeta, context) {
|
|
|
7148
7460
|
await spawnPromise;
|
|
7149
7461
|
}
|
|
7150
7462
|
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
hidden: false,
|
|
7155
|
-
flags: {
|
|
7156
|
-
...flags.commonFlags,
|
|
7157
|
-
bazel: {
|
|
7158
|
-
type: 'string',
|
|
7159
|
-
description: 'Path to bazel/bazelisk binary; default: $(which bazelisk) || $(which bazel)'
|
|
7160
|
-
},
|
|
7161
|
-
bazelFlags: {
|
|
7162
|
-
type: 'string',
|
|
7163
|
-
description: 'Flags forwarded to every bazel invocation (single quoted string)'
|
|
7164
|
-
},
|
|
7165
|
-
bazelOutputBase: {
|
|
7166
|
-
type: 'string',
|
|
7167
|
-
description: 'Bazel --output_base for read-only-cache CI environments'
|
|
7168
|
-
},
|
|
7169
|
-
bazelRc: {
|
|
7170
|
-
type: 'string',
|
|
7171
|
-
description: 'Path to additional .bazelrc fragments forwarded to bazel'
|
|
7172
|
-
},
|
|
7173
|
-
out: {
|
|
7174
|
-
type: 'string',
|
|
7175
|
-
description: 'Output directory for generated manifests; default: ./.socket/bazel-manifests/'
|
|
7176
|
-
},
|
|
7177
|
-
verbose: {
|
|
7178
|
-
type: 'boolean',
|
|
7179
|
-
description: 'Stream bazel stdout/stderr'
|
|
7180
|
-
}
|
|
7181
|
-
},
|
|
7182
|
-
help: (command, config) => `
|
|
7183
|
-
Usage
|
|
7184
|
-
$ ${command} [options] [CWD=.]
|
|
7185
|
-
|
|
7186
|
-
Options
|
|
7187
|
-
${utils.getFlagListOutput(config.flags)}
|
|
7188
|
-
|
|
7189
|
-
[beta] Generates Bazel JVM SBOM manifests (\`maven_install.json\`-shaped)
|
|
7190
|
-
by running \`bazel query\` against discovered Maven repos. Output is
|
|
7191
|
-
consumed by \`socket scan create\`'s server-side parser.
|
|
7463
|
+
// Maximum size (bytes) we will read for any single Bazel workspace file.
|
|
7464
|
+
// Prevents DoS via maliciously large MODULE.bazel / WORKSPACE / .bzl files.
|
|
7465
|
+
const MAX_WORKSPACE_FILE_BYTES = 5 * 1024 * 1024;
|
|
7192
7466
|
|
|
7193
|
-
|
|
7194
|
-
|
|
7467
|
+
// Maximum candidate count we will return (deduped) before failing.
|
|
7468
|
+
// Real repos have <20; this is a hard ceiling against pathological inputs.
|
|
7469
|
+
const MAX_CANDIDATES = 256;
|
|
7195
7470
|
|
|
7196
|
-
|
|
7197
|
-
instead — it detects Bazel workspaces, runs the same extraction, and uploads
|
|
7198
|
-
the result. This subcommand is for generation only.
|
|
7471
|
+
// Regex strategy: anchored, bounded character classes, no nested quantifiers.
|
|
7199
7472
|
|
|
7200
|
-
|
|
7473
|
+
// Bzlmod: discover `use_extension(..., "pip")` bindings, then match
|
|
7474
|
+
// `${binding}.parse(...)` to find pip hub declarations.
|
|
7475
|
+
// Bounded: matches up to ~256 chars of path to avoid catastrophic backtracking.
|
|
7476
|
+
const USE_EXTENSION_PIP_RE = /(\w+)\s*=\s*use_extension\s*\(\s*["'][^"']{0,256}pip\.bzl["']\s*,\s*["']pip["']\s*\)/g;
|
|
7477
|
+
|
|
7478
|
+
// Extract hub_name, requirements_lock, and python_version from a pip.parse
|
|
7479
|
+
// argument blob. Bounded character classes and length caps.
|
|
7480
|
+
const HUB_NAME_ATTR_RE = /hub_name\s*=\s*(["'])([A-Za-z0-9_]{1,129})\1/;
|
|
7481
|
+
const REQUIREMENTS_LOCK_ATTR_RE = /requirements_lock\s*=\s*(["'])([^"']{1,512})\1/;
|
|
7482
|
+
const PYTHON_VERSION_ATTR_RE = /python_version\s*=\s*(["'])([0-9._+!]{1,32})\1/;
|
|
7483
|
+
|
|
7484
|
+
// Legacy WORKSPACE patterns: pip_parse, pip_install, pip_repository.
|
|
7485
|
+
// Bounded: matches up to ~8KB of argument list.
|
|
7486
|
+
const PIP_PARSE_NAME_RE = /pip_parse\s*\(\s*([^)]{0,8192})\)/g;
|
|
7487
|
+
const PIP_INSTALL_NAME_RE = /pip_install\s*\(\s*([^)]{0,8192})\)/g;
|
|
7488
|
+
const PIP_REPOSITORY_NAME_RE = /pip_repository\s*\(\s*([^)]{0,8192})\)/g;
|
|
7489
|
+
const NAME_ATTR_RE = /name\s*=\s*(["'])([A-Za-z0-9_]{1,129})\1/;
|
|
7490
|
+
const LEGACY_REQ_LOCK_RE = /requirements_lock\s*=\s*(["'])([^"']{1,512})\1/;
|
|
7491
|
+
const MOD_SHOW_PIP_PARSE_RE = /pip\.parse\s*\(\s*([^)]{0,8192})\)/g;
|
|
7492
|
+
const MOD_SHOW_USE_REPO_RE = /use_repo\s*\(\s*\w+\s*,\s*(["'])([A-Za-z0-9_]{1,129})\1\s*\)/g;
|
|
7493
|
+
|
|
7494
|
+
// Hub validation: accept alias rules or `:pkg` targets in probe stdout.
|
|
7495
|
+
// Does NOT require `pypi_name=` (that marker lives on spoke repos).
|
|
7496
|
+
const PYPI_HUB_MARKER_RE = /:pkg\b|alias\s*\(/;
|
|
7497
|
+
function parseBazelModPipExtensionCandidates(stdout, verbose) {
|
|
7498
|
+
const useRepoNames = new Set();
|
|
7499
|
+
for (const m of stdout.matchAll(MOD_SHOW_USE_REPO_RE)) {
|
|
7500
|
+
useRepoNames.add(m[2]);
|
|
7501
|
+
}
|
|
7502
|
+
const candidates = [];
|
|
7503
|
+
for (const m of stdout.matchAll(MOD_SHOW_PIP_PARSE_RE)) {
|
|
7504
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', 'bazel-mod-show-extension', 'bzlmod');
|
|
7505
|
+
if (!info) {
|
|
7506
|
+
continue;
|
|
7507
|
+
}
|
|
7508
|
+
if (useRepoNames.size && !useRepoNames.has(info.hubName)) {
|
|
7509
|
+
if (verbose) {
|
|
7510
|
+
logger.logger.log(`[VERBOSE] discovery: dropping pip.parse hub '${info.hubName}' because show_extension did not report matching use_repo.`);
|
|
7511
|
+
}
|
|
7512
|
+
continue;
|
|
7513
|
+
}
|
|
7514
|
+
candidates.push(info);
|
|
7515
|
+
}
|
|
7516
|
+
if (verbose) {
|
|
7517
|
+
logger.logger.log('[VERBOSE] discovery: bazel mod show_extension pip.parse hits:', candidates.length, 'use_repo:', Array.from(useRepoNames));
|
|
7518
|
+
}
|
|
7519
|
+
return dedupCapped(candidates, verbose);
|
|
7520
|
+
}
|
|
7521
|
+
|
|
7522
|
+
// Reads file contents, refusing files that exceed MAX_WORKSPACE_FILE_BYTES.
|
|
7523
|
+
// Returns null when the file is missing, oversized, or unreadable.
|
|
7524
|
+
function safeReadFile(file) {
|
|
7525
|
+
if (!fs$1.existsSync(file)) {
|
|
7526
|
+
return null;
|
|
7527
|
+
}
|
|
7528
|
+
try {
|
|
7529
|
+
const stat = fs$1.statSync(file);
|
|
7530
|
+
if (stat.size > MAX_WORKSPACE_FILE_BYTES) {
|
|
7531
|
+
return null;
|
|
7532
|
+
}
|
|
7533
|
+
return fs$1.readFileSync(file, 'utf8');
|
|
7534
|
+
} catch {
|
|
7535
|
+
return null;
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
// Walks workspace root for legacy Starlark sources we can scan: WORKSPACE
|
|
7540
|
+
// (and WORKSPACE.bazel) plus top-level .bzl files. Non-recursive by design;
|
|
7541
|
+
// Phase 1 explicitly avoids static Starlark parsing at depth.
|
|
7542
|
+
function listLegacyStarlarkFiles(cwd) {
|
|
7543
|
+
const files = [];
|
|
7544
|
+
const candidates = ['WORKSPACE', 'WORKSPACE.bazel'];
|
|
7545
|
+
for (const c of candidates) {
|
|
7546
|
+
const p = path.join(cwd, c);
|
|
7547
|
+
if (fs$1.existsSync(p)) {
|
|
7548
|
+
files.push(p);
|
|
7549
|
+
}
|
|
7550
|
+
}
|
|
7551
|
+
// Top-level .bzl files only.
|
|
7552
|
+
try {
|
|
7553
|
+
for (const entry of fs$1.readdirSync(cwd)) {
|
|
7554
|
+
if (entry.endsWith('.bzl')) {
|
|
7555
|
+
files.push(path.join(cwd, entry));
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
} catch {
|
|
7559
|
+
// Ignore unreadable cwd.
|
|
7560
|
+
}
|
|
7561
|
+
return files;
|
|
7562
|
+
}
|
|
7563
|
+
|
|
7564
|
+
// Returns deduplicated list of items, capped at MAX_CANDIDATES.
|
|
7565
|
+
// Precedence: the first occurrence of a given hubName wins. Callers
|
|
7566
|
+
// must order inputs so the preferred source comes first (e.g., Bzlmod
|
|
7567
|
+
// hits before legacy WORKSPACE hits during migration).
|
|
7568
|
+
// Throws a clear error if the cap is exceeded so callers do not silently
|
|
7569
|
+
// truncate. Emits a verbose warning when a later entry is dropped due to
|
|
7570
|
+
// a name collision so users can see implicit precedence at work.
|
|
7571
|
+
function dedupCapped(items, verbose) {
|
|
7572
|
+
const seen = new Map();
|
|
7573
|
+
const out = [];
|
|
7574
|
+
for (const item of items) {
|
|
7575
|
+
const existing = seen.get(item.hubName);
|
|
7576
|
+
if (!existing) {
|
|
7577
|
+
seen.set(item.hubName, item);
|
|
7578
|
+
out.push(item);
|
|
7579
|
+
if (out.length >= MAX_CANDIDATES) {
|
|
7580
|
+
throw new Error(`Discovered more than ${MAX_CANDIDATES} pip hub candidates. ` + 'This exceeds the safety ceiling; aborting discovery.');
|
|
7581
|
+
}
|
|
7582
|
+
} else if (verbose) {
|
|
7583
|
+
logger.logger.log(`[VERBOSE] discovery: dropping duplicate pip hub candidate '${item.hubName}' ` + `(kept first occurrence from ${existing.source}/${existing.workspaceMode}, ` + `dropped ${item.source}/${item.workspaceMode}).`);
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
return out;
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7589
|
+
// Build a dynamic regex for `${binding}.parse(...)` given a validated binding
|
|
7590
|
+
// name (word characters only, so safe to embed). Bounded arg list.
|
|
7591
|
+
function buildPipParseRe(binding) {
|
|
7592
|
+
return new RegExp(`${binding}\\.parse\\s*\\(\\s*([^)]{0,8192})\\)`, 'g');
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
// Extract candidate hub fields from a pip.parse / pip_parse / pip_install /
|
|
7596
|
+
// pip_repository argument blob (without probeStdout or visibleRepoNames).
|
|
7597
|
+
function extractHubInfoFromArgBlob(argBlob, source, workspaceMode) {
|
|
7598
|
+
const hubMatch = HUB_NAME_ATTR_RE.exec(argBlob);
|
|
7599
|
+
const nameMatch = NAME_ATTR_RE.exec(argBlob);
|
|
7600
|
+
const hubName = hubMatch?.[2] ?? nameMatch?.[2];
|
|
7601
|
+
if (!hubName) {
|
|
7602
|
+
return undefined;
|
|
7603
|
+
}
|
|
7604
|
+
const lockMatch = REQUIREMENTS_LOCK_ATTR_RE.exec(argBlob) ?? LEGACY_REQ_LOCK_RE.exec(argBlob);
|
|
7605
|
+
const pythonVersion = PYTHON_VERSION_ATTR_RE.exec(argBlob)?.[2];
|
|
7606
|
+
return {
|
|
7607
|
+
hubName,
|
|
7608
|
+
source,
|
|
7609
|
+
workspaceMode,
|
|
7610
|
+
pythonVersion,
|
|
7611
|
+
requirementsLockLabel: lockMatch?.[2]
|
|
7612
|
+
};
|
|
7613
|
+
}
|
|
7614
|
+
|
|
7615
|
+
// Step 1: parse candidate pip hub names from Bzlmod MODULE.bazel and legacy
|
|
7616
|
+
// WORKSPACE / .bzl entry points.
|
|
7617
|
+
//
|
|
7618
|
+
// Precedence: Bzlmod (MODULE.bazel pip.parse) hits are pushed first, then
|
|
7619
|
+
// legacy (pip_parse / pip_install / pip_repository) hits. dedupCapped keeps
|
|
7620
|
+
// the first occurrence, so during migration scenarios where both
|
|
7621
|
+
// MODULE.bazel and WORKSPACE define a hub with the same name, the Bzlmod
|
|
7622
|
+
// entry wins implicitly. Pass verbose=true to surface dropped duplicates.
|
|
7623
|
+
function parsePypiHubCandidates(cwd, verbose) {
|
|
7624
|
+
const candidates = [];
|
|
7625
|
+
|
|
7626
|
+
// Bzlmod path: parse MODULE.bazel for use_extension bindings to pip,
|
|
7627
|
+
// then match ${binding}.parse(...).
|
|
7628
|
+
const moduleBazel = path.join(cwd, 'MODULE.bazel');
|
|
7629
|
+
const moduleContent = safeReadFile(moduleBazel);
|
|
7630
|
+
if (moduleContent) {
|
|
7631
|
+
const bindings = [];
|
|
7632
|
+
for (const m of moduleContent.matchAll(USE_EXTENSION_PIP_RE)) {
|
|
7633
|
+
bindings.push(m[1]);
|
|
7634
|
+
}
|
|
7635
|
+
if (verbose) {
|
|
7636
|
+
logger.logger.log('[VERBOSE] discovery: scanned', moduleBazel, `(${bindings.length} use_extension pip binding(s))`);
|
|
7637
|
+
}
|
|
7638
|
+
for (const binding of bindings) {
|
|
7639
|
+
const parseRe = buildPipParseRe(binding);
|
|
7640
|
+
for (const m of moduleContent.matchAll(parseRe)) {
|
|
7641
|
+
const argBlob = m[1] ?? '';
|
|
7642
|
+
const info = extractHubInfoFromArgBlob(argBlob, 'MODULE.bazel', 'bzlmod');
|
|
7643
|
+
if (info) {
|
|
7644
|
+
candidates.push(info);
|
|
7645
|
+
}
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
if (verbose) {
|
|
7649
|
+
logger.logger.log('[VERBOSE] discovery: MODULE.bazel pip.parse hits:', candidates.length);
|
|
7650
|
+
}
|
|
7651
|
+
} else if (verbose) {
|
|
7652
|
+
logger.logger.log('[VERBOSE] discovery:', moduleBazel, 'not present (skipping bzlmod scan)');
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
// Legacy path: scan WORKSPACE + top-level .bzl files for pip_parse,
|
|
7656
|
+
// pip_install, and pip_repository.
|
|
7657
|
+
const legacyFiles = listLegacyStarlarkFiles(cwd);
|
|
7658
|
+
if (verbose) {
|
|
7659
|
+
logger.logger.log('[VERBOSE] discovery: legacy files considered:', legacyFiles.length ? legacyFiles : '(none)');
|
|
7660
|
+
}
|
|
7661
|
+
for (const file of legacyFiles) {
|
|
7662
|
+
const content = safeReadFile(file);
|
|
7663
|
+
if (!content) {
|
|
7664
|
+
continue;
|
|
7665
|
+
}
|
|
7666
|
+
const fileHits = [];
|
|
7667
|
+
const source = file.endsWith('.bzl') ? '.bzl' : path.basename(file) === 'WORKSPACE.bazel' ? 'WORKSPACE.bazel' : 'WORKSPACE';
|
|
7668
|
+
for (const m of content.matchAll(PIP_PARSE_NAME_RE)) {
|
|
7669
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', source, 'legacy');
|
|
7670
|
+
if (info) {
|
|
7671
|
+
fileHits.push(info);
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
for (const m of content.matchAll(PIP_INSTALL_NAME_RE)) {
|
|
7675
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', source, 'legacy');
|
|
7676
|
+
if (info) {
|
|
7677
|
+
fileHits.push(info);
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
for (const m of content.matchAll(PIP_REPOSITORY_NAME_RE)) {
|
|
7681
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', source, 'legacy');
|
|
7682
|
+
if (info) {
|
|
7683
|
+
fileHits.push(info);
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
candidates.push(...fileHits);
|
|
7687
|
+
if (verbose) {
|
|
7688
|
+
logger.logger.log('[VERBOSE] discovery: scanned', file, `(${fileHits.length} legacy pip hub match(es))`);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
return dedupCapped(candidates, verbose);
|
|
7692
|
+
}
|
|
7693
|
+
|
|
7694
|
+
// Step 2: validate a candidate by running the probe and confirming
|
|
7695
|
+
// `:pkg` labels or alias rules appear in stdout. Does NOT require
|
|
7696
|
+
// `pypi_name=` (that marker lives on spoke repos).
|
|
7697
|
+
async function validatePypiHub(hubName, probe, verbose) {
|
|
7698
|
+
try {
|
|
7699
|
+
const result = await probe(hubName);
|
|
7700
|
+
if (result.code !== 0) {
|
|
7701
|
+
if (verbose) {
|
|
7702
|
+
logger.logger.log(`[VERBOSE] discovery: probe @${hubName}: REJECT (code=${result.code})`);
|
|
7703
|
+
}
|
|
7704
|
+
return {
|
|
7705
|
+
valid: false,
|
|
7706
|
+
stdout: result.stdout
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
const valid = PYPI_HUB_MARKER_RE.test(result.stdout);
|
|
7710
|
+
if (verbose) {
|
|
7711
|
+
logger.logger.log(`[VERBOSE] discovery: probe @${hubName}:`, valid ? 'ACCEPT (hub alias/pkg marker found)' : 'REJECT (no hub alias/pkg marker in probe stdout)');
|
|
7712
|
+
}
|
|
7713
|
+
return {
|
|
7714
|
+
valid,
|
|
7715
|
+
stdout: result.stdout
|
|
7716
|
+
};
|
|
7717
|
+
} catch (e) {
|
|
7718
|
+
if (verbose) {
|
|
7719
|
+
logger.logger.log(`[VERBOSE] discovery: probe @${hubName}: REJECT (probe threw):`, utils.getErrorCause(e));
|
|
7720
|
+
}
|
|
7721
|
+
return {
|
|
7722
|
+
valid: false,
|
|
7723
|
+
stdout: ''
|
|
7724
|
+
};
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7728
|
+
// The default pip hub name when no explicit hub_name/name is given.
|
|
7729
|
+
// Included as a seed so repos whose pip.parse is in a sub-module (not
|
|
7730
|
+
// found by static scanning) can still be discovered via probe validation.
|
|
7731
|
+
const DEFAULT_PYPI_HUB_SEED = 'pypi';
|
|
7732
|
+
|
|
7733
|
+
// Composition: parse, then validate each candidate; return validated subset
|
|
7734
|
+
// as a Map keyed by hub name with the validated PypiHubInfo.
|
|
7735
|
+
// Always seeds with the default 'pypi' hub name first.
|
|
7736
|
+
async function discoverPypiHubs(cwd, probe, nativeCandidates, verbose, bazelCommandCandidates) {
|
|
7737
|
+
// Always run the static parse so MODULE.bazel pip.parse metadata
|
|
7738
|
+
// (requirements_lock, python_version) is available for downstream
|
|
7739
|
+
// lockfile resolution. Native repo-mapping candidates are intentionally
|
|
7740
|
+
// corroborating data only: many non-PyPI repositories expose alias or :pkg
|
|
7741
|
+
// targets, so bare visible repos are too broad to probe as PyPI hubs.
|
|
7742
|
+
const parsedAll = bazelCommandCandidates?.length ? dedupCapped(bazelCommandCandidates, verbose) : parsePypiHubCandidates(cwd, verbose);
|
|
7743
|
+
const parsed = parsedAll;
|
|
7744
|
+
if (verbose) {
|
|
7745
|
+
logger.logger.log('[VERBOSE] discovery: candidate source:', bazelCommandCandidates?.length ? `bazel mod show_extension (${parsed.length})` : nativeCandidates && nativeCandidates.length ? `static parse (${parsed.length}) with bzlmod visible-repos (${nativeCandidates.length}) as corroboration` : `static parse (${parsed.length})`);
|
|
7746
|
+
}
|
|
7747
|
+
// Prepend the default hub seed unless parsed metadata already covers it.
|
|
7748
|
+
const candidates = parsed.some(c => c.hubName === DEFAULT_PYPI_HUB_SEED) ? parsed : [{
|
|
7749
|
+
hubName: DEFAULT_PYPI_HUB_SEED,
|
|
7750
|
+
source: 'default-seed',
|
|
7751
|
+
workspaceMode: 'unknown'
|
|
7752
|
+
}, ...parsed];
|
|
7753
|
+
if (verbose) {
|
|
7754
|
+
logger.logger.log('[VERBOSE] discovery: candidate set to probe (seed-first, deduped):', candidates.map(c => c.hubName));
|
|
7755
|
+
}
|
|
7756
|
+
const validated = new Map();
|
|
7757
|
+
for (const c of candidates) {
|
|
7758
|
+
// eslint-disable-next-line no-await-in-loop
|
|
7759
|
+
const result = await validatePypiHub(c.hubName, probe, verbose);
|
|
7760
|
+
if (result.valid) {
|
|
7761
|
+
validated.set(c.hubName, {
|
|
7762
|
+
...c,
|
|
7763
|
+
probeStdout: result.stdout
|
|
7764
|
+
});
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
if (verbose) {
|
|
7768
|
+
logger.logger.log('[VERBOSE] discovery: validated pip hubs:', Array.from(validated.keys()));
|
|
7769
|
+
}
|
|
7770
|
+
return validated;
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
/**
|
|
7774
|
+
* Parse Bazel PyPI extraction inputs into the pinned `name==version` lines
|
|
7775
|
+
* needed for generated `requirements.txt` output.
|
|
7776
|
+
*
|
|
7777
|
+
* This is deliberately not a general-purpose requirements.txt parser. It only
|
|
7778
|
+
* accepts pinned lockfile-style entries needed to map reached Bazel labels to
|
|
7779
|
+
* exact package versions; depscan remains the owner of full PEP 508
|
|
7780
|
+
* requirements ingestion during scan processing.
|
|
7781
|
+
*
|
|
7782
|
+
* Security gate: every regex uses bounded character classes to prevent
|
|
7783
|
+
* catastrophic backtracking on hostile input.
|
|
7784
|
+
*/
|
|
7785
|
+
|
|
7786
|
+
|
|
7787
|
+
// Maximum size (bytes) we will read for any requirements lockfile.
|
|
7788
|
+
// Prevents DoS via maliciously large lockfiles.
|
|
7789
|
+
const MAX_REQUIREMENTS_FILE_BYTES = 5 * 1024 * 1024;
|
|
7790
|
+
// Normalize a PyPI package name per PEP 503:
|
|
7791
|
+
// lowercase, then collapse `.`, `_`, and `-` runs to a single `-`.
|
|
7792
|
+
function normalizePypiName(name) {
|
|
7793
|
+
return name.toLowerCase().replace(/[._-]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
// Convert a Bazel underscore_name to a PyPI hyphenated-name.
|
|
7797
|
+
function bazelNameToPypiName(bazelName) {
|
|
7798
|
+
return bazelName.replace(/_/g, '-');
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
// Validate that a resolved path stays within the workspace root.
|
|
7802
|
+
function isWithinWorkspace(resolved, cwd) {
|
|
7803
|
+
const rel = path.relative(cwd, resolved);
|
|
7804
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
7805
|
+
}
|
|
7806
|
+
|
|
7807
|
+
// Resolves a Bazel label or workspace-relative path to a filesystem path.
|
|
7808
|
+
// Returns undefined for labels that cannot be resolved locally.
|
|
7809
|
+
function resolveRequirementsLockPath(label, cwd) {
|
|
7810
|
+
if (!label) {
|
|
7811
|
+
return undefined;
|
|
7812
|
+
}
|
|
7813
|
+
// Reject labels with path-traversal segments.
|
|
7814
|
+
if (label.includes('..')) {
|
|
7815
|
+
return undefined;
|
|
7816
|
+
}
|
|
7817
|
+
// Reject external repository labels.
|
|
7818
|
+
if (label.startsWith('@')) {
|
|
7819
|
+
return undefined;
|
|
7820
|
+
}
|
|
7821
|
+
// Bazel local label forms:
|
|
7822
|
+
// //:requirements_lock.txt
|
|
7823
|
+
// //subdir:requirements_lock.txt
|
|
7824
|
+
// :requirements_lock.txt
|
|
7825
|
+
let filePart;
|
|
7826
|
+
if (label.startsWith('//')) {
|
|
7827
|
+
const colon = label.indexOf(':');
|
|
7828
|
+
if (colon < 0) {
|
|
7829
|
+
return undefined;
|
|
7830
|
+
}
|
|
7831
|
+
const pkgPath = label.slice(2, colon);
|
|
7832
|
+
const filePart = label.slice(colon + 1);
|
|
7833
|
+
if (!filePart) {
|
|
7834
|
+
return undefined;
|
|
7835
|
+
}
|
|
7836
|
+
const resolved = path.join(cwd, pkgPath, filePart);
|
|
7837
|
+
if (!isWithinWorkspace(resolved, cwd)) {
|
|
7838
|
+
return undefined;
|
|
7839
|
+
}
|
|
7840
|
+
return resolved;
|
|
7841
|
+
}
|
|
7842
|
+
if (label.startsWith(':')) {
|
|
7843
|
+
filePart = label.slice(1);
|
|
7844
|
+
if (!filePart) {
|
|
7845
|
+
return undefined;
|
|
7846
|
+
}
|
|
7847
|
+
const resolved = path.join(cwd, filePart);
|
|
7848
|
+
if (!isWithinWorkspace(resolved, cwd)) {
|
|
7849
|
+
return undefined;
|
|
7850
|
+
}
|
|
7851
|
+
return resolved;
|
|
7852
|
+
}
|
|
7853
|
+
// Reject absolute paths (only for non-label inputs).
|
|
7854
|
+
if (path.isAbsolute(label)) {
|
|
7855
|
+
return undefined;
|
|
7856
|
+
}
|
|
7857
|
+
// Bare workspace-relative path (no leading // or :).
|
|
7858
|
+
const resolved = path.join(cwd, label);
|
|
7859
|
+
if (!isWithinWorkspace(resolved, cwd)) {
|
|
7860
|
+
return undefined;
|
|
7861
|
+
}
|
|
7862
|
+
return resolved;
|
|
7863
|
+
}
|
|
7864
|
+
|
|
7865
|
+
// Parses a single pinned `name==version` lockfile line.
|
|
7866
|
+
// Group 1 = package name, Group 2 = version string (includes ==).
|
|
7867
|
+
const REQUIREMENT_LINE_RE = /^([A-Za-z0-9][A-Za-z0-9._-]*)==([A-Za-z0-9._+!]+)/;
|
|
7868
|
+
const BAZEL_STRING_LABEL_RE = /[@A-Za-z0-9_~/.:+-]+/;
|
|
7869
|
+
const ALIAS_ACTUAL_RE = new RegExp(`actual\\s*=\\s*(["'])(${BAZEL_STRING_LABEL_RE.source})\\1`);
|
|
7870
|
+
|
|
7871
|
+
// Skippable line prefixes.
|
|
7872
|
+
function shouldSkipLine(line) {
|
|
7873
|
+
const trimmed = line.trim();
|
|
7874
|
+
if (!trimmed) {
|
|
7875
|
+
return true;
|
|
7876
|
+
}
|
|
7877
|
+
if (trimmed.startsWith('#')) {
|
|
7878
|
+
return true;
|
|
7879
|
+
}
|
|
7880
|
+
// Hash continuations start with `--hash=`.
|
|
7881
|
+
if (trimmed.startsWith('--hash=')) {
|
|
7882
|
+
return true;
|
|
7883
|
+
}
|
|
7884
|
+
// Index options, constraint options, editable installs, includes, direct URLs.
|
|
7885
|
+
if (trimmed.startsWith('--') || trimmed.startsWith('-e ') || trimmed.startsWith('-r ') || trimmed.startsWith('https://') || trimmed.startsWith('http://')) {
|
|
7886
|
+
return true;
|
|
7887
|
+
}
|
|
7888
|
+
return false;
|
|
7889
|
+
}
|
|
7890
|
+
|
|
7891
|
+
// Parse a `requirements_lock.txt`-style file into a map keyed by normalized
|
|
7892
|
+
// PyPI name. This intentionally ignores unpinned PEP 508 requirement forms
|
|
7893
|
+
// because the Bazel extractor must emit exact package versions.
|
|
7894
|
+
function parseRequirementsLock(text) {
|
|
7895
|
+
const out = new Map();
|
|
7896
|
+
const lines = text.split('\n');
|
|
7897
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7898
|
+
const rawLine = lines[i];
|
|
7899
|
+
if (rawLine === undefined) {
|
|
7900
|
+
continue;
|
|
7901
|
+
}
|
|
7902
|
+
if (shouldSkipLine(rawLine)) {
|
|
7903
|
+
continue;
|
|
7904
|
+
}
|
|
7905
|
+
// Handle trailing backslash continuation by concatenating subsequent lines.
|
|
7906
|
+
let line = rawLine.trimEnd();
|
|
7907
|
+
while (line.endsWith('\\') && i + 1 < lines.length) {
|
|
7908
|
+
i++;
|
|
7909
|
+
const next = lines[i];
|
|
7910
|
+
if (next !== undefined) {
|
|
7911
|
+
line = line.slice(0, -1).trimEnd() + ' ' + next.trimStart();
|
|
7912
|
+
}
|
|
7913
|
+
}
|
|
7914
|
+
const m = REQUIREMENT_LINE_RE.exec(line);
|
|
7915
|
+
if (!m) {
|
|
7916
|
+
continue;
|
|
7917
|
+
}
|
|
7918
|
+
const [, rawName, version] = m;
|
|
7919
|
+
if (!rawName || !version) {
|
|
7920
|
+
continue;
|
|
7921
|
+
}
|
|
7922
|
+
const bazelName = rawName.replace(/-/g, '_');
|
|
7923
|
+
const normalized = normalizePypiName(rawName);
|
|
7924
|
+
const existing = out.get(normalized);
|
|
7925
|
+
if (existing) {
|
|
7926
|
+
if (existing.version !== version) {
|
|
7927
|
+
throw new Error(`Conflicting versions for normalized PyPI package ${normalized}: ` + `${existing.originalLine ?? existing.name + '==' + existing.version} ` + `conflicts with ${line}.`);
|
|
7928
|
+
}
|
|
7929
|
+
continue;
|
|
7930
|
+
}
|
|
7931
|
+
out.set(normalized, {
|
|
7932
|
+
name: rawName,
|
|
7933
|
+
version,
|
|
7934
|
+
bazelName,
|
|
7935
|
+
source: 'lockfile',
|
|
7936
|
+
originalLine: line
|
|
7937
|
+
});
|
|
7938
|
+
}
|
|
7939
|
+
return out;
|
|
7940
|
+
}
|
|
7941
|
+
|
|
7942
|
+
// Read and parse a requirements lockfile from a resolved path, capping file
|
|
7943
|
+
// size. Returns undefined when the file is missing, oversized, or unreadable.
|
|
7944
|
+
function readRequirementsLockFile(resolvedPath) {
|
|
7945
|
+
if (!resolvedPath) {
|
|
7946
|
+
return undefined;
|
|
7947
|
+
}
|
|
7948
|
+
if (!fs$1.existsSync(resolvedPath)) {
|
|
7949
|
+
return undefined;
|
|
7950
|
+
}
|
|
7951
|
+
let text;
|
|
7952
|
+
try {
|
|
7953
|
+
const stat = fs$1.statSync(resolvedPath);
|
|
7954
|
+
if (stat.size > MAX_REQUIREMENTS_FILE_BYTES) {
|
|
7955
|
+
return undefined;
|
|
7956
|
+
}
|
|
7957
|
+
text = fs$1.readFileSync(resolvedPath, 'utf8');
|
|
7958
|
+
} catch {
|
|
7959
|
+
return undefined;
|
|
7960
|
+
}
|
|
7961
|
+
return parseRequirementsLock(text);
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
// Extract `pypi_name=` and `pypi_version=` tags from `--output=build` text of a
|
|
7965
|
+
// spoke target. Returns null when either tag is missing.
|
|
7966
|
+
const PYPI_NAME_TAG_RE = /pypi_name=\s*([A-Za-z0-9][A-Za-z0-9._-]*)/;
|
|
7967
|
+
const PYPI_VERSION_TAG_RE = /pypi_version=\s*([A-Za-z0-9._+!]+)/;
|
|
7968
|
+
function parsePypiTagsFromBuildOutput(text) {
|
|
7969
|
+
const nameM = PYPI_NAME_TAG_RE.exec(text);
|
|
7970
|
+
const versionM = PYPI_VERSION_TAG_RE.exec(text);
|
|
7971
|
+
if (!nameM || !versionM) {
|
|
7972
|
+
return null;
|
|
7973
|
+
}
|
|
7974
|
+
const rawName = nameM[1];
|
|
7975
|
+
const version = versionM[1];
|
|
7976
|
+
if (!rawName || !version) {
|
|
7977
|
+
return null;
|
|
7978
|
+
}
|
|
7979
|
+
return {
|
|
7980
|
+
name: rawName,
|
|
7981
|
+
version,
|
|
7982
|
+
bazelName: rawName.replace(/-/g, '_'),
|
|
7983
|
+
source: 'spoke-tag'
|
|
7984
|
+
};
|
|
7985
|
+
}
|
|
7986
|
+
function parseAliasActualFromBuildOutput(text) {
|
|
7987
|
+
const match = ALIAS_ACTUAL_RE.exec(text);
|
|
7988
|
+
return match?.[2];
|
|
7989
|
+
}
|
|
7990
|
+
|
|
7991
|
+
// Extract hub package labels from `bazel query` output that match
|
|
7992
|
+
// `@<hub>//<name>:pkg` patterns (both line-start and embedded in
|
|
7993
|
+
// `--output=build` deps arrays).
|
|
7994
|
+
function filterReachedPypiPackages(queryOutput, hubName) {
|
|
7995
|
+
const out = [];
|
|
7996
|
+
const prefix = `@${hubName}//`;
|
|
7997
|
+
// Match from the start of a label token (preceded by whitespace, quote, or
|
|
7998
|
+
// start of line) to improve robustness across output formats.
|
|
7999
|
+
const labelRe = new RegExp(`(?:^|[\\s"])${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s:"]+):pkg`, 'g');
|
|
8000
|
+
let m;
|
|
8001
|
+
while ((m = labelRe.exec(queryOutput)) !== null) {
|
|
8002
|
+
const pkgPart = m[1];
|
|
8003
|
+
if (!pkgPart) {
|
|
8004
|
+
continue;
|
|
8005
|
+
}
|
|
8006
|
+
const bazelName = pkgPart;
|
|
8007
|
+
const normalized = normalizePypiName(bazelNameToPypiName(bazelName));
|
|
8008
|
+
const apparentLabel = `${prefix}${bazelName}:pkg`;
|
|
8009
|
+
out.push({
|
|
8010
|
+
hubName,
|
|
8011
|
+
originalLabel: apparentLabel,
|
|
8012
|
+
bazelName,
|
|
8013
|
+
normalizedName: normalized,
|
|
8014
|
+
apparentLabel
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
8017
|
+
return out;
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
// Collect name==version pairs for the reached closure, resolving versions
|
|
8021
|
+
// from the lockfile fast path or spoke-tag fallback. Enforces version
|
|
8022
|
+
// conflict detection and deterministic output.
|
|
8023
|
+
function collectPypiPackages(reached, lockfile, spokeTagLookup) {
|
|
8024
|
+
const collected = new Map();
|
|
8025
|
+
for (const r of reached) {
|
|
8026
|
+
const normalized = r.normalizedName;
|
|
8027
|
+
// Lockfile fast path.
|
|
8028
|
+
const lockEntry = lockfile?.get(normalized);
|
|
8029
|
+
if (lockEntry) {
|
|
8030
|
+
const existing = collected.get(normalized);
|
|
8031
|
+
if (existing && existing.version !== lockEntry.version) {
|
|
8032
|
+
throw new Error(`Conflicting versions for ${normalized}: ${existing.label} has ${existing.version}, ${r.originalLabel} has ${lockEntry.version} (lockfile).`);
|
|
8033
|
+
}
|
|
8034
|
+
if (!existing) {
|
|
8035
|
+
collected.set(normalized, {
|
|
8036
|
+
name: lockEntry.name,
|
|
8037
|
+
version: lockEntry.version,
|
|
8038
|
+
source: 'lockfile',
|
|
8039
|
+
label: r.originalLabel
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
continue;
|
|
8043
|
+
}
|
|
8044
|
+
// Spoke-tag fallback.
|
|
8045
|
+
const spokeEntry = spokeTagLookup?.get(normalized);
|
|
8046
|
+
if (spokeEntry) {
|
|
8047
|
+
const existing = collected.get(normalized);
|
|
8048
|
+
if (existing && existing.version !== spokeEntry.version) {
|
|
8049
|
+
throw new Error(`Conflicting versions for ${normalized}: ${existing.label} has ${existing.version}, ${r.originalLabel} has ${spokeEntry.version} (spoke tag).`);
|
|
8050
|
+
}
|
|
8051
|
+
if (!existing) {
|
|
8052
|
+
collected.set(normalized, {
|
|
8053
|
+
name: spokeEntry.name,
|
|
8054
|
+
version: spokeEntry.version,
|
|
8055
|
+
source: 'spoke-tag',
|
|
8056
|
+
label: r.originalLabel
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
continue;
|
|
8060
|
+
}
|
|
8061
|
+
// Unresolvable package — fail rather than emit an unpinned entry.
|
|
8062
|
+
throw new Error(`No version found for ${r.originalLabel}. ` + 'Check that the package is present in the requirements_lock.txt ' + 'or reachable via a spoke target with pypi_name and pypi_version tags.');
|
|
8063
|
+
}
|
|
8064
|
+
return Array.from(collected.values());
|
|
8065
|
+
}
|
|
8066
|
+
|
|
8067
|
+
// Sort package lines deterministically (locale-aware, lowercase comparison).
|
|
8068
|
+
function sortPackageLines(lines) {
|
|
8069
|
+
return lines.sort((a, b) => {
|
|
8070
|
+
const aLow = a.name.toLowerCase();
|
|
8071
|
+
const bLow = b.name.toLowerCase();
|
|
8072
|
+
if (aLow < bLow) {
|
|
8073
|
+
return -1;
|
|
8074
|
+
}
|
|
8075
|
+
if (aLow > bLow) {
|
|
8076
|
+
return 1;
|
|
8077
|
+
}
|
|
8078
|
+
return a.name.localeCompare(b.name);
|
|
8079
|
+
});
|
|
8080
|
+
}
|
|
8081
|
+
async function extractBazelToPypi(opts) {
|
|
8082
|
+
const {
|
|
8083
|
+
cwd,
|
|
8084
|
+
out,
|
|
8085
|
+
verbose
|
|
8086
|
+
} = opts;
|
|
8087
|
+
logger.logger.group('bazel2pypi:');
|
|
8088
|
+
logger.logger.info(`- src dir: \`${cwd}\``);
|
|
8089
|
+
logger.logger.info(`- out dir: \`${out}\``);
|
|
8090
|
+
if (!fs$1.existsSync(cwd)) {
|
|
8091
|
+
logger.logger.warn(`Warning: cwd does not exist: ${cwd}`);
|
|
8092
|
+
}
|
|
8093
|
+
logger.logger.groupEnd();
|
|
8094
|
+
try {
|
|
8095
|
+
// Validate caller-provided Bazel filesystem settings before invoking Bazel.
|
|
8096
|
+
if (opts.bazelOutputBase) {
|
|
8097
|
+
validateOutputBase(opts.bazelOutputBase, opts.cwd);
|
|
8098
|
+
}
|
|
8099
|
+
// Python shim (for rules_python workspace discovery).
|
|
8100
|
+
const shim = await provisionPythonShim();
|
|
8101
|
+
const baseEnv = shim.augmentedEnv ?? opts.env;
|
|
8102
|
+
|
|
8103
|
+
// Step 1: workspace detection.
|
|
8104
|
+
const mode = detectWorkspaceMode(cwd);
|
|
8105
|
+
logger.logger.info(`Workspace mode: bzlmod=${mode.bzlmod} workspace=${mode.workspace}`);
|
|
8106
|
+
const invocationFlags = getBazelInvocationFlags(mode);
|
|
8107
|
+
|
|
8108
|
+
// Step 2: bazel binary resolution.
|
|
8109
|
+
const bin = await resolveBazelBinary(opts.bin);
|
|
8110
|
+
logger.logger.info(`Using bazel: ${bin}`);
|
|
8111
|
+
if (verbose) {
|
|
8112
|
+
logger.logger.log('[VERBOSE] resolved options:', {
|
|
8113
|
+
bin,
|
|
8114
|
+
bazelRc: opts.bazelRc ?? '(unset)',
|
|
8115
|
+
bazelOutputBase: opts.bazelOutputBase ?? '(unset)',
|
|
8116
|
+
bazelFlags: opts.bazelFlags ?? '(unset)',
|
|
8117
|
+
invocationFlags
|
|
8118
|
+
});
|
|
8119
|
+
}
|
|
8120
|
+
|
|
8121
|
+
// Step 3: build the shared query options object.
|
|
8122
|
+
const queryOpts = {
|
|
8123
|
+
bin,
|
|
8124
|
+
cwd,
|
|
8125
|
+
invocationFlags,
|
|
8126
|
+
...(opts.bazelRc ? {
|
|
8127
|
+
bazelRc: opts.bazelRc
|
|
8128
|
+
} : {}),
|
|
8129
|
+
...(opts.bazelFlags ? {
|
|
8130
|
+
bazelFlags: opts.bazelFlags
|
|
8131
|
+
} : {}),
|
|
8132
|
+
...(opts.bazelOutputBase ? {
|
|
8133
|
+
bazelOutputBase: opts.bazelOutputBase
|
|
8134
|
+
} : {}),
|
|
8135
|
+
...(baseEnv ? {
|
|
8136
|
+
env: baseEnv
|
|
8137
|
+
} : {}),
|
|
8138
|
+
verbose
|
|
8139
|
+
};
|
|
8140
|
+
|
|
8141
|
+
// Step 4: discover validated PyPI hubs via the two-step recipe.
|
|
8142
|
+
let bazelCommandCandidates;
|
|
8143
|
+
let nativeCandidates;
|
|
8144
|
+
if (mode.bzlmod) {
|
|
8145
|
+
const extensionResult = await runBazelModShowPipExtension(queryOpts);
|
|
8146
|
+
if (extensionResult.code === 0) {
|
|
8147
|
+
bazelCommandCandidates = parseBazelModPipExtensionCandidates(extensionResult.stdout, verbose);
|
|
8148
|
+
} else if (verbose) {
|
|
8149
|
+
logger.logger.log('[VERBOSE] bazel mod show_extension failed; falling back to bounded static candidate parsing:', extensionResult.stderr);
|
|
8150
|
+
}
|
|
8151
|
+
const visibleRepos = await runBazelModShowVisibleRepos(queryOpts);
|
|
8152
|
+
if (visibleRepos.code === 0) {
|
|
8153
|
+
nativeCandidates = parseVisibleRepoCandidates(visibleRepos.stdout);
|
|
8154
|
+
if (verbose) {
|
|
8155
|
+
logger.logger.log('[VERBOSE] Bzlmod visible repo candidates:', nativeCandidates);
|
|
8156
|
+
}
|
|
8157
|
+
} else if (verbose) {
|
|
8158
|
+
logger.logger.log('[VERBOSE] bazel mod show_repo failed; falling back to static candidate parsing:', visibleRepos.stderr);
|
|
8159
|
+
}
|
|
8160
|
+
}
|
|
8161
|
+
const probe = buildPypiProbeFor(queryOpts);
|
|
8162
|
+
const hubs = await discoverPypiHubs(cwd, probe, nativeCandidates, verbose, bazelCommandCandidates);
|
|
8163
|
+
const hubNames = Array.from(hubs.keys());
|
|
8164
|
+
logger.logger.info(`Discovered ${hubs.size} PyPI hub(s): ${hubNames.join(', ') || '(none)'}`);
|
|
8165
|
+
if (!hubs.size) {
|
|
8166
|
+
if (verbose) {
|
|
8167
|
+
logger.logger.info('No PyPI hubs discovered. failureCategory=no-supported-ecosystem');
|
|
8168
|
+
}
|
|
8169
|
+
return {
|
|
8170
|
+
artifactCount: 0,
|
|
8171
|
+
ok: false,
|
|
8172
|
+
noEcosystemFound: true
|
|
8173
|
+
};
|
|
8174
|
+
}
|
|
8175
|
+
|
|
8176
|
+
// Step 5: for each hub, resolve the requirements lockfile (fast path),
|
|
8177
|
+
// run the reached-closure query, and collect name==version pairs.
|
|
8178
|
+
const allLines = [];
|
|
8179
|
+
const warnings = [];
|
|
8180
|
+
for (const [hubName, hubInfo] of hubs) {
|
|
8181
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8182
|
+
const lockfileMap = await resolveHubLockfile(hubInfo, cwd, verbose);
|
|
8183
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8184
|
+
const reached = await queryReachedPypiLabels(hubName, queryOpts, verbose);
|
|
8185
|
+
const labelsToQuery = lockfileMap ? reached.filter(label => !lockfileMap.has(label.normalizedName)) : reached;
|
|
8186
|
+
const divergenceLabels = lockfileMap && verbose ? reached : labelsToQuery;
|
|
8187
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8188
|
+
const spokeTagLookup = await buildSpokeTagLookup(divergenceLabels, queryOpts, verbose);
|
|
8189
|
+
|
|
8190
|
+
// Check for lockfile-vs-spoke-tag divergence and log warnings.
|
|
8191
|
+
if (lockfileMap) {
|
|
8192
|
+
for (const label of reached) {
|
|
8193
|
+
const lockEntry = lockfileMap.get(label.normalizedName);
|
|
8194
|
+
const spokeEntry = spokeTagLookup?.get(label.normalizedName);
|
|
8195
|
+
if (lockEntry && spokeEntry && lockEntry.version !== spokeEntry.version) {
|
|
8196
|
+
warnings.push(`Version divergence for ${label.originalLabel}: lockfile says ${lockEntry.version}, spoke tag says ${spokeEntry.version}. Using lockfile.`);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
const lines = collectPypiPackages(reached, lockfileMap, spokeTagLookup);
|
|
8201
|
+
for (const l of lines) {
|
|
8202
|
+
allLines.push({
|
|
8203
|
+
name: l.name,
|
|
8204
|
+
version: l.version,
|
|
8205
|
+
source: l.source
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
logger.logger.info(`@${hubName}: ${lines.length} package(s)`);
|
|
8209
|
+
}
|
|
8210
|
+
|
|
8211
|
+
// Step 6: cross-hub conflict check (same normalized name, different
|
|
8212
|
+
// version across multiple hubs).
|
|
8213
|
+
const crossHubVersions = new Map();
|
|
8214
|
+
for (const l of allLines) {
|
|
8215
|
+
const normalized = normalizePypiName(l.name);
|
|
8216
|
+
const existing = crossHubVersions.get(normalized);
|
|
8217
|
+
if (existing && existing !== l.version) {
|
|
8218
|
+
throw new Error(`Conflicting versions for ${l.name}: ${existing} vs ${l.version} across hubs.`);
|
|
8219
|
+
}
|
|
8220
|
+
crossHubVersions.set(normalized, l.version);
|
|
8221
|
+
}
|
|
8222
|
+
|
|
8223
|
+
// Step 7: sort and write requirements.txt.
|
|
8224
|
+
const sorted = sortPackageLines(allLines);
|
|
8225
|
+
const lines = sorted.map(p => `${p.name}==${p.version}\n`);
|
|
8226
|
+
const layout = opts.outLayout ?? 'standalone';
|
|
8227
|
+
const manifestDir = layout === 'flat' ? path.join(out, '.socket-auto-manifest') : out;
|
|
8228
|
+
fs$1.mkdirSync(manifestDir, {
|
|
8229
|
+
recursive: true
|
|
8230
|
+
});
|
|
8231
|
+
const manifestPath = path.join(manifestDir, 'requirements.txt');
|
|
8232
|
+
await fs$1.promises.writeFile(manifestPath, lines.join(''), 'utf8');
|
|
8233
|
+
if (verbose) {
|
|
8234
|
+
logger.logger.log('[VERBOSE] outputs:', {
|
|
8235
|
+
artifactCount: allLines.length,
|
|
8236
|
+
generatedManifest: path.relative(out, manifestPath),
|
|
8237
|
+
layout,
|
|
8238
|
+
manifest: manifestPath,
|
|
8239
|
+
pypiHubs: hubNames,
|
|
8240
|
+
tool: 'socket manifest bazel',
|
|
8241
|
+
workspace: {
|
|
8242
|
+
bzlmod: mode.bzlmod,
|
|
8243
|
+
legacyWorkspace: mode.workspace
|
|
8244
|
+
}
|
|
8245
|
+
});
|
|
8246
|
+
}
|
|
8247
|
+
for (const w of warnings) {
|
|
8248
|
+
logger.logger.warn(w);
|
|
8249
|
+
}
|
|
8250
|
+
if (!allLines.length) {
|
|
8251
|
+
logger.logger.fail('No PyPI packages extracted. failureCategory=ecosystem-detected-but-empty. See warnings above.');
|
|
8252
|
+
return {
|
|
8253
|
+
artifactCount: 0,
|
|
8254
|
+
manifestPath,
|
|
8255
|
+
ok: false
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
logger.logger.success(`Wrote ${allLines.length} package(s) to ${path.relative(cwd, manifestPath)}.`);
|
|
8259
|
+
return {
|
|
8260
|
+
artifactCount: allLines.length,
|
|
8261
|
+
manifestPath,
|
|
8262
|
+
ok: true
|
|
8263
|
+
};
|
|
8264
|
+
} catch (e) {
|
|
8265
|
+
logger.logger.fail(`Unexpected error in bazel2pypi: ${utils.getErrorCause(e)}`);
|
|
8266
|
+
if (verbose) {
|
|
8267
|
+
logger.logger.group('[VERBOSE] error:');
|
|
8268
|
+
logger.logger.log(e);
|
|
8269
|
+
logger.logger.groupEnd();
|
|
8270
|
+
} else {
|
|
8271
|
+
logger.logger.info('Re-run with --verbose for the full stack.');
|
|
8272
|
+
}
|
|
8273
|
+
return {
|
|
8274
|
+
artifactCount: 0,
|
|
8275
|
+
ok: false
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
|
|
8280
|
+
// Resolve lockfile path and read/parse if within bounds.
|
|
8281
|
+
async function resolveHubLockfile(hubInfo, cwd, verbose) {
|
|
8282
|
+
const resolved = hubInfo.requirementsLockPath ?? resolveRequirementsLockPath(hubInfo.requirementsLockLabel, cwd);
|
|
8283
|
+
if (verbose) {
|
|
8284
|
+
logger.logger.log('[VERBOSE] lockfile resolved:', resolved ?? '(none from label/path)');
|
|
8285
|
+
}
|
|
8286
|
+
const result = readRequirementsLockFile(resolved);
|
|
8287
|
+
if (verbose && result) {
|
|
8288
|
+
logger.logger.log('[VERBOSE] lockfile parsed:', result.size, 'package(s)');
|
|
8289
|
+
}
|
|
8290
|
+
return result;
|
|
8291
|
+
}
|
|
8292
|
+
|
|
8293
|
+
// Run the reached-closure query for Python targets and filter to hub labels.
|
|
8294
|
+
async function queryReachedPypiLabels(hubName, queryOpts, verbose) {
|
|
8295
|
+
const queryStr = 'deps(kind("py_library|py_binary|py_test", //...))';
|
|
8296
|
+
const result = await runBazelQuery(queryStr, queryOpts, 'label');
|
|
8297
|
+
if (result.code !== 0) {
|
|
8298
|
+
if (verbose) {
|
|
8299
|
+
logger.logger.log(`[VERBOSE] reached query failed for ${hubName}:`, result.stderr);
|
|
8300
|
+
}
|
|
8301
|
+
return [];
|
|
8302
|
+
}
|
|
8303
|
+
return filterReachedPypiPackages(result.stdout, hubName);
|
|
8304
|
+
}
|
|
8305
|
+
|
|
8306
|
+
// Build a spoke-tag lookup map for reached labels that don't have lockfile
|
|
8307
|
+
// entries. For each reached label, if the lockfile missed it, resolve the
|
|
8308
|
+
// actual target via `--output=build` and extract pypi_name/pypi_version.
|
|
8309
|
+
async function buildSpokeTagLookup(reached, queryOpts, verbose) {
|
|
8310
|
+
const lookup = new Map();
|
|
8311
|
+
for (const label of reached) {
|
|
8312
|
+
// Only query the spoke if we haven't already resolved it.
|
|
8313
|
+
if (lookup.has(label.normalizedName)) {
|
|
8314
|
+
continue;
|
|
8315
|
+
}
|
|
8316
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8317
|
+
const buildResult = await runBazelQuery(`${label.apparentLabel}`, {
|
|
8318
|
+
...queryOpts,
|
|
8319
|
+
verbose: false
|
|
8320
|
+
});
|
|
8321
|
+
if (buildResult.code !== 0) {
|
|
8322
|
+
if (verbose) {
|
|
8323
|
+
logger.logger.log(`[VERBOSE] spoke build query failed for ${label.apparentLabel}:`, buildResult.stderr);
|
|
8324
|
+
}
|
|
8325
|
+
continue;
|
|
8326
|
+
}
|
|
8327
|
+
let parsed = parsePypiTagsFromBuildOutput(buildResult.stdout);
|
|
8328
|
+
if (!parsed) {
|
|
8329
|
+
const actualLabel = parseAliasActualFromBuildOutput(buildResult.stdout);
|
|
8330
|
+
if (actualLabel && actualLabel !== label.apparentLabel) {
|
|
8331
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8332
|
+
const actualResult = await runBazelQuery(actualLabel, {
|
|
8333
|
+
...queryOpts,
|
|
8334
|
+
verbose: false
|
|
8335
|
+
});
|
|
8336
|
+
if (actualResult.code === 0) {
|
|
8337
|
+
parsed = parsePypiTagsFromBuildOutput(actualResult.stdout);
|
|
8338
|
+
} else if (verbose) {
|
|
8339
|
+
logger.logger.log(`[VERBOSE] spoke actual query failed for ${actualLabel}:`, actualResult.stderr);
|
|
8340
|
+
}
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
if (parsed) {
|
|
8344
|
+
lookup.set(normalizePypiName(parsed.name), parsed);
|
|
8345
|
+
}
|
|
8346
|
+
}
|
|
8347
|
+
return lookup;
|
|
8348
|
+
}
|
|
8349
|
+
|
|
8350
|
+
const config$e = {
|
|
8351
|
+
commandName: 'bazel',
|
|
8352
|
+
description: '[beta] Bazel SBOM support — generate manifest files for a Bazel project (Maven, PyPI)',
|
|
8353
|
+
hidden: false,
|
|
8354
|
+
flags: {
|
|
8355
|
+
...flags.commonFlags,
|
|
8356
|
+
bazel: {
|
|
8357
|
+
type: 'string',
|
|
8358
|
+
description: 'Path to bazel/bazelisk binary; default: $(which bazelisk) || $(which bazel)'
|
|
8359
|
+
},
|
|
8360
|
+
bazelFlags: {
|
|
8361
|
+
type: 'string',
|
|
8362
|
+
description: 'Flags forwarded to every bazel invocation (single quoted string)'
|
|
8363
|
+
},
|
|
8364
|
+
bazelOutputBase: {
|
|
8365
|
+
type: 'string',
|
|
8366
|
+
description: 'Bazel --output_base for read-only-cache CI environments'
|
|
8367
|
+
},
|
|
8368
|
+
bazelRc: {
|
|
8369
|
+
type: 'string',
|
|
8370
|
+
description: 'Path to additional .bazelrc fragments forwarded to bazel'
|
|
8371
|
+
},
|
|
8372
|
+
ecosystem: {
|
|
8373
|
+
type: 'string',
|
|
8374
|
+
isMultiple: true,
|
|
8375
|
+
description: 'Ecosystem(s) to extract; repeatable. Supported: maven, pypi. Default: maven.'
|
|
8376
|
+
},
|
|
8377
|
+
out: {
|
|
8378
|
+
type: 'string',
|
|
8379
|
+
description: 'Output directory for generated manifests; default: ./.socket/bazel-manifests/'
|
|
8380
|
+
},
|
|
8381
|
+
verbose: {
|
|
8382
|
+
type: 'boolean',
|
|
8383
|
+
description: 'Emit bounded Bazel diagnostics with argv, duration, exit status, and output sizes'
|
|
8384
|
+
}
|
|
8385
|
+
},
|
|
8386
|
+
help: (command, config) => `
|
|
8387
|
+
Usage
|
|
8388
|
+
$ ${command} [options] [CWD=.]
|
|
8389
|
+
|
|
8390
|
+
Options
|
|
8391
|
+
${utils.getFlagListOutput(config.flags)}
|
|
8392
|
+
|
|
8393
|
+
[beta] Generates Bazel SBOM manifests for Maven (\`maven_install.json\`)
|
|
8394
|
+
by running \`bazel query\` against discovered dependency repos.
|
|
8395
|
+
PyPI requirements generation is available with \`--ecosystem pypi\`.
|
|
8396
|
+
Output is consumed by
|
|
8397
|
+
\`socket scan create\`'s server-side parser.
|
|
8398
|
+
|
|
8399
|
+
--ecosystem may be repeated to select which ecosystems to extract.
|
|
8400
|
+
When omitted, Maven is generated by default. PyPI is explicit opt-in.
|
|
8401
|
+
|
|
8402
|
+
Note: this command generates dependency manifests for Bazel workspaces.
|
|
8403
|
+
It does not run reachability analysis.
|
|
8404
|
+
|
|
8405
|
+
To generate AND upload in one step, use \`socket scan create --auto-manifest\`
|
|
8406
|
+
instead — it detects Bazel workspaces, generates Maven manifests by
|
|
8407
|
+
default, and uploads the result. This subcommand is for generation only.
|
|
8408
|
+
|
|
8409
|
+
Examples
|
|
7201
8410
|
$ ${command} .
|
|
8411
|
+
$ ${command} --ecosystem pypi .
|
|
8412
|
+
$ ${command} --ecosystem maven --ecosystem pypi .
|
|
7202
8413
|
$ ${command} --bazel=/usr/local/bin/bazelisk .
|
|
7203
8414
|
`
|
|
7204
8415
|
};
|
|
@@ -7207,6 +8418,42 @@ const cmdManifestBazel = {
|
|
|
7207
8418
|
hidden: config$e.hidden,
|
|
7208
8419
|
run: run$F
|
|
7209
8420
|
};
|
|
8421
|
+
// Pure outcome-matrix evaluator. Exported so dispatcher behavior can be
|
|
8422
|
+
// unit-tested without spawning the CLI binary. Throws InputError on
|
|
8423
|
+
// failures that must propagate to a non-zero CLI exit; returns void on
|
|
8424
|
+
// success.
|
|
8425
|
+
//
|
|
8426
|
+
// - Hard failure: ok === false && !noEcosystemFound. The ecosystem was
|
|
8427
|
+
// detected (or the runner crashed), but extraction failed. Always a
|
|
8428
|
+
// non-zero exit, even when another ecosystem succeeded.
|
|
8429
|
+
// - No-discovery: noEcosystemFound === true. Genuinely absent ecosystem.
|
|
8430
|
+
// Auto-detect mode tolerates this when at least one other ecosystem
|
|
8431
|
+
// succeeded; explicit mode treats it as an error.
|
|
8432
|
+
function evaluateEcosystemOutcomes(outcomes, isExplicit) {
|
|
8433
|
+
const hardFailures = outcomes.filter(o => !o.ok && !o.noEcosystemFound);
|
|
8434
|
+
const noDiscoveries = outcomes.filter(o => o.noEcosystemFound);
|
|
8435
|
+
const successes = outcomes.filter(o => o.ok && o.manifestPath);
|
|
8436
|
+
if (!isExplicit) {
|
|
8437
|
+
if (hardFailures.length) {
|
|
8438
|
+
throw new utils.InputError(`Bazel auto-manifest generation hit hard failure(s) in ecosystem(s): ${hardFailures.map(f => f.ecosystem).join(', ')}.`);
|
|
8439
|
+
}
|
|
8440
|
+
if (successes.length) {
|
|
8441
|
+
return;
|
|
8442
|
+
}
|
|
8443
|
+
if (noDiscoveries.length === outcomes.length) {
|
|
8444
|
+
throw new utils.InputError('No supported Bazel ecosystems detected (maven, pypi). Ensure rules_jvm_external, rules_python pip_parse/pip_install/pip_repository, or pip.parse is configured.');
|
|
8445
|
+
}
|
|
8446
|
+
return;
|
|
8447
|
+
}
|
|
8448
|
+
|
|
8449
|
+
// Explicit mode: every requested ecosystem must succeed.
|
|
8450
|
+
if (noDiscoveries.length) {
|
|
8451
|
+
throw new utils.InputError(`No Bazel rules found for explicitly requested ecosystem(s): ${noDiscoveries.map(f => f.ecosystem).join(', ')}.`);
|
|
8452
|
+
}
|
|
8453
|
+
if (hardFailures.length) {
|
|
8454
|
+
throw new utils.InputError(`Bazel manifest generation failed for explicitly requested ecosystem(s): ${hardFailures.map(f => f.ecosystem).join(', ')}.`);
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
7210
8457
|
async function run$F(argv, importMeta, {
|
|
7211
8458
|
parentName
|
|
7212
8459
|
}) {
|
|
@@ -7230,6 +8477,9 @@ async function run$F(argv, importMeta, {
|
|
|
7230
8477
|
cwd = path.resolve(process.cwd(), cwd);
|
|
7231
8478
|
const sockJson = utils.readOrDefaultSocketJson(cwd);
|
|
7232
8479
|
require$$9.debugFn('inspect', `override: ${constants.SOCKET_JSON} bazel`, sockJson?.defaults?.manifest?.bazel);
|
|
8480
|
+
const {
|
|
8481
|
+
ecosystem
|
|
8482
|
+
} = cli.flags;
|
|
7233
8483
|
let {
|
|
7234
8484
|
bazel,
|
|
7235
8485
|
bazelFlags,
|
|
@@ -7312,15 +8562,56 @@ async function run$F(argv, importMeta, {
|
|
|
7312
8562
|
logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
|
|
7313
8563
|
return;
|
|
7314
8564
|
}
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
8565
|
+
|
|
8566
|
+
// Ecosystem dispatch: Maven is the default. PyPI is explicit opt-in because
|
|
8567
|
+
// its no-lockfile recovery value is narrower than Maven's inline-decl path.
|
|
8568
|
+
const wasExplicitEcosystemSelection = Array.isArray(ecosystem) && ecosystem.length > 0;
|
|
8569
|
+
const ecosystems = wasExplicitEcosystemSelection ? ecosystem : ['maven'];
|
|
8570
|
+
for (const eco of ecosystems) {
|
|
8571
|
+
if (!['maven', 'pypi'].includes(eco)) {
|
|
8572
|
+
throw new utils.InputError(`Unsupported --ecosystem value: ${eco}. Supported values: maven, pypi.`);
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
const outcomes = [];
|
|
8576
|
+
for (const eco of ecosystems) {
|
|
8577
|
+
if (eco === 'maven') {
|
|
8578
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8579
|
+
const mavenResult = await extractBazelToMaven({
|
|
8580
|
+
bazelFlags: bazelFlags,
|
|
8581
|
+
bazelOutputBase: bazelOutputBase,
|
|
8582
|
+
bazelRc: bazelRc,
|
|
8583
|
+
bin: bazel,
|
|
8584
|
+
cwd,
|
|
8585
|
+
out: out,
|
|
8586
|
+
verbose: Boolean(verbose)
|
|
8587
|
+
});
|
|
8588
|
+
outcomes.push({
|
|
8589
|
+
ecosystem: 'maven',
|
|
8590
|
+
ok: mavenResult.ok,
|
|
8591
|
+
noEcosystemFound: mavenResult.noEcosystemFound,
|
|
8592
|
+
manifestPath: mavenResult.manifestPath
|
|
8593
|
+
});
|
|
8594
|
+
} else if (eco === 'pypi') {
|
|
8595
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8596
|
+
const pypiResult = await extractBazelToPypi({
|
|
8597
|
+
bazelFlags: bazelFlags,
|
|
8598
|
+
bazelOutputBase: bazelOutputBase,
|
|
8599
|
+
bazelRc: bazelRc,
|
|
8600
|
+
bin: bazel,
|
|
8601
|
+
cwd,
|
|
8602
|
+
out: out,
|
|
8603
|
+
verbose: Boolean(verbose),
|
|
8604
|
+
explicitEcosystem: wasExplicitEcosystemSelection
|
|
8605
|
+
});
|
|
8606
|
+
outcomes.push({
|
|
8607
|
+
ecosystem: 'pypi',
|
|
8608
|
+
ok: pypiResult.ok,
|
|
8609
|
+
noEcosystemFound: pypiResult.noEcosystemFound,
|
|
8610
|
+
manifestPath: pypiResult.manifestPath
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
evaluateEcosystemOutcomes(outcomes, wasExplicitEcosystemSelection);
|
|
7324
8615
|
}
|
|
7325
8616
|
|
|
7326
8617
|
const config$d = {
|
|
@@ -7583,6 +8874,10 @@ const config$b = {
|
|
|
7583
8874
|
type: 'string',
|
|
7584
8875
|
description: 'Location of gradlew binary to use, default: CWD/gradlew'
|
|
7585
8876
|
},
|
|
8877
|
+
facts: {
|
|
8878
|
+
type: 'boolean',
|
|
8879
|
+
description: 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files'
|
|
8880
|
+
},
|
|
7586
8881
|
gradleOpts: {
|
|
7587
8882
|
type: 'string',
|
|
7588
8883
|
description: 'Additional options to pass on to ./gradlew, see `./gradlew --help`'
|
|
@@ -7655,6 +8950,7 @@ async function run$C(argv, importMeta, {
|
|
|
7655
8950
|
require$$9.debugFn('inspect', `override: ${constants.SOCKET_JSON} gradle`, sockJson?.defaults?.manifest?.gradle);
|
|
7656
8951
|
let {
|
|
7657
8952
|
bin,
|
|
8953
|
+
facts,
|
|
7658
8954
|
gradleOpts,
|
|
7659
8955
|
verbose
|
|
7660
8956
|
} = cli.flags;
|
|
@@ -7684,6 +8980,14 @@ async function run$C(argv, importMeta, {
|
|
|
7684
8980
|
verbose = false;
|
|
7685
8981
|
}
|
|
7686
8982
|
}
|
|
8983
|
+
if (facts === undefined) {
|
|
8984
|
+
if (sockJson.defaults?.manifest?.gradle?.facts !== undefined) {
|
|
8985
|
+
facts = sockJson.defaults?.manifest?.gradle?.facts;
|
|
8986
|
+
logger.logger.info(`Using default --facts from ${constants.SOCKET_JSON}:`, facts);
|
|
8987
|
+
} else {
|
|
8988
|
+
facts = false;
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
7687
8991
|
if (verbose) {
|
|
7688
8992
|
logger.logger.group('- ', parentName, config$b.commandName, ':');
|
|
7689
8993
|
logger.logger.group('- flags:', cli.flags);
|
|
@@ -7715,10 +9019,20 @@ async function run$C(argv, importMeta, {
|
|
|
7715
9019
|
logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
|
|
7716
9020
|
return;
|
|
7717
9021
|
}
|
|
9022
|
+
const parsedGradleOpts = String(gradleOpts || '').split(' ').map(s => s.trim()).filter(Boolean);
|
|
9023
|
+
if (facts) {
|
|
9024
|
+
await convertGradleToFacts({
|
|
9025
|
+
bin: String(bin),
|
|
9026
|
+
cwd,
|
|
9027
|
+
gradleOpts: parsedGradleOpts,
|
|
9028
|
+
verbose: Boolean(verbose)
|
|
9029
|
+
});
|
|
9030
|
+
return;
|
|
9031
|
+
}
|
|
7718
9032
|
await convertGradleToMaven({
|
|
7719
9033
|
bin: String(bin),
|
|
7720
9034
|
cwd,
|
|
7721
|
-
gradleOpts:
|
|
9035
|
+
gradleOpts: parsedGradleOpts,
|
|
7722
9036
|
verbose: Boolean(verbose)
|
|
7723
9037
|
});
|
|
7724
9038
|
}
|
|
@@ -7738,6 +9052,10 @@ const config$a = {
|
|
|
7738
9052
|
type: 'string',
|
|
7739
9053
|
description: 'Location of gradlew binary to use, default: CWD/gradlew'
|
|
7740
9054
|
},
|
|
9055
|
+
facts: {
|
|
9056
|
+
type: 'boolean',
|
|
9057
|
+
description: 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files'
|
|
9058
|
+
},
|
|
7741
9059
|
gradleOpts: {
|
|
7742
9060
|
type: 'string',
|
|
7743
9061
|
description: 'Additional options to pass on to ./gradlew, see `./gradlew --help`'
|
|
@@ -7810,6 +9128,7 @@ async function run$B(argv, importMeta, {
|
|
|
7810
9128
|
require$$9.debugFn('inspect', `override: ${constants.SOCKET_JSON} gradle`, sockJson?.defaults?.manifest?.gradle);
|
|
7811
9129
|
let {
|
|
7812
9130
|
bin,
|
|
9131
|
+
facts,
|
|
7813
9132
|
gradleOpts,
|
|
7814
9133
|
verbose
|
|
7815
9134
|
} = cli.flags;
|
|
@@ -7839,6 +9158,14 @@ async function run$B(argv, importMeta, {
|
|
|
7839
9158
|
verbose = false;
|
|
7840
9159
|
}
|
|
7841
9160
|
}
|
|
9161
|
+
if (facts === undefined) {
|
|
9162
|
+
if (sockJson.defaults?.manifest?.gradle?.facts !== undefined) {
|
|
9163
|
+
facts = sockJson.defaults?.manifest?.gradle?.facts;
|
|
9164
|
+
logger.logger.info(`Using default --facts from ${constants.SOCKET_JSON}:`, facts);
|
|
9165
|
+
} else {
|
|
9166
|
+
facts = false;
|
|
9167
|
+
}
|
|
9168
|
+
}
|
|
7842
9169
|
if (verbose) {
|
|
7843
9170
|
logger.logger.group('- ', parentName, config$a.commandName, ':');
|
|
7844
9171
|
logger.logger.group('- flags:', cli.flags);
|
|
@@ -7870,10 +9197,20 @@ async function run$B(argv, importMeta, {
|
|
|
7870
9197
|
logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
|
|
7871
9198
|
return;
|
|
7872
9199
|
}
|
|
9200
|
+
const parsedGradleOpts = String(gradleOpts || '').split(' ').map(s => s.trim()).filter(Boolean);
|
|
9201
|
+
if (facts) {
|
|
9202
|
+
await convertGradleToFacts({
|
|
9203
|
+
bin: String(bin),
|
|
9204
|
+
cwd,
|
|
9205
|
+
gradleOpts: parsedGradleOpts,
|
|
9206
|
+
verbose: Boolean(verbose)
|
|
9207
|
+
});
|
|
9208
|
+
return;
|
|
9209
|
+
}
|
|
7873
9210
|
await convertGradleToMaven({
|
|
7874
9211
|
bin: String(bin),
|
|
7875
9212
|
cwd,
|
|
7876
|
-
gradleOpts:
|
|
9213
|
+
gradleOpts: parsedGradleOpts,
|
|
7877
9214
|
verbose: Boolean(verbose)
|
|
7878
9215
|
});
|
|
7879
9216
|
}
|
|
@@ -8291,6 +9628,14 @@ async function setupGradle(config) {
|
|
|
8291
9628
|
} else {
|
|
8292
9629
|
delete config.gradleOpts;
|
|
8293
9630
|
}
|
|
9631
|
+
const facts = await askForFactsFlag(config.facts);
|
|
9632
|
+
if (facts === undefined) {
|
|
9633
|
+
return canceledByUser$1();
|
|
9634
|
+
} else if (facts === 'yes' || facts === 'no') {
|
|
9635
|
+
config.facts = facts === 'yes';
|
|
9636
|
+
} else {
|
|
9637
|
+
delete config.facts;
|
|
9638
|
+
}
|
|
8294
9639
|
const verbose = await askForVerboseFlag(config.verbose);
|
|
8295
9640
|
if (verbose === undefined) {
|
|
8296
9641
|
return canceledByUser$1();
|
|
@@ -8439,6 +9784,25 @@ async function askForVerboseFlag(current) {
|
|
|
8439
9784
|
default: current === true ? 'yes' : current === false ? 'no' : ''
|
|
8440
9785
|
});
|
|
8441
9786
|
}
|
|
9787
|
+
async function askForFactsFlag(current) {
|
|
9788
|
+
return await prompts.select({
|
|
9789
|
+
message: '(--facts) Emit a Socket facts JSON file instead of generating pom.xml?',
|
|
9790
|
+
choices: [{
|
|
9791
|
+
name: 'no',
|
|
9792
|
+
value: 'no',
|
|
9793
|
+
description: 'Generate pom.xml files (default behavior)'
|
|
9794
|
+
}, {
|
|
9795
|
+
name: 'yes',
|
|
9796
|
+
value: 'yes',
|
|
9797
|
+
description: 'Generate a .socket.facts.json file describing the resolved dependency graph'
|
|
9798
|
+
}, {
|
|
9799
|
+
name: '(leave default)',
|
|
9800
|
+
value: '',
|
|
9801
|
+
description: 'Do not store a setting for this'
|
|
9802
|
+
}],
|
|
9803
|
+
default: current === true ? 'yes' : current === false ? 'no' : ''
|
|
9804
|
+
});
|
|
9805
|
+
}
|
|
8442
9806
|
function canceledByUser$1() {
|
|
8443
9807
|
logger.logger.log('');
|
|
8444
9808
|
logger.logger.info('User canceled');
|
|
@@ -17255,5 +18619,5 @@ process.on('unhandledRejection', async (reason, promise) => {
|
|
|
17255
18619
|
// eslint-disable-next-line n/no-process-exit
|
|
17256
18620
|
process.exit(1);
|
|
17257
18621
|
});
|
|
17258
|
-
//# debugId=
|
|
18622
|
+
//# debugId=932c44e2-d146-411e-8818-31f6bf237a5b
|
|
17259
18623
|
//# sourceMappingURL=cli.js.map
|