@socketsecurity/cli-with-sentry 1.10.1 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +5 -5
- package/dist/cli.js +983 -447
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/ci/handle-ci.d.mts.map +1 -1
- package/dist/types/commands/fix/handle-fix.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-bin-detect.d.mts +1 -1
- package/dist/types/commands/manifest/bazel/bazel-bin-detect.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-cleanup.d.mts +8 -0
- package/dist/types/commands/manifest/bazel/bazel-cleanup.d.mts.map +1 -0
- package/dist/types/commands/manifest/bazel/bazel-cquery.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-java-shim.d.mts +1 -9
- package/dist/types/commands/manifest/bazel/bazel-java-shim.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-process.d.mts +25 -0
- package/dist/types/commands/manifest/bazel/bazel-process.d.mts.map +1 -0
- package/dist/types/commands/manifest/bazel/bazel-python-shim.d.mts +2 -5
- package/dist/types/commands/manifest/bazel/bazel-python-shim.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts +1 -9
- package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts.map +1 -1
- 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.map +1 -1
- package/dist/types/commands/manifest/bazel/extract_bazel_to_pypi.d.mts.map +1 -1
- package/dist/types/commands/manifest/discover-manifest-roots.d.mts +16 -4
- package/dist/types/commands/manifest/discover-manifest-roots.d.mts.map +1 -1
- package/dist/types/commands/manifest/enumerate-workspaces.d.mts +3 -1
- package/dist/types/commands/manifest/enumerate-workspaces.d.mts.map +1 -1
- package/dist/types/commands/manifest/generate-recursive-manifests.d.mts +2 -2
- package/dist/types/commands/manifest/generate-recursive-manifests.d.mts.map +1 -1
- package/dist/types/commands/manifest/output-manifest-dynamic-sbom-inference.d.mts.map +1 -1
- package/dist/types/commands/manifest/run-manifest-facts.d.mts +3 -1
- package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/build-tool.d.mts +14 -0
- package/dist/types/commands/manifest/scripts/build-tool.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/run.d.mts +2 -0
- package/dist/types/commands/manifest/scripts/run.d.mts.map +1 -1
- package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts +14 -16
- package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts.map +1 -1
- package/dist/types/commands/scan/cmd-scan-create.d.mts.map +1 -1
- package/dist/types/commands/scan/cmd-scan-reach.d.mts.map +1 -1
- package/dist/types/commands/scan/perform-reachability-analysis.d.mts +2 -0
- package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
- package/dist/types/commands/scan/reach-advisories.d.mts +7 -0
- package/dist/types/commands/scan/reach-advisories.d.mts.map +1 -0
- package/dist/types/commands/scan/reachability-flags.d.mts.map +1 -1
- package/dist/types/constants.d.mts +1 -1
- package/dist/types/constants.d.mts.map +1 -1
- package/dist/types/utils/advisory-ids.d.mts +14 -0
- package/dist/types/utils/advisory-ids.d.mts.map +1 -0
- package/dist/types/utils/config.d.mts +1 -0
- package/dist/types/utils/config.d.mts.map +1 -1
- package/dist/types/utils/cve-to-ghsa.d.mts +2 -0
- package/dist/types/utils/cve-to-ghsa.d.mts.map +1 -1
- package/dist/utils.js +82 -3
- package/dist/utils.js.map +1 -1
- package/dist/vendor.js +553 -550
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -19,8 +19,9 @@ var prompts = require('../external/@socketsecurity/registry/lib/prompts');
|
|
|
19
19
|
var require$$0 = require('node:crypto');
|
|
20
20
|
var os = require('node:os');
|
|
21
21
|
var spawn = require('../external/@socketsecurity/registry/lib/spawn');
|
|
22
|
-
var bin = require('../external/@socketsecurity/registry/lib/bin');
|
|
23
22
|
var childProcess = require('node:child_process');
|
|
23
|
+
var bin = require('../external/@socketsecurity/registry/lib/bin');
|
|
24
|
+
var require$$7 = require('node:timers/promises');
|
|
24
25
|
var fs$1 = require('../external/@socketsecurity/registry/lib/fs');
|
|
25
26
|
var strings = require('../external/@socketsecurity/registry/lib/strings');
|
|
26
27
|
var path$1 = require('../external/@socketsecurity/registry/lib/path');
|
|
@@ -2010,7 +2011,7 @@ async function performReachabilityAnalysis(options) {
|
|
|
2010
2011
|
// Build Coana arguments.
|
|
2011
2012
|
const coanaArgs = ['run', analysisTarget, '--output-dir', path.dirname(outputFilePath), '--socket-mode', outputFilePath, '--disable-report-submission', ...(isOmittedReachValue(reachabilityOptions.reachAnalysisTimeout) ? [] : ['--analysis-timeout', reachabilityOptions.reachAnalysisTimeout]), ...(isOmittedReachValue(reachabilityOptions.reachAnalysisMemoryLimit) ? [] : ['--memory-limit', reachabilityOptions.reachAnalysisMemoryLimit]), ...(reachabilityOptions.reachConcurrency ? ['--concurrency', `${reachabilityOptions.reachConcurrency}`] : []), ...(reachabilityOptions.reachContinueOnAnalysisErrors ? ['--reach-continue-on-analysis-errors'] : []), ...(reachabilityOptions.reachContinueOnInstallErrors ? ['--reach-continue-on-install-errors'] : []), ...(reachabilityOptions.reachContinueOnMissingLockFiles ? ['--reach-continue-on-missing-lock-files'] : []), ...(reachabilityOptions.reachContinueOnNoSourceFiles ? ['--reach-continue-on-no-source-files'] : []), ...(reachabilityOptions.reachDebug ? ['--debug'] : []), ...(reachabilityOptions.reachDetailedAnalysisLogFile ? ['--print-analysis-log-file'] : []), ...(reachabilityOptions.reachDisableAnalytics ? ['--disable-analytics-sharing'] : []), ...(reachabilityOptions.reachDisableExternalToolChecks ? ['--disable-external-tool-checks'] : []), ...(reachabilityOptions.reachEnableAnalysisSplitting ? [] : ['--disable-analysis-splitting']), '--run-without-docker', '--manifests-tar-hash', tarHash,
|
|
2012
2013
|
// Empty reachEcosystems implies scanning all ecosystems.
|
|
2013
|
-
...(reachabilityOptions.reachEcosystems.length ? ['--purl-types', ...reachabilityOptions.reachEcosystems] : []), ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []),
|
|
2014
|
+
...(reachabilityOptions.reachEcosystems.length ? ['--purl-types', ...reachabilityOptions.reachEcosystems] : []), ...(reachabilityOptions.reachAdvisories.length ? ['--ghsas', ...reachabilityOptions.reachAdvisories] : []), ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []),
|
|
2014
2015
|
// sidecarPath is always set above when this is true - Coana rejects this
|
|
2015
2016
|
// flag without one.
|
|
2016
2017
|
...(reachabilityOptions.dynamicSbomInference ? ['--maven-use-only-socket-facts'] : []), ...(reachabilityOptions.reachSkipCache ? ['--skip-cache-usage'] : []), ...(reachabilityOptions.reachUseOnlyPregeneratedSboms ? ['--use-only-pregenerated-sboms'] : []), ...(sidecarPath ? ['--compute-artifacts-sidecar', sidecarPath] : [])];
|
|
@@ -2081,23 +2082,87 @@ async function performReachabilityAnalysis(options) {
|
|
|
2081
2082
|
}
|
|
2082
2083
|
}
|
|
2083
2084
|
|
|
2084
|
-
|
|
2085
|
+
// PATH fallback when no `bin` and no project wrapper.
|
|
2086
|
+
const DEFAULT_BUILD_TOOL_BIN = {
|
|
2087
|
+
__proto__: null,
|
|
2088
|
+
gradle: 'gradle',
|
|
2089
|
+
maven: 'mvn',
|
|
2090
|
+
sbt: 'sbt'
|
|
2091
|
+
};
|
|
2085
2092
|
|
|
2086
|
-
//
|
|
2087
|
-
//
|
|
2088
|
-
const
|
|
2093
|
+
// Project-local wrapper, preferred because it pins the expected build-tool
|
|
2094
|
+
// version. sbt has no wrapper convention. POSIX names only (no win32 target).
|
|
2095
|
+
const BUILD_TOOL_WRAPPER = {
|
|
2089
2096
|
__proto__: null,
|
|
2090
|
-
gradle:
|
|
2091
|
-
maven:
|
|
2092
|
-
sbt: ['build.sbt']
|
|
2097
|
+
gradle: 'gradlew',
|
|
2098
|
+
maven: 'mvnw'
|
|
2093
2099
|
};
|
|
2094
|
-
|
|
2095
|
-
|
|
2100
|
+
|
|
2101
|
+
// Maven builds by file (`mvn --file`), so a directory can hold several: the
|
|
2102
|
+
// POMs a scan uploads, matched case-insensitively. `walkGlob` is a
|
|
2103
|
+
// case-sensitive superset of `nocaseGlob`, for the walk.
|
|
2104
|
+
|
|
2105
|
+
// `settings.gradle(.kts)` covers Kotlin-DSL roots that have no root
|
|
2106
|
+
// `build.gradle`.
|
|
2107
|
+
const BUILD_FILES = {
|
|
2108
|
+
__proto__: null,
|
|
2109
|
+
gradle: {
|
|
2110
|
+
byFile: false,
|
|
2111
|
+
names: ['build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts']
|
|
2112
|
+
},
|
|
2113
|
+
maven: {
|
|
2114
|
+
byFile: true,
|
|
2115
|
+
nocaseGlob: '{*-*.,}pom{.xml,}',
|
|
2116
|
+
walkGlob: '*[pP][oO][mM]*'
|
|
2117
|
+
},
|
|
2118
|
+
sbt: {
|
|
2119
|
+
byFile: false,
|
|
2120
|
+
names: ['build.sbt']
|
|
2121
|
+
}
|
|
2096
2122
|
};
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2123
|
+
|
|
2124
|
+
// sbt happily runs in any directory, synthesizing a default project from its
|
|
2125
|
+
// name, so an sbt run outside a build yields a plausible but bogus SBOM. Maven
|
|
2126
|
+
// and Gradle refuse such a directory themselves.
|
|
2127
|
+
function looksLikeSbtBuild(projectDir) {
|
|
2128
|
+
return u.existsSync(path.resolve(projectDir, 'build.sbt')) || u.existsSync(path.resolve(projectDir, 'project'));
|
|
2129
|
+
}
|
|
2130
|
+
function resolveBuildToolBin(tool, projectDir, bin) {
|
|
2131
|
+
if (bin) {
|
|
2132
|
+
return bin;
|
|
2133
|
+
}
|
|
2134
|
+
const wrapperName = BUILD_TOOL_WRAPPER[tool];
|
|
2135
|
+
if (wrapperName && u.existsSync(path.resolve(projectDir, wrapperName))) {
|
|
2136
|
+
return `./${wrapperName}`;
|
|
2137
|
+
}
|
|
2138
|
+
return DEFAULT_BUILD_TOOL_BIN[tool];
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Distinct for every build sharing a directory.
|
|
2142
|
+
function socketFactsFileName(tool, entryFile) {
|
|
2143
|
+
if (!BUILD_FILES[tool].byFile) {
|
|
2144
|
+
return `${tool}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}`;
|
|
2100
2145
|
}
|
|
2146
|
+
return entryFile ? `${path.basename(entryFile)}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}` : undefined;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
const BUILD_TOOLS = ['gradle', 'maven', 'sbt'];
|
|
2150
|
+
const BUILD_FILE_MATCHERS = new Map(BUILD_TOOLS.map(tool => {
|
|
2151
|
+
const files = BUILD_FILES[tool];
|
|
2152
|
+
return [tool, files.byFile ? vendor.micromatchExports.matcher(files.nocaseGlob, {
|
|
2153
|
+
dot: true,
|
|
2154
|
+
nocase: true
|
|
2155
|
+
}) : basename => files.names.includes(basename)];
|
|
2156
|
+
}));
|
|
2157
|
+
|
|
2158
|
+
// `entryFile` (a basename in `dir`) is set only for a tool that addresses its
|
|
2159
|
+
// builds by file, where one directory can hold several builds.
|
|
2160
|
+
|
|
2161
|
+
function buildRootPath(root) {
|
|
2162
|
+
return path.join(root.dir, root.entryFile ?? '');
|
|
2163
|
+
}
|
|
2164
|
+
function buildFileTool(tools, basename) {
|
|
2165
|
+
return tools.find(tool => BUILD_FILE_MATCHERS.get(tool)?.(basename));
|
|
2101
2166
|
}
|
|
2102
2167
|
|
|
2103
2168
|
// A disabled ecosystem is skipped entirely by the glob scan below, so a
|
|
@@ -2148,20 +2213,24 @@ async function addCoveredBuildFiles(covered, buildRoot, projects) {
|
|
|
2148
2213
|
}
|
|
2149
2214
|
}
|
|
2150
2215
|
|
|
2151
|
-
// `dir` must be realpath-resolved, as findBuildToolCandidates returns it.
|
|
2152
|
-
function isBuildRootCovered(covered, tool,
|
|
2153
|
-
|
|
2216
|
+
// `root.dir` must be realpath-resolved, as findBuildToolCandidates returns it.
|
|
2217
|
+
function isBuildRootCovered(covered, tool, root) {
|
|
2218
|
+
const files = BUILD_FILES[tool];
|
|
2219
|
+
if (files.byFile) {
|
|
2220
|
+
return !!root.entryFile && covered.has(buildRootPath(root));
|
|
2221
|
+
}
|
|
2222
|
+
return files.names.some(name => covered.has(path.join(root.dir, name)));
|
|
2154
2223
|
}
|
|
2155
|
-
function
|
|
2156
|
-
return
|
|
2157
|
-
|
|
2158
|
-
|
|
2224
|
+
function compareStrings(a, b) {
|
|
2225
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2226
|
+
}
|
|
2227
|
+
function sortByDepthThenPath(roots, cwd) {
|
|
2228
|
+
return [...roots].sort((a, b) => {
|
|
2229
|
+
const relA = path.relative(cwd, a.dir);
|
|
2230
|
+
const relB = path.relative(cwd, b.dir);
|
|
2159
2231
|
const depthA = relA === '' ? 0 : relA.split(path.sep).length;
|
|
2160
2232
|
const depthB = relB === '' ? 0 : relB.split(path.sep).length;
|
|
2161
|
-
|
|
2162
|
-
return depthA - depthB;
|
|
2163
|
-
}
|
|
2164
|
-
return relA < relB ? -1 : relA > relB ? 1 : 0;
|
|
2233
|
+
return depthA - depthB || compareStrings(relA, relB) || compareStrings(a.entryFile ?? '', b.entryFile ?? '');
|
|
2165
2234
|
});
|
|
2166
2235
|
}
|
|
2167
2236
|
|
|
@@ -2170,36 +2239,56 @@ function sortByDepthThenPath(dirs, cwd) {
|
|
|
2170
2239
|
async function findBuildToolCandidates({
|
|
2171
2240
|
cwd,
|
|
2172
2241
|
excludePaths,
|
|
2173
|
-
sockJson
|
|
2242
|
+
sockJson,
|
|
2243
|
+
socketConfig
|
|
2174
2244
|
}) {
|
|
2175
2245
|
const enabledTools = BUILD_TOOLS.filter(tool => !sockJson.defaults?.manifest?.[tool]?.disabled);
|
|
2176
2246
|
const result = new Map();
|
|
2177
2247
|
if (!enabledTools.length) {
|
|
2178
2248
|
return result;
|
|
2179
2249
|
}
|
|
2180
|
-
const patterns = enabledTools.flatMap(tool => MARKERS_BY_TOOL[tool].map(marker => `**/${marker}`));
|
|
2181
2250
|
const additionalIgnores = (excludePaths ?? []).flatMap(excludePathToScanIgnores);
|
|
2182
|
-
|
|
2251
|
+
// Build files are matched after the walk, as a scan matches them after its
|
|
2252
|
+
// ignores, so --exclude-paths and .gitignore stay case-sensitive.
|
|
2253
|
+
const hits = await utils.globWithGitIgnore(enabledTools.flatMap(tool => {
|
|
2254
|
+
const files = BUILD_FILES[tool];
|
|
2255
|
+
return (files.byFile ? [files.walkGlob] : files.names).map(glob => `**/${glob}`);
|
|
2256
|
+
}), {
|
|
2183
2257
|
absolute: true,
|
|
2184
2258
|
additionalIgnores,
|
|
2185
|
-
cwd
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2259
|
+
cwd,
|
|
2260
|
+
socketConfig
|
|
2261
|
+
});
|
|
2262
|
+
const rootsByTool = new Map(enabledTools.map(tool => [tool, new Map()]));
|
|
2263
|
+
const matched = hits.flatMap(hit => {
|
|
2264
|
+
const basename = path.basename(hit);
|
|
2265
|
+
const tool = buildFileTool(enabledTools, basename);
|
|
2266
|
+
return tool ? [{
|
|
2267
|
+
basename,
|
|
2268
|
+
hit,
|
|
2269
|
+
tool
|
|
2270
|
+
}] : [];
|
|
2271
|
+
});
|
|
2272
|
+
const roots = await Promise.all(matched.map(async ({
|
|
2273
|
+
basename,
|
|
2274
|
+
hit,
|
|
2275
|
+
tool
|
|
2276
|
+
}) => ({
|
|
2277
|
+
root: {
|
|
2278
|
+
dir: await utils.realpathOrResolved(path.dirname(hit)),
|
|
2279
|
+
entryFile: BUILD_FILES[tool].byFile ? basename : undefined
|
|
2280
|
+
},
|
|
2281
|
+
tool
|
|
2191
2282
|
})));
|
|
2192
2283
|
for (const {
|
|
2193
|
-
|
|
2284
|
+
root,
|
|
2194
2285
|
tool
|
|
2195
|
-
} of
|
|
2196
|
-
|
|
2197
|
-
dirSetsByTool.get(tool)?.add(dir);
|
|
2198
|
-
}
|
|
2286
|
+
} of roots) {
|
|
2287
|
+
rootsByTool.get(tool)?.set(buildRootPath(root), root);
|
|
2199
2288
|
}
|
|
2200
2289
|
const realCwd = await utils.realpathOrResolved(cwd);
|
|
2201
2290
|
for (const tool of enabledTools) {
|
|
2202
|
-
result.set(tool, sortByDepthThenPath([...(
|
|
2291
|
+
result.set(tool, sortByDepthThenPath([...(rootsByTool.get(tool)?.values() ?? [])], realCwd));
|
|
2203
2292
|
}
|
|
2204
2293
|
return result;
|
|
2205
2294
|
}
|
|
@@ -2292,56 +2381,6 @@ function formatMissingEnvVarRefs(missing) {
|
|
|
2292
2381
|
return `references ${names}, which ${verb} not set in this environment`;
|
|
2293
2382
|
}
|
|
2294
2383
|
|
|
2295
|
-
// PATH fallback when no `bin` and no project wrapper.
|
|
2296
|
-
const DEFAULT_BUILD_TOOL_BIN = {
|
|
2297
|
-
__proto__: null,
|
|
2298
|
-
gradle: 'gradle',
|
|
2299
|
-
maven: 'mvn',
|
|
2300
|
-
sbt: 'sbt'
|
|
2301
|
-
};
|
|
2302
|
-
|
|
2303
|
-
// Project-local wrapper, preferred because it pins the expected build-tool
|
|
2304
|
-
// version. sbt has no wrapper convention. POSIX names only (no win32 target).
|
|
2305
|
-
const BUILD_TOOL_WRAPPER = {
|
|
2306
|
-
__proto__: null,
|
|
2307
|
-
gradle: 'gradlew',
|
|
2308
|
-
maven: 'mvnw'
|
|
2309
|
-
};
|
|
2310
|
-
|
|
2311
|
-
// Gradle (8+) and sbt hold one build per directory; Maven builds are addressed
|
|
2312
|
-
// by POM file, so `mvn -f other-pom.xml` puts a second build in the directory.
|
|
2313
|
-
const ADDRESSED_BY_FILE = {
|
|
2314
|
-
__proto__: null,
|
|
2315
|
-
gradle: false,
|
|
2316
|
-
maven: true,
|
|
2317
|
-
sbt: false
|
|
2318
|
-
};
|
|
2319
|
-
|
|
2320
|
-
// sbt happily runs in any directory, synthesizing a default project from its
|
|
2321
|
-
// name, so an sbt run outside a build yields a plausible but bogus SBOM. Maven
|
|
2322
|
-
// and Gradle refuse such a directory themselves.
|
|
2323
|
-
function looksLikeSbtBuild(projectDir) {
|
|
2324
|
-
return u.existsSync(path.resolve(projectDir, 'build.sbt')) || u.existsSync(path.resolve(projectDir, 'project'));
|
|
2325
|
-
}
|
|
2326
|
-
function resolveBuildToolBin(tool, projectDir, bin) {
|
|
2327
|
-
if (bin) {
|
|
2328
|
-
return bin;
|
|
2329
|
-
}
|
|
2330
|
-
const wrapperName = BUILD_TOOL_WRAPPER[tool];
|
|
2331
|
-
if (wrapperName && u.existsSync(path.resolve(projectDir, wrapperName))) {
|
|
2332
|
-
return `./${wrapperName}`;
|
|
2333
|
-
}
|
|
2334
|
-
return DEFAULT_BUILD_TOOL_BIN[tool];
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
// Distinct for every build sharing a directory.
|
|
2338
|
-
function socketFactsFileName(tool, entryFile) {
|
|
2339
|
-
if (!ADDRESSED_BY_FILE[tool]) {
|
|
2340
|
-
return `${tool}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}`;
|
|
2341
|
-
}
|
|
2342
|
-
return entryFile ? `${path.basename(entryFile)}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}` : undefined;
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
2384
|
// Gradle's variant-aware resolver: distinct exceptions give mutually-exclusive
|
|
2346
2385
|
// phrasing, so most-specific-first substring checks classify reliably.
|
|
2347
2386
|
function classifyGradleFailure(detail) {
|
|
@@ -3805,7 +3844,8 @@ async function invokeMaven(opts, coanaTask, tmpDirPrefix) {
|
|
|
3805
3844
|
// `validate` is the cheapest phase that triggers the extension; no compile
|
|
3806
3845
|
// needed (analysis uses configured paths, not classes).
|
|
3807
3846
|
const props = [`-Dmaven.ext.class.path=${jarPath}`, `-Dcoana.task=${coanaTask}`, `-Dsocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-D')];
|
|
3808
|
-
const
|
|
3847
|
+
const fileOpts = opts.entryFile ? ['--file', opts.entryFile] : [];
|
|
3848
|
+
const args = [...props, ...fileOpts, ...(opts.toolOpts ?? []), '--batch-mode', 'validate'];
|
|
3809
3849
|
const out = await runNeverThrow(bin, args, opts);
|
|
3810
3850
|
return await assembleFromRecords('maven', out, recordsFile);
|
|
3811
3851
|
});
|
|
@@ -3841,6 +3881,7 @@ async function runManifestFacts({
|
|
|
3841
3881
|
buildOpts,
|
|
3842
3882
|
cwd,
|
|
3843
3883
|
ecosystem,
|
|
3884
|
+
entryFile,
|
|
3844
3885
|
excludeConfigs,
|
|
3845
3886
|
excludePaths,
|
|
3846
3887
|
hints = FLAG_HINTS,
|
|
@@ -3867,9 +3908,14 @@ async function runManifestFacts({
|
|
|
3867
3908
|
logger.logger.fail(`No sbt build found at \`${cwd}\` (expected a build.sbt file or a project directory).`);
|
|
3868
3909
|
return null;
|
|
3869
3910
|
}
|
|
3870
|
-
|
|
3911
|
+
const location = buildRootPath({
|
|
3912
|
+
dir: cwd,
|
|
3913
|
+
entryFile
|
|
3914
|
+
});
|
|
3915
|
+
logger.logger.info(`Generating Socket facts for the ${ecosystem} project at \`${location}\` ...`);
|
|
3871
3916
|
const scriptOpts = {
|
|
3872
3917
|
bin: bin || undefined,
|
|
3918
|
+
entryFile,
|
|
3873
3919
|
excludeConfigs: excludeConfigs || undefined,
|
|
3874
3920
|
excludePaths: excludePaths?.length ? excludePaths : undefined,
|
|
3875
3921
|
// `env` replaces the spawned process's whole environment, not just JAVA_HOME.
|
|
@@ -3969,7 +4015,7 @@ async function runManifestFacts({
|
|
|
3969
4015
|
// regardless of `ignoreUnresolved` — that flag only tolerates dependencies a
|
|
3970
4016
|
// successful run couldn't resolve.
|
|
3971
4017
|
process.exitCode = 1;
|
|
3972
|
-
logger.logger.fail(`The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`);
|
|
4018
|
+
logger.logger.fail(`The ${ecosystem} build at \`${location}\` failed (exit code ${code}) before producing any Socket facts.`);
|
|
3973
4019
|
return null;
|
|
3974
4020
|
}
|
|
3975
4021
|
|
|
@@ -4032,13 +4078,40 @@ function getSkipReason(ecosystem, disabled, facts) {
|
|
|
4032
4078
|
}
|
|
4033
4079
|
return undefined;
|
|
4034
4080
|
}
|
|
4035
|
-
//
|
|
4081
|
+
// Maven 3's options starting with `f`; its GnuParser takes a long name after
|
|
4082
|
+
// one dash too, and splits `-fX` and `-f=X` into `-f X`.
|
|
4083
|
+
const MAVEN_FILE_OPTS = new Set(['f', 'file']);
|
|
4084
|
+
const MAVEN_OTHER_F_OPTS = new Set(['fae', 'fail-at-end', 'fail-fast', 'fail-never', 'ff', 'fn']);
|
|
4085
|
+
|
|
4086
|
+
// Each POM is built by its own `--file`, so a user one is dropped.
|
|
4087
|
+
function withoutMavenFileOpts(dir, opts) {
|
|
4088
|
+
const kept = [];
|
|
4089
|
+
for (let i = 0, {
|
|
4090
|
+
length
|
|
4091
|
+
} = opts; i < length; i += 1) {
|
|
4092
|
+
const opt = opts[i];
|
|
4093
|
+
const name = opt.replace(/^--?/, '');
|
|
4094
|
+
if (opt === name || MAVEN_OTHER_F_OPTS.has(name)) {
|
|
4095
|
+
kept.push(opt);
|
|
4096
|
+
} else if (MAVEN_FILE_OPTS.has(name)) {
|
|
4097
|
+
i += 1;
|
|
4098
|
+
} else if (!MAVEN_FILE_OPTS.has(name.split('=')[0]) && !(opt.startsWith('-f') && !opt.startsWith('--'))) {
|
|
4099
|
+
kept.push(opt);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
if (kept.length !== opts.length) {
|
|
4103
|
+
logger.logger.warn(`Ignoring --file in mavenOpts for ${dir}: each POM there is built by its own file.`);
|
|
4104
|
+
}
|
|
4105
|
+
return kept;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
// Nearest already-confirmed-disabled `dir` or ancestor of it, if any - lets the
|
|
4036
4109
|
// caller shorten `readSocketJsonCascade`'s walk to start there instead of
|
|
4037
4110
|
// all the way back at `cwd`, without skipping any nested override.
|
|
4038
4111
|
function nearestDisabledRoot(dir, disabledRoots) {
|
|
4039
4112
|
let nearest;
|
|
4040
4113
|
for (const root of disabledRoots) {
|
|
4041
|
-
if (dir.startsWith(`${root.dir}${path.sep}`) && (!nearest || root.dir.length > nearest.dir.length)) {
|
|
4114
|
+
if ((dir === root.dir || dir.startsWith(`${root.dir}${path.sep}`)) && (!nearest || root.dir.length > nearest.dir.length)) {
|
|
4042
4115
|
nearest = root;
|
|
4043
4116
|
}
|
|
4044
4117
|
}
|
|
@@ -4058,54 +4131,71 @@ async function runEcosystemCandidates({
|
|
|
4058
4131
|
}) {
|
|
4059
4132
|
const outcomes = [];
|
|
4060
4133
|
const covered = new Set();
|
|
4061
|
-
for (const [ecosystem,
|
|
4134
|
+
for (const [ecosystem, roots] of candidatesByTool) {
|
|
4062
4135
|
const disabledRoots = [];
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4136
|
+
const configByDir = new Map();
|
|
4137
|
+
for (const {
|
|
4138
|
+
0: rootIndex,
|
|
4139
|
+
1: root
|
|
4140
|
+
} of roots.entries()) {
|
|
4141
|
+
const {
|
|
4142
|
+
dir,
|
|
4143
|
+
entryFile
|
|
4144
|
+
} = root;
|
|
4145
|
+
if (isBuildRootCovered(covered, ecosystem, root)) {
|
|
4066
4146
|
outcomes.push({
|
|
4067
|
-
|
|
4147
|
+
...root,
|
|
4068
4148
|
ecosystem,
|
|
4069
4149
|
status: 'skippedCovered'
|
|
4070
4150
|
});
|
|
4071
4151
|
continue;
|
|
4072
4152
|
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4153
|
+
let dirConfig = configByDir.get(dir);
|
|
4154
|
+
if (!dirConfig) {
|
|
4155
|
+
const nearestRoot = nearestDisabledRoot(dir, disabledRoots);
|
|
4156
|
+
const sockJson = nearestRoot ? utils.readSocketJsonCascade(dir, nearestRoot.dir, nearestRoot.sockJson) : utils.readSocketJsonCascade(dir, realCwd, rootSockJson);
|
|
4157
|
+
dirConfig = {
|
|
4158
|
+
...resolveEcosystemConfig(ecosystem, dir, sockJson),
|
|
4159
|
+
excludePaths: projectIgnorePathsToReachExcludePaths(excludePaths, {
|
|
4160
|
+
cwd: realCwd,
|
|
4161
|
+
target: dir
|
|
4162
|
+
})
|
|
4163
|
+
};
|
|
4164
|
+
configByDir.set(dir, dirConfig);
|
|
4165
|
+
if (dirConfig.skipReason) {
|
|
4166
|
+
// Only warn for a genuinely new disabled root, not one already
|
|
4167
|
+
// covered by an ancestor's warning above - otherwise a big disabled
|
|
4168
|
+
// reactor with hundreds of nested poms would spam one warning line
|
|
4169
|
+
// per pom for what's really a single root cause. The aggregate
|
|
4170
|
+
// count still shows up in the final summary either way.
|
|
4171
|
+
if (!nearestRoot) {
|
|
4172
|
+
logger.logger.warn(`Skipping ${dir} (${ecosystem}): ${dirConfig.skipReason}.`);
|
|
4173
|
+
}
|
|
4174
|
+
disabledRoots.push({
|
|
4175
|
+
dir,
|
|
4176
|
+
sockJson
|
|
4177
|
+
});
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4075
4180
|
const {
|
|
4076
4181
|
bin,
|
|
4077
4182
|
buildOpts,
|
|
4078
4183
|
excludeConfigs,
|
|
4184
|
+
excludePaths: excludePathsForRoot,
|
|
4079
4185
|
ignoreUnresolved,
|
|
4080
4186
|
includeConfigs,
|
|
4081
4187
|
javaHome,
|
|
4082
4188
|
skipReason,
|
|
4083
4189
|
verbose: configVerbose
|
|
4084
|
-
} =
|
|
4190
|
+
} = dirConfig;
|
|
4085
4191
|
if (skipReason) {
|
|
4086
|
-
// Only warn for a genuinely new disabled root, not one already
|
|
4087
|
-
// covered by an ancestor's warning above - otherwise a big disabled
|
|
4088
|
-
// reactor with hundreds of nested poms would spam one warning line
|
|
4089
|
-
// per pom for what's really a single root cause. The aggregate
|
|
4090
|
-
// count still shows up in the final summary either way.
|
|
4091
|
-
if (!nearestRoot) {
|
|
4092
|
-
logger.logger.warn(`Skipping ${dir} (${ecosystem}): ${skipReason}.`);
|
|
4093
|
-
}
|
|
4094
4192
|
outcomes.push({
|
|
4095
|
-
|
|
4193
|
+
...root,
|
|
4096
4194
|
ecosystem,
|
|
4097
4195
|
status: 'skippedDisabled'
|
|
4098
4196
|
});
|
|
4099
|
-
disabledRoots.push({
|
|
4100
|
-
dir,
|
|
4101
|
-
sockJson
|
|
4102
|
-
});
|
|
4103
4197
|
continue;
|
|
4104
4198
|
}
|
|
4105
|
-
const excludePathsForRoot = projectIgnorePathsToReachExcludePaths(excludePaths, {
|
|
4106
|
-
cwd: realCwd,
|
|
4107
|
-
target: dir
|
|
4108
|
-
});
|
|
4109
4199
|
|
|
4110
4200
|
// eslint-disable-next-line no-await-in-loop
|
|
4111
4201
|
const result = await runManifestFacts({
|
|
@@ -4113,6 +4203,7 @@ async function runEcosystemCandidates({
|
|
|
4113
4203
|
buildOpts,
|
|
4114
4204
|
cwd: dir,
|
|
4115
4205
|
ecosystem,
|
|
4206
|
+
entryFile,
|
|
4116
4207
|
excludeConfigs,
|
|
4117
4208
|
excludePaths: excludePathsForRoot,
|
|
4118
4209
|
hints: {
|
|
@@ -4131,17 +4222,17 @@ async function runEcosystemCandidates({
|
|
|
4131
4222
|
});
|
|
4132
4223
|
if (result === null) {
|
|
4133
4224
|
outcomes.push({
|
|
4134
|
-
|
|
4225
|
+
...root,
|
|
4135
4226
|
ecosystem,
|
|
4136
4227
|
status: 'failed'
|
|
4137
4228
|
});
|
|
4138
|
-
const
|
|
4139
|
-
if (
|
|
4140
|
-
logger.logger.warn(`Aborting ${ecosystem} discovery: ${
|
|
4229
|
+
const abortedRoots = roots.slice(rootIndex + 1);
|
|
4230
|
+
if (abortedRoots.length) {
|
|
4231
|
+
logger.logger.warn(`Aborting ${ecosystem} discovery: ${buildRootPath(root)} failed to generate Socket facts, and without them the remaining build roots cannot be safely classified as covered or independent.`);
|
|
4141
4232
|
}
|
|
4142
|
-
for (const
|
|
4233
|
+
for (const aborted of abortedRoots) {
|
|
4143
4234
|
outcomes.push({
|
|
4144
|
-
|
|
4235
|
+
...aborted,
|
|
4145
4236
|
ecosystem,
|
|
4146
4237
|
status: 'aborted'
|
|
4147
4238
|
});
|
|
@@ -4150,18 +4241,18 @@ async function runEcosystemCandidates({
|
|
|
4150
4241
|
}
|
|
4151
4242
|
if (!result) {
|
|
4152
4243
|
outcomes.push({
|
|
4153
|
-
|
|
4244
|
+
...root,
|
|
4154
4245
|
ecosystem,
|
|
4155
4246
|
status: 'empty'
|
|
4156
4247
|
});
|
|
4157
4248
|
continue;
|
|
4158
4249
|
}
|
|
4159
4250
|
|
|
4160
|
-
// `-
|
|
4251
|
+
// Gradle's `-p` can root the build away from `dir`.
|
|
4161
4252
|
// eslint-disable-next-line no-await-in-loop
|
|
4162
4253
|
await addCoveredBuildFiles(covered, path.dirname(result.factsPath), result.projects);
|
|
4163
4254
|
outcomes.push({
|
|
4164
|
-
|
|
4255
|
+
...root,
|
|
4165
4256
|
ecosystem,
|
|
4166
4257
|
factsPath: result.factsPath,
|
|
4167
4258
|
status: 'generated'
|
|
@@ -4194,6 +4285,7 @@ async function generateRecursiveManifests({
|
|
|
4194
4285
|
const candidatesByTool = await findBuildToolCandidates({
|
|
4195
4286
|
cwd,
|
|
4196
4287
|
excludePaths,
|
|
4288
|
+
socketConfig: utils.loadSocketConfig(cwd),
|
|
4197
4289
|
sockJson: withoutDisabledFlags(rootSockJson)
|
|
4198
4290
|
});
|
|
4199
4291
|
const runAll = resolvedSbtTmpDir => runEcosystemCandidates({
|
|
@@ -4259,7 +4351,7 @@ function resolveEcosystemConfig(ecosystem, dir, sockJson) {
|
|
|
4259
4351
|
const bin = config?.bin ?? undefined;
|
|
4260
4352
|
return {
|
|
4261
4353
|
bin: bin ?? resolveBuildToolBin('maven', dir),
|
|
4262
|
-
buildOpts: parseBuildToolOpts(config?.mavenOpts ?? undefined),
|
|
4354
|
+
buildOpts: withoutMavenFileOpts(dir, parseBuildToolOpts(config?.mavenOpts ?? undefined)),
|
|
4263
4355
|
excludeConfigs: config?.excludeConfigs ?? '',
|
|
4264
4356
|
ignoreUnresolved: Boolean(config?.ignoreUnresolved),
|
|
4265
4357
|
includeConfigs: config?.includeConfigs ?? '',
|
|
@@ -4377,6 +4469,234 @@ sockJson, cwd = process.cwd()) {
|
|
|
4377
4469
|
return output;
|
|
4378
4470
|
}
|
|
4379
4471
|
|
|
4472
|
+
function findEnvironmentKey(env, name, platform = process.platform) {
|
|
4473
|
+
if (platform !== 'win32') {
|
|
4474
|
+
return name;
|
|
4475
|
+
}
|
|
4476
|
+
const lower = name.toLowerCase();
|
|
4477
|
+
return Object.keys(env).find(key => key.toLowerCase() === lower) ?? name;
|
|
4478
|
+
}
|
|
4479
|
+
function getBazelEnvironment(overrides = {}, platform = process.platform) {
|
|
4480
|
+
const env = {
|
|
4481
|
+
...process.env
|
|
4482
|
+
};
|
|
4483
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
4484
|
+
env[findEnvironmentKey(env, key, platform)] = value;
|
|
4485
|
+
}
|
|
4486
|
+
return env;
|
|
4487
|
+
}
|
|
4488
|
+
function getBazelEnvironmentValue(env, name) {
|
|
4489
|
+
return env[findEnvironmentKey(env, name)];
|
|
4490
|
+
}
|
|
4491
|
+
function prependBazelPath(env, dir) {
|
|
4492
|
+
const key = findEnvironmentKey(env, 'PATH');
|
|
4493
|
+
return {
|
|
4494
|
+
...env,
|
|
4495
|
+
[key]: `${dir}${path.delimiter}${env[key] ?? ''}`
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4498
|
+
async function findBazelExecutable(name, env, cwd) {
|
|
4499
|
+
const searchPath = (getBazelEnvironmentValue(env, 'PATH') ?? '').split(path.delimiter).map(entry => path.resolve(cwd, entry.replace(/^"(.*)"$/, '$1'))).join(path.delimiter);
|
|
4500
|
+
const found = await vendor.libExports$1(name, {
|
|
4501
|
+
nothrow: true,
|
|
4502
|
+
path: searchPath,
|
|
4503
|
+
pathExt: getBazelEnvironmentValue(env, 'PATHEXT')
|
|
4504
|
+
});
|
|
4505
|
+
return found ? resolveBazelExecutable(found) : undefined;
|
|
4506
|
+
}
|
|
4507
|
+
function resolveBazelExecutable(file) {
|
|
4508
|
+
const resolved = u.realpathSync(file);
|
|
4509
|
+
const extension = path.extname(resolved).toLowerCase();
|
|
4510
|
+
if (process.platform !== 'win32' || !['', '.cmd', '.ps1'].includes(extension)) {
|
|
4511
|
+
return resolved;
|
|
4512
|
+
}
|
|
4513
|
+
try {
|
|
4514
|
+
return bin.resolveBinPathSync(resolved);
|
|
4515
|
+
} catch (e) {
|
|
4516
|
+
if (extension === '.ps1') {
|
|
4517
|
+
throw new utils.InputError('This PowerShell Bazel launcher could not be resolved to a Node entry point. Pass a native .exe, a .cmd or .bat wrapper, or a JavaScript entry point with --bazel.');
|
|
4518
|
+
}
|
|
4519
|
+
if (utils.isErrnoException(e) && e.code === 'ENOENT') {
|
|
4520
|
+
return resolved;
|
|
4521
|
+
}
|
|
4522
|
+
throw e;
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
function isNodeScript(bin) {
|
|
4526
|
+
if (/\.[cm]?js$/i.test(bin)) {
|
|
4527
|
+
return true;
|
|
4528
|
+
}
|
|
4529
|
+
if (path.extname(bin) || process.platform !== 'win32' || !u.existsSync(bin)) {
|
|
4530
|
+
return false;
|
|
4531
|
+
}
|
|
4532
|
+
const fd = u.openSync(bin, 'r');
|
|
4533
|
+
try {
|
|
4534
|
+
const header = Buffer.alloc(256);
|
|
4535
|
+
const length = u.readSync(fd, header);
|
|
4536
|
+
return /^#![^\r\n]*\bnode(?:\s|$)/.test(header.toString('utf8', 0, length));
|
|
4537
|
+
} finally {
|
|
4538
|
+
u.closeSync(fd);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
const BATCH_METACHARACTERS = /[ !%^&()<>|"]/g;
|
|
4542
|
+
function escapeBatchMetacharacters(value) {
|
|
4543
|
+
return value.replace(BATCH_METACHARACTERS, '^$&');
|
|
4544
|
+
}
|
|
4545
|
+
function escapeBatchArgument(value) {
|
|
4546
|
+
if (/[\r\n\0]/.test(value)) {
|
|
4547
|
+
throw new utils.InputError('Bazel batch launchers cannot accept arguments containing newlines or NUL characters. Use a native .exe launcher.');
|
|
4548
|
+
}
|
|
4549
|
+
const quoted = `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
4550
|
+
// Batch forwarding parses arguments twice, so protect each shell metacharacter twice.
|
|
4551
|
+
return escapeBatchMetacharacters(escapeBatchMetacharacters(quoted));
|
|
4552
|
+
}
|
|
4553
|
+
function buildBazelProcessInvocation(bin, args, env) {
|
|
4554
|
+
if (isNodeScript(bin)) {
|
|
4555
|
+
return {
|
|
4556
|
+
command: process.execPath,
|
|
4557
|
+
args: [bin, ...args],
|
|
4558
|
+
windowsVerbatimArguments: false
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
if (process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(bin)) {
|
|
4562
|
+
const script = [escapeBatchMetacharacters(bin), ...args.map(escapeBatchArgument)].join(' ');
|
|
4563
|
+
return {
|
|
4564
|
+
command: getBazelEnvironmentValue(env, 'ComSpec') ?? 'cmd.exe',
|
|
4565
|
+
args: ['/d', '/v:off', '/s', '/c', `"${script}"`],
|
|
4566
|
+
windowsVerbatimArguments: true
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
return {
|
|
4570
|
+
command: bin,
|
|
4571
|
+
args,
|
|
4572
|
+
windowsVerbatimArguments: false
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
class BazelTimeoutError extends Error {
|
|
4576
|
+
constructor(timeout, cause) {
|
|
4577
|
+
super(`Bazel did not finish within ${timeout}ms`, {
|
|
4578
|
+
cause
|
|
4579
|
+
});
|
|
4580
|
+
this.stdout = readOutput(cause, 'stdout');
|
|
4581
|
+
this.stderr = readOutput(cause, 'stderr');
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
function readOutput(error, key) {
|
|
4585
|
+
if (error && typeof error === 'object' && key in error) {
|
|
4586
|
+
const value = Reflect.get(error, key);
|
|
4587
|
+
return typeof value === 'string' ? value : '';
|
|
4588
|
+
}
|
|
4589
|
+
return '';
|
|
4590
|
+
}
|
|
4591
|
+
function makeAbortError(reason) {
|
|
4592
|
+
const error = new Error('The operation was aborted', {
|
|
4593
|
+
cause: reason
|
|
4594
|
+
});
|
|
4595
|
+
error.name = 'AbortError';
|
|
4596
|
+
return error;
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
// Launchers can leave Bazel holding the output pipes after the launcher is killed.
|
|
4600
|
+
const PIPE_RELEASE_GRACE_MS = 5_000;
|
|
4601
|
+
async function killProcessTree(child) {
|
|
4602
|
+
const {
|
|
4603
|
+
pid
|
|
4604
|
+
} = child;
|
|
4605
|
+
if (pid === undefined) {
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
if (process.platform === 'win32') {
|
|
4609
|
+
await new Promise(resolve => {
|
|
4610
|
+
childProcess.execFile('taskkill', ['/pid', String(pid), '/t', '/f'], () => resolve());
|
|
4611
|
+
});
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
try {
|
|
4615
|
+
process.kill(-pid, 'SIGTERM');
|
|
4616
|
+
} catch (e) {
|
|
4617
|
+
if (!utils.isErrnoException(e) || e.code !== 'ESRCH') {
|
|
4618
|
+
throw e;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
async function spawnBazel(bin, args, {
|
|
4623
|
+
signal = constants.default.abortSignal,
|
|
4624
|
+
timeout,
|
|
4625
|
+
...options
|
|
4626
|
+
}) {
|
|
4627
|
+
const env = getBazelEnvironment(options.env);
|
|
4628
|
+
const invocation = buildBazelProcessInvocation(bin, args, env);
|
|
4629
|
+
const controller = new AbortController();
|
|
4630
|
+
const pending = spawn.spawn(invocation.command, invocation.args, {
|
|
4631
|
+
...options,
|
|
4632
|
+
// A process group of its own lets a timeout or abort reach everything the launcher started.
|
|
4633
|
+
detached: process.platform !== 'win32',
|
|
4634
|
+
env,
|
|
4635
|
+
shell: false,
|
|
4636
|
+
signal: controller.signal,
|
|
4637
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
4638
|
+
});
|
|
4639
|
+
let timedOut = false;
|
|
4640
|
+
let releaseTimer;
|
|
4641
|
+
const stop = () => {
|
|
4642
|
+
releaseTimer ??= setTimeout(() => {
|
|
4643
|
+
pending.process.stdout?.destroy();
|
|
4644
|
+
pending.process.stderr?.destroy();
|
|
4645
|
+
}, PIPE_RELEASE_GRACE_MS);
|
|
4646
|
+
return killProcessTree(pending.process);
|
|
4647
|
+
};
|
|
4648
|
+
const onAbort = () => {
|
|
4649
|
+
// taskkill finds descendants through the launcher, so the launcher must outlive the tree kill.
|
|
4650
|
+
void stop().finally(() => controller.abort(signal.reason));
|
|
4651
|
+
};
|
|
4652
|
+
if (signal.aborted) {
|
|
4653
|
+
onAbort();
|
|
4654
|
+
} else {
|
|
4655
|
+
signal.addEventListener('abort', onAbort, {
|
|
4656
|
+
once: true
|
|
4657
|
+
});
|
|
4658
|
+
}
|
|
4659
|
+
const timer = timeout ? setTimeout(() => {
|
|
4660
|
+
timedOut = true;
|
|
4661
|
+
void stop();
|
|
4662
|
+
}, timeout) : undefined;
|
|
4663
|
+
let result;
|
|
4664
|
+
try {
|
|
4665
|
+
result = await pending;
|
|
4666
|
+
} catch (e) {
|
|
4667
|
+
if (timedOut && timeout) {
|
|
4668
|
+
throw new BazelTimeoutError(timeout, e);
|
|
4669
|
+
}
|
|
4670
|
+
// On Windows the tree kill ends the launcher first, so the spawn rejects with its exit code.
|
|
4671
|
+
if (signal.aborted && !(e instanceof Error && e.name === 'AbortError')) {
|
|
4672
|
+
throw makeAbortError(signal.reason);
|
|
4673
|
+
}
|
|
4674
|
+
throw e;
|
|
4675
|
+
} finally {
|
|
4676
|
+
clearTimeout(timer);
|
|
4677
|
+
clearTimeout(releaseTimer);
|
|
4678
|
+
signal.removeEventListener('abort', onAbort);
|
|
4679
|
+
}
|
|
4680
|
+
// Output is incomplete when the pipes were released after a timeout.
|
|
4681
|
+
if (timedOut && timeout) {
|
|
4682
|
+
throw new BazelTimeoutError(timeout, result);
|
|
4683
|
+
}
|
|
4684
|
+
return result;
|
|
4685
|
+
}
|
|
4686
|
+
function buildBazelStartupFlags(opts) {
|
|
4687
|
+
const startup = [];
|
|
4688
|
+
if (opts.bazelRc) {
|
|
4689
|
+
startup.push(`--bazelrc=${opts.bazelRc}`);
|
|
4690
|
+
}
|
|
4691
|
+
if (opts.outputUserRoot) {
|
|
4692
|
+
startup.push(`--output_user_root=${opts.outputUserRoot}`);
|
|
4693
|
+
}
|
|
4694
|
+
if (opts.bazelOutputBase) {
|
|
4695
|
+
startup.push(`--output_base=${opts.bazelOutputBase}`);
|
|
4696
|
+
}
|
|
4697
|
+
return startup;
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4380
4700
|
/**
|
|
4381
4701
|
* Resolve the bazel binary to invoke for `socket manifest bazel`.
|
|
4382
4702
|
*
|
|
@@ -4386,70 +4706,285 @@ sockJson, cwd = process.cwd()) {
|
|
|
4386
4706
|
* 3. Fall back to `bazel` on PATH.
|
|
4387
4707
|
* 4. If neither is found, throw InputError with install instructions.
|
|
4388
4708
|
*/
|
|
4389
|
-
async function resolveBazelBinary(explicit) {
|
|
4709
|
+
async function resolveBazelBinary(explicit, env, cwd) {
|
|
4390
4710
|
if (explicit) {
|
|
4391
|
-
|
|
4392
|
-
|
|
4711
|
+
const resolved = path.resolve(cwd, explicit);
|
|
4712
|
+
if (!u.existsSync(resolved)) {
|
|
4713
|
+
throw new utils.InputError(`--bazel path does not exist: ${resolved}. Install bazelisk or bazel, or pass an existing path via --bazel.`);
|
|
4393
4714
|
}
|
|
4394
|
-
return
|
|
4715
|
+
return resolveBazelExecutable(resolved);
|
|
4395
4716
|
}
|
|
4396
4717
|
// Prefer bazelisk: respects .bazelversion in the workspace.
|
|
4397
|
-
const bazelisk = await
|
|
4398
|
-
nothrow: true
|
|
4399
|
-
});
|
|
4718
|
+
const bazelisk = await findBazelExecutable('bazelisk', env, cwd);
|
|
4400
4719
|
if (bazelisk) {
|
|
4401
4720
|
return bazelisk;
|
|
4402
4721
|
}
|
|
4403
|
-
const bazel = await
|
|
4404
|
-
nothrow: true
|
|
4405
|
-
});
|
|
4722
|
+
const bazel = await findBazelExecutable('bazel', env, cwd);
|
|
4406
4723
|
if (bazel) {
|
|
4407
4724
|
return bazel;
|
|
4408
4725
|
}
|
|
4409
4726
|
throw new utils.InputError('Could not find bazelisk or bazel on PATH. ' + 'Install bazelisk (recommended; https://github.com/bazelbuild/bazelisk) ' + 'or bazel, or pass --bazel <path>.');
|
|
4410
4727
|
}
|
|
4411
4728
|
|
|
4729
|
+
const BAZEL_OUTPUT_ROOT_PREFIX = 'socket-bazel-';
|
|
4730
|
+
function makeBazelOutputUserRoot(env, cwd) {
|
|
4731
|
+
const parent = getBazelEnvironmentValue(env, 'SOCKET_BAZEL_OUTPUT_ROOT');
|
|
4732
|
+
const resolved = parent ? path.resolve(cwd, parent) : os.tmpdir();
|
|
4733
|
+
u.mkdirSync(resolved, {
|
|
4734
|
+
recursive: true
|
|
4735
|
+
});
|
|
4736
|
+
return u.mkdtempSync(path.join(resolved, BAZEL_OUTPUT_ROOT_PREFIX));
|
|
4737
|
+
}
|
|
4738
|
+
const RM_OPTIONS = {
|
|
4739
|
+
recursive: true,
|
|
4740
|
+
force: true,
|
|
4741
|
+
maxRetries: 5,
|
|
4742
|
+
retryDelay: 200
|
|
4743
|
+
};
|
|
4744
|
+
async function removeBazelTempdir(dir) {
|
|
4745
|
+
try {
|
|
4746
|
+
try {
|
|
4747
|
+
await u.promises.rm(dir, RM_OPTIONS);
|
|
4748
|
+
} catch (e) {
|
|
4749
|
+
if (!utils.isErrnoException(e) || !['EACCES', 'EPERM'].includes(e.code ?? '')) {
|
|
4750
|
+
throw e;
|
|
4751
|
+
}
|
|
4752
|
+
await makeDirectoriesWritable(dir);
|
|
4753
|
+
await u.promises.rm(dir, RM_OPTIONS);
|
|
4754
|
+
}
|
|
4755
|
+
return true;
|
|
4756
|
+
} catch (error) {
|
|
4757
|
+
logger.logger.warn(`Bazel temporary directory could not be removed: ${dir}. ${utils.getErrorCause(error)}`);
|
|
4758
|
+
return false;
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
async function makeDirectoriesWritable(root) {
|
|
4762
|
+
const pending = [root];
|
|
4763
|
+
while (pending.length) {
|
|
4764
|
+
const directory = pending.pop();
|
|
4765
|
+
if (!directory) {
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
try {
|
|
4769
|
+
// Repository rules can unpack read-only directories. Keep links to user files untouched.
|
|
4770
|
+
// eslint-disable-next-line no-await-in-loop
|
|
4771
|
+
const stat = await u.promises.lstat(directory);
|
|
4772
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
4773
|
+
continue;
|
|
4774
|
+
}
|
|
4775
|
+
// eslint-disable-next-line no-await-in-loop
|
|
4776
|
+
await u.promises.chmod(directory, 0o700);
|
|
4777
|
+
// eslint-disable-next-line no-await-in-loop
|
|
4778
|
+
const entries = await u.promises.readdir(directory, {
|
|
4779
|
+
withFileTypes: true
|
|
4780
|
+
});
|
|
4781
|
+
for (const entry of entries) {
|
|
4782
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
4783
|
+
pending.push(path.join(directory, entry.name));
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
} catch (e) {
|
|
4787
|
+
if (!utils.isErrnoException(e) || e.code !== 'ENOENT') {
|
|
4788
|
+
throw e;
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
const SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
4794
|
+
const SERVER_EXIT_TIMEOUT_MS = 5_000;
|
|
4795
|
+
async function cleanUpBazelOutputRoot(root, contexts) {
|
|
4796
|
+
// Ctrl-C aborts the scan's signal while the CLI keeps running, and cleanup must still run.
|
|
4797
|
+
const signal = AbortSignal.timeout(3 * SHUTDOWN_TIMEOUT_MS);
|
|
4798
|
+
let listed;
|
|
4799
|
+
try {
|
|
4800
|
+
listed = findServerPids(root);
|
|
4801
|
+
} catch (e) {
|
|
4802
|
+
logger.logger.warn(`Bazel output root kept because its servers could not be checked: ${root}. ${utils.getErrorCause(e)}`);
|
|
4803
|
+
}
|
|
4804
|
+
const servers = new Set(listed);
|
|
4805
|
+
const shutDown = await Promise.all(Array.from(contexts.values(), opts => shutDownBazelServer(opts, signal)));
|
|
4806
|
+
for (const pid of shutDown) {
|
|
4807
|
+
if (pid !== undefined) {
|
|
4808
|
+
servers.add(pid);
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
const stopped = await Promise.all(Array.from(servers, stopBazelServer));
|
|
4812
|
+
if (!stopped.every(Boolean)) {
|
|
4813
|
+
logger.logger.warn(`Bazel output root kept because its server is still running: ${root}`);
|
|
4814
|
+
return false;
|
|
4815
|
+
}
|
|
4816
|
+
if (!listed) {
|
|
4817
|
+
return false;
|
|
4818
|
+
}
|
|
4819
|
+
return await removeBazelTempdir(root);
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
// Returns the pid of a server this scan owns, read before shutdown removes its pid file.
|
|
4823
|
+
async function shutDownBazelServer(opts, signal) {
|
|
4824
|
+
const root = opts.outputUserRoot;
|
|
4825
|
+
if (!root) {
|
|
4826
|
+
return undefined;
|
|
4827
|
+
}
|
|
4828
|
+
const startupFlags = buildBazelStartupFlags(opts);
|
|
4829
|
+
const spawnOptions = {
|
|
4830
|
+
cwd: opts.cwd,
|
|
4831
|
+
...(opts.env ? {
|
|
4832
|
+
env: opts.env
|
|
4833
|
+
} : {}),
|
|
4834
|
+
signal,
|
|
4835
|
+
timeout: SHUTDOWN_TIMEOUT_MS
|
|
4836
|
+
};
|
|
4837
|
+
let pid;
|
|
4838
|
+
try {
|
|
4839
|
+
// Only this scan can have started a server whose install or output base is in its fresh root.
|
|
4840
|
+
// An rc file can point the scan at a caller's install and output base.
|
|
4841
|
+
const info = await spawnBazel(opts.bin, [...startupFlags, 'info', 'install_base', 'output_base'], spawnOptions);
|
|
4842
|
+
const bases = parseBazelInfo(info.stdout);
|
|
4843
|
+
const outputBase = bases.get('output_base');
|
|
4844
|
+
if (!outputBase || ![bases.get('install_base'), outputBase].some(dir => dir && isPathInside(dir, root))) {
|
|
4845
|
+
return undefined;
|
|
4846
|
+
}
|
|
4847
|
+
pid = readServerPid(outputBase);
|
|
4848
|
+
await spawnBazel(opts.bin, [...startupFlags, 'shutdown'], spawnOptions);
|
|
4849
|
+
} catch (error) {
|
|
4850
|
+
logger.logger.warn(`Bazel shutdown failed in ${opts.cwd}. ${utils.getErrorCause(error)}`);
|
|
4851
|
+
}
|
|
4852
|
+
return pid;
|
|
4853
|
+
}
|
|
4854
|
+
function findServerPids(root) {
|
|
4855
|
+
if (!u.existsSync(root)) {
|
|
4856
|
+
return [];
|
|
4857
|
+
}
|
|
4858
|
+
const pids = [];
|
|
4859
|
+
for (const entry of u.readdirSync(root, {
|
|
4860
|
+
withFileTypes: true
|
|
4861
|
+
})) {
|
|
4862
|
+
const pid = entry.isDirectory() ? readServerPid(path.join(root, entry.name)) : undefined;
|
|
4863
|
+
if (pid !== undefined) {
|
|
4864
|
+
pids.push(pid);
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
return pids;
|
|
4868
|
+
}
|
|
4869
|
+
function readServerPid(outputBase) {
|
|
4870
|
+
let contents;
|
|
4871
|
+
try {
|
|
4872
|
+
contents = u.readFileSync(path.join(outputBase, 'server', 'server.pid.txt'), 'utf8');
|
|
4873
|
+
} catch (e) {
|
|
4874
|
+
if (utils.isErrnoException(e) && e.code === 'ENOENT') {
|
|
4875
|
+
return undefined;
|
|
4876
|
+
}
|
|
4877
|
+
throw e;
|
|
4878
|
+
}
|
|
4879
|
+
const pid = Number(contents.trim());
|
|
4880
|
+
return Number.isSafeInteger(pid) && pid > 0 && isProcessAlive(pid) ? pid : undefined;
|
|
4881
|
+
}
|
|
4882
|
+
function isProcessAlive(pid) {
|
|
4883
|
+
try {
|
|
4884
|
+
process.kill(pid, 0);
|
|
4885
|
+
return true;
|
|
4886
|
+
} catch (e) {
|
|
4887
|
+
return utils.isErrnoException(e) && e.code === 'EPERM';
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
async function waitForProcessExit(pid) {
|
|
4891
|
+
const deadline = Date.now() + SERVER_EXIT_TIMEOUT_MS;
|
|
4892
|
+
while (isProcessAlive(pid)) {
|
|
4893
|
+
if (Date.now() >= deadline) {
|
|
4894
|
+
return false;
|
|
4895
|
+
}
|
|
4896
|
+
// eslint-disable-next-line no-await-in-loop
|
|
4897
|
+
await require$$7.setTimeout(100);
|
|
4898
|
+
}
|
|
4899
|
+
return true;
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
// A shutdown that exits successfully can still leave the server running, for example with
|
|
4903
|
+
// `shutdown --iff_heap_size_greater_than` in an rc file.
|
|
4904
|
+
async function stopBazelServer(pid) {
|
|
4905
|
+
if (await waitForProcessExit(pid)) {
|
|
4906
|
+
return true;
|
|
4907
|
+
}
|
|
4908
|
+
try {
|
|
4909
|
+
process.kill(pid, 'SIGKILL');
|
|
4910
|
+
} catch (e) {
|
|
4911
|
+
if (!utils.isErrnoException(e) || e.code !== 'ESRCH') {
|
|
4912
|
+
logger.logger.warn(`Bazel server ${pid} could not be stopped. ${utils.getErrorCause(e)}`);
|
|
4913
|
+
return false;
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
return await waitForProcessExit(pid);
|
|
4917
|
+
}
|
|
4918
|
+
function parseBazelInfo(stdout) {
|
|
4919
|
+
const values = new Map();
|
|
4920
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
4921
|
+
const separator = line.indexOf(': ');
|
|
4922
|
+
if (separator > 0) {
|
|
4923
|
+
values.set(line.slice(0, separator), line.slice(separator + 2).trim());
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
return values;
|
|
4927
|
+
}
|
|
4928
|
+
function isPathInside(child, parent) {
|
|
4929
|
+
const relative = path.relative(u.realpathSync.native(parent), u.realpathSync.native(child));
|
|
4930
|
+
return relative !== '' && relative.split(path.sep)[0] !== '..' && !path.isAbsolute(relative);
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4412
4933
|
// Default per-invocation timeout for bazel queries. Bazel cold-cache starts
|
|
4413
4934
|
// can take several minutes; 10 minutes is generous while still bounding CI hangs.
|
|
4414
4935
|
const BAZEL_QUERY_TIMEOUT_MS = 600_000;
|
|
4415
4936
|
const STDERR_TAIL_BYTES = 4_096;
|
|
4416
4937
|
const STDOUT_EXCERPT_BYTES = 1_024;
|
|
4417
|
-
|
|
4418
|
-
// Splits the user-supplied --bazel-flags string on whitespace.
|
|
4419
|
-
// Empty / undefined returns []. No shell parsing — quoted args with embedded
|
|
4420
|
-
// whitespace are not supported (documented limitation; same trust model as
|
|
4421
|
-
// gradleOpts).
|
|
4422
4938
|
function splitBazelFlags(flags) {
|
|
4423
4939
|
if (!flags) {
|
|
4424
4940
|
return [];
|
|
4425
4941
|
}
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4942
|
+
const args = [];
|
|
4943
|
+
let argument = '';
|
|
4944
|
+
let quote;
|
|
4945
|
+
let started = false;
|
|
4946
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
4947
|
+
const character = flags.charAt(index);
|
|
4948
|
+
if (quote) {
|
|
4949
|
+
if (character === quote) {
|
|
4950
|
+
if (flags.charAt(index + 1) === quote) {
|
|
4951
|
+
argument += quote;
|
|
4952
|
+
index += 1;
|
|
4953
|
+
} else {
|
|
4954
|
+
quote = undefined;
|
|
4955
|
+
}
|
|
4956
|
+
} else {
|
|
4957
|
+
argument += character;
|
|
4958
|
+
}
|
|
4959
|
+
} else if ((character === '"' || character === "'") && (!started || argument.endsWith('='))) {
|
|
4960
|
+
quote = character;
|
|
4961
|
+
started = true;
|
|
4962
|
+
} else if (/\s/.test(character)) {
|
|
4963
|
+
if (started) {
|
|
4964
|
+
args.push(argument);
|
|
4965
|
+
argument = '';
|
|
4966
|
+
started = false;
|
|
4967
|
+
}
|
|
4968
|
+
} else {
|
|
4969
|
+
argument += character;
|
|
4970
|
+
started = true;
|
|
4971
|
+
}
|
|
4437
4972
|
}
|
|
4438
|
-
if (
|
|
4439
|
-
|
|
4973
|
+
if (quote) {
|
|
4974
|
+
throw new utils.InputError('--bazel-flags contains an unterminated quote. Close each quoted value. Backslashes are preserved literally.');
|
|
4440
4975
|
}
|
|
4441
|
-
if (
|
|
4442
|
-
|
|
4976
|
+
if (started) {
|
|
4977
|
+
args.push(argument);
|
|
4443
4978
|
}
|
|
4444
|
-
return
|
|
4979
|
+
return args;
|
|
4445
4980
|
}
|
|
4446
4981
|
function buildBazelModShowVisibleReposArgv(opts) {
|
|
4447
4982
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
4448
|
-
return [...
|
|
4983
|
+
return [...buildBazelStartupFlags(opts), 'mod', 'dump_repo_mapping', '', '--output=json', ...userFlags];
|
|
4449
4984
|
}
|
|
4450
4985
|
function buildBazelModShowMavenExtensionArgv(opts) {
|
|
4451
4986
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
4452
|
-
return [...
|
|
4987
|
+
return [...buildBazelStartupFlags(opts), 'mod', 'show_extension', '@rules_jvm_external//:extensions.bzl%maven',
|
|
4453
4988
|
// A read-only scan must never rewrite the user's MODULE.bazel.lock; pin
|
|
4454
4989
|
// the lockfile read-only before user flags, mirroring the query/cquery
|
|
4455
4990
|
// argv builders.
|
|
@@ -4462,7 +4997,7 @@ function buildBazelModShowMavenExtensionArgv(opts) {
|
|
|
4462
4997
|
}
|
|
4463
4998
|
function buildBazelModShowPipExtensionArgv(opts) {
|
|
4464
4999
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
4465
|
-
return [...
|
|
5000
|
+
return [...buildBazelStartupFlags(opts), 'mod', 'show_extension', '@rules_python//python/extensions:pip.bzl%pip',
|
|
4466
5001
|
// A read-only scan must never rewrite the user's MODULE.bazel.lock; pin
|
|
4467
5002
|
// the lockfile read-only before user flags, mirroring the query/cquery
|
|
4468
5003
|
// argv builders.
|
|
@@ -4474,7 +5009,7 @@ function buildBazelArgv(queryStr, opts, output = 'build') {
|
|
|
4474
5009
|
// Keep query output stable and avoid updating Bazel lockfiles while extracting.
|
|
4475
5010
|
const queryFlags = ['--lockfile_mode=off', '--noshow_progress'];
|
|
4476
5011
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
4477
|
-
return [...
|
|
5012
|
+
return [...buildBazelStartupFlags(opts), 'query', ...queryFlags, ...opts.invocationFlags, queryStr, `--output=${output}`, ...userFlags];
|
|
4478
5013
|
}
|
|
4479
5014
|
|
|
4480
5015
|
// Lightweight presence-check cquery used by the tri-state probe classifier.
|
|
@@ -4484,7 +5019,7 @@ function buildBazelArgv(queryStr, opts, output = 'build') {
|
|
|
4484
5019
|
// `bazel-cquery.mts` needs but the probe does not).
|
|
4485
5020
|
function buildBazelProbeCqueryArgv(repoName, opts) {
|
|
4486
5021
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
4487
|
-
return [...
|
|
5022
|
+
return [...buildBazelStartupFlags(opts), 'cquery', '--lockfile_mode=off', '--noshow_progress', ...opts.invocationFlags, `@${repoName}//...`, '--output=label', '--keep_going', ...userFlags];
|
|
4488
5023
|
}
|
|
4489
5024
|
function stringField(value) {
|
|
4490
5025
|
return typeof value === 'string' ? value : '';
|
|
@@ -4559,7 +5094,7 @@ async function runBazelQuery(queryStr, opts, output) {
|
|
|
4559
5094
|
let result;
|
|
4560
5095
|
try {
|
|
4561
5096
|
spinner.start(`Running bazel query (${queryStr.slice(0, 80)})...`);
|
|
4562
|
-
const output = await
|
|
5097
|
+
const output = await spawnBazel(opts.bin, argv, {
|
|
4563
5098
|
cwd: opts.cwd,
|
|
4564
5099
|
timeout: BAZEL_QUERY_TIMEOUT_MS,
|
|
4565
5100
|
...(opts.env ? {
|
|
@@ -4605,7 +5140,7 @@ async function runBazelOneShot(argv, opts, step) {
|
|
|
4605
5140
|
const startedAt = Date.now();
|
|
4606
5141
|
let result;
|
|
4607
5142
|
try {
|
|
4608
|
-
const output = await
|
|
5143
|
+
const output = await spawnBazel(opts.bin, argv, {
|
|
4609
5144
|
cwd: opts.cwd,
|
|
4610
5145
|
timeout: BAZEL_QUERY_TIMEOUT_MS,
|
|
4611
5146
|
...(opts.env ? {
|
|
@@ -4770,16 +5305,7 @@ function buildMetadataCqueryExpr(repoName) {
|
|
|
4770
5305
|
// Build the full cquery argv for a per-repo metadata cquery. Exposed for
|
|
4771
5306
|
// argv-shape unit tests without touching `spawn`.
|
|
4772
5307
|
function buildMetadataCqueryArgv(repoName, opts) {
|
|
4773
|
-
const startup =
|
|
4774
|
-
if (opts.bazelRc) {
|
|
4775
|
-
startup.push(`--bazelrc=${opts.bazelRc}`);
|
|
4776
|
-
}
|
|
4777
|
-
if (opts.outputUserRoot) {
|
|
4778
|
-
startup.push(`--output_user_root=${opts.outputUserRoot}`);
|
|
4779
|
-
}
|
|
4780
|
-
if (opts.bazelOutputBase) {
|
|
4781
|
-
startup.push(`--output_base=${opts.bazelOutputBase}`);
|
|
4782
|
-
}
|
|
5308
|
+
const startup = buildBazelStartupFlags(opts);
|
|
4783
5309
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
4784
5310
|
return [...startup, 'cquery', '--lockfile_mode=off', '--noshow_progress', ...opts.invocationFlags, buildMetadataCqueryExpr(repoName), '--output=jsonproto', '--proto:output_rule_attrs=tags,maven_coordinates,deps,exports,runtime_deps', '--keep_going', ...userFlags];
|
|
4785
5311
|
}
|
|
@@ -5117,7 +5643,7 @@ async function runMetadataCqueryForRepo(args) {
|
|
|
5117
5643
|
const argv = buildMetadataCqueryArgv(repoName, opts);
|
|
5118
5644
|
const startedAt = Date.now();
|
|
5119
5645
|
try {
|
|
5120
|
-
const result = await
|
|
5646
|
+
const result = await spawnBazel(opts.bin, argv, {
|
|
5121
5647
|
cwd: workspaceRoot,
|
|
5122
5648
|
timeout: timeoutMs,
|
|
5123
5649
|
...(opts.env ? {
|
|
@@ -5146,10 +5672,7 @@ async function runMetadataCqueryForRepo(args) {
|
|
|
5146
5672
|
const err = e;
|
|
5147
5673
|
const stdout = typeof err.stdout === 'string' ? err.stdout : '';
|
|
5148
5674
|
const stderr = typeof err.stderr === 'string' ? err.stderr : '';
|
|
5149
|
-
|
|
5150
|
-
// `killed: true` and `signal: 'SIGTERM'` (or `SIGKILL`). There is no
|
|
5151
|
-
// `timedOut` flag on the real rejection, so do not test for one.
|
|
5152
|
-
const timedOut = err.killed === true || err.signal === 'SIGTERM' || err.signal === 'SIGKILL';
|
|
5675
|
+
const timedOut = e instanceof BazelTimeoutError;
|
|
5153
5676
|
const {
|
|
5154
5677
|
artifacts,
|
|
5155
5678
|
unresolvedLabels
|
|
@@ -5176,25 +5699,19 @@ async function runMetadataCqueryForRepo(args) {
|
|
|
5176
5699
|
}
|
|
5177
5700
|
}
|
|
5178
5701
|
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
// JDK; the CLI does not attempt to discover Homebrew installs or mutate the
|
|
5184
|
-
// caller's PATH/JAVA_HOME. If `java -version` fails we throw with an
|
|
5185
|
-
// actionable message so the surfaced error names the prerequisite directly
|
|
5186
|
-
// instead of relying on Bazel's downstream diagnostic.
|
|
5187
|
-
function ensureJavaOnPath() {
|
|
5188
|
-
if (probed) {
|
|
5189
|
-
return;
|
|
5190
|
-
}
|
|
5702
|
+
function ensureJavaOnPath(env, cwd) {
|
|
5703
|
+
const javaHome = getBazelEnvironmentValue(env, 'JAVA_HOME');
|
|
5704
|
+
const executable = process.platform === 'win32' ? 'java.exe' : 'java';
|
|
5705
|
+
const java = javaHome ? path.resolve(cwd, javaHome, 'bin', executable) : executable;
|
|
5191
5706
|
try {
|
|
5192
|
-
childProcess.
|
|
5193
|
-
|
|
5707
|
+
childProcess.execFileSync(java, ['-version'], {
|
|
5708
|
+
cwd,
|
|
5709
|
+
env,
|
|
5710
|
+
stdio: 'ignore',
|
|
5711
|
+
timeout: 10_000
|
|
5194
5712
|
});
|
|
5195
|
-
probed = true;
|
|
5196
5713
|
} catch {
|
|
5197
|
-
throw new
|
|
5714
|
+
throw new utils.InputError('Java is required for Bazel JVM manifest extraction. Install a JDK and ensure JAVA_HOME points to it or java is on PATH.');
|
|
5198
5715
|
}
|
|
5199
5716
|
}
|
|
5200
5717
|
|
|
@@ -5233,75 +5750,55 @@ function validateOutputBase(outputBase, cwd) {
|
|
|
5233
5750
|
}
|
|
5234
5751
|
}
|
|
5235
5752
|
|
|
5236
|
-
|
|
5237
|
-
// socket-cli invocations on the same machine share it. The symlink target
|
|
5238
|
-
// is whatever python3 resolves to NOW; if PATH changes between invocations
|
|
5239
|
-
// we replace the symlink.
|
|
5240
|
-
const SHIM_SUBDIR = 'socket-cli-bazel-python-shim';
|
|
5241
|
-
|
|
5242
|
-
// Cache the result for the lifetime of this process.
|
|
5243
|
-
let cached = null;
|
|
5244
|
-
|
|
5245
|
-
// Safe wrapper around whichBin that returns null instead of throwing when
|
|
5246
|
-
// nothrow semantics are broken in older registry versions (realpath 'null' bug).
|
|
5247
|
-
async function safeWhichBin(name) {
|
|
5753
|
+
function isPython3(executable, env, cwd) {
|
|
5248
5754
|
try {
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5755
|
+
const version = childProcess.execFileSync(executable, ['-c', 'import sys\nprint(sys.version_info.major)'], {
|
|
5756
|
+
cwd,
|
|
5757
|
+
env,
|
|
5758
|
+
encoding: 'utf8',
|
|
5759
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
5760
|
+
timeout: 10_000
|
|
5761
|
+
});
|
|
5762
|
+
return version.trim() === '3';
|
|
5252
5763
|
} catch {
|
|
5253
|
-
|
|
5764
|
+
// Windows application aliases can resolve without a working interpreter.
|
|
5765
|
+
return false;
|
|
5254
5766
|
}
|
|
5255
5767
|
}
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
const
|
|
5261
|
-
if (
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
shimDir: undefined
|
|
5265
|
-
};
|
|
5266
|
-
return cached;
|
|
5267
|
-
}
|
|
5268
|
-
const python3OnPath = await safeWhichBin('python3');
|
|
5269
|
-
if (!python3OnPath) {
|
|
5270
|
-
throw new utils.InputError('Neither `python` nor `python3` found on PATH. Older versions of ' + 'rules_jvm_external require a `python` interpreter for repository ' + 'rules. Install Python 3 and ensure it is on PATH, then retry.');
|
|
5271
|
-
}
|
|
5272
|
-
const shimDir = path.join(os.tmpdir(), SHIM_SUBDIR);
|
|
5273
|
-
u.mkdirSync(shimDir, {
|
|
5274
|
-
recursive: true
|
|
5275
|
-
});
|
|
5276
|
-
const linkPath = path.join(shimDir, 'python');
|
|
5277
|
-
// Replace the symlink defensively in case python3's resolved path moved.
|
|
5278
|
-
if (u.existsSync(linkPath)) {
|
|
5279
|
-
try {
|
|
5280
|
-
u.unlinkSync(linkPath);
|
|
5281
|
-
} catch {
|
|
5282
|
-
// Tolerate races; the next symlinkSync may still succeed.
|
|
5768
|
+
function notPython3Error(executable) {
|
|
5769
|
+
return new utils.InputError(`Python 3 is required for Bazel manifest extraction. ${executable} did not run as Python 3. Install Python 3 and ensure python${process.platform === 'win32' ? '.exe' : ''} is on PATH.`);
|
|
5770
|
+
}
|
|
5771
|
+
async function provisionPythonShim(env, cwd) {
|
|
5772
|
+
const python = await findBazelExecutable('python', env, cwd);
|
|
5773
|
+
if (python) {
|
|
5774
|
+
if (process.platform === 'win32' && path.extname(python).toLowerCase() !== '.exe') {
|
|
5775
|
+
throw new utils.InputError('Bazel on Windows requires a working Python 3 python.exe on PATH. Python batch launchers are not supported.');
|
|
5283
5776
|
}
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
if (e.code === 'EEXIST' && u.existsSync(linkPath)) ; else {
|
|
5293
|
-
throw e;
|
|
5777
|
+
if (isPython3(python, env, cwd)) {
|
|
5778
|
+
return {
|
|
5779
|
+
env,
|
|
5780
|
+
shimDir: undefined
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
if (process.platform === 'win32') {
|
|
5784
|
+
throw notPython3Error(python);
|
|
5294
5785
|
}
|
|
5786
|
+
} else if (process.platform === 'win32') {
|
|
5787
|
+
throw new utils.InputError('Bazel on Windows requires a working Python 3 python.exe on PATH. Install Python 3 with the PATH option enabled, then retry.');
|
|
5295
5788
|
}
|
|
5296
|
-
const
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5789
|
+
const python3 = await findBazelExecutable('python3', env, cwd);
|
|
5790
|
+
if (!python3) {
|
|
5791
|
+
throw python ? notPython3Error(python) : new utils.InputError('Neither python nor python3 was found on PATH. Install Python 3 and ensure it is on PATH, then retry.');
|
|
5792
|
+
}
|
|
5793
|
+
if (!isPython3(python3, env, cwd)) {
|
|
5794
|
+
throw notPython3Error(python3);
|
|
5795
|
+
}
|
|
5796
|
+
const shimDir = u.mkdtempSync(path.join(os.tmpdir(), 'socket-bazel-python-'));
|
|
5797
|
+
u.symlinkSync(python3, path.join(shimDir, 'python'));
|
|
5798
|
+
return {
|
|
5799
|
+
env: prependBazelPath(env, shimDir),
|
|
5302
5800
|
shimDir
|
|
5303
5801
|
};
|
|
5304
|
-
return cached;
|
|
5305
5802
|
}
|
|
5306
5803
|
|
|
5307
5804
|
/**
|
|
@@ -5761,7 +6258,6 @@ function findWorkspaceRoots(opts) {
|
|
|
5761
6258
|
// hard failure when nothing else was analyzable, and to a partial otherwise.
|
|
5762
6259
|
|
|
5763
6260
|
const DEFAULT_PER_REPO_TIMEOUT_MS = 60_000;
|
|
5764
|
-
const REAP_TIMEOUT_MS = 10_000;
|
|
5765
6261
|
|
|
5766
6262
|
// Machine-readable completeness signal emitted alongside the synthetic
|
|
5767
6263
|
// manifests. A `complete: false` summary tells a downstream consumer (e.g.
|
|
@@ -6156,39 +6652,6 @@ async function discoverCandidatesForWorkspace(workspaceRoot, mode, queryOpts, ve
|
|
|
6156
6652
|
};
|
|
6157
6653
|
}
|
|
6158
6654
|
|
|
6159
|
-
// Best-effort reap of a Bazel server. Spawned with a short timeout so
|
|
6160
|
-
// a wedged server can't itself hang the cleanup; failures are swallowed
|
|
6161
|
-
// because the caller will `rm -rf` the output_user_root regardless.
|
|
6162
|
-
async function reapBazelServer(bin, outputUserRoot, verbose) {
|
|
6163
|
-
try {
|
|
6164
|
-
await spawn.spawn(bin, [`--output_user_root=${outputUserRoot}`, 'shutdown'], {
|
|
6165
|
-
timeout: REAP_TIMEOUT_MS
|
|
6166
|
-
});
|
|
6167
|
-
} catch (e) {
|
|
6168
|
-
// Server may already be dead, or shutdown itself timed out — the
|
|
6169
|
-
// tempdir removal below is sufficient cleanup.
|
|
6170
|
-
if (verbose) {
|
|
6171
|
-
logger.logger.log(`[VERBOSE] reapBazelServer: shutdown failed for ${outputUserRoot} (${utils.getErrorCause(e)}); tempdir removal will still run`);
|
|
6172
|
-
}
|
|
6173
|
-
}
|
|
6174
|
-
}
|
|
6175
|
-
async function removeTempdir(dir, verbose) {
|
|
6176
|
-
try {
|
|
6177
|
-
await u.promises.rm(dir, {
|
|
6178
|
-
recursive: true,
|
|
6179
|
-
force: true
|
|
6180
|
-
});
|
|
6181
|
-
} catch (e) {
|
|
6182
|
-
// Best effort. The next CLI invocation lands a fresh tempdir.
|
|
6183
|
-
if (verbose) {
|
|
6184
|
-
logger.logger.log(`[VERBOSE] removeTempdir: ${dir} not fully removed (${utils.getErrorCause(e)}); a stale dir may linger until the next OS tempdir sweep`);
|
|
6185
|
-
}
|
|
6186
|
-
}
|
|
6187
|
-
}
|
|
6188
|
-
function makeOutputUserRoot() {
|
|
6189
|
-
return u.mkdtempSync(path.join(os.tmpdir(), 'socket-bazel-'));
|
|
6190
|
-
}
|
|
6191
|
-
|
|
6192
6655
|
// Construct the BazelQueryOptions shape used for a single workspace's
|
|
6193
6656
|
// queries. Lifted to module scope (out of the per-workspace loop) so
|
|
6194
6657
|
// ESLint's consistent-function-scoping is happy; takes everything it
|
|
@@ -6209,17 +6672,15 @@ function buildQueryOpts(args) {
|
|
|
6209
6672
|
invocationFlags,
|
|
6210
6673
|
outputUserRoot,
|
|
6211
6674
|
...(opts.bazelRc ? {
|
|
6212
|
-
bazelRc: opts.bazelRc
|
|
6675
|
+
bazelRc: path.resolve(opts.cwd, opts.bazelRc)
|
|
6213
6676
|
} : {}),
|
|
6214
6677
|
...(opts.bazelFlags ? {
|
|
6215
6678
|
bazelFlags: opts.bazelFlags
|
|
6216
6679
|
} : {}),
|
|
6217
6680
|
...(opts.bazelOutputBase ? {
|
|
6218
|
-
bazelOutputBase: opts.bazelOutputBase
|
|
6219
|
-
} : {}),
|
|
6220
|
-
...(baseEnv ? {
|
|
6221
|
-
env: baseEnv
|
|
6681
|
+
bazelOutputBase: path.resolve(opts.cwd, opts.bazelOutputBase)
|
|
6222
6682
|
} : {}),
|
|
6683
|
+
env: baseEnv,
|
|
6223
6684
|
verbose
|
|
6224
6685
|
};
|
|
6225
6686
|
}
|
|
@@ -6240,16 +6701,23 @@ async function extractBazelToMaven(opts) {
|
|
|
6240
6701
|
|
|
6241
6702
|
// Validate config + ensure toolchains BEFORE we mint a tempdir.
|
|
6242
6703
|
let bin;
|
|
6243
|
-
let baseEnv;
|
|
6704
|
+
let baseEnv = getBazelEnvironment(opts.env);
|
|
6705
|
+
let pythonShimDir;
|
|
6706
|
+
let outputUserRoot;
|
|
6244
6707
|
try {
|
|
6245
6708
|
if (opts.bazelOutputBase) {
|
|
6246
6709
|
validateOutputBase(opts.bazelOutputBase, opts.cwd);
|
|
6247
6710
|
}
|
|
6248
|
-
ensureJavaOnPath();
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6711
|
+
ensureJavaOnPath(baseEnv, cwd);
|
|
6712
|
+
bin = await resolveBazelBinary(opts.bin, baseEnv, cwd);
|
|
6713
|
+
const shim = await provisionPythonShim(baseEnv, cwd);
|
|
6714
|
+
pythonShimDir = shim.shimDir;
|
|
6715
|
+
baseEnv = shim.env;
|
|
6716
|
+
outputUserRoot = makeBazelOutputUserRoot(baseEnv, cwd);
|
|
6252
6717
|
} catch (e) {
|
|
6718
|
+
if (pythonShimDir) {
|
|
6719
|
+
await removeBazelTempdir(pythonShimDir);
|
|
6720
|
+
}
|
|
6253
6721
|
logger.logger.fail(`Unexpected error in bazel2maven: ${utils.getErrorCause(e)}`);
|
|
6254
6722
|
if (verbose) {
|
|
6255
6723
|
logger.logger.group('[VERBOSE] error:');
|
|
@@ -6268,8 +6736,7 @@ async function extractBazelToMaven(opts) {
|
|
|
6268
6736
|
|
|
6269
6737
|
// Track every output_user_root we mint so we can reap them all in
|
|
6270
6738
|
// the cleanup pass, even if a per-repo timeout forced a re-mint.
|
|
6271
|
-
|
|
6272
|
-
const mintedRoots = [outputUserRoot];
|
|
6739
|
+
const mintedRoots = new Map([[outputUserRoot, new Map()]]);
|
|
6273
6740
|
if (verbose) {
|
|
6274
6741
|
logger.logger.log(`[VERBOSE] initial --output_user_root=${outputUserRoot} (will be reaped on completion)`);
|
|
6275
6742
|
}
|
|
@@ -6296,7 +6763,7 @@ async function extractBazelToMaven(opts) {
|
|
|
6296
6763
|
// Always apply the default prune policy so no caller can forget it;
|
|
6297
6764
|
// callers EXTEND it via ignoreDirNames/ignoreDirPrefixes.
|
|
6298
6765
|
const ignoreDirNames = new Set([...DEFAULT_BAZEL_WALKER_IGNORE_DIR_NAMES, ...(opts.ignoreDirNames ?? [])]);
|
|
6299
|
-
const ignoreDirPrefixes = [...DEFAULT_BAZEL_WALKER_IGNORE_DIR_PREFIXES, ...(opts.ignoreDirPrefixes ?? [])];
|
|
6766
|
+
const ignoreDirPrefixes = [...DEFAULT_BAZEL_WALKER_IGNORE_DIR_PREFIXES, BAZEL_OUTPUT_ROOT_PREFIX, ...(opts.ignoreDirPrefixes ?? [])];
|
|
6300
6767
|
const workspaceRoots = findWorkspaceRoots({
|
|
6301
6768
|
cwd,
|
|
6302
6769
|
ignoreDirNames,
|
|
@@ -6343,15 +6810,19 @@ async function extractBazelToMaven(opts) {
|
|
|
6343
6810
|
}
|
|
6344
6811
|
logger.logger.info(`Workspace ${relPath || '.'}: bzlmod=${mode.bzlmod} workspace=${mode.workspace}`);
|
|
6345
6812
|
const invocationFlags = getBazelInvocationFlags(mode);
|
|
6346
|
-
const queryOptsFor = userRoot =>
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6813
|
+
const queryOptsFor = userRoot => {
|
|
6814
|
+
const queryOpts = buildQueryOpts({
|
|
6815
|
+
baseEnv,
|
|
6816
|
+
bin,
|
|
6817
|
+
invocationFlags,
|
|
6818
|
+
opts,
|
|
6819
|
+
outputUserRoot: userRoot,
|
|
6820
|
+
spawnCwd: workspaceRoot,
|
|
6821
|
+
verbose
|
|
6822
|
+
});
|
|
6823
|
+
mintedRoots.get(userRoot)?.set(workspaceRoot, queryOpts);
|
|
6824
|
+
return queryOpts;
|
|
6825
|
+
};
|
|
6355
6826
|
const {
|
|
6356
6827
|
candidates,
|
|
6357
6828
|
discoveryIndeterminate,
|
|
@@ -6430,11 +6901,12 @@ async function extractBazelToMaven(opts) {
|
|
|
6430
6901
|
state: 'failed'
|
|
6431
6902
|
});
|
|
6432
6903
|
// eslint-disable-next-line no-await-in-loop
|
|
6433
|
-
await
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6904
|
+
const cleaned = await cleanUpBazelOutputRoot(outputUserRoot, mintedRoots.get(outputUserRoot) ?? new Map());
|
|
6905
|
+
if (cleaned) {
|
|
6906
|
+
mintedRoots.delete(outputUserRoot);
|
|
6907
|
+
}
|
|
6908
|
+
outputUserRoot = makeBazelOutputUserRoot(baseEnv, cwd);
|
|
6909
|
+
mintedRoots.set(outputUserRoot, new Map());
|
|
6438
6910
|
if (verbose) {
|
|
6439
6911
|
logger.logger.log(`[VERBOSE] minted fresh --output_user_root=${outputUserRoot} after timeout`);
|
|
6440
6912
|
}
|
|
@@ -6663,11 +7135,9 @@ async function extractBazelToMaven(opts) {
|
|
|
6663
7135
|
workspaceOutcomes
|
|
6664
7136
|
};
|
|
6665
7137
|
} finally {
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
await
|
|
6669
|
-
// eslint-disable-next-line no-await-in-loop
|
|
6670
|
-
await removeTempdir(dir, verbose);
|
|
7138
|
+
await Promise.all(Array.from(mintedRoots, ([root, contexts]) => cleanUpBazelOutputRoot(root, contexts)));
|
|
7139
|
+
if (pythonShimDir) {
|
|
7140
|
+
await removeBazelTempdir(pythonShimDir);
|
|
6671
7141
|
}
|
|
6672
7142
|
}
|
|
6673
7143
|
}
|
|
@@ -7806,6 +8276,7 @@ async function handleCi(autoManifest) {
|
|
|
7806
8276
|
reach: {
|
|
7807
8277
|
dynamicSbomInference: false,
|
|
7808
8278
|
excludePaths: [],
|
|
8279
|
+
reachAdvisories: [],
|
|
7809
8280
|
reachAnalysisMemoryLimit: '',
|
|
7810
8281
|
reachAnalysisTimeout: '',
|
|
7811
8282
|
reachConcurrency: 1,
|
|
@@ -9934,8 +10405,6 @@ async function outputFixResult(result, outputKind) {
|
|
|
9934
10405
|
logger.logger.success('Finished!');
|
|
9935
10406
|
}
|
|
9936
10407
|
|
|
9937
|
-
const GHSA_FORMAT_REGEXP = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/;
|
|
9938
|
-
const CVE_FORMAT_REGEXP = /^CVE-\d{4}-\d{4,}$/;
|
|
9939
10408
|
/**
|
|
9940
10409
|
* Converts mixed CVE/GHSA/PURL IDs to GHSA IDs only.
|
|
9941
10410
|
* Filters out invalid IDs and logs conversion results.
|
|
@@ -9951,51 +10420,12 @@ async function convertIdsToGhsas(ids, options) {
|
|
|
9951
10420
|
debug.debugDir('inspect', {
|
|
9952
10421
|
ids
|
|
9953
10422
|
});
|
|
9954
|
-
const
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
if (GHSA_FORMAT_REGEXP.test(trimmedId)) {
|
|
9961
|
-
validGhsas.push(trimmedId);
|
|
9962
|
-
} else {
|
|
9963
|
-
errors.push(`Invalid GHSA format: ${trimmedId}`);
|
|
9964
|
-
}
|
|
9965
|
-
} else if (trimmedId.startsWith('CVE-')) {
|
|
9966
|
-
// Convert CVE to GHSA
|
|
9967
|
-
if (!CVE_FORMAT_REGEXP.test(trimmedId)) {
|
|
9968
|
-
errors.push(`Invalid CVE format: ${trimmedId}`);
|
|
9969
|
-
continue;
|
|
9970
|
-
}
|
|
9971
|
-
|
|
9972
|
-
// eslint-disable-next-line no-await-in-loop
|
|
9973
|
-
const conversionResult = await utils.convertCveToGhsa(trimmedId);
|
|
9974
|
-
if (conversionResult.ok) {
|
|
9975
|
-
validGhsas.push(conversionResult.data);
|
|
9976
|
-
if (!silence) {
|
|
9977
|
-
logger.logger.info(`Converted ${trimmedId} to ${conversionResult.data}`);
|
|
9978
|
-
}
|
|
9979
|
-
} else {
|
|
9980
|
-
errors.push(`${trimmedId}: ${conversionResult.message}`);
|
|
9981
|
-
}
|
|
9982
|
-
} else if (trimmedId.startsWith('pkg:')) {
|
|
9983
|
-
// Convert PURL to GHSAs.
|
|
9984
|
-
// eslint-disable-next-line no-await-in-loop
|
|
9985
|
-
const conversionResult = await utils.convertPurlToGhsas(trimmedId);
|
|
9986
|
-
if (conversionResult.ok && conversionResult.data.length) {
|
|
9987
|
-
validGhsas.push(...conversionResult.data);
|
|
9988
|
-
if (!silence) {
|
|
9989
|
-
logger.logger.info(`Converted ${trimmedId} to ${conversionResult.data.length} GHSA(s): ${arrays.joinAnd(conversionResult.data)}`);
|
|
9990
|
-
}
|
|
9991
|
-
} else {
|
|
9992
|
-
errors.push(`${trimmedId}: ${conversionResult.message || 'No GHSAs found'}`);
|
|
9993
|
-
}
|
|
9994
|
-
} else {
|
|
9995
|
-
// Neither CVE, GHSA, nor PURL, skip
|
|
9996
|
-
errors.push(`Unsupported ID format (expected CVE, GHSA, or PURL): ${trimmedId}`);
|
|
9997
|
-
}
|
|
9998
|
-
}
|
|
10423
|
+
const {
|
|
10424
|
+
errors,
|
|
10425
|
+
ghsas: validGhsas
|
|
10426
|
+
} = await utils.resolveAdvisoryIds(ids, {
|
|
10427
|
+
silence
|
|
10428
|
+
});
|
|
9999
10429
|
if (errors.length) {
|
|
10000
10430
|
if (!silence) {
|
|
10001
10431
|
logger.logger.warn(`Skipped ${errors.length} invalid IDs:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
@@ -10112,6 +10542,11 @@ const reachabilityFlags = {
|
|
|
10112
10542
|
type: 'string',
|
|
10113
10543
|
description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.COANA_CLI_VERSION}.`
|
|
10114
10544
|
},
|
|
10545
|
+
reachAdvisories: {
|
|
10546
|
+
type: 'string',
|
|
10547
|
+
isMultiple: true,
|
|
10548
|
+
description: 'List of advisories to run reachability analysis for, as GHSA or CVE IDs, either as a comma separated value or as multiple flags. CVE IDs are converted to GHSA IDs through the GitHub API. Vulnerabilities from other advisories get unknown reachability. Defaults to all advisories.'
|
|
10549
|
+
},
|
|
10115
10550
|
reachAnalysisMemoryLimit: {
|
|
10116
10551
|
type: 'string',
|
|
10117
10552
|
default: '8192',
|
|
@@ -12262,14 +12697,20 @@ async function extractBazelToPypi(opts) {
|
|
|
12262
12697
|
logger.logger.warn(`Warning: cwd does not exist: ${cwd}`);
|
|
12263
12698
|
}
|
|
12264
12699
|
logger.logger.groupEnd();
|
|
12700
|
+
let pythonShimDir;
|
|
12265
12701
|
try {
|
|
12702
|
+
if (process.platform === 'win32') {
|
|
12703
|
+
throw new utils.InputError('Bazel PyPI extraction on Windows is not supported yet. Platform-specific dependency selection still needs verification. Use --ecosystem maven for Windows SCA scans.');
|
|
12704
|
+
}
|
|
12266
12705
|
// Validate caller-provided Bazel filesystem settings before invoking Bazel.
|
|
12267
12706
|
if (opts.bazelOutputBase) {
|
|
12268
12707
|
validateOutputBase(opts.bazelOutputBase, opts.cwd);
|
|
12269
12708
|
}
|
|
12270
12709
|
// Python shim (for rules_python workspace discovery).
|
|
12271
|
-
const
|
|
12272
|
-
const
|
|
12710
|
+
const env = getBazelEnvironment(opts.env);
|
|
12711
|
+
const shim = await provisionPythonShim(env, cwd);
|
|
12712
|
+
pythonShimDir = shim.shimDir;
|
|
12713
|
+
const baseEnv = shim.env;
|
|
12273
12714
|
|
|
12274
12715
|
// Step 1: workspace detection.
|
|
12275
12716
|
const mode = detectWorkspaceMode(cwd);
|
|
@@ -12277,7 +12718,7 @@ async function extractBazelToPypi(opts) {
|
|
|
12277
12718
|
const invocationFlags = getBazelInvocationFlags(mode);
|
|
12278
12719
|
|
|
12279
12720
|
// Step 2: bazel binary resolution.
|
|
12280
|
-
const bin = await resolveBazelBinary(opts.bin);
|
|
12721
|
+
const bin = await resolveBazelBinary(opts.bin, baseEnv, cwd);
|
|
12281
12722
|
logger.logger.info(`Using bazel: ${bin}`);
|
|
12282
12723
|
if (verbose) {
|
|
12283
12724
|
logger.logger.log('[VERBOSE] resolved options:', {
|
|
@@ -12303,9 +12744,7 @@ async function extractBazelToPypi(opts) {
|
|
|
12303
12744
|
...(opts.bazelOutputBase ? {
|
|
12304
12745
|
bazelOutputBase: opts.bazelOutputBase
|
|
12305
12746
|
} : {}),
|
|
12306
|
-
|
|
12307
|
-
env: baseEnv
|
|
12308
|
-
} : {}),
|
|
12747
|
+
env: baseEnv,
|
|
12309
12748
|
verbose
|
|
12310
12749
|
};
|
|
12311
12750
|
|
|
@@ -12445,6 +12884,10 @@ async function extractBazelToPypi(opts) {
|
|
|
12445
12884
|
artifactCount: 0,
|
|
12446
12885
|
ok: false
|
|
12447
12886
|
};
|
|
12887
|
+
} finally {
|
|
12888
|
+
if (pythonShimDir) {
|
|
12889
|
+
await removeBazelTempdir(pythonShimDir);
|
|
12890
|
+
}
|
|
12448
12891
|
}
|
|
12449
12892
|
}
|
|
12450
12893
|
|
|
@@ -12530,7 +12973,7 @@ const config$g = {
|
|
|
12530
12973
|
},
|
|
12531
12974
|
bazelFlags: {
|
|
12532
12975
|
type: 'string',
|
|
12533
|
-
description: '
|
|
12976
|
+
description: 'Bazel command flags, with quoted values and literal backslashes'
|
|
12534
12977
|
},
|
|
12535
12978
|
bazelOutputBase: {
|
|
12536
12979
|
type: 'string',
|
|
@@ -12577,6 +13020,20 @@ const config$g = {
|
|
|
12577
13020
|
Note: this command generates dependency manifests for Bazel workspaces.
|
|
12578
13021
|
It does not run reachability analysis.
|
|
12579
13022
|
|
|
13023
|
+
Native Windows support currently covers Maven. Use PowerShell or
|
|
13024
|
+
Command Prompt with a working Python 3 python.exe and a JDK on PATH.
|
|
13025
|
+
PyPI extraction on Windows is not supported yet.
|
|
13026
|
+
|
|
13027
|
+
Set SOCKET_BAZEL_OUTPUT_ROOT to a short, writable parent directory,
|
|
13028
|
+
such as C:/sb, to shorten connector-owned Bazel output paths. Each
|
|
13029
|
+
invocation gets a separate directory. An explicit --bazel-output-base
|
|
13030
|
+
takes precedence for Bazel's cache and is kept after the scan. The
|
|
13031
|
+
scan only stops Bazel servers it started.
|
|
13032
|
+
|
|
13033
|
+
In --bazel-flags, single or double quotes at the start of a value or
|
|
13034
|
+
after = group values containing spaces. Backslashes stay literal. Double a matching quote to include
|
|
13035
|
+
it within a quoted value. Shell variables are not expanded.
|
|
13036
|
+
|
|
12580
13037
|
Maven hub discovery: under Bzlmod, hubs are enumerated from
|
|
12581
13038
|
\`bazel mod show_extension\` and filtered to the root module's own hubs.
|
|
12582
13039
|
Under legacy WORKSPACE mode (no \`show_extension\`), only conventionally
|
|
@@ -12725,7 +13182,9 @@ async function run$H(argv, importMeta, {
|
|
|
12725
13182
|
let perRepoTimeout = cli.flags['perRepoTimeout'];
|
|
12726
13183
|
|
|
12727
13184
|
// Set defaults for any flag/arg that is not given. Check socket.json first.
|
|
12728
|
-
if (
|
|
13185
|
+
if (bazel) {
|
|
13186
|
+
bazel = path.resolve(process.cwd(), String(bazel));
|
|
13187
|
+
} else {
|
|
12729
13188
|
const defaultBazel = sockJson.defaults?.manifest?.bazel?.bazel ?? sockJson.defaults?.manifest?.bazel?.bin;
|
|
12730
13189
|
if (defaultBazel) {
|
|
12731
13190
|
bazel = defaultBazel;
|
|
@@ -12804,7 +13263,7 @@ async function run$H(argv, importMeta, {
|
|
|
12804
13263
|
logger.logger.groupEnd();
|
|
12805
13264
|
}
|
|
12806
13265
|
if (dryRun) {
|
|
12807
|
-
logger.logger.log(constants.
|
|
13266
|
+
logger.logger.log(constants.DRY_RUN_BAILING_NOW);
|
|
12808
13267
|
return;
|
|
12809
13268
|
}
|
|
12810
13269
|
|
|
@@ -13126,7 +13585,7 @@ async function run$F(argv, importMeta, {
|
|
|
13126
13585
|
|
|
13127
13586
|
function renderTable(outcomes) {
|
|
13128
13587
|
// A covered reactor member is implied by its parent's line above it.
|
|
13129
|
-
return outcomes.filter(o => o.status !== 'skippedCovered').map(o => `- ${o
|
|
13588
|
+
return outcomes.filter(o => o.status !== 'skippedCovered').map(o => `- ${buildRootPath(o)} (${o.ecosystem}): ${o.status}${o.factsPath ? ` -> ${o.factsPath}` : ''}`).join('\n');
|
|
13130
13589
|
}
|
|
13131
13590
|
function summarize(outcomes) {
|
|
13132
13591
|
const generated = outcomes.filter(o => o.status === 'generated').length;
|
|
@@ -14940,6 +15399,7 @@ async function enumerateWorkspaces({
|
|
|
14940
15399
|
buildOpts,
|
|
14941
15400
|
cwd,
|
|
14942
15401
|
ecosystem,
|
|
15402
|
+
entryFile,
|
|
14943
15403
|
excludePaths,
|
|
14944
15404
|
javaHome,
|
|
14945
15405
|
verbose
|
|
@@ -14956,6 +15416,7 @@ async function enumerateWorkspaces({
|
|
|
14956
15416
|
}
|
|
14957
15417
|
const scriptOpts = {
|
|
14958
15418
|
bin: bin || undefined,
|
|
15419
|
+
entryFile,
|
|
14959
15420
|
excludePaths: excludePaths?.length ? excludePaths : undefined,
|
|
14960
15421
|
// `env` replaces the spawned process's whole environment, not just JAVA_HOME.
|
|
14961
15422
|
env: resolvedJavaHome ? {
|
|
@@ -14990,8 +15451,8 @@ async function enumerateWorkspaces({
|
|
|
14990
15451
|
};
|
|
14991
15452
|
}
|
|
14992
15453
|
|
|
14993
|
-
//
|
|
14994
|
-
//
|
|
15454
|
+
// One prompt per directory, as settings are per directory; Maven may have
|
|
15455
|
+
// several POMs, so several roots, there.
|
|
14995
15456
|
|
|
14996
15457
|
const ROOT_ECOSYSTEMS = ['maven', 'gradle', 'sbt'];
|
|
14997
15458
|
const ECOSYSTEM_LABELS = {
|
|
@@ -15026,23 +15487,6 @@ function notCanceled$1() {
|
|
|
15026
15487
|
function getEcosystemSection(sockJson, ecosystem) {
|
|
15027
15488
|
return sockJson.defaults?.manifest?.[ecosystem] ?? {};
|
|
15028
15489
|
}
|
|
15029
|
-
|
|
15030
|
-
// Depth-then-path sort: for exclusion roots it's just stable log ordering;
|
|
15031
|
-
// for candidates it's load-bearing, since a parent must be prompted before
|
|
15032
|
-
// its children so a child's shown default reflects a parent's just-written
|
|
15033
|
-
// cascade instead of its pre-run value.
|
|
15034
|
-
function sortCandidatesForDisplay(candidates, cwd) {
|
|
15035
|
-
return [...candidates].sort((a, b) => {
|
|
15036
|
-
const relA = path.relative(cwd, a.dir);
|
|
15037
|
-
const relB = path.relative(cwd, b.dir);
|
|
15038
|
-
const depthA = relA.split(path.sep).length;
|
|
15039
|
-
const depthB = relB.split(path.sep).length;
|
|
15040
|
-
if (depthA !== depthB) {
|
|
15041
|
-
return depthA - depthB;
|
|
15042
|
-
}
|
|
15043
|
-
return relA < relB ? -1 : relA > relB ? 1 : 0;
|
|
15044
|
-
});
|
|
15045
|
-
}
|
|
15046
15490
|
function toPosixRelative(cwd, dir) {
|
|
15047
15491
|
return path.relative(cwd, dir).split(path.sep).join('/');
|
|
15048
15492
|
}
|
|
@@ -15070,12 +15514,15 @@ async function scanBuildRoots({
|
|
|
15070
15514
|
excludePaths,
|
|
15071
15515
|
sockJson
|
|
15072
15516
|
}) {
|
|
15517
|
+
const socketConfig = utils.loadSocketConfig(cwd);
|
|
15073
15518
|
const [fullByTool, includedByTool] = await Promise.all([findBuildToolCandidates({
|
|
15074
15519
|
cwd,
|
|
15520
|
+
socketConfig,
|
|
15075
15521
|
sockJson
|
|
15076
15522
|
}), findBuildToolCandidates({
|
|
15077
15523
|
cwd,
|
|
15078
15524
|
excludePaths,
|
|
15525
|
+
socketConfig,
|
|
15079
15526
|
sockJson
|
|
15080
15527
|
})]);
|
|
15081
15528
|
return {
|
|
@@ -15098,20 +15545,47 @@ async function discoverBuildRoots({
|
|
|
15098
15545
|
const included = [];
|
|
15099
15546
|
const ignorePatterns = (excludePaths ?? []).flatMap(excludePathToScanIgnores);
|
|
15100
15547
|
const ecosystemsByRoot = new Map();
|
|
15101
|
-
for (const [ecosystem,
|
|
15102
|
-
const
|
|
15103
|
-
|
|
15104
|
-
|
|
15548
|
+
for (const [ecosystem, fullRoots] of fullByTool) {
|
|
15549
|
+
const includedPaths = new Set((includedByTool.get(ecosystem) ?? []).map(buildRootPath));
|
|
15550
|
+
const rootsByDir = new Map();
|
|
15551
|
+
for (const root of fullRoots) {
|
|
15552
|
+
if (root.dir === realCwd) {
|
|
15105
15553
|
continue;
|
|
15106
15554
|
}
|
|
15107
|
-
|
|
15555
|
+
const roots = rootsByDir.get(root.dir);
|
|
15556
|
+
if (roots) {
|
|
15557
|
+
roots.push(root);
|
|
15558
|
+
} else {
|
|
15559
|
+
rootsByDir.set(root.dir, [root]);
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
for (const {
|
|
15563
|
+
0: dir,
|
|
15564
|
+
1: roots
|
|
15565
|
+
} of rootsByDir) {
|
|
15566
|
+
const relDir = toPosixRelative(realCwd, dir);
|
|
15567
|
+
const includedRoots = [];
|
|
15568
|
+
const excludedRoots = [];
|
|
15569
|
+
for (const root of roots) {
|
|
15570
|
+
if (includedPaths.has(buildRootPath(root))) {
|
|
15571
|
+
includedRoots.push(root);
|
|
15572
|
+
} else {
|
|
15573
|
+
excludedRoots.push(root);
|
|
15574
|
+
}
|
|
15575
|
+
}
|
|
15576
|
+
if (includedRoots.length) {
|
|
15108
15577
|
included.push({
|
|
15109
15578
|
dir,
|
|
15110
|
-
ecosystem
|
|
15579
|
+
ecosystem,
|
|
15580
|
+
roots: includedRoots
|
|
15111
15581
|
});
|
|
15582
|
+
// `disabled` is per directory, so it cannot exclude one build file
|
|
15583
|
+
// without its included siblings.
|
|
15584
|
+
for (const root of excludedRoots) {
|
|
15585
|
+
logger.logger.warn(`Not disabling ${ecosystem} in ${relDir}: --exclude-paths matches ${buildRootPath(root)} but not the other build files there. Pass the same --exclude-paths to the scan instead.`);
|
|
15586
|
+
}
|
|
15112
15587
|
continue;
|
|
15113
15588
|
}
|
|
15114
|
-
const relDir = toPosixRelative(realCwd, dir);
|
|
15115
15589
|
const rootRelDir = findExclusionRoot(relDir, ignorePatterns);
|
|
15116
15590
|
const rootDir = path.join(realCwd, rootRelDir);
|
|
15117
15591
|
const ecosystems = ecosystemsByRoot.get(rootDir) ?? new Set();
|
|
@@ -15129,48 +15603,65 @@ async function discoverBuildRoots({
|
|
|
15129
15603
|
}
|
|
15130
15604
|
|
|
15131
15605
|
// Fails closed: without the members' build files, covered and independent
|
|
15132
|
-
// candidates can't be told apart.
|
|
15606
|
+
// candidates can't be told apart. In order, as a root's enumeration can cover
|
|
15607
|
+
// the roots after it.
|
|
15133
15608
|
async function markWorkspaceCoverage({
|
|
15134
|
-
candidate,
|
|
15135
15609
|
covered,
|
|
15136
15610
|
cwd,
|
|
15611
|
+
dir,
|
|
15612
|
+
ecosystem,
|
|
15137
15613
|
excludePaths,
|
|
15138
|
-
rootSockJson
|
|
15614
|
+
rootSockJson,
|
|
15615
|
+
roots
|
|
15139
15616
|
}) {
|
|
15140
|
-
|
|
15617
|
+
if (roots.every(root => isBuildRootCovered(covered, ecosystem, root))) {
|
|
15618
|
+
return {
|
|
15619
|
+
ok: true,
|
|
15620
|
+
data: undefined
|
|
15621
|
+
};
|
|
15622
|
+
}
|
|
15623
|
+
const cascade = utils.readSocketJsonCascade(dir, cwd, rootSockJson);
|
|
15141
15624
|
const {
|
|
15142
15625
|
bin,
|
|
15143
15626
|
buildOpts,
|
|
15144
15627
|
javaHome,
|
|
15145
15628
|
skipReason
|
|
15146
|
-
} = resolveEcosystemConfig(
|
|
15629
|
+
} = resolveEcosystemConfig(ecosystem, dir, cascade);
|
|
15147
15630
|
if (skipReason) {
|
|
15148
15631
|
return {
|
|
15149
15632
|
ok: true,
|
|
15150
15633
|
data: undefined
|
|
15151
15634
|
};
|
|
15152
15635
|
}
|
|
15153
|
-
const
|
|
15636
|
+
const excludePathsForDir = projectIgnorePathsToReachExcludePaths(excludePaths, {
|
|
15154
15637
|
cwd,
|
|
15155
|
-
target:
|
|
15638
|
+
target: dir
|
|
15156
15639
|
});
|
|
15157
|
-
const
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
};
|
|
15640
|
+
for (const root of roots) {
|
|
15641
|
+
if (isBuildRootCovered(covered, ecosystem, root)) {
|
|
15642
|
+
continue;
|
|
15643
|
+
}
|
|
15644
|
+
// eslint-disable-next-line no-await-in-loop
|
|
15645
|
+
const enumResult = await enumerateWorkspaces({
|
|
15646
|
+
bin,
|
|
15647
|
+
buildOpts,
|
|
15648
|
+
cwd: dir,
|
|
15649
|
+
ecosystem,
|
|
15650
|
+
entryFile: root.entryFile,
|
|
15651
|
+
excludePaths: excludePathsForDir,
|
|
15652
|
+
javaHome,
|
|
15653
|
+
verbose: false
|
|
15654
|
+
});
|
|
15655
|
+
if (!enumResult) {
|
|
15656
|
+
const relRoot = path.relative(cwd, buildRootPath(root)) || '.';
|
|
15657
|
+
return {
|
|
15658
|
+
ok: false,
|
|
15659
|
+
message: `Could not determine ${ecosystem} workspace layout for ${relRoot}; aborting rather than risk misclassifying its members.`
|
|
15660
|
+
};
|
|
15661
|
+
}
|
|
15662
|
+
// eslint-disable-next-line no-await-in-loop
|
|
15663
|
+
await addCoveredBuildFiles(covered, enumResult.buildRoot, enumResult.projects);
|
|
15172
15664
|
}
|
|
15173
|
-
await addCoveredBuildFiles(covered, enumResult.buildRoot, enumResult.projects);
|
|
15174
15665
|
return {
|
|
15175
15666
|
ok: true,
|
|
15176
15667
|
data: undefined
|
|
@@ -15580,7 +16071,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15580
16071
|
logger.logger.log('');
|
|
15581
16072
|
logger.logger.log(`Configuring ${cwd} ...`);
|
|
15582
16073
|
const initialSockJson = utils.readOrDefaultSocketJson(cwd);
|
|
15583
|
-
// Resolved once here for
|
|
16074
|
+
// Resolved once here for sortByDepthThenPath/disableExclusionRoot's
|
|
15584
16075
|
// relative-path math, consistent with discoverBuildRoots' own internal
|
|
15585
16076
|
// resolution (see its comment for why this matters).
|
|
15586
16077
|
const realCwd = await utils.realpathOrResolved(cwd);
|
|
@@ -15619,7 +16110,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15619
16110
|
|
|
15620
16111
|
// Which detected ecosystems cwd is itself a build root for - see
|
|
15621
16112
|
// setupRecursiveRootDefaults' wording split.
|
|
15622
|
-
const ecosystemsAtCwd = ROOT_ECOSYSTEMS.filter(ecosystem => (fullByTool.get(ecosystem) ?? []).
|
|
16113
|
+
const ecosystemsAtCwd = ROOT_ECOSYSTEMS.filter(ecosystem => (fullByTool.get(ecosystem) ?? []).some(root => root.dir === realCwd));
|
|
15623
16114
|
const rootResult = await setupRecursiveRootDefaults(cwd, defaultOnReadError, detectedEcosystems, ecosystemsAtCwd);
|
|
15624
16115
|
if (!rootResult.ok) {
|
|
15625
16116
|
return rootResult;
|
|
@@ -15646,7 +16137,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15646
16137
|
if (!excluded.length) {
|
|
15647
16138
|
logger.logger.log('None matched --exclude-paths; nothing disabled.');
|
|
15648
16139
|
} else {
|
|
15649
|
-
const orderedExcluded =
|
|
16140
|
+
const orderedExcluded = sortByDepthThenPath(excluded, realCwd);
|
|
15650
16141
|
for (const candidate of orderedExcluded) {
|
|
15651
16142
|
// eslint-disable-next-line no-await-in-loop
|
|
15652
16143
|
const result = await disableExclusionRoot({
|
|
@@ -15684,17 +16175,16 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15684
16175
|
// cwd never appears in `included`, but a reactor root often sits exactly
|
|
15685
16176
|
// at cwd - seed coverage from it first so its declared members still get
|
|
15686
16177
|
// recognized.
|
|
15687
|
-
for (const ecosystem of
|
|
16178
|
+
for (const ecosystem of ROOT_ECOSYSTEMS) {
|
|
15688
16179
|
// eslint-disable-next-line no-await-in-loop
|
|
15689
16180
|
const coverageResult = await markWorkspaceCoverage({
|
|
15690
|
-
candidate: {
|
|
15691
|
-
dir: realCwd,
|
|
15692
|
-
ecosystem
|
|
15693
|
-
},
|
|
15694
16181
|
covered,
|
|
15695
16182
|
cwd: realCwd,
|
|
16183
|
+
dir: realCwd,
|
|
16184
|
+
ecosystem,
|
|
15696
16185
|
excludePaths,
|
|
15697
|
-
rootSockJson
|
|
16186
|
+
rootSockJson,
|
|
16187
|
+
roots: (includedByTool.get(ecosystem) ?? []).filter(root => root.dir === realCwd)
|
|
15698
16188
|
});
|
|
15699
16189
|
if (!coverageResult.ok) {
|
|
15700
16190
|
return coverageResult;
|
|
@@ -15707,9 +16197,14 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15707
16197
|
reenabled: 0,
|
|
15708
16198
|
skipped: 0
|
|
15709
16199
|
};
|
|
15710
|
-
|
|
16200
|
+
// A parent is prompted before its children, so a child's shown default
|
|
16201
|
+
// reflects the parent's just-written cascade.
|
|
16202
|
+
const orderedIncluded = sortByDepthThenPath(included, realCwd);
|
|
15711
16203
|
for (const candidate of orderedIncluded) {
|
|
15712
|
-
|
|
16204
|
+
const {
|
|
16205
|
+
ecosystem
|
|
16206
|
+
} = candidate;
|
|
16207
|
+
if (candidate.roots.every(root => isBuildRootCovered(covered, ecosystem, root))) {
|
|
15713
16208
|
counts.covered += 1;
|
|
15714
16209
|
continue;
|
|
15715
16210
|
}
|
|
@@ -15718,7 +16213,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15718
16213
|
const result = await processCandidate({
|
|
15719
16214
|
cwd: realCwd,
|
|
15720
16215
|
dir: candidate.dir,
|
|
15721
|
-
ecosystem
|
|
16216
|
+
ecosystem,
|
|
15722
16217
|
rootSockJson
|
|
15723
16218
|
});
|
|
15724
16219
|
if (!result.ok) {
|
|
@@ -15745,11 +16240,13 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
|
|
|
15745
16240
|
// build-tool invocation only runs for a project already prompted about.
|
|
15746
16241
|
// eslint-disable-next-line no-await-in-loop
|
|
15747
16242
|
const coverageResult = await markWorkspaceCoverage({
|
|
15748
|
-
candidate,
|
|
15749
16243
|
covered,
|
|
15750
16244
|
cwd: realCwd,
|
|
16245
|
+
dir: candidate.dir,
|
|
16246
|
+
ecosystem,
|
|
15751
16247
|
excludePaths,
|
|
15752
|
-
rootSockJson
|
|
16248
|
+
rootSockJson,
|
|
16249
|
+
roots: candidate.roots
|
|
15753
16250
|
});
|
|
15754
16251
|
if (!coverageResult.ok) {
|
|
15755
16252
|
return coverageResult;
|
|
@@ -20058,6 +20555,37 @@ function resolveUvMemberDirs(targets, cwd) {
|
|
|
20058
20555
|
return Array.from(memberDirs);
|
|
20059
20556
|
}
|
|
20060
20557
|
|
|
20558
|
+
// GitHub publishes GHSA IDs with a lowercase body and CVE IDs uppercase.
|
|
20559
|
+
function normalizeAdvisoryId(id) {
|
|
20560
|
+
const trimmed = id.trim();
|
|
20561
|
+
return /^ghsa-/i.test(trimmed) ? `GHSA-${trimmed.slice(5).toLowerCase()}` : trimmed.toUpperCase();
|
|
20562
|
+
}
|
|
20563
|
+
|
|
20564
|
+
/**
|
|
20565
|
+
* Resolves the --reach-advisories IDs to GHSA IDs, converting CVE IDs through
|
|
20566
|
+
* the GitHub API. Any ID that cannot be resolved fails the command, since
|
|
20567
|
+
* dropping it would leave that advisory without reachability analysis.
|
|
20568
|
+
*/
|
|
20569
|
+
async function resolveReachAdvisories(ids) {
|
|
20570
|
+
const normalizedIds = arrays.arrayUnique(ids.map(normalizeAdvisoryId));
|
|
20571
|
+
// Checked up front so PURLs never reach resolveAdvisoryIds and a typo fails
|
|
20572
|
+
// before any GitHub lookup.
|
|
20573
|
+
const invalidIds = normalizedIds.filter(id => !utils.GHSA_FORMAT_REGEXP.test(id) && !utils.CVE_FORMAT_REGEXP.test(id));
|
|
20574
|
+
if (invalidIds.length) {
|
|
20575
|
+
throw new utils.InputError(`Invalid --reach-advisories IDs: ${invalidIds.join(', ')}. Use GHSA IDs (GHSA-xxxx-xxxx-xxxx) or CVE IDs (CVE-YYYY-NNNN).`);
|
|
20576
|
+
}
|
|
20577
|
+
const {
|
|
20578
|
+
errors,
|
|
20579
|
+
ghsas
|
|
20580
|
+
} = await utils.resolveAdvisoryIds(normalizedIds, {
|
|
20581
|
+
silence: true
|
|
20582
|
+
});
|
|
20583
|
+
if (errors.length) {
|
|
20584
|
+
throw new utils.InputError(`Could not resolve --reach-advisories IDs to GHSA IDs:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
20585
|
+
}
|
|
20586
|
+
return arrays.arrayUnique(ghsas);
|
|
20587
|
+
}
|
|
20588
|
+
|
|
20061
20589
|
async function suggestTarget() {
|
|
20062
20590
|
// We could prefill this with sub-dirs of the current
|
|
20063
20591
|
// dir ... but is that going to be useful?
|
|
@@ -20487,8 +21015,10 @@ async function run$d(argv, importMeta, {
|
|
|
20487
21015
|
const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
|
|
20488
21016
|
assertValidExcludePaths(excludePaths);
|
|
20489
21017
|
const reachExcludePaths = utils.cmdFlagValueToArray(cli.flags['reachExcludePaths']);
|
|
21018
|
+
const reachAdvisoriesRaw = utils.cmdFlagValueToArray(cli.flags['reachAdvisories']);
|
|
20490
21019
|
|
|
20491
21020
|
// Validation helpers for better readability.
|
|
21021
|
+
const hasReachAdvisories = reachAdvisoriesRaw.length > 0;
|
|
20492
21022
|
const hasReachEcosystems = reachEcosystems.length > 0;
|
|
20493
21023
|
const hasReachExcludePaths = reachExcludePaths.length > 0;
|
|
20494
21024
|
|
|
@@ -20506,7 +21036,7 @@ async function run$d(argv, importMeta, {
|
|
|
20506
21036
|
// added flags require --reach automatically instead of relying on a
|
|
20507
21037
|
// hand-maintained list. Deprecated no-ops are excluded on purpose.
|
|
20508
21038
|
const isUsingAnyBooleanReachFlag = Object.entries(reachabilityFlags).some(([name, flag]) => flag.type === 'boolean' && !DEPRECATED_NO_OP_REACH_FLAGS.has(name) && cli.flags[name] !== flag.default);
|
|
20509
|
-
const isUsingAnyReachabilityFlags = hasReachEcosystems || hasReachExcludePaths || isUsingAnyBooleanReachFlag || isUsingNonDefaultConcurrency || isUsingNonDefaultMemoryLimit || isUsingNonDefaultTimeout || isUsingNonDefaultVersion;
|
|
21039
|
+
const isUsingAnyReachabilityFlags = hasReachAdvisories || hasReachEcosystems || hasReachExcludePaths || isUsingAnyBooleanReachFlag || isUsingNonDefaultConcurrency || isUsingNonDefaultMemoryLimit || isUsingNonDefaultTimeout || isUsingNonDefaultVersion;
|
|
20510
21040
|
|
|
20511
21041
|
// Validate target constraints when --reach is enabled.
|
|
20512
21042
|
const reachTargetValidation = reach ? await validateReachabilityTarget(targets, cwd) : {
|
|
@@ -20583,6 +21113,7 @@ async function run$d(argv, importMeta, {
|
|
|
20583
21113
|
logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
|
|
20584
21114
|
return;
|
|
20585
21115
|
}
|
|
21116
|
+
const reachAdvisories = await resolveReachAdvisories(reachAdvisoriesRaw);
|
|
20586
21117
|
await handleCreateNewScan({
|
|
20587
21118
|
autoManifest: Boolean(autoManifest),
|
|
20588
21119
|
branchName: branchName,
|
|
@@ -20600,6 +21131,7 @@ async function run$d(argv, importMeta, {
|
|
|
20600
21131
|
reach: {
|
|
20601
21132
|
dynamicSbomInference: Boolean(dynamicSbomInference),
|
|
20602
21133
|
excludePaths,
|
|
21134
|
+
reachAdvisories,
|
|
20603
21135
|
reachAnalysisMemoryLimit,
|
|
20604
21136
|
reachAnalysisTimeout,
|
|
20605
21137
|
reachConcurrency: Number(reachConcurrency),
|
|
@@ -21318,6 +21850,7 @@ async function scanOneRepo(repoSlug, {
|
|
|
21318
21850
|
reach: {
|
|
21319
21851
|
dynamicSbomInference: false,
|
|
21320
21852
|
excludePaths: [],
|
|
21853
|
+
reachAdvisories: [],
|
|
21321
21854
|
reachAnalysisMemoryLimit: '',
|
|
21322
21855
|
reachAnalysisTimeout: '',
|
|
21323
21856
|
reachConcurrency: 1,
|
|
@@ -22746,6 +23279,7 @@ async function run$7(argv, importMeta, {
|
|
|
22746
23279
|
|
|
22747
23280
|
// Process comma-separated values for isMultiple flags.
|
|
22748
23281
|
const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
|
|
23282
|
+
const reachAdvisoriesRaw = utils.cmdFlagValueToArray(cli.flags['reachAdvisories']);
|
|
22749
23283
|
const reachEcosystemsRaw = utils.cmdFlagValueToArray(cli.flags['reachEcosystems']);
|
|
22750
23284
|
const reachExcludePaths = utils.cmdFlagValueToArray(cli.flags['reachExcludePaths']);
|
|
22751
23285
|
assertValidExcludePaths(excludePaths);
|
|
@@ -22818,6 +23352,7 @@ async function run$7(argv, importMeta, {
|
|
|
22818
23352
|
logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
|
|
22819
23353
|
return;
|
|
22820
23354
|
}
|
|
23355
|
+
const reachAdvisories = await resolveReachAdvisories(reachAdvisoriesRaw);
|
|
22821
23356
|
await handleScanReach({
|
|
22822
23357
|
cwd,
|
|
22823
23358
|
interactive,
|
|
@@ -22827,6 +23362,7 @@ async function run$7(argv, importMeta, {
|
|
|
22827
23362
|
reachabilityOptions: {
|
|
22828
23363
|
dynamicSbomInference: Boolean(dynamicSbomInference),
|
|
22829
23364
|
excludePaths,
|
|
23365
|
+
reachAdvisories,
|
|
22830
23366
|
reachAnalysisMemoryLimit,
|
|
22831
23367
|
reachAnalysisTimeout,
|
|
22832
23368
|
reachConcurrency: Number(reachConcurrency),
|
|
@@ -24791,5 +25327,5 @@ process.on('unhandledRejection', async (reason, promise) => {
|
|
|
24791
25327
|
// eslint-disable-next-line n/no-process-exit
|
|
24792
25328
|
process.exit(1);
|
|
24793
25329
|
});
|
|
24794
|
-
//# debugId=
|
|
25330
|
+
//# debugId=b77ced41-382f-4a5e-a03f-154a33fa6ec
|
|
24795
25331
|
//# sourceMappingURL=cli.js.map
|