@cdxgen/cdxgen 13.0.1 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -13
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +125 -4
- package/bin/evinse.js +1 -1
- package/data/component-tags.json +8 -1
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1144 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/cli/bomAssembly.js +20 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +76 -48
- package/lib/cli/jsBom.js +32 -3
- package/lib/cli/jvmBom.js +1494 -1315
- package/lib/cli/managedBom.js +97 -28
- package/lib/cli/nativeBom.js +189 -10
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +69 -1
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/ecosystems/ecosystems.js +9 -0
- package/lib/ecosystems/gradleutils.js +355 -15
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-misc.js +16 -2
- package/lib/ecosystems/parsers-python.js +171 -35
- package/lib/ecosystems/parsers-rust.js +12 -2
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/evinser/evinser.js +30 -12
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/auditCategories.js +9 -0
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +431 -22
- package/lib/inventory/envcontext.js +206 -18
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +227 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +115 -2
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +14 -14
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +33 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +20 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +5 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +8 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +20 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +9 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +2 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isDryRun,
|
|
5
|
+
isSecureMode,
|
|
6
|
+
readEnvironmentVariable,
|
|
7
|
+
} from "../core/activity.js";
|
|
8
|
+
import { isDeno } from "../core/env.js";
|
|
9
|
+
import { safeExistsSync, safeReadFileSync } from "../core/fs.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Upper bound on the content read from a single tool-requirements file, so a
|
|
13
|
+
* corrupt or hostile file cannot force unbounded reads.
|
|
14
|
+
*
|
|
15
|
+
* @type {number}
|
|
16
|
+
*/
|
|
17
|
+
const MAX_REQUIREMENTS_FILE_BYTES = 256 * 1024;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maps a declared build-tool name to the cdxgen ecosystem whose events it
|
|
21
|
+
* belongs to. Tools outside the map are attributed to the reserved
|
|
22
|
+
* "generic" ecosystem so `.tool-versions` entries for arbitrary tools
|
|
23
|
+
* (terraform, ghc, …) stay recordable without inventing ecosystems.
|
|
24
|
+
*
|
|
25
|
+
* @type {Object<string, string>}
|
|
26
|
+
*/
|
|
27
|
+
const TOOL_ECOSYSTEMS = {
|
|
28
|
+
bazel: "java",
|
|
29
|
+
cargo: "rust",
|
|
30
|
+
cc: "c",
|
|
31
|
+
clang: "c",
|
|
32
|
+
cmake: "c",
|
|
33
|
+
conda: "python",
|
|
34
|
+
dart: "dart",
|
|
35
|
+
deno: "npm",
|
|
36
|
+
dotnet: "csharp",
|
|
37
|
+
flutter: "dart",
|
|
38
|
+
gem: "ruby",
|
|
39
|
+
go: "go",
|
|
40
|
+
golang: "go",
|
|
41
|
+
gradle: "java",
|
|
42
|
+
java: "java",
|
|
43
|
+
javac: "java",
|
|
44
|
+
mix: "elixir",
|
|
45
|
+
mvn: "java",
|
|
46
|
+
maven: "java",
|
|
47
|
+
node: "npm",
|
|
48
|
+
nodejs: "npm",
|
|
49
|
+
npm: "npm",
|
|
50
|
+
pnpm: "npm",
|
|
51
|
+
poetry: "python",
|
|
52
|
+
python: "python",
|
|
53
|
+
python3: "python",
|
|
54
|
+
pip: "python",
|
|
55
|
+
pip3: "python",
|
|
56
|
+
rbenv: "ruby",
|
|
57
|
+
ruby: "ruby",
|
|
58
|
+
rust: "rust",
|
|
59
|
+
rustc: "rust",
|
|
60
|
+
rustup: "rust",
|
|
61
|
+
sbt: "java",
|
|
62
|
+
scala: "java",
|
|
63
|
+
swift: "swift",
|
|
64
|
+
uv: "python",
|
|
65
|
+
yarn: "npm",
|
|
66
|
+
bundler: "ruby",
|
|
67
|
+
bun: "npm",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the ecosystem a declared tool belongs to.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} tool Declared tool name
|
|
74
|
+
* @returns {string} cdxgen ecosystem name, or "generic" for unknown tools.
|
|
75
|
+
*/
|
|
76
|
+
export function ecosystemForTool(tool) {
|
|
77
|
+
return TOOL_ECOSYSTEMS[`${tool}`.toLowerCase()] || "generic";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether a version identifier carries a prerelease suffix such as
|
|
82
|
+
* `-rc-3`, `-M2`, or `-milestone-1`. The marker list is exact so that vendor
|
|
83
|
+
* suffixes that merely begin with the same letters, such as the `-crac` java
|
|
84
|
+
* distributions, stay classified as stable.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} version Version identifier
|
|
87
|
+
* @returns {boolean} True for prerelease versions.
|
|
88
|
+
*/
|
|
89
|
+
export function isPrereleaseVersion(version) {
|
|
90
|
+
if (typeof version !== "string") {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const separatorIndex = version.indexOf("-");
|
|
94
|
+
if (separatorIndex < 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return /^(rc-?\d*|m\d+|milestone|alpha|beta|ea)\b/i.test(
|
|
98
|
+
version.slice(separatorIndex + 1),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compare two dotted version identifiers. Numeric components are compared
|
|
104
|
+
* numerically, stable releases rank above prereleases, and prerelease
|
|
105
|
+
* suffixes are compared token-wise so `rc-9` beats `rc-10` only when
|
|
106
|
+
* numerically larger. Missing components count as zero, so `21` equals
|
|
107
|
+
* `21.0.0`.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} a First version
|
|
110
|
+
* @param {string} b Second version
|
|
111
|
+
* @returns {number} Negative when a < b, positive when a > b, zero on equality.
|
|
112
|
+
*/
|
|
113
|
+
export function compareVersions(a, b) {
|
|
114
|
+
const aCore = `${a}`.split("-")[0];
|
|
115
|
+
const bCore = `${b}`.split("-")[0];
|
|
116
|
+
const aComponents = aCore.split(".");
|
|
117
|
+
const bComponents = bCore.split(".");
|
|
118
|
+
const maxComponents = Math.max(aComponents.length, bComponents.length);
|
|
119
|
+
for (let i = 0; i < maxComponents; i++) {
|
|
120
|
+
const aPart = Number.parseInt(aComponents[i] ?? "0", 10) || 0;
|
|
121
|
+
const bPart = Number.parseInt(bComponents[i] ?? "0", 10) || 0;
|
|
122
|
+
if (aPart !== bPart) {
|
|
123
|
+
return aPart - bPart;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const aPre = isPrereleaseVersion(`${a}`);
|
|
127
|
+
const bPre = isPrereleaseVersion(`${b}`);
|
|
128
|
+
if (aPre !== bPre) {
|
|
129
|
+
return aPre ? -1 : 1;
|
|
130
|
+
}
|
|
131
|
+
if (!aPre) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
const aTokens = `${a}`.slice(aCore.length + 1).split(/[.-]/);
|
|
135
|
+
const bTokens = `${b}`.slice(bCore.length + 1).split(/[.-]/);
|
|
136
|
+
const maxTokens = Math.max(aTokens.length, bTokens.length);
|
|
137
|
+
for (let i = 0; i < maxTokens; i++) {
|
|
138
|
+
const aToken = aTokens[i] ?? "";
|
|
139
|
+
const bToken = bTokens[i] ?? "";
|
|
140
|
+
const aNum = Number.parseInt(aToken, 10);
|
|
141
|
+
const bNum = Number.parseInt(bToken, 10);
|
|
142
|
+
let comparison;
|
|
143
|
+
if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) {
|
|
144
|
+
comparison = aNum - bNum;
|
|
145
|
+
} else {
|
|
146
|
+
comparison = aToken.localeCompare(bToken);
|
|
147
|
+
}
|
|
148
|
+
if (comparison !== 0) {
|
|
149
|
+
return comparison;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return aTokens.length - bTokens.length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract the JDK major from a `java --version` style description such as
|
|
157
|
+
* `openjdk 21.0.11 2025-04-15` or a bare sdkman java identifier such as
|
|
158
|
+
* `25-tem`. General availability releases print an undotted major
|
|
159
|
+
* (`openjdk 25 2025-09-16`), so the first token starting with a digit is
|
|
160
|
+
* used rather than requiring a dotted version.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} versionDesc Version description string
|
|
163
|
+
* @returns {number|undefined} JDK major version.
|
|
164
|
+
*/
|
|
165
|
+
export function extractJavaMajor(versionDesc) {
|
|
166
|
+
if (typeof versionDesc !== "string") {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
for (const rawToken of versionDesc.trim().split(/\s+/)) {
|
|
170
|
+
// `java -version` quotes the identifier, e.g. openjdk version "17.0.9"
|
|
171
|
+
const token = rawToken.replaceAll('"', "");
|
|
172
|
+
const match = token.match(/^(\d+)(?:[.\-+_].*)?$/);
|
|
173
|
+
if (!match) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const major = Number.parseInt(match[1], 10);
|
|
177
|
+
if (Number.isNaN(major)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Legacy identifiers such as 1.8.0_452 name the major after the leading 1.
|
|
181
|
+
if (major === 1) {
|
|
182
|
+
const legacyMatch = token.match(/^1\.(\d+)/);
|
|
183
|
+
return legacyMatch ? Number.parseInt(legacyMatch[1], 10) : undefined;
|
|
184
|
+
}
|
|
185
|
+
return major;
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract the first version-looking token from a tool version description,
|
|
192
|
+
* such as `go version go1.23.1 darwin/arm64` or `rustc 1.82.0 (f6e511eec
|
|
193
|
+
* 2024-10-08)`. Leading letters (`v20.11.1`, `go1.23.1`) and quoting are
|
|
194
|
+
* stripped before validation.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} versionDesc Raw version description
|
|
197
|
+
* @returns {string|undefined} The version token, when one is found.
|
|
198
|
+
*/
|
|
199
|
+
export function extractVersionToken(versionDesc) {
|
|
200
|
+
if (typeof versionDesc !== "string") {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
for (const rawToken of versionDesc.trim().split(/\s+/)) {
|
|
204
|
+
const token = rawToken.replaceAll('"', "").replace(/^[^0-9]+/, "");
|
|
205
|
+
if (!token || !/^\d/.test(token)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (/^\d+(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/.test(token)) {
|
|
209
|
+
return token;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Verdict of a declared-requirement check. `unparseable` means the wanted or
|
|
217
|
+
* found value carries no comparable version, which is never evidence of a
|
|
218
|
+
* defect: an unparseable requirement must not produce a mismatch.
|
|
219
|
+
*
|
|
220
|
+
* @typedef {"satisfied"|"violated"|"unparseable"} RequirementVerdict
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check whether a found version satisfies a declared requirement. Wanted
|
|
225
|
+
* forms handled: exact versions (`3.9.9`), major-only and partial pins
|
|
226
|
+
* (`21`, `20.11`), `.x` ranges (`20.x`), caret and tilde ranges (`^20.11`,
|
|
227
|
+
* `~4.9`), comparison ranges (`>=18`, `<21`), hyphen ranges (`1.9 - 2.3`),
|
|
228
|
+
* whitespace-conjoined ranges (`>=18 <=20`), `||` alternatives, and the
|
|
229
|
+
* unconstrained `*`/`latest`. Anything else is unparseable and never
|
|
230
|
+
* violates.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} found Version actually present, possibly embedded in a
|
|
233
|
+
* longer description such as `go version go1.23.1 darwin/arm64`.
|
|
234
|
+
* @param {string} wanted Declared requirement.
|
|
235
|
+
* @returns {RequirementVerdict} Verdict of the check.
|
|
236
|
+
*/
|
|
237
|
+
export function checkVersionRequirement(found, wanted) {
|
|
238
|
+
const wantedText = `${wanted ?? ""}`.trim();
|
|
239
|
+
if (!wantedText || ["*", "x", "latest"].includes(wantedText.toLowerCase())) {
|
|
240
|
+
return "satisfied";
|
|
241
|
+
}
|
|
242
|
+
const foundToken = extractVersionToken(found);
|
|
243
|
+
if (!foundToken) {
|
|
244
|
+
return "unparseable";
|
|
245
|
+
}
|
|
246
|
+
let anyUnparseable = false;
|
|
247
|
+
for (const alternative of wantedText.split("||")) {
|
|
248
|
+
const verdict = checkAlternativeRequirement(foundToken, alternative.trim());
|
|
249
|
+
if (verdict === "satisfied") {
|
|
250
|
+
return "satisfied";
|
|
251
|
+
}
|
|
252
|
+
if (verdict === "unparseable") {
|
|
253
|
+
anyUnparseable = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return anyUnparseable ? "unparseable" : "violated";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check one `||` alternative, which may itself conjoin several whitespace
|
|
261
|
+
* separated constraints or a hyphen range.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} foundToken Extracted found version token
|
|
264
|
+
* @param {string} alternative One alternative of the wanted expression
|
|
265
|
+
* @returns {RequirementVerdict} Verdict of the check.
|
|
266
|
+
*/
|
|
267
|
+
function checkAlternativeRequirement(foundToken, alternative) {
|
|
268
|
+
if (!alternative) {
|
|
269
|
+
return "satisfied";
|
|
270
|
+
}
|
|
271
|
+
const hyphenRange = alternative.match(/^(\S+)\s+-\s+(\S+)$/);
|
|
272
|
+
if (hyphenRange) {
|
|
273
|
+
const lower = extractVersionToken(hyphenRange[1]);
|
|
274
|
+
const upper = extractVersionToken(hyphenRange[2]);
|
|
275
|
+
if (!lower || !upper) {
|
|
276
|
+
return "unparseable";
|
|
277
|
+
}
|
|
278
|
+
return compareVersions(foundToken, lower) >= 0 &&
|
|
279
|
+
compareVersions(foundToken, upper) <= 0
|
|
280
|
+
? "satisfied"
|
|
281
|
+
: "violated";
|
|
282
|
+
}
|
|
283
|
+
let unparseable = false;
|
|
284
|
+
for (const constraint of alternative.split(/\s+/).filter(Boolean)) {
|
|
285
|
+
const verdict = checkSingleConstraint(foundToken, constraint);
|
|
286
|
+
if (verdict === "violated") {
|
|
287
|
+
return "violated";
|
|
288
|
+
}
|
|
289
|
+
if (verdict === "unparseable") {
|
|
290
|
+
unparseable = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return unparseable ? "unparseable" : "satisfied";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check one constraint token against a found version token.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} foundToken Extracted found version token
|
|
300
|
+
* @param {string} constraint A single constraint such as `>=18` or `^20.11`
|
|
301
|
+
* @returns {RequirementVerdict} Verdict of the check.
|
|
302
|
+
*/
|
|
303
|
+
function checkSingleConstraint(foundToken, constraint) {
|
|
304
|
+
const operatorMatch = constraint.match(/^(>=|<=|==|=|>|<)\s*(.+)$/);
|
|
305
|
+
if (operatorMatch) {
|
|
306
|
+
const bound = extractVersionToken(operatorMatch[2]);
|
|
307
|
+
if (!bound) {
|
|
308
|
+
return "unparseable";
|
|
309
|
+
}
|
|
310
|
+
const comparison = compareVersions(foundToken, bound);
|
|
311
|
+
const satisfied = {
|
|
312
|
+
">=": comparison >= 0,
|
|
313
|
+
">": comparison > 0,
|
|
314
|
+
"<=": comparison <= 0,
|
|
315
|
+
"<": comparison < 0,
|
|
316
|
+
"=": comparison === 0,
|
|
317
|
+
"==": comparison === 0,
|
|
318
|
+
}[operatorMatch[1]];
|
|
319
|
+
return satisfied ? "satisfied" : "violated";
|
|
320
|
+
}
|
|
321
|
+
if (constraint.startsWith("^") || constraint.startsWith("~")) {
|
|
322
|
+
const bound = extractVersionToken(constraint.slice(1));
|
|
323
|
+
if (!bound) {
|
|
324
|
+
return "unparseable";
|
|
325
|
+
}
|
|
326
|
+
const core = bound.split("-")[0].split(".");
|
|
327
|
+
const major = Number.parseInt(core[0], 10) || 0;
|
|
328
|
+
const minor = Number.parseInt(core[1], 10) || 0;
|
|
329
|
+
let upper;
|
|
330
|
+
if (constraint.startsWith("^")) {
|
|
331
|
+
upper =
|
|
332
|
+
major > 0
|
|
333
|
+
? `${major + 1}.0.0`
|
|
334
|
+
: minor > 0
|
|
335
|
+
? `0.${minor + 1}.0`
|
|
336
|
+
: `0.0.${Number.parseInt(core[2], 10) ? Number.parseInt(core[2], 10) + 1 : 1}`;
|
|
337
|
+
} else {
|
|
338
|
+
upper = `${major}.${minor + 1}.0`;
|
|
339
|
+
}
|
|
340
|
+
return compareVersions(foundToken, bound) >= 0 &&
|
|
341
|
+
compareVersions(foundToken, upper) < 0
|
|
342
|
+
? "satisfied"
|
|
343
|
+
: "violated";
|
|
344
|
+
}
|
|
345
|
+
// Trailing `.x`/`.*` segments lower the pin's precision: `20.x` is a
|
|
346
|
+
// major-line pin, `20.11.x` a minor-line pin.
|
|
347
|
+
let cleaned = constraint;
|
|
348
|
+
while (/\.(x|X|\*)$/.test(cleaned)) {
|
|
349
|
+
cleaned = cleaned.slice(0, cleaned.lastIndexOf("."));
|
|
350
|
+
}
|
|
351
|
+
const core = cleaned.split("-")[0];
|
|
352
|
+
const parts = core.split(".");
|
|
353
|
+
if (!/^\d+$/.test(parts[0] || "")) {
|
|
354
|
+
return "unparseable";
|
|
355
|
+
}
|
|
356
|
+
if (parts.length === 1) {
|
|
357
|
+
// A bare major names the major line rather than an exact release.
|
|
358
|
+
return Number.parseInt(foundToken, 10) === Number.parseInt(parts[0], 10)
|
|
359
|
+
? "satisfied"
|
|
360
|
+
: "violated";
|
|
361
|
+
}
|
|
362
|
+
if (parts.length === 2) {
|
|
363
|
+
const foundMajor = extractMajorMinor(foundToken);
|
|
364
|
+
if (!foundMajor) {
|
|
365
|
+
return "unparseable";
|
|
366
|
+
}
|
|
367
|
+
return foundMajor.major === Number.parseInt(parts[0], 10) &&
|
|
368
|
+
foundMajor.minor === Number.parseInt(parts[1], 10)
|
|
369
|
+
? "satisfied"
|
|
370
|
+
: "violated";
|
|
371
|
+
}
|
|
372
|
+
return compareVersions(foundToken, cleaned) === 0 ? "satisfied" : "violated";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Extract the major and minor components of a version token.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} versionToken Version token such as `20.11.1`
|
|
379
|
+
* @returns {Object|undefined} `{ major, minor }`, when both are present.
|
|
380
|
+
*/
|
|
381
|
+
function extractMajorMinor(versionToken) {
|
|
382
|
+
const core = `${versionToken}`.split("-")[0].split(".");
|
|
383
|
+
if (!core[1]) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
const major = Number.parseInt(core[0], 10);
|
|
387
|
+
const minor = Number.parseInt(core[1], 10);
|
|
388
|
+
if (Number.isNaN(major) || Number.isNaN(minor)) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
return { major, minor };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Classify the result of a failed tool probe. A probe that was denied the
|
|
396
|
+
* right to run must be reported as `denied` — the tool may well be present —
|
|
397
|
+
* while only evidence that the command does not exist counts as `missing`.
|
|
398
|
+
*
|
|
399
|
+
* @param {Object|string|undefined} result Result object from spawnSync, or
|
|
400
|
+
* undefined when no result shape is known.
|
|
401
|
+
* @returns {"found"|"denied"|"missing"} Classification of the probe result.
|
|
402
|
+
*/
|
|
403
|
+
export function classifyProbeResult(result) {
|
|
404
|
+
if (!result || typeof result !== "object") {
|
|
405
|
+
return "denied";
|
|
406
|
+
}
|
|
407
|
+
if (result.status === 0 && !result.error) {
|
|
408
|
+
return "found";
|
|
409
|
+
}
|
|
410
|
+
if (
|
|
411
|
+
result.pid === undefined &&
|
|
412
|
+
result.status === undefined &&
|
|
413
|
+
result.signal === undefined &&
|
|
414
|
+
result.stdout === undefined &&
|
|
415
|
+
result.stderr === undefined
|
|
416
|
+
) {
|
|
417
|
+
// Deno's node:child_process returns an empty result when the runtime
|
|
418
|
+
// denies the spawn outright.
|
|
419
|
+
return "denied";
|
|
420
|
+
}
|
|
421
|
+
const error = result.error;
|
|
422
|
+
const errorCode = error?.code;
|
|
423
|
+
if (errorCode === "EACCES" || errorCode === "EPERM") {
|
|
424
|
+
return "denied";
|
|
425
|
+
}
|
|
426
|
+
const errorText =
|
|
427
|
+
typeof error === "string" ? error : `${error?.message || ""}`;
|
|
428
|
+
if (
|
|
429
|
+
error?.dryRun ||
|
|
430
|
+
errorCode === "CDXGEN_DRY_RUN" ||
|
|
431
|
+
/dry run|permission/i.test(errorText)
|
|
432
|
+
) {
|
|
433
|
+
return "denied";
|
|
434
|
+
}
|
|
435
|
+
return "missing";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Classify the active environment restriction that would stop a command from
|
|
440
|
+
* running, without the human-readable wording. `describeSpawnRestriction`
|
|
441
|
+
* derives its message from this classifier, and consumers that need to act on
|
|
442
|
+
* the cause rather than the prose use it directly.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} command Command that would run
|
|
445
|
+
* @returns {"dry-run"|"secure-mode"|"allowlist"|"deno"|undefined} Restriction cause, or undefined when nothing restricts the command.
|
|
446
|
+
*/
|
|
447
|
+
export function classifySpawnRestriction(command) {
|
|
448
|
+
if (isDryRun) {
|
|
449
|
+
return "dry-run";
|
|
450
|
+
}
|
|
451
|
+
if (
|
|
452
|
+
isSecureMode &&
|
|
453
|
+
globalThis.process?.permission?.has &&
|
|
454
|
+
!globalThis.process.permission.has("child")
|
|
455
|
+
) {
|
|
456
|
+
return "secure-mode";
|
|
457
|
+
}
|
|
458
|
+
const commandName = `${command}`.trim().split(" ")[0];
|
|
459
|
+
const allowlist = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS");
|
|
460
|
+
if (
|
|
461
|
+
allowlist &&
|
|
462
|
+
!allowlist
|
|
463
|
+
.split(",")
|
|
464
|
+
.map((entry) => entry.trim())
|
|
465
|
+
.includes(commandName)
|
|
466
|
+
) {
|
|
467
|
+
return "allowlist";
|
|
468
|
+
}
|
|
469
|
+
if (isDeno) {
|
|
470
|
+
const query = globalThis.Deno?.permissions?.querySync?.({
|
|
471
|
+
name: "run",
|
|
472
|
+
command: commandName,
|
|
473
|
+
});
|
|
474
|
+
if (query && query.state !== "granted") {
|
|
475
|
+
return "deno";
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const SPAWN_RESTRICTION_DESCRIPTIONS = {
|
|
482
|
+
"dry-run": "dry-run mode blocks command execution",
|
|
483
|
+
"secure-mode": "secure mode denies child process execution",
|
|
484
|
+
allowlist: "the command is not present in CDXGEN_ALLOWED_COMMANDS",
|
|
485
|
+
deno: "the Deno run permission is not granted for this command",
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Explain why a command probe may not run in the current environment, so a
|
|
490
|
+
* failed probe can be reported as degraded evidence rather than a missing
|
|
491
|
+
* tool.
|
|
492
|
+
*
|
|
493
|
+
* @param {string} command Command that was probed
|
|
494
|
+
* @returns {string|undefined} Human-readable restriction, or undefined when
|
|
495
|
+
* the environment does not restrict the command.
|
|
496
|
+
*/
|
|
497
|
+
export function describeSpawnRestriction(command) {
|
|
498
|
+
const cause = classifySpawnRestriction(command);
|
|
499
|
+
return cause ? SPAWN_RESTRICTION_DESCRIPTIONS[cause] : undefined;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Parse the content of an asdf/mise `.tool-versions` file into the first
|
|
504
|
+
* version each tool declares. Comment lines and inline comments are ignored;
|
|
505
|
+
* a tool line listing several versions keeps the first.
|
|
506
|
+
*
|
|
507
|
+
* @param {string} content `.tool-versions` file content
|
|
508
|
+
* @returns {Object|undefined} Map of tool name to declared version.
|
|
509
|
+
*/
|
|
510
|
+
export function parseToolVersionsFile(content) {
|
|
511
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
const pins = {};
|
|
515
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
516
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
517
|
+
if (!line) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const pieces = line.split(/\s+/);
|
|
521
|
+
const tool = (pieces.shift() || "").toLowerCase();
|
|
522
|
+
const wanted = pieces.find((piece) => /^[\w.+-]+$/.test(piece));
|
|
523
|
+
if (!tool || !/^[a-z][a-z0-9-]*$/.test(tool) || !wanted) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (!(tool in pins)) {
|
|
527
|
+
pins[tool] = wanted;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return Object.keys(pins).length ? pins : undefined;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Parse the `go` and `toolchain` directives of a `go.mod` file. The
|
|
535
|
+
* `toolchain` directive wins when both are present because it names the
|
|
536
|
+
* exact toolchain the module asks for.
|
|
537
|
+
*
|
|
538
|
+
* @param {string} content `go.mod` file content
|
|
539
|
+
* @returns {Object|undefined} `{ tool, version, directive }` with
|
|
540
|
+
* `directive` naming the line the version came from.
|
|
541
|
+
*/
|
|
542
|
+
export function parseGoModFile(content) {
|
|
543
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
let goDirective;
|
|
547
|
+
let toolchainDirective;
|
|
548
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
549
|
+
const line = rawLine.trim();
|
|
550
|
+
let match = line.match(/^toolchain\s+(\S+)$/);
|
|
551
|
+
if (match) {
|
|
552
|
+
toolchainDirective = match[1];
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
match = line.match(/^go\s+(\S+)$/);
|
|
556
|
+
if (match) {
|
|
557
|
+
goDirective = match[1];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const directive = toolchainDirective ? "toolchain" : "go";
|
|
561
|
+
const declared = toolchainDirective || goDirective;
|
|
562
|
+
if (!declared) {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
// Toolchain identifiers spell the prefix out (`go1.24.1`); the directive
|
|
566
|
+
// line does not (`1.23.0`).
|
|
567
|
+
const version = declared.replace(/^go(?=\d)/, "");
|
|
568
|
+
if (!/^\d[\w.-]*$/.test(version)) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
return { tool: "go", version, directive };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Check whether a rust channel value looks plausible: a semver, a channel
|
|
576
|
+
* name such as `stable`, or a dated nightly.
|
|
577
|
+
*
|
|
578
|
+
* @param {string} channel Raw channel value
|
|
579
|
+
* @returns {boolean} True when the value is a plausible channel.
|
|
580
|
+
*/
|
|
581
|
+
function isPlausibleRustChannel(channel) {
|
|
582
|
+
return /^[\w][\w.-]*$/.test(channel);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Parse a `rust-toolchain.toml` or plain `rust-toolchain` file into its
|
|
587
|
+
* channel. Target-specific subsections are ignored, so only the `[toolchain]`
|
|
588
|
+
* section's `channel` key is read.
|
|
589
|
+
*
|
|
590
|
+
* @param {string} content Toolchain file content
|
|
591
|
+
* @returns {Object|undefined} `{ channel }` when a channel is declared.
|
|
592
|
+
*/
|
|
593
|
+
export function parseRustToolchainFile(content) {
|
|
594
|
+
if (typeof content !== "string") {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const lines = content
|
|
598
|
+
.split(/\r?\n/)
|
|
599
|
+
.map((line) => line.replace(/#.*$/, "").trim())
|
|
600
|
+
.filter(Boolean);
|
|
601
|
+
if (!lines.length) {
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
if (!content.includes("=")) {
|
|
605
|
+
// Plain toolchain files hold just the channel string.
|
|
606
|
+
return isPlausibleRustChannel(lines[0]) ? { channel: lines[0] } : undefined;
|
|
607
|
+
}
|
|
608
|
+
let channel;
|
|
609
|
+
let inToolchainSection = false;
|
|
610
|
+
for (const line of lines) {
|
|
611
|
+
if (line.startsWith("[")) {
|
|
612
|
+
inToolchainSection = line.replace(/^\[|\]$/g, "").trim() === "toolchain";
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const separatorIndex = line.indexOf("=");
|
|
616
|
+
if (separatorIndex <= 0 || !inToolchainSection) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (line.slice(0, separatorIndex).trim() !== "channel") {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
channel = line
|
|
623
|
+
.slice(separatorIndex + 1)
|
|
624
|
+
.trim()
|
|
625
|
+
.replace(/^["']|["']$/g, "");
|
|
626
|
+
}
|
|
627
|
+
return channel && isPlausibleRustChannel(channel) ? { channel } : undefined;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Parse a `package.json` content into the tool requirements it declares
|
|
632
|
+
* through `engines` and `packageManager`. The `packageManager` pin's
|
|
633
|
+
* integrity-hash suffix (`pnpm@9.1.0+sha512.…`) is stripped to the bare
|
|
634
|
+
* version.
|
|
635
|
+
*
|
|
636
|
+
* @param {string} content `package.json` content
|
|
637
|
+
* @returns {Object|undefined} Map of tool name to declared version.
|
|
638
|
+
*/
|
|
639
|
+
export function parsePackageJsonToolRequirements(content) {
|
|
640
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
let parsed;
|
|
644
|
+
try {
|
|
645
|
+
parsed = JSON.parse(content);
|
|
646
|
+
} catch {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
if (!parsed || typeof parsed !== "object") {
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
const requirements = {};
|
|
653
|
+
const enginesNode = parsed.engines?.node;
|
|
654
|
+
if (typeof enginesNode === "string" && enginesNode.trim()) {
|
|
655
|
+
requirements.node = enginesNode.trim();
|
|
656
|
+
}
|
|
657
|
+
const packageManager = parsed.packageManager;
|
|
658
|
+
if (typeof packageManager === "string") {
|
|
659
|
+
const atSignIndex = packageManager.indexOf("@");
|
|
660
|
+
if (atSignIndex > 0) {
|
|
661
|
+
const name = packageManager.slice(0, atSignIndex).toLowerCase();
|
|
662
|
+
const version = packageManager.slice(atSignIndex + 1).split("+")[0];
|
|
663
|
+
if (/^[a-z][a-z0-9-]*$/.test(name) && /^[\w.+-]+$/.test(version)) {
|
|
664
|
+
requirements[name] = version;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return Object.keys(requirements).length ? requirements : undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Parse a `global.json` file into the .NET SDK version it pins.
|
|
673
|
+
*
|
|
674
|
+
* @param {string} content `global.json` content
|
|
675
|
+
* @returns {Object|undefined} Map with the `dotnet` requirement.
|
|
676
|
+
*/
|
|
677
|
+
export function parseGlobalJsonToolRequirements(content) {
|
|
678
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
let parsed;
|
|
682
|
+
try {
|
|
683
|
+
parsed = JSON.parse(content);
|
|
684
|
+
} catch {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
const version = parsed?.sdk?.version;
|
|
688
|
+
if (typeof version !== "string" || !/^[\w.+-]+$/.test(version.trim())) {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
return { dotnet: version.trim() };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Parse the `requires-python` declaration of a `pyproject.toml` file.
|
|
696
|
+
*
|
|
697
|
+
* @param {string} content `pyproject.toml` content
|
|
698
|
+
* @returns {Object|undefined} Map with the `python` requirement.
|
|
699
|
+
*/
|
|
700
|
+
export function parsePyprojectRequiresPython(content) {
|
|
701
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
705
|
+
const match = rawLine.match(/^\s*requires-python\s*=\s*["']([^"']+)["']/);
|
|
706
|
+
if (match) {
|
|
707
|
+
return { python: match[1].trim() };
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Parse an `.nvmrc` file into the Node.js version it requests.
|
|
715
|
+
*
|
|
716
|
+
* @param {string} content `.nvmrc` content
|
|
717
|
+
* @returns {Object|undefined} Map with the `node` requirement.
|
|
718
|
+
*/
|
|
719
|
+
export function parseNvmrc(content) {
|
|
720
|
+
if (typeof content !== "string") {
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
const value = content
|
|
724
|
+
.split(/\r?\n/)[0]
|
|
725
|
+
.replace(/#.*$/, "")
|
|
726
|
+
.trim()
|
|
727
|
+
.replace(/^v/, "");
|
|
728
|
+
return value ? { node: value } : undefined;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Parse a `.python-version` file into the interpreter version it requests.
|
|
733
|
+
*
|
|
734
|
+
* @param {string} content `.python-version` content
|
|
735
|
+
* @returns {Object|undefined} Map with the `python` requirement.
|
|
736
|
+
*/
|
|
737
|
+
export function parsePythonVersionFile(content) {
|
|
738
|
+
if (typeof content !== "string") {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
const value = content.split(/\r?\n/)[0].replace(/#.*$/, "").trim();
|
|
742
|
+
return value ? { python: value } : undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Parse a `.java-version` file into the JDK version it pins. Both the bare
|
|
747
|
+
* major (`21`) and a full version (`21.0.5`) occur in the wild, and both are
|
|
748
|
+
* passed through verbatim — an install command needs the exact declaration.
|
|
749
|
+
*
|
|
750
|
+
* The legacy `1.N` spelling of a pre-9 release (`1.8`, `1.8.0_302`) is
|
|
751
|
+
* normalised to its modern major (`8`), because it is the major line that
|
|
752
|
+
* names a package: the literal declaration yields `Temurin.1.JDK`, which no
|
|
753
|
+
* provisioner offers.
|
|
754
|
+
*
|
|
755
|
+
* Distribution-prefixed values written by version managers (`temurin-21`,
|
|
756
|
+
* `openjdk64-17.0.1`) name a vendor build rather than a release, so they are
|
|
757
|
+
* left unparsed rather than turned into an install argument no provisioner
|
|
758
|
+
* accepts; the run then reports the version as unresolved and the agent asks.
|
|
759
|
+
*
|
|
760
|
+
* @param {string} content `.java-version` content
|
|
761
|
+
* @returns {Object|undefined} Map with the `java` requirement.
|
|
762
|
+
*/
|
|
763
|
+
export function parseJavaVersionFile(content) {
|
|
764
|
+
if (typeof content !== "string") {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
const value = content.split(/\r?\n/)[0].replace(/#.*$/, "").trim();
|
|
768
|
+
if (!/^\d[\w.+-]*$/.test(value)) {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
const legacy = /^1\.(\d+)(?:[._-].*)?$/.exec(value);
|
|
772
|
+
return { java: legacy ? legacy[1] : value };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Parse the `BUNDLED WITH` section of a `Gemfile.lock` into the bundler
|
|
777
|
+
* version the lockfile was written with.
|
|
778
|
+
*
|
|
779
|
+
* @param {string} content `Gemfile.lock` content
|
|
780
|
+
* @returns {string|undefined} Bundler version, when declared.
|
|
781
|
+
*/
|
|
782
|
+
export function parseGemfileLockBundlerVersion(content) {
|
|
783
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
const lines = content.split(/\r?\n/);
|
|
787
|
+
for (let index = 0; index < lines.length; index++) {
|
|
788
|
+
if (lines[index].trim() !== "BUNDLED WITH") {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
for (
|
|
792
|
+
let next = index + 1;
|
|
793
|
+
next < Math.min(index + 3, lines.length);
|
|
794
|
+
next++
|
|
795
|
+
) {
|
|
796
|
+
const value = lines[next].trim();
|
|
797
|
+
if (!value) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
return /^\d[\w.-]*$/.test(value) ? value : undefined;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return undefined;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Read every declared tool requirement a project directory declares through
|
|
808
|
+
* its well-known tool pin files: `.tool-versions`, `.nvmrc`, `.java-version`,
|
|
809
|
+
* `.python-version`, `pyproject.toml`, `go.mod`, `rust-toolchain.toml`,
|
|
810
|
+
* `rust-toolchain`, `package.json`, and `global.json`. Missing files are
|
|
811
|
+
* skipped; each entry names the file that declared it so the report can
|
|
812
|
+
* trace every expectation back to its source.
|
|
813
|
+
*
|
|
814
|
+
* @param {string} projectPath Project directory to inspect
|
|
815
|
+
* @returns {Array<{tool: string, wanted: string, source: string, ecosystem: string, path: string}>}
|
|
816
|
+
* Declared requirements, empty when the directory declares none.
|
|
817
|
+
*/
|
|
818
|
+
export function readDeclaredToolRequirements(projectPath) {
|
|
819
|
+
const requirements = [];
|
|
820
|
+
if (!projectPath) {
|
|
821
|
+
return requirements;
|
|
822
|
+
}
|
|
823
|
+
const addRequirements = (pins, fileName) => {
|
|
824
|
+
if (!pins) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const filePath = join(projectPath, fileName);
|
|
828
|
+
for (const [tool, wanted] of Object.entries(pins)) {
|
|
829
|
+
const trimmedWanted = `${wanted}`.trim();
|
|
830
|
+
if (!tool || !trimmedWanted) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
requirements.push({
|
|
834
|
+
tool,
|
|
835
|
+
wanted: trimmedWanted,
|
|
836
|
+
source: fileName,
|
|
837
|
+
ecosystem: ecosystemForTool(tool),
|
|
838
|
+
path: filePath,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
const readRequirementFile = (fileName) => {
|
|
843
|
+
const filePath = join(projectPath, fileName);
|
|
844
|
+
if (!safeExistsSync(filePath)) {
|
|
845
|
+
return undefined;
|
|
846
|
+
}
|
|
847
|
+
const content = safeReadFileSync(filePath, "utf-8");
|
|
848
|
+
return typeof content === "string"
|
|
849
|
+
? content.slice(0, MAX_REQUIREMENTS_FILE_BYTES)
|
|
850
|
+
: undefined;
|
|
851
|
+
};
|
|
852
|
+
const toolVersionsPins = parseToolVersionsFile(
|
|
853
|
+
readRequirementFile(".tool-versions"),
|
|
854
|
+
);
|
|
855
|
+
addRequirements(toolVersionsPins, ".tool-versions");
|
|
856
|
+
addRequirements(parseNvmrc(readRequirementFile(".nvmrc")), ".nvmrc");
|
|
857
|
+
addRequirements(
|
|
858
|
+
parseJavaVersionFile(readRequirementFile(".java-version")),
|
|
859
|
+
".java-version",
|
|
860
|
+
);
|
|
861
|
+
addRequirements(
|
|
862
|
+
parsePythonVersionFile(readRequirementFile(".python-version")),
|
|
863
|
+
".python-version",
|
|
864
|
+
);
|
|
865
|
+
const pyprojectPins = parsePyprojectRequiresPython(
|
|
866
|
+
readRequirementFile("pyproject.toml"),
|
|
867
|
+
);
|
|
868
|
+
if (pyprojectPins) {
|
|
869
|
+
addRequirements(
|
|
870
|
+
{ python: pyprojectPins.python },
|
|
871
|
+
"pyproject.toml:requires-python",
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
const goMod = parseGoModFile(readRequirementFile("go.mod"));
|
|
875
|
+
if (goMod) {
|
|
876
|
+
addRequirements(
|
|
877
|
+
{ [goMod.tool]: goMod.version },
|
|
878
|
+
`go.mod:${goMod.directive}`,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
const rustToml = parseRustToolchainFile(
|
|
882
|
+
readRequirementFile("rust-toolchain.toml"),
|
|
883
|
+
);
|
|
884
|
+
if (rustToml) {
|
|
885
|
+
addRequirements({ rustc: rustToml.channel }, "rust-toolchain.toml");
|
|
886
|
+
} else {
|
|
887
|
+
const rustPlain = parseRustToolchainFile(
|
|
888
|
+
readRequirementFile("rust-toolchain"),
|
|
889
|
+
);
|
|
890
|
+
if (rustPlain) {
|
|
891
|
+
addRequirements({ rustc: rustPlain.channel }, "rust-toolchain");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
addRequirements(
|
|
895
|
+
parsePackageJsonToolRequirements(readRequirementFile("package.json")),
|
|
896
|
+
"package.json",
|
|
897
|
+
);
|
|
898
|
+
addRequirements(
|
|
899
|
+
parseGlobalJsonToolRequirements(readRequirementFile("global.json")),
|
|
900
|
+
"global.json",
|
|
901
|
+
);
|
|
902
|
+
return requirements;
|
|
903
|
+
}
|