@cyclonedx/cdxgen 12.3.3 → 12.4.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/README.md +69 -25
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +270 -127
- package/bin/convert.js +34 -15
- package/bin/hbom.js +495 -0
- package/bin/repl.js +592 -37
- package/bin/validate.js +31 -4
- package/bin/verify.js +18 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +14 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +210 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +527 -99
- package/lib/cli/index.poku.js +1469 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +350 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/protobom.js +209 -45
- package/lib/helpers/protobom.poku.js +183 -5
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +4 -28
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +1366 -31
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +406 -8
- package/lib/stages/postgen/postgen.poku.js +484 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/lib/validator/bomValidator.js +90 -38
- package/lib/validator/bomValidator.poku.js +90 -0
- package/lib/validator/complianceRules.js +4 -2
- package/lib/validator/index.poku.js +14 -0
- package/package.json +23 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +5 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/protobomLoader.d.ts +17 -0
- package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +45 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -1
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -1
- 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/third-party/arborist/lib/node.d.ts +23 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
package/lib/helpers/utils.js
CHANGED
|
@@ -56,17 +56,25 @@ import { getTreeWithPlugin } from "../managers/piptree.js";
|
|
|
56
56
|
import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
|
|
57
57
|
import Arborist from "../third-party/arborist/lib/index.js";
|
|
58
58
|
import { analyzeSuspiciousJsFile } from "./analyzer.js";
|
|
59
|
+
import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "./auditCategories.js";
|
|
59
60
|
import { parseWorkflowFile } from "./ciParsers/githubActions.js";
|
|
60
61
|
import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
|
|
62
|
+
import {
|
|
63
|
+
createOccurrenceEvidence,
|
|
64
|
+
parseOccurrenceEvidenceLocation,
|
|
65
|
+
} from "./evidenceUtils.js";
|
|
66
|
+
import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
|
|
61
67
|
import { thoughtLog, traceLog } from "./logger.js";
|
|
62
68
|
import { createLolbasProperties } from "./lolbas.js";
|
|
63
69
|
import {
|
|
70
|
+
createOsQueryFallbackBomRef,
|
|
64
71
|
createOsQueryPurl,
|
|
65
72
|
deriveOsQueryDescription,
|
|
66
73
|
deriveOsQueryName,
|
|
67
74
|
deriveOsQueryPublisher,
|
|
68
75
|
deriveOsQueryVersion,
|
|
69
76
|
sanitizeOsQueryIdentity,
|
|
77
|
+
shouldCreateOsQueryPurl,
|
|
70
78
|
} from "./osqueryTransform.js";
|
|
71
79
|
import {
|
|
72
80
|
collectPyLockFileComponents,
|
|
@@ -117,6 +125,596 @@ export const DRY_RUN_ERROR_CODE = "CDXGEN_DRY_RUN";
|
|
|
117
125
|
const activityLedger = [];
|
|
118
126
|
let activityCounter = 0;
|
|
119
127
|
let currentActivityContext = {};
|
|
128
|
+
const dryRunReadTraceState =
|
|
129
|
+
globalThis.__cdxgenDryRunReadTraceState ||
|
|
130
|
+
(globalThis.__cdxgenDryRunReadTraceState = {
|
|
131
|
+
environmentReads: new Map(),
|
|
132
|
+
observations: new Map(),
|
|
133
|
+
recordActivity: undefined,
|
|
134
|
+
sensitiveFileReads: new Map(),
|
|
135
|
+
});
|
|
136
|
+
const SENSITIVE_ENV_VAR_PATTERN =
|
|
137
|
+
/(^|_)(?:token|key|secret|pass(?:word)?|credential(?:s)?|cred|auth|session|cookie|email|user)$/i;
|
|
138
|
+
const DIRECTORY_DISCOVERY_NAMES = new Set([
|
|
139
|
+
".cargo",
|
|
140
|
+
".docker",
|
|
141
|
+
".gem",
|
|
142
|
+
".github",
|
|
143
|
+
".m2",
|
|
144
|
+
".nuget",
|
|
145
|
+
".venv",
|
|
146
|
+
".yarn",
|
|
147
|
+
"blobs",
|
|
148
|
+
"extensions",
|
|
149
|
+
"node_modules",
|
|
150
|
+
"target",
|
|
151
|
+
"vendor",
|
|
152
|
+
]);
|
|
153
|
+
const LOCKFILE_ACTIVITY_HINTS = new Map([
|
|
154
|
+
[
|
|
155
|
+
"bun.lock",
|
|
156
|
+
{ classification: "lockfile", ecosystem: "bun", label: "Bun lockfile" },
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"cargo.lock",
|
|
160
|
+
{ classification: "lockfile", ecosystem: "cargo", label: "Cargo lockfile" },
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"composer.lock",
|
|
164
|
+
{
|
|
165
|
+
classification: "lockfile",
|
|
166
|
+
ecosystem: "composer",
|
|
167
|
+
label: "Composer lockfile",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"gemfile.lock",
|
|
172
|
+
{
|
|
173
|
+
classification: "lockfile",
|
|
174
|
+
ecosystem: "rubygems",
|
|
175
|
+
label: "Bundler lockfile",
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"package-lock.json",
|
|
180
|
+
{ classification: "lockfile", ecosystem: "npm", label: "npm lockfile" },
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
"packages.lock.json",
|
|
184
|
+
{ classification: "lockfile", ecosystem: "nuget", label: "NuGet lockfile" },
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
"pdm.lock",
|
|
188
|
+
{ classification: "lockfile", ecosystem: "python", label: "PDM lockfile" },
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
"pnpm-lock.yaml",
|
|
192
|
+
{ classification: "lockfile", ecosystem: "pnpm", label: "pnpm lockfile" },
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
"poetry.lock",
|
|
196
|
+
{
|
|
197
|
+
classification: "lockfile",
|
|
198
|
+
ecosystem: "python",
|
|
199
|
+
label: "Poetry lockfile",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"podfile.lock",
|
|
204
|
+
{
|
|
205
|
+
classification: "lockfile",
|
|
206
|
+
ecosystem: "cocoapods",
|
|
207
|
+
label: "CocoaPods lockfile",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
"pylock.toml",
|
|
212
|
+
{
|
|
213
|
+
classification: "lockfile",
|
|
214
|
+
ecosystem: "python",
|
|
215
|
+
label: "PEP 751 lockfile",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
"uv.lock",
|
|
220
|
+
{ classification: "lockfile", ecosystem: "python", label: "uv lockfile" },
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"yarn.lock",
|
|
224
|
+
{ classification: "lockfile", ecosystem: "yarn", label: "Yarn lockfile" },
|
|
225
|
+
],
|
|
226
|
+
]);
|
|
227
|
+
const MANIFEST_ACTIVITY_HINTS = new Map([
|
|
228
|
+
[
|
|
229
|
+
"cargo.toml",
|
|
230
|
+
{ classification: "manifest", ecosystem: "cargo", label: "Cargo manifest" },
|
|
231
|
+
],
|
|
232
|
+
[
|
|
233
|
+
"composer.json",
|
|
234
|
+
{
|
|
235
|
+
classification: "manifest",
|
|
236
|
+
ecosystem: "composer",
|
|
237
|
+
label: "Composer manifest",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
"gemfile",
|
|
242
|
+
{
|
|
243
|
+
classification: "manifest",
|
|
244
|
+
ecosystem: "rubygems",
|
|
245
|
+
label: "Gem manifest",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
"package.json",
|
|
250
|
+
{ classification: "manifest", ecosystem: "npm", label: "package manifest" },
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
"pom.xml",
|
|
254
|
+
{ classification: "manifest", ecosystem: "maven", label: "Maven manifest" },
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
"pyproject.toml",
|
|
258
|
+
{
|
|
259
|
+
classification: "manifest",
|
|
260
|
+
ecosystem: "python",
|
|
261
|
+
label: "Python project manifest",
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
[
|
|
265
|
+
"requirements.txt",
|
|
266
|
+
{
|
|
267
|
+
classification: "manifest",
|
|
268
|
+
ecosystem: "python",
|
|
269
|
+
label: "Python requirements manifest",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"setup.py",
|
|
274
|
+
{
|
|
275
|
+
classification: "manifest",
|
|
276
|
+
ecosystem: "python",
|
|
277
|
+
label: "Python setup manifest",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
]);
|
|
281
|
+
const SENSITIVE_CONFIG_ACTIVITY_HINTS = [
|
|
282
|
+
{
|
|
283
|
+
matcher: (lowerPath, _baseName) =>
|
|
284
|
+
lowerPath.includes("/.cargo/config.toml") ||
|
|
285
|
+
lowerPath.endsWith("/.cargo/credentials") ||
|
|
286
|
+
lowerPath.endsWith("/.cargo/credentials.toml"),
|
|
287
|
+
metadata: {
|
|
288
|
+
classification: "config",
|
|
289
|
+
ecosystem: "cargo",
|
|
290
|
+
label: "Cargo registry configuration",
|
|
291
|
+
sensitive: true,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
matcher: (lowerPath, baseName) =>
|
|
296
|
+
lowerPath.includes("/.docker/config.json") ||
|
|
297
|
+
(baseName === "config.json" && lowerPath.includes("/docker")),
|
|
298
|
+
metadata: {
|
|
299
|
+
classification: "credential",
|
|
300
|
+
ecosystem: "oci",
|
|
301
|
+
label: "Docker credential file",
|
|
302
|
+
sensitive: true,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
matcher: (lowerPath) => lowerPath.endsWith("/.gem/credentials"),
|
|
307
|
+
metadata: {
|
|
308
|
+
classification: "credential",
|
|
309
|
+
ecosystem: "rubygems",
|
|
310
|
+
label: "RubyGems credentials file",
|
|
311
|
+
sensitive: true,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
matcher: (_lowerPath, baseName) =>
|
|
316
|
+
baseName === ".npmrc" || baseName === ".pnpmrc" || baseName === ".yarnrc",
|
|
317
|
+
metadata: {
|
|
318
|
+
classification: "config",
|
|
319
|
+
ecosystem: "npm",
|
|
320
|
+
label: "JavaScript package manager configuration",
|
|
321
|
+
sensitive: true,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
matcher: (_lowerPath, baseName) => baseName === ".yarnrc.yml",
|
|
326
|
+
metadata: {
|
|
327
|
+
classification: "config",
|
|
328
|
+
ecosystem: "yarn",
|
|
329
|
+
label: "Yarn configuration",
|
|
330
|
+
sensitive: true,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
matcher: (_lowerPath, baseName) =>
|
|
335
|
+
baseName === ".pypirc" || baseName === "pip.conf",
|
|
336
|
+
metadata: {
|
|
337
|
+
classification: "config",
|
|
338
|
+
ecosystem: "python",
|
|
339
|
+
label: "Python package publishing configuration",
|
|
340
|
+
sensitive: true,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
matcher: (_lowerPath, baseName) =>
|
|
345
|
+
baseName === "uv.toml" || baseName === "poetry.toml",
|
|
346
|
+
metadata: {
|
|
347
|
+
classification: "config",
|
|
348
|
+
ecosystem: "python",
|
|
349
|
+
label: "Python package manager configuration",
|
|
350
|
+
sensitive: true,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
matcher: (_lowerPath, baseName) => baseName === "nuget.config",
|
|
355
|
+
metadata: {
|
|
356
|
+
classification: "config",
|
|
357
|
+
ecosystem: "nuget",
|
|
358
|
+
label: "NuGet configuration",
|
|
359
|
+
sensitive: true,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
matcher: (_lowerPath, baseName) => baseName === "settings.xml",
|
|
364
|
+
metadata: {
|
|
365
|
+
classification: "config",
|
|
366
|
+
ecosystem: "maven",
|
|
367
|
+
label: "Maven settings.xml",
|
|
368
|
+
sensitive: true,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
const CERTIFICATE_FILE_EXTENSIONS = new Set([".crt", ".cer", ".pem"]);
|
|
373
|
+
const KEY_FILE_EXTENSIONS = new Set([
|
|
374
|
+
".key",
|
|
375
|
+
".jks",
|
|
376
|
+
".keystore",
|
|
377
|
+
".p12",
|
|
378
|
+
".pfx",
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
const buildReadCountSuffix = (count) => (count > 1 ? ` (${count} times)` : "");
|
|
382
|
+
|
|
383
|
+
const buildEnvironmentReadReason = (varName, count, sensitive) =>
|
|
384
|
+
`Read ${sensitive ? "sensitive " : ""}environment variable ${varName}${buildReadCountSuffix(count)}.`;
|
|
385
|
+
|
|
386
|
+
const buildSensitiveFileReadReason = (filePath, count, label) =>
|
|
387
|
+
`Read ${label} ${filePath}${buildReadCountSuffix(count)}.`;
|
|
388
|
+
|
|
389
|
+
function emitActivity(activity) {
|
|
390
|
+
if (typeof dryRunReadTraceState.recordActivity !== "function") {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
return dryRunReadTraceState.recordActivity(activity);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function classifyActivityPath(filePath) {
|
|
397
|
+
if (typeof filePath !== "string" || !filePath.length) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
401
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
402
|
+
const baseName = basename(lowerPath);
|
|
403
|
+
if (LOCKFILE_ACTIVITY_HINTS.has(baseName)) {
|
|
404
|
+
return LOCKFILE_ACTIVITY_HINTS.get(baseName);
|
|
405
|
+
}
|
|
406
|
+
if (MANIFEST_ACTIVITY_HINTS.has(baseName)) {
|
|
407
|
+
return MANIFEST_ACTIVITY_HINTS.get(baseName);
|
|
408
|
+
}
|
|
409
|
+
for (const { matcher, metadata } of SENSITIVE_CONFIG_ACTIVITY_HINTS) {
|
|
410
|
+
if (matcher(lowerPath, baseName)) {
|
|
411
|
+
return metadata;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
lowerPath.includes("/cache/") ||
|
|
416
|
+
lowerPath.includes("/.cache/") ||
|
|
417
|
+
lowerPath.includes("/caches/")
|
|
418
|
+
) {
|
|
419
|
+
return {
|
|
420
|
+
classification: "cache",
|
|
421
|
+
label: "cache path",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (
|
|
425
|
+
CERTIFICATE_FILE_EXTENSIONS.has(extname(baseName)) ||
|
|
426
|
+
baseName === "cert.pem"
|
|
427
|
+
) {
|
|
428
|
+
return {
|
|
429
|
+
classification: "certificate",
|
|
430
|
+
label: "certificate file",
|
|
431
|
+
sensitive: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (
|
|
435
|
+
KEY_FILE_EXTENSIONS.has(extname(baseName)) ||
|
|
436
|
+
baseName === "key.pem" ||
|
|
437
|
+
baseName.startsWith("id_")
|
|
438
|
+
) {
|
|
439
|
+
return {
|
|
440
|
+
classification: "key",
|
|
441
|
+
label: "private key file",
|
|
442
|
+
sensitive: true,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const trimmedPath = normalizedPath.endsWith("/")
|
|
446
|
+
? normalizedPath.slice(0, -1)
|
|
447
|
+
: normalizedPath;
|
|
448
|
+
const directoryName = basename(trimmedPath.toLowerCase());
|
|
449
|
+
if (DIRECTORY_DISCOVERY_NAMES.has(directoryName)) {
|
|
450
|
+
return {
|
|
451
|
+
classification: "directory",
|
|
452
|
+
label: "directory discovery path",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function classifyDiscoveryPattern(pattern) {
|
|
459
|
+
const patternValue = Array.isArray(pattern)
|
|
460
|
+
? pattern.join(",")
|
|
461
|
+
: String(pattern);
|
|
462
|
+
const lowerPattern = patternValue.toLowerCase();
|
|
463
|
+
if (
|
|
464
|
+
lowerPattern.includes("package-lock.json") ||
|
|
465
|
+
lowerPattern.includes("pnpm-lock.yaml") ||
|
|
466
|
+
lowerPattern.includes("yarn.lock") ||
|
|
467
|
+
lowerPattern.includes("poetry.lock") ||
|
|
468
|
+
lowerPattern.includes("uv.lock") ||
|
|
469
|
+
lowerPattern.includes("cargo.lock") ||
|
|
470
|
+
lowerPattern.includes("gemfile.lock")
|
|
471
|
+
) {
|
|
472
|
+
return {
|
|
473
|
+
discoveryType: "lockfile-discovery",
|
|
474
|
+
label: "lockfile discovery",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (
|
|
478
|
+
lowerPattern.includes("package.json") ||
|
|
479
|
+
lowerPattern.includes("pom.xml") ||
|
|
480
|
+
lowerPattern.includes("pyproject.toml") ||
|
|
481
|
+
lowerPattern.includes("cargo.toml") ||
|
|
482
|
+
lowerPattern.includes("composer.json")
|
|
483
|
+
) {
|
|
484
|
+
return {
|
|
485
|
+
discoveryType: "manifest-discovery",
|
|
486
|
+
label: "manifest discovery",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
discoveryType: "directory-enumeration",
|
|
491
|
+
label: "directory enumeration",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function recordDeduplicatedRead(traceMap, traceKey, activity, createReason) {
|
|
496
|
+
const existingTrace = traceMap.get(traceKey);
|
|
497
|
+
if (existingTrace) {
|
|
498
|
+
existingTrace.count += 1;
|
|
499
|
+
if (existingTrace.entry) {
|
|
500
|
+
existingTrace.entry.count = existingTrace.count;
|
|
501
|
+
existingTrace.entry.reason = createReason(existingTrace.count);
|
|
502
|
+
}
|
|
503
|
+
return existingTrace.entry;
|
|
504
|
+
}
|
|
505
|
+
const entry = emitActivity({
|
|
506
|
+
...activity,
|
|
507
|
+
reason: createReason(1),
|
|
508
|
+
});
|
|
509
|
+
if (entry) {
|
|
510
|
+
entry.count = 1;
|
|
511
|
+
}
|
|
512
|
+
traceMap.set(traceKey, {
|
|
513
|
+
count: 1,
|
|
514
|
+
entry,
|
|
515
|
+
});
|
|
516
|
+
return entry;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function isSensitiveEnvironmentVariableName(varName) {
|
|
520
|
+
return typeof varName === "string" && SENSITIVE_ENV_VAR_PATTERN.test(varName);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function recordObservedActivity(kind, target, options = {}) {
|
|
524
|
+
if (!(isDryRun || DEBUG_MODE) || !kind || !target) {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
const status = options.status || "completed";
|
|
528
|
+
const traceKey =
|
|
529
|
+
options.traceKey ||
|
|
530
|
+
`${kind}:${status}:${target}:${options.traceDetail || ""}`;
|
|
531
|
+
const metadata = options.metadata || {};
|
|
532
|
+
const reasonBuilder =
|
|
533
|
+
options.reasonBuilder ||
|
|
534
|
+
((count) =>
|
|
535
|
+
options.reason
|
|
536
|
+
? `${options.reason}${buildReadCountSuffix(count)}`
|
|
537
|
+
: `Recorded ${kind} activity for ${target}${buildReadCountSuffix(count)}.`);
|
|
538
|
+
return recordDeduplicatedRead(
|
|
539
|
+
dryRunReadTraceState.observations,
|
|
540
|
+
traceKey,
|
|
541
|
+
{
|
|
542
|
+
kind,
|
|
543
|
+
status,
|
|
544
|
+
target,
|
|
545
|
+
...metadata,
|
|
546
|
+
},
|
|
547
|
+
reasonBuilder,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function recordDecisionActivity(target, options = {}) {
|
|
552
|
+
return recordObservedActivity(options.kind || "decision", target, options);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function recordDiscoveryActivity(target, options = {}) {
|
|
556
|
+
return recordObservedActivity(options.kind || "discover", target, options);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function recordPolicyActivity(target, options = {}) {
|
|
560
|
+
return recordObservedActivity(options.kind || "policy", target, options);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizeRecordedPathForComparison(
|
|
564
|
+
candidatePath,
|
|
565
|
+
basePath = undefined,
|
|
566
|
+
) {
|
|
567
|
+
if (typeof candidatePath !== "string" || !candidatePath.length) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
let normalizedPath = candidatePath.replaceAll("\\", "/");
|
|
571
|
+
if (basePath && path.isAbsolute(candidatePath)) {
|
|
572
|
+
const resolvedBasePath = resolve(basePath);
|
|
573
|
+
const normalizedBasePath = resolvedBasePath.replaceAll("\\", "/");
|
|
574
|
+
const isWithinBasePath = (candidate) => {
|
|
575
|
+
const normalizedCandidate = candidate.replaceAll("\\", "/");
|
|
576
|
+
return (
|
|
577
|
+
normalizedCandidate === normalizedBasePath ||
|
|
578
|
+
normalizedCandidate.startsWith(`${normalizedBasePath}/`)
|
|
579
|
+
);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const resolvedCandidatePath = resolve(candidatePath);
|
|
583
|
+
if (isWithinBasePath(resolvedCandidatePath)) {
|
|
584
|
+
normalizedPath = relative(
|
|
585
|
+
resolvedBasePath,
|
|
586
|
+
resolvedCandidatePath,
|
|
587
|
+
).replaceAll("\\", "/");
|
|
588
|
+
} else {
|
|
589
|
+
const rebasedCandidatePath = resolve(
|
|
590
|
+
resolvedBasePath,
|
|
591
|
+
candidatePath.replace(/^([A-Za-z]:)?[\\/]+/, ""),
|
|
592
|
+
);
|
|
593
|
+
if (isWithinBasePath(rebasedCandidatePath)) {
|
|
594
|
+
normalizedPath = relative(
|
|
595
|
+
resolvedBasePath,
|
|
596
|
+
rebasedCandidatePath,
|
|
597
|
+
).replaceAll("\\", "/");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return normalizedPath;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function recordSymlinkResolution(
|
|
605
|
+
sourcePath,
|
|
606
|
+
resolvedPath,
|
|
607
|
+
options = {},
|
|
608
|
+
) {
|
|
609
|
+
const normalizedSourcePath = normalizeRecordedPathForComparison(
|
|
610
|
+
sourcePath,
|
|
611
|
+
options.basePath,
|
|
612
|
+
);
|
|
613
|
+
const normalizedResolvedPath = normalizeRecordedPathForComparison(
|
|
614
|
+
resolvedPath,
|
|
615
|
+
options.basePath,
|
|
616
|
+
);
|
|
617
|
+
const status = options.status || "completed";
|
|
618
|
+
if (
|
|
619
|
+
!normalizedSourcePath ||
|
|
620
|
+
(status === "completed" &&
|
|
621
|
+
(!normalizedResolvedPath ||
|
|
622
|
+
normalizedSourcePath === normalizedResolvedPath))
|
|
623
|
+
) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
const metadata = {
|
|
627
|
+
capability: "symlink-resolution",
|
|
628
|
+
...(normalizedResolvedPath ? { resolvedPath: normalizedResolvedPath } : {}),
|
|
629
|
+
...(options.errorCode ? { errorCode: options.errorCode } : {}),
|
|
630
|
+
...(options.metadata || {}),
|
|
631
|
+
};
|
|
632
|
+
return recordObservedActivity("symlink-resolution", normalizedSourcePath, {
|
|
633
|
+
metadata,
|
|
634
|
+
reason:
|
|
635
|
+
options.reason ||
|
|
636
|
+
(status === "failed"
|
|
637
|
+
? `Failed to resolve symlink ${normalizedSourcePath}.`
|
|
638
|
+
: `Resolved symlink ${normalizedSourcePath} to ${normalizedResolvedPath}.`),
|
|
639
|
+
status,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function getArchiveSourceByteSize(sourcePath) {
|
|
644
|
+
if (!sourcePath || !safeExistsSync(sourcePath)) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const sourceStats = lstatSync(sourcePath);
|
|
649
|
+
return sourceStats.isFile() ? sourceStats.size : undefined;
|
|
650
|
+
} catch {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function recordEnvironmentRead(varName, options = {}) {
|
|
656
|
+
// Read tracing intentionally mirrors the activity ledger's dry-run/debug behavior.
|
|
657
|
+
if (!(isDryRun || DEBUG_MODE) || !varName) {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
const source = options.source || "process.env";
|
|
661
|
+
const sensitive =
|
|
662
|
+
options.sensitive ?? isSensitiveEnvironmentVariableName(varName);
|
|
663
|
+
const status = options.status || "completed";
|
|
664
|
+
const traceKey = `${source}:${varName}:${status}`;
|
|
665
|
+
const target = `${source}:${varName}`;
|
|
666
|
+
return recordDeduplicatedRead(
|
|
667
|
+
dryRunReadTraceState.environmentReads,
|
|
668
|
+
traceKey,
|
|
669
|
+
{
|
|
670
|
+
kind: "env",
|
|
671
|
+
redacted: sensitive,
|
|
672
|
+
secretCategory: sensitive ? "environment-variable" : undefined,
|
|
673
|
+
sensitive,
|
|
674
|
+
status,
|
|
675
|
+
target,
|
|
676
|
+
},
|
|
677
|
+
(count) =>
|
|
678
|
+
options.reason || buildEnvironmentReadReason(varName, count, sensitive),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function recordSensitiveFileRead(filePath, options = {}) {
|
|
683
|
+
// Read tracing intentionally mirrors the activity ledger's dry-run/debug behavior.
|
|
684
|
+
if (!(isDryRun || DEBUG_MODE) || !filePath) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
const kind = options.kind || "read";
|
|
688
|
+
const pathMetadata = classifyActivityPath(filePath) || {};
|
|
689
|
+
const label = options.label || pathMetadata.label || "sensitive file";
|
|
690
|
+
const status = options.status || "completed";
|
|
691
|
+
const traceKey = `${kind}:${status}:${filePath}`;
|
|
692
|
+
return recordDeduplicatedRead(
|
|
693
|
+
dryRunReadTraceState.sensitiveFileReads,
|
|
694
|
+
traceKey,
|
|
695
|
+
{
|
|
696
|
+
classification: pathMetadata.classification,
|
|
697
|
+
ecosystem: pathMetadata.ecosystem,
|
|
698
|
+
kind,
|
|
699
|
+
redacted: pathMetadata.sensitive ?? true,
|
|
700
|
+
secretCategory:
|
|
701
|
+
pathMetadata.classification === "key"
|
|
702
|
+
? "private-key"
|
|
703
|
+
: pathMetadata.classification === "certificate"
|
|
704
|
+
? "certificate"
|
|
705
|
+
: "credential-file",
|
|
706
|
+
status,
|
|
707
|
+
target: filePath,
|
|
708
|
+
},
|
|
709
|
+
(count) =>
|
|
710
|
+
options.reason || buildSensitiveFileReadReason(filePath, count, label),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function readEnvironmentVariable(varName, options = {}) {
|
|
715
|
+
recordEnvironmentRead(varName, options);
|
|
716
|
+
return process.env[varName];
|
|
717
|
+
}
|
|
120
718
|
|
|
121
719
|
export function setDryRunMode(enabled) {
|
|
122
720
|
isDryRun = !!enabled;
|
|
@@ -161,10 +759,8 @@ export function recordActivity(activity) {
|
|
|
161
759
|
const identifier = `ACT-${String(++activityCounter).padStart(4, "0")}`;
|
|
162
760
|
const entry = {
|
|
163
761
|
identifier,
|
|
762
|
+
...currentActivityContext,
|
|
164
763
|
timestamp: new Date().toISOString(),
|
|
165
|
-
projectType: currentActivityContext.projectType,
|
|
166
|
-
packageType: currentActivityContext.packageType,
|
|
167
|
-
sourcePath: currentActivityContext.sourcePath,
|
|
168
764
|
...activity,
|
|
169
765
|
};
|
|
170
766
|
activityLedger.push(entry);
|
|
@@ -172,6 +768,8 @@ export function recordActivity(activity) {
|
|
|
172
768
|
return entry;
|
|
173
769
|
}
|
|
174
770
|
|
|
771
|
+
dryRunReadTraceState.recordActivity = recordActivity;
|
|
772
|
+
|
|
175
773
|
export function getRecordedActivities() {
|
|
176
774
|
return [...activityLedger];
|
|
177
775
|
}
|
|
@@ -179,11 +777,21 @@ export function getRecordedActivities() {
|
|
|
179
777
|
export function resetRecordedActivities() {
|
|
180
778
|
activityLedger.length = 0;
|
|
181
779
|
activityCounter = 0;
|
|
780
|
+
dryRunReadTraceState.environmentReads.clear();
|
|
781
|
+
dryRunReadTraceState.observations.clear();
|
|
782
|
+
dryRunReadTraceState.sensitiveFileReads.clear();
|
|
182
783
|
}
|
|
183
784
|
|
|
184
|
-
function recordFilesystemActivity(
|
|
785
|
+
function recordFilesystemActivity(
|
|
786
|
+
kind,
|
|
787
|
+
target,
|
|
788
|
+
status,
|
|
789
|
+
reason = undefined,
|
|
790
|
+
metadata = {},
|
|
791
|
+
) {
|
|
185
792
|
return recordActivity({
|
|
186
793
|
kind,
|
|
794
|
+
...metadata,
|
|
187
795
|
reason,
|
|
188
796
|
status,
|
|
189
797
|
target,
|
|
@@ -218,13 +826,40 @@ function hasWritePermission(filePath) {
|
|
|
218
826
|
* @Boolean True if the path exists. False otherwise
|
|
219
827
|
*/
|
|
220
828
|
export function safeExistsSync(filePath) {
|
|
829
|
+
const pathMetadata = classifyActivityPath(filePath);
|
|
221
830
|
if (!hasReadPermission(filePath)) {
|
|
222
831
|
if (DEBUG_MODE) {
|
|
223
832
|
console.log("cdxgen lacks read permission for a requested path.");
|
|
224
833
|
}
|
|
834
|
+
if (pathMetadata) {
|
|
835
|
+
recordPolicyActivity(filePath, {
|
|
836
|
+
metadata: {
|
|
837
|
+
classification: pathMetadata.classification,
|
|
838
|
+
ecosystem: pathMetadata.ecosystem,
|
|
839
|
+
policyType: "fs.read",
|
|
840
|
+
},
|
|
841
|
+
reason: `Denied inspection of ${pathMetadata.label} ${filePath} due to missing fs.read permission.`,
|
|
842
|
+
status: "blocked",
|
|
843
|
+
});
|
|
844
|
+
}
|
|
225
845
|
return false;
|
|
226
846
|
}
|
|
227
|
-
|
|
847
|
+
const exists = existsSync(filePath);
|
|
848
|
+
if (pathMetadata) {
|
|
849
|
+
const inspectionKind =
|
|
850
|
+
pathMetadata.classification === "directory" ? "discover" : "inspect";
|
|
851
|
+
recordObservedActivity(inspectionKind, filePath, {
|
|
852
|
+
metadata: {
|
|
853
|
+
classification: pathMetadata.classification,
|
|
854
|
+
ecosystem: pathMetadata.ecosystem,
|
|
855
|
+
exists,
|
|
856
|
+
redacted: pathMetadata.sensitive ?? false,
|
|
857
|
+
},
|
|
858
|
+
reasonBuilder: (count) =>
|
|
859
|
+
`${exists ? "Inspected" : "Checked for"} ${pathMetadata.label} ${filePath}${buildReadCountSuffix(count)}.`,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return exists;
|
|
228
863
|
}
|
|
229
864
|
|
|
230
865
|
export function safeWriteSync(filePath, data, options) {
|
|
@@ -249,9 +884,8 @@ export function safeWriteSync(filePath, data, options) {
|
|
|
249
884
|
);
|
|
250
885
|
return undefined;
|
|
251
886
|
}
|
|
252
|
-
|
|
887
|
+
writeFileSync(filePath, data, options);
|
|
253
888
|
recordFilesystemActivity("write", filePath, "completed");
|
|
254
|
-
return result;
|
|
255
889
|
}
|
|
256
890
|
|
|
257
891
|
/**
|
|
@@ -283,24 +917,32 @@ export function safeMkdirSync(filePath, options) {
|
|
|
283
917
|
);
|
|
284
918
|
return undefined;
|
|
285
919
|
}
|
|
286
|
-
|
|
920
|
+
mkdirSync(filePath, options);
|
|
287
921
|
recordFilesystemActivity("mkdir", filePath, "completed");
|
|
288
|
-
return result;
|
|
289
922
|
}
|
|
290
923
|
|
|
291
924
|
export function safeMkdtempSync(prefix, options = undefined) {
|
|
925
|
+
const resourceType =
|
|
926
|
+
typeof prefix === "string" && prefix.toLowerCase().includes("cache")
|
|
927
|
+
? "cache"
|
|
928
|
+
: "temporary-workspace";
|
|
292
929
|
if (isDryRun) {
|
|
293
930
|
const tempPath = `${prefix}${randomUUID().replaceAll("-", "").slice(0, 6)}`;
|
|
294
931
|
recordFilesystemActivity(
|
|
295
932
|
"temp-dir",
|
|
296
933
|
tempPath,
|
|
297
934
|
"blocked",
|
|
298
|
-
|
|
935
|
+
`Dry run mode blocks temporary directory creation for ${resourceType}.`,
|
|
936
|
+
{
|
|
937
|
+
resourceType,
|
|
938
|
+
},
|
|
299
939
|
);
|
|
300
940
|
return tempPath;
|
|
301
941
|
}
|
|
302
942
|
const tempPath = mkdtempSync(prefix, options);
|
|
303
|
-
recordFilesystemActivity("temp-dir", tempPath, "completed"
|
|
943
|
+
recordFilesystemActivity("temp-dir", tempPath, "completed", undefined, {
|
|
944
|
+
resourceType,
|
|
945
|
+
});
|
|
304
946
|
return tempPath;
|
|
305
947
|
}
|
|
306
948
|
|
|
@@ -314,9 +956,8 @@ export function safeRmSync(filePath, options = undefined) {
|
|
|
314
956
|
);
|
|
315
957
|
return undefined;
|
|
316
958
|
}
|
|
317
|
-
|
|
959
|
+
rmSync(filePath, options);
|
|
318
960
|
recordFilesystemActivity("cleanup", filePath, "completed");
|
|
319
|
-
return result;
|
|
320
961
|
}
|
|
321
962
|
|
|
322
963
|
export function safeUnlinkSync(filePath) {
|
|
@@ -329,9 +970,8 @@ export function safeUnlinkSync(filePath) {
|
|
|
329
970
|
);
|
|
330
971
|
return undefined;
|
|
331
972
|
}
|
|
332
|
-
|
|
973
|
+
unlinkSync(filePath);
|
|
333
974
|
recordFilesystemActivity("cleanup", filePath, "completed");
|
|
334
|
-
return result;
|
|
335
975
|
}
|
|
336
976
|
|
|
337
977
|
export function safeCopyFileSync(src, dest, mode = undefined) {
|
|
@@ -357,32 +997,69 @@ export async function safeExtractArchive(
|
|
|
357
997
|
targetPath,
|
|
358
998
|
extractor,
|
|
359
999
|
kind = "unzip",
|
|
1000
|
+
options = undefined,
|
|
360
1001
|
) {
|
|
1002
|
+
const traceArchiveStats = isDryRun || DEBUG_MODE;
|
|
1003
|
+
const sourceBytes = traceArchiveStats
|
|
1004
|
+
? getArchiveSourceByteSize(sourcePath)
|
|
1005
|
+
: undefined;
|
|
361
1006
|
if (isDryRun) {
|
|
362
1007
|
recordActivity({
|
|
1008
|
+
archiveKind: kind,
|
|
1009
|
+
capability: "archive-extraction",
|
|
363
1010
|
kind,
|
|
364
|
-
|
|
1011
|
+
...(options?.metadata || {}),
|
|
1012
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
1013
|
+
reason:
|
|
1014
|
+
options?.blockedReason ||
|
|
1015
|
+
`Dry run mode blocks ${kind} extraction from ${sourcePath} into ${targetPath}.`,
|
|
365
1016
|
status: "blocked",
|
|
366
1017
|
target: `${sourcePath} -> ${targetPath}`,
|
|
367
1018
|
});
|
|
368
1019
|
return false;
|
|
369
1020
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
await extractor();
|
|
1023
|
+
recordActivity({
|
|
1024
|
+
archiveKind: kind,
|
|
1025
|
+
capability: "archive-extraction",
|
|
1026
|
+
kind,
|
|
1027
|
+
...(options?.metadata || {}),
|
|
1028
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
1029
|
+
status: "completed",
|
|
1030
|
+
target: `${sourcePath} -> ${targetPath}`,
|
|
1031
|
+
});
|
|
1032
|
+
return true;
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
recordActivity({
|
|
1035
|
+
archiveKind: kind,
|
|
1036
|
+
capability: "archive-extraction",
|
|
1037
|
+
kind,
|
|
1038
|
+
...(options?.metadata || {}),
|
|
1039
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
1040
|
+
...(error?.code ? { errorCode: error.code } : {}),
|
|
1041
|
+
reason:
|
|
1042
|
+
options?.failureReason ||
|
|
1043
|
+
`Failed ${kind} extraction from ${sourcePath} into ${targetPath}: ${error.message}`,
|
|
1044
|
+
status: "failed",
|
|
1045
|
+
target: `${sourcePath} -> ${targetPath}`,
|
|
1046
|
+
});
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
377
1049
|
}
|
|
378
1050
|
|
|
379
1051
|
export const commandsExecuted = new Set();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1052
|
+
function isAllowedCommand(
|
|
1053
|
+
command,
|
|
1054
|
+
allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS"),
|
|
1055
|
+
) {
|
|
1056
|
+
if (!allowedCommandsEnv) {
|
|
383
1057
|
return true;
|
|
384
1058
|
}
|
|
385
|
-
return
|
|
1059
|
+
return allowedCommandsEnv
|
|
1060
|
+
.split(",")
|
|
1061
|
+
.map((entry) => entry.trim())
|
|
1062
|
+
.includes(command.trim());
|
|
386
1063
|
}
|
|
387
1064
|
|
|
388
1065
|
const ALLOWED_WRAPPERS = new Set(["gradlew", "mvnw"]);
|
|
@@ -429,6 +1106,71 @@ function isWindowsShellHijackRisk(command, options) {
|
|
|
429
1106
|
return false;
|
|
430
1107
|
}
|
|
431
1108
|
|
|
1109
|
+
const VERSION_PROBE_ARGS = new Set(["--version", "-version", "version"]);
|
|
1110
|
+
|
|
1111
|
+
function detectProbeType(command, args = []) {
|
|
1112
|
+
const normalizedCommand = basename(String(command || "")).toLowerCase();
|
|
1113
|
+
const normalizedArgs = (args || []).map((arg) => String(arg).toLowerCase());
|
|
1114
|
+
if (
|
|
1115
|
+
normalizedArgs.some((arg) => VERSION_PROBE_ARGS.has(arg)) ||
|
|
1116
|
+
(normalizedArgs.length === 1 && normalizedArgs[0] === "-v")
|
|
1117
|
+
) {
|
|
1118
|
+
return "version-check";
|
|
1119
|
+
}
|
|
1120
|
+
if (normalizedCommand === "which" || normalizedArgs.includes("--help")) {
|
|
1121
|
+
return "capability-probe";
|
|
1122
|
+
}
|
|
1123
|
+
if (
|
|
1124
|
+
normalizedCommand.startsWith("python") &&
|
|
1125
|
+
normalizedArgs.includes("-c") &&
|
|
1126
|
+
normalizedArgs.some((arg) => arg.includes("import"))
|
|
1127
|
+
) {
|
|
1128
|
+
return "runtime-probe";
|
|
1129
|
+
}
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function buildCommandActivityDescriptor(command, args, options) {
|
|
1134
|
+
const target = `${command}${args?.length ? ` ${args.join(" ")}` : ""}`;
|
|
1135
|
+
const cdxgenActivity = options?.cdxgenActivity || {};
|
|
1136
|
+
const probeType = cdxgenActivity.probeType || detectProbeType(command, args);
|
|
1137
|
+
const metadata = {
|
|
1138
|
+
...(cdxgenActivity.metadata || {}),
|
|
1139
|
+
};
|
|
1140
|
+
if (probeType) {
|
|
1141
|
+
metadata.capability = metadata.capability || "tool-runtime-probe";
|
|
1142
|
+
metadata.probeType = probeType;
|
|
1143
|
+
}
|
|
1144
|
+
if (cdxgenActivity.gitOperation) {
|
|
1145
|
+
metadata.gitOperation = cdxgenActivity.gitOperation;
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
blockedReason:
|
|
1149
|
+
cdxgenActivity.blockedReason ||
|
|
1150
|
+
(probeType
|
|
1151
|
+
? `Dry run mode blocks ${probeType.replaceAll("-", " ")} command execution.`
|
|
1152
|
+
: "Dry run mode blocks child process execution."),
|
|
1153
|
+
kind: cdxgenActivity.kind || "execute",
|
|
1154
|
+
metadata,
|
|
1155
|
+
target: cdxgenActivity.target || target,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function getOutputByteSize(value, encoding = "utf-8") {
|
|
1160
|
+
if (value === undefined || value === null) {
|
|
1161
|
+
return 0;
|
|
1162
|
+
}
|
|
1163
|
+
if (Buffer.isBuffer(value)) {
|
|
1164
|
+
return value.length;
|
|
1165
|
+
}
|
|
1166
|
+
if (ArrayBuffer.isView(value)) {
|
|
1167
|
+
return value.byteLength;
|
|
1168
|
+
}
|
|
1169
|
+
const safeEncoding =
|
|
1170
|
+
typeof encoding === "string" && encoding !== "buffer" ? encoding : "utf8";
|
|
1171
|
+
return Buffer.byteLength(String(value), safeEncoding);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
432
1174
|
/**
|
|
433
1175
|
* Safe wrapper around spawnSync that enforces permission checks, injects default
|
|
434
1176
|
* options (maxBuffer, encoding, timeout), warns about unsafe Python and pip/uv
|
|
@@ -440,17 +1182,49 @@ function isWindowsShellHijackRisk(command, options) {
|
|
|
440
1182
|
* @returns {Object} spawnSync result object with status, stdout, stderr, and error fields
|
|
441
1183
|
*/
|
|
442
1184
|
export function safeSpawnSync(command, args, options) {
|
|
1185
|
+
const activityDescriptor = buildCommandActivityDescriptor(
|
|
1186
|
+
command,
|
|
1187
|
+
args,
|
|
1188
|
+
options,
|
|
1189
|
+
);
|
|
1190
|
+
const allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS");
|
|
1191
|
+
const commandAllowed = isAllowedCommand(command, allowedCommandsEnv);
|
|
1192
|
+
if (allowedCommandsEnv) {
|
|
1193
|
+
recordPolicyActivity(command, {
|
|
1194
|
+
metadata: {
|
|
1195
|
+
allowed: commandAllowed,
|
|
1196
|
+
allowlist: allowedCommandsEnv,
|
|
1197
|
+
policyType: "command-allowlist",
|
|
1198
|
+
},
|
|
1199
|
+
reason: `${commandAllowed ? "Allowed" : "Blocked"} command ${command} against CDXGEN_ALLOWED_COMMANDS.`,
|
|
1200
|
+
status: commandAllowed ? "completed" : "blocked",
|
|
1201
|
+
traceDetail: "allowlist",
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (isSecureMode && process.permission) {
|
|
1205
|
+
const hasChildPermission = process.permission.has("child");
|
|
1206
|
+
recordPolicyActivity(command, {
|
|
1207
|
+
metadata: {
|
|
1208
|
+
allowed: hasChildPermission,
|
|
1209
|
+
policyType: "child-process",
|
|
1210
|
+
},
|
|
1211
|
+
reason: `${hasChildPermission ? "Confirmed" : "Denied"} child-process permission for ${command}.`,
|
|
1212
|
+
status: hasChildPermission ? "completed" : "blocked",
|
|
1213
|
+
traceDetail: "child-permission",
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
443
1216
|
if (isDryRun) {
|
|
444
1217
|
const error = createDryRunError(
|
|
445
1218
|
"execute",
|
|
446
1219
|
command,
|
|
447
|
-
|
|
1220
|
+
activityDescriptor.blockedReason,
|
|
448
1221
|
);
|
|
449
1222
|
recordActivity({
|
|
450
|
-
kind:
|
|
1223
|
+
kind: activityDescriptor.kind,
|
|
1224
|
+
...activityDescriptor.metadata,
|
|
451
1225
|
reason: error.message,
|
|
452
1226
|
status: "blocked",
|
|
453
|
-
target:
|
|
1227
|
+
target: activityDescriptor.target,
|
|
454
1228
|
});
|
|
455
1229
|
return {
|
|
456
1230
|
status: 1,
|
|
@@ -461,16 +1235,17 @@ export function safeSpawnSync(command, args, options) {
|
|
|
461
1235
|
}
|
|
462
1236
|
if (
|
|
463
1237
|
(isSecureMode && process.permission && !process.permission.has("child")) ||
|
|
464
|
-
!
|
|
1238
|
+
!commandAllowed
|
|
465
1239
|
) {
|
|
466
1240
|
if (DEBUG_MODE) {
|
|
467
1241
|
console.log(`cdxgen lacks execute permission for ${command}`);
|
|
468
1242
|
}
|
|
469
1243
|
recordActivity({
|
|
470
|
-
kind:
|
|
1244
|
+
kind: activityDescriptor.kind,
|
|
1245
|
+
...activityDescriptor.metadata,
|
|
471
1246
|
reason: "cdxgen lacks execute permission for this command.",
|
|
472
1247
|
status: "blocked",
|
|
473
|
-
target:
|
|
1248
|
+
target: activityDescriptor.target,
|
|
474
1249
|
});
|
|
475
1250
|
return {
|
|
476
1251
|
status: 1,
|
|
@@ -484,10 +1259,11 @@ export function safeSpawnSync(command, args, options) {
|
|
|
484
1259
|
const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
|
|
485
1260
|
console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
|
|
486
1261
|
recordActivity({
|
|
487
|
-
kind:
|
|
1262
|
+
kind: activityDescriptor.kind,
|
|
1263
|
+
...activityDescriptor.metadata,
|
|
488
1264
|
reason: blockedReason,
|
|
489
1265
|
status: "blocked",
|
|
490
|
-
target:
|
|
1266
|
+
target: activityDescriptor.target,
|
|
491
1267
|
});
|
|
492
1268
|
return {
|
|
493
1269
|
status: 1,
|
|
@@ -506,6 +1282,11 @@ export function safeSpawnSync(command, args, options) {
|
|
|
506
1282
|
}
|
|
507
1283
|
if (!options) {
|
|
508
1284
|
options = {};
|
|
1285
|
+
} else if (options.cdxgenActivity) {
|
|
1286
|
+
options = {
|
|
1287
|
+
...options,
|
|
1288
|
+
};
|
|
1289
|
+
delete options.cdxgenActivity;
|
|
509
1290
|
}
|
|
510
1291
|
// Inject maxBuffer
|
|
511
1292
|
if (!options.maxBuffer) {
|
|
@@ -605,10 +1386,13 @@ export function safeSpawnSync(command, args, options) {
|
|
|
605
1386
|
}
|
|
606
1387
|
const result = spawnSync(command, args, options);
|
|
607
1388
|
recordActivity({
|
|
608
|
-
kind:
|
|
1389
|
+
kind: activityDescriptor.kind,
|
|
1390
|
+
...activityDescriptor.metadata,
|
|
1391
|
+
stderrBytes: getOutputByteSize(result.stderr, options.encoding),
|
|
609
1392
|
reason: result.error?.message,
|
|
610
1393
|
status: result.status === 0 && !result.error ? "completed" : "failed",
|
|
611
|
-
|
|
1394
|
+
stdoutBytes: getOutputByteSize(result.stdout, options.encoding),
|
|
1395
|
+
target: activityDescriptor.target,
|
|
612
1396
|
});
|
|
613
1397
|
return result;
|
|
614
1398
|
}
|
|
@@ -985,6 +1769,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
985
1769
|
c: ["c", "cpp", "c++", "conan", "collider"],
|
|
986
1770
|
clojure: ["clojure", "edn", "clj", "leiningen"],
|
|
987
1771
|
github: ["github", "actions"],
|
|
1772
|
+
hbom: ["hbom", "hardware"],
|
|
988
1773
|
os: ["os", "osquery", "windows", "linux", "mac", "macos", "darwin"],
|
|
989
1774
|
jenkins: ["jenkins", "hpi"],
|
|
990
1775
|
helm: ["helm", "charts"],
|
|
@@ -1020,12 +1805,14 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
1020
1805
|
scala: ["scala", "scala3", "sbt", "mill"],
|
|
1021
1806
|
nix: ["nix", "nixos", "flake"],
|
|
1022
1807
|
caxa: ["caxa"],
|
|
1808
|
+
asar: ["asar", "electron", "electron-asar"],
|
|
1023
1809
|
"vscode-extension": [
|
|
1024
1810
|
"vscode-extension",
|
|
1025
1811
|
"vsix",
|
|
1026
1812
|
"vscode",
|
|
1027
1813
|
"openvsx",
|
|
1028
1814
|
"vscode-extensions",
|
|
1815
|
+
"ide-extension",
|
|
1029
1816
|
"ide-extensions",
|
|
1030
1817
|
],
|
|
1031
1818
|
"chrome-extension": [
|
|
@@ -1155,6 +1942,90 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
|
|
|
1155
1942
|
return shouldInclude;
|
|
1156
1943
|
}
|
|
1157
1944
|
|
|
1945
|
+
/**
|
|
1946
|
+
* Determine whether the predictive dependency audit should run for the current
|
|
1947
|
+
* CLI invocation.
|
|
1948
|
+
*
|
|
1949
|
+
* OBOM-focused runs (`obom` or explicit `-t os` / OS aliases only) should keep
|
|
1950
|
+
* the direct BOM audit findings but skip the predictive dependency audit.
|
|
1951
|
+
*
|
|
1952
|
+
* @param {object} options CLI options
|
|
1953
|
+
* @param {string} [commandPath] Invoked command path or name
|
|
1954
|
+
* @returns {boolean} True when predictive dependency audit should run
|
|
1955
|
+
*/
|
|
1956
|
+
export function shouldRunPredictiveBomAudit(options, commandPath) {
|
|
1957
|
+
const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
|
|
1958
|
+
if (normalizedCommandPath.includes("obom")) {
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
if (normalizedCommandPath.includes("hbom")) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
const projectTypes = Array.isArray(options?.projectType)
|
|
1965
|
+
? options.projectType
|
|
1966
|
+
: typeof options?.projectType === "string"
|
|
1967
|
+
? options.projectType.split(",")
|
|
1968
|
+
: [];
|
|
1969
|
+
const normalizedProjectTypes = projectTypes
|
|
1970
|
+
.map((projectType) => `${projectType || ""}`.trim().toLowerCase())
|
|
1971
|
+
.filter(Boolean);
|
|
1972
|
+
if (!normalizedProjectTypes.length) {
|
|
1973
|
+
return true;
|
|
1974
|
+
}
|
|
1975
|
+
const hbomProjectTypes = new Set(["hbom", "hardware"]);
|
|
1976
|
+
if (
|
|
1977
|
+
normalizedProjectTypes.every((projectType) =>
|
|
1978
|
+
hbomProjectTypes.has(projectType),
|
|
1979
|
+
)
|
|
1980
|
+
) {
|
|
1981
|
+
return false;
|
|
1982
|
+
}
|
|
1983
|
+
const osProjectTypes = new Set(["os", ...(PROJECT_TYPE_ALIASES.os || [])]);
|
|
1984
|
+
return !normalizedProjectTypes.every((projectType) =>
|
|
1985
|
+
osProjectTypes.has(projectType),
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Determine the default BOM audit categories for the current CLI invocation.
|
|
1991
|
+
*
|
|
1992
|
+
* OBOM-focused runs should default to the runtime-specific rule pack unless the
|
|
1993
|
+
* user explicitly requests other categories.
|
|
1994
|
+
*
|
|
1995
|
+
* @param {object} options CLI options
|
|
1996
|
+
* @param {string} [commandPath] Invoked command path or name
|
|
1997
|
+
* @returns {string | undefined} Default category string, if any
|
|
1998
|
+
*/
|
|
1999
|
+
export function getDefaultBomAuditCategories(options, commandPath) {
|
|
2000
|
+
const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
|
|
2001
|
+
const defaultHbomCategories = options?.includeRuntime
|
|
2002
|
+
? `${DEFAULT_HBOM_AUDIT_CATEGORIES},host-topology`
|
|
2003
|
+
: DEFAULT_HBOM_AUDIT_CATEGORIES;
|
|
2004
|
+
if (normalizedCommandPath.includes("hbom")) {
|
|
2005
|
+
return defaultHbomCategories;
|
|
2006
|
+
}
|
|
2007
|
+
const projectTypes = Array.isArray(options?.projectType)
|
|
2008
|
+
? options.projectType
|
|
2009
|
+
: typeof options?.projectType === "string"
|
|
2010
|
+
? options.projectType.split(",")
|
|
2011
|
+
: [];
|
|
2012
|
+
const normalizedProjectTypes = projectTypes
|
|
2013
|
+
.map((projectType) => `${projectType || ""}`.trim().toLowerCase())
|
|
2014
|
+
.filter(Boolean);
|
|
2015
|
+
if (
|
|
2016
|
+
normalizedProjectTypes.length &&
|
|
2017
|
+
normalizedProjectTypes.every((projectType) =>
|
|
2018
|
+
["hbom", "hardware"].includes(projectType),
|
|
2019
|
+
)
|
|
2020
|
+
) {
|
|
2021
|
+
return defaultHbomCategories;
|
|
2022
|
+
}
|
|
2023
|
+
if (!shouldRunPredictiveBomAudit(options, commandPath)) {
|
|
2024
|
+
return "obom-runtime";
|
|
2025
|
+
}
|
|
2026
|
+
return undefined;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1158
2029
|
/**
|
|
1159
2030
|
* Convenient method to check if the given package manager is allowed.
|
|
1160
2031
|
*
|
|
@@ -1196,23 +2067,76 @@ function isCacheDisabled() {
|
|
|
1196
2067
|
const cache = isCacheDisabled() ? undefined : gotHttpCache;
|
|
1197
2068
|
export const remoteHostsAccessed = new Set();
|
|
1198
2069
|
|
|
1199
|
-
function
|
|
1200
|
-
|
|
2070
|
+
export function isAllowedHttpHost(
|
|
2071
|
+
hostname,
|
|
2072
|
+
allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS"),
|
|
2073
|
+
) {
|
|
2074
|
+
if (!allowedHostsEnv) {
|
|
1201
2075
|
return true;
|
|
1202
2076
|
}
|
|
1203
|
-
|
|
2077
|
+
if (!hostname || hasDangerousUnicode(hostname)) {
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
2081
|
+
const allow_hosts = allowedHostsEnv
|
|
2082
|
+
.split(",")
|
|
2083
|
+
.map((host) => host.trim().toLowerCase())
|
|
2084
|
+
.filter(Boolean);
|
|
1204
2085
|
for (const ahost of allow_hosts) {
|
|
1205
|
-
if (
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
if (hostname === ahost) {
|
|
2086
|
+
if (normalizedHostname === ahost) {
|
|
1209
2087
|
return true;
|
|
1210
2088
|
}
|
|
1211
2089
|
// wildcard support
|
|
1212
|
-
if (
|
|
2090
|
+
if (
|
|
2091
|
+
ahost.startsWith("*.") &&
|
|
2092
|
+
normalizedHostname.length > ahost.length - 1 &&
|
|
2093
|
+
normalizedHostname.endsWith(`.${ahost.slice(2)}`)
|
|
2094
|
+
) {
|
|
1213
2095
|
return true;
|
|
1214
2096
|
}
|
|
1215
2097
|
}
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function hostnameMatches(hostname, candidateHost) {
|
|
2102
|
+
return hostname === candidateHost || hostname.endsWith(`.${candidateHost}`);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function inferNetworkIntent(requestUrl) {
|
|
2106
|
+
const hostname = requestUrl?.hostname?.toLowerCase() || "";
|
|
2107
|
+
const pathname = requestUrl?.pathname?.toLowerCase() || "";
|
|
2108
|
+
if (pathname.includes("/api/v1/bom")) {
|
|
2109
|
+
return "sbom-submit";
|
|
2110
|
+
}
|
|
2111
|
+
if (pathname.includes("/manifests/")) {
|
|
2112
|
+
return "oci-manifest-access";
|
|
2113
|
+
}
|
|
2114
|
+
if (pathname.includes("/blobs/")) {
|
|
2115
|
+
return "oci-layer-access";
|
|
2116
|
+
}
|
|
2117
|
+
if (
|
|
2118
|
+
pathname.includes("license") ||
|
|
2119
|
+
hostnameMatches(hostname, "spdx.org") ||
|
|
2120
|
+
hostnameMatches(hostname, "opensource.org")
|
|
2121
|
+
) {
|
|
2122
|
+
return "license-fetch";
|
|
2123
|
+
}
|
|
2124
|
+
if (
|
|
2125
|
+
hostnameMatches(hostname, "registry.npmjs.org") ||
|
|
2126
|
+
hostnameMatches(hostname, "pypi.org") ||
|
|
2127
|
+
hostnameMatches(hostname, "rubygems.org") ||
|
|
2128
|
+
hostnameMatches(hostname, "repo.maven.apache.org") ||
|
|
2129
|
+
hostnameMatches(hostname, "repo1.maven.org") ||
|
|
2130
|
+
hostnameMatches(hostname, "crates.io") ||
|
|
2131
|
+
hostnameMatches(hostname, "pub.dev") ||
|
|
2132
|
+
hostnameMatches(hostname, "nuget.org")
|
|
2133
|
+
) {
|
|
2134
|
+
return "registry-lookup";
|
|
2135
|
+
}
|
|
2136
|
+
if (hostnameMatches(hostname, "github.com") && pathname.endsWith(".git")) {
|
|
2137
|
+
return "git-fetch";
|
|
2138
|
+
}
|
|
2139
|
+
return "metadata-fetch";
|
|
1216
2140
|
}
|
|
1217
2141
|
|
|
1218
2142
|
// Custom user-agent for cdxgen
|
|
@@ -1228,18 +2152,40 @@ export const cdxgenAgent = got.extend({
|
|
|
1228
2152
|
hooks: {
|
|
1229
2153
|
beforeRequest: [
|
|
1230
2154
|
(options) => {
|
|
2155
|
+
const networkIntent =
|
|
2156
|
+
options.context?.activityIntent || inferNetworkIntent(options.url);
|
|
2157
|
+
const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
|
|
2158
|
+
const hostAllowed = isAllowedHttpHost(
|
|
2159
|
+
options.url.hostname,
|
|
2160
|
+
allowedHostsEnv,
|
|
2161
|
+
);
|
|
1231
2162
|
options.context = {
|
|
1232
2163
|
...options.context,
|
|
2164
|
+
activityIntent: networkIntent,
|
|
1233
2165
|
activityTarget: options.url.toString(),
|
|
1234
2166
|
};
|
|
2167
|
+
if (allowedHostsEnv) {
|
|
2168
|
+
recordPolicyActivity(options.url.hostname, {
|
|
2169
|
+
metadata: {
|
|
2170
|
+
allowed: hostAllowed,
|
|
2171
|
+
allowlist: allowedHostsEnv,
|
|
2172
|
+
networkIntent,
|
|
2173
|
+
policyType: "host-allowlist",
|
|
2174
|
+
},
|
|
2175
|
+
reason: `${hostAllowed ? "Allowed" : "Blocked"} host ${options.url.hostname} against CDXGEN_ALLOWED_HOSTS.`,
|
|
2176
|
+
status: hostAllowed ? "completed" : "blocked",
|
|
2177
|
+
traceDetail: "host-allowlist",
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
1235
2180
|
if (isDryRun) {
|
|
1236
2181
|
const error = createDryRunError(
|
|
1237
2182
|
"network",
|
|
1238
2183
|
options.url.toString(),
|
|
1239
|
-
|
|
2184
|
+
`Dry run mode blocks outbound network access (${networkIntent}).`,
|
|
1240
2185
|
);
|
|
1241
2186
|
recordActivity({
|
|
1242
2187
|
kind: "network",
|
|
2188
|
+
networkIntent,
|
|
1243
2189
|
reason: error.message,
|
|
1244
2190
|
status: "blocked",
|
|
1245
2191
|
target: options.url.toString(),
|
|
@@ -1247,12 +2193,13 @@ export const cdxgenAgent = got.extend({
|
|
|
1247
2193
|
options.context.activityBlocked = true;
|
|
1248
2194
|
throw error;
|
|
1249
2195
|
}
|
|
1250
|
-
if (!
|
|
2196
|
+
if (!hostAllowed) {
|
|
1251
2197
|
console.log(
|
|
1252
2198
|
`Access to the remote host '${options.url.hostname}' is not permitted.`,
|
|
1253
2199
|
);
|
|
1254
2200
|
recordActivity({
|
|
1255
2201
|
kind: "network",
|
|
2202
|
+
networkIntent,
|
|
1256
2203
|
reason: "The remote host is not permitted.",
|
|
1257
2204
|
status: "blocked",
|
|
1258
2205
|
target: options.url.toString(),
|
|
@@ -1267,6 +2214,7 @@ export const cdxgenAgent = got.extend({
|
|
|
1267
2214
|
);
|
|
1268
2215
|
recordActivity({
|
|
1269
2216
|
kind: "network",
|
|
2217
|
+
networkIntent,
|
|
1270
2218
|
reason: `The '${options.url.protocol}' protocol is not permitted in secure mode.`,
|
|
1271
2219
|
status: "blocked",
|
|
1272
2220
|
target: options.url.toString(),
|
|
@@ -1293,6 +2241,7 @@ export const cdxgenAgent = got.extend({
|
|
|
1293
2241
|
response.url;
|
|
1294
2242
|
recordActivity({
|
|
1295
2243
|
kind: "network",
|
|
2244
|
+
networkIntent: response.request.options.context?.activityIntent,
|
|
1296
2245
|
status: "completed",
|
|
1297
2246
|
target: activityTarget,
|
|
1298
2247
|
});
|
|
@@ -1306,6 +2255,7 @@ export const cdxgenAgent = got.extend({
|
|
|
1306
2255
|
}
|
|
1307
2256
|
recordActivity({
|
|
1308
2257
|
kind: "network",
|
|
2258
|
+
networkIntent: error.options?.context?.activityIntent,
|
|
1309
2259
|
reason: error.message,
|
|
1310
2260
|
status: "failed",
|
|
1311
2261
|
target:
|
|
@@ -1389,6 +2339,10 @@ export function getAllFilesWithIgnore(
|
|
|
1389
2339
|
includeDot,
|
|
1390
2340
|
ignoreList,
|
|
1391
2341
|
) {
|
|
2342
|
+
const patternValue = Array.isArray(pattern)
|
|
2343
|
+
? pattern.join(",")
|
|
2344
|
+
: String(pattern);
|
|
2345
|
+
const discoveryMetadata = classifyDiscoveryPattern(patternValue);
|
|
1392
2346
|
try {
|
|
1393
2347
|
const files = globSync(pattern, {
|
|
1394
2348
|
cwd: dirPath,
|
|
@@ -1399,6 +2353,16 @@ export function getAllFilesWithIgnore(
|
|
|
1399
2353
|
follow: false,
|
|
1400
2354
|
ignore: ignoreList,
|
|
1401
2355
|
});
|
|
2356
|
+
recordDiscoveryActivity(`${dirPath} :: ${patternValue}`, {
|
|
2357
|
+
metadata: {
|
|
2358
|
+
discoveryType: discoveryMetadata.discoveryType,
|
|
2359
|
+
matchedCount: files.length,
|
|
2360
|
+
pattern: patternValue,
|
|
2361
|
+
},
|
|
2362
|
+
traceDetail: `matched:${files.length}`,
|
|
2363
|
+
reasonBuilder: (count) =>
|
|
2364
|
+
`Scanned ${dirPath} with glob '${patternValue}' for ${discoveryMetadata.label}; matched ${files.length} path(s)${buildReadCountSuffix(count)}.`,
|
|
2365
|
+
});
|
|
1402
2366
|
if (files.length > 1) {
|
|
1403
2367
|
thoughtLog(
|
|
1404
2368
|
`Found ${files.length} files for the pattern '${pattern}' at '${dirPath}'.`,
|
|
@@ -1406,6 +2370,14 @@ export function getAllFilesWithIgnore(
|
|
|
1406
2370
|
}
|
|
1407
2371
|
return files;
|
|
1408
2372
|
} catch (err) {
|
|
2373
|
+
recordDiscoveryActivity(`${dirPath} :: ${patternValue}`, {
|
|
2374
|
+
metadata: {
|
|
2375
|
+
discoveryType: discoveryMetadata.discoveryType,
|
|
2376
|
+
pattern: patternValue,
|
|
2377
|
+
},
|
|
2378
|
+
reason: `File discovery failed for glob '${patternValue}' under ${dirPath}: ${err.message}`,
|
|
2379
|
+
status: "failed",
|
|
2380
|
+
});
|
|
1409
2381
|
if (DEBUG_MODE) {
|
|
1410
2382
|
console.error(err);
|
|
1411
2383
|
}
|
|
@@ -7089,7 +8061,7 @@ function addComponentProperty(component, name, value) {
|
|
|
7089
8061
|
}
|
|
7090
8062
|
|
|
7091
8063
|
const PYTHON_DIRECT_REFERENCE_PATTERN =
|
|
7092
|
-
/^([A-Za-z0-9_.-]+)(?:\[[^\]]
|
|
8064
|
+
/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?\s*@\s*(\S+)$/;
|
|
7093
8065
|
|
|
7094
8066
|
function isWindowsAbsolutePath(value) {
|
|
7095
8067
|
return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
|
|
@@ -7180,7 +8152,7 @@ function extractPythonDependencyKey(value) {
|
|
|
7180
8152
|
}
|
|
7181
8153
|
const packageMatch =
|
|
7182
8154
|
typeof value === "string"
|
|
7183
|
-
? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]
|
|
8155
|
+
? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?/)
|
|
7184
8156
|
: undefined;
|
|
7185
8157
|
return normalizePythonDependencyKey(packageMatch?.[1]);
|
|
7186
8158
|
}
|
|
@@ -8463,7 +9435,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
8463
9435
|
|
|
8464
9436
|
// Handle extras (e.g., package[extra1,extra2])
|
|
8465
9437
|
let extras = null;
|
|
8466
|
-
const extrasMatch = l.match(/^([a-zA-Z0-9_
|
|
9438
|
+
const extrasMatch = l.match(/^([a-zA-Z0-9_\-.]+)(\[([^\]]+)])?(.*)$/);
|
|
8467
9439
|
if (extrasMatch) {
|
|
8468
9440
|
const [, packageName, , extrasStr, versionSpecifiers] = extrasMatch;
|
|
8469
9441
|
const name = packageName;
|
|
@@ -8532,7 +9504,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
8532
9504
|
}
|
|
8533
9505
|
pkgList.push(apkg);
|
|
8534
9506
|
} else {
|
|
8535
|
-
const match = l.match(/^([a-zA-Z0-9_
|
|
9507
|
+
const match = l.match(/^([a-zA-Z0-9_\-.]+)(.*)$/);
|
|
8536
9508
|
if (!match) {
|
|
8537
9509
|
continue;
|
|
8538
9510
|
}
|
|
@@ -13599,7 +14571,7 @@ export function parseFlakeNix(flakeNixFile) {
|
|
|
13599
14571
|
const flakeContent = readFileSync(flakeNixFile, "utf-8");
|
|
13600
14572
|
|
|
13601
14573
|
// Extract inputs from flake.nix using regex
|
|
13602
|
-
const inputsRegex = /inputs\s*=\s*\{[^}]
|
|
14574
|
+
const inputsRegex = /inputs\s*=\s*\{[^}]*}/g;
|
|
13603
14575
|
let match;
|
|
13604
14576
|
while ((match = inputsRegex.exec(flakeContent)) !== null) {
|
|
13605
14577
|
const inputBlock = match[0];
|
|
@@ -13607,7 +14579,7 @@ export function parseFlakeNix(flakeNixFile) {
|
|
|
13607
14579
|
// Match different input patterns including nested inputs
|
|
13608
14580
|
const inputPatterns = [
|
|
13609
14581
|
/([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
|
|
13610
|
-
/([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]
|
|
14582
|
+
/([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
|
|
13611
14583
|
];
|
|
13612
14584
|
|
|
13613
14585
|
const addedPackages = new Set();
|
|
@@ -15549,16 +16521,22 @@ export function convertOSQueryResults(
|
|
|
15549
16521
|
if (name) {
|
|
15550
16522
|
name = sanitizeOsQueryIdentity(name);
|
|
15551
16523
|
group = sanitizeOsQueryIdentity(group);
|
|
15552
|
-
const
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
16524
|
+
const isCryptoAsset = queryObj.componentType === "cryptographic-asset";
|
|
16525
|
+
const purl = shouldCreateOsQueryPurl(queryObj.componentType)
|
|
16526
|
+
? createOsQueryPurl(
|
|
16527
|
+
queryObj.purlType,
|
|
16528
|
+
group,
|
|
16529
|
+
name,
|
|
16530
|
+
version,
|
|
16531
|
+
qualifiers,
|
|
16532
|
+
subpath,
|
|
16533
|
+
)
|
|
16534
|
+
: undefined;
|
|
15560
16535
|
const props = [{ name: "cdx:osquery:category", value: queryCategory }];
|
|
15561
16536
|
props.push(...createLolbasProperties(queryCategory, res));
|
|
16537
|
+
if (platform() === "linux") {
|
|
16538
|
+
props.push(...createGtfoBinsPropertiesFromRow(queryCategory, res));
|
|
16539
|
+
}
|
|
15562
16540
|
let providesList;
|
|
15563
16541
|
if (enhance) {
|
|
15564
16542
|
switch (queryObj.purlType) {
|
|
@@ -15584,25 +16562,39 @@ export function convertOSQueryResults(
|
|
|
15584
16562
|
if (providesList) {
|
|
15585
16563
|
props.push({ name: "PkgProvides", value: providesList.join(", ") });
|
|
15586
16564
|
}
|
|
16565
|
+
const cryptoProperties = isCryptoAsset
|
|
16566
|
+
? createOsQueryCryptoProperties(queryCategory, res, version)
|
|
16567
|
+
: undefined;
|
|
16568
|
+
const hashes = createOsQueryComponentHashes(res);
|
|
15587
16569
|
const apkg = {
|
|
15588
16570
|
name,
|
|
15589
16571
|
group,
|
|
15590
16572
|
version: version || "",
|
|
15591
16573
|
description,
|
|
15592
16574
|
publisher,
|
|
15593
|
-
"bom-ref":
|
|
16575
|
+
"bom-ref": createOsQueryBomRef(
|
|
16576
|
+
queryCategory,
|
|
16577
|
+
queryObj.componentType,
|
|
16578
|
+
res,
|
|
16579
|
+
name,
|
|
16580
|
+
version,
|
|
16581
|
+
purl,
|
|
16582
|
+
),
|
|
15594
16583
|
purl,
|
|
15595
16584
|
scope,
|
|
15596
16585
|
type: queryObj.componentType,
|
|
15597
16586
|
};
|
|
16587
|
+
if (hashes?.length) {
|
|
16588
|
+
apkg.hashes = hashes;
|
|
16589
|
+
}
|
|
16590
|
+
if (cryptoProperties) {
|
|
16591
|
+
apkg.cryptoProperties = cryptoProperties;
|
|
16592
|
+
}
|
|
15598
16593
|
for (const k of Object.keys(res).filter((p) => {
|
|
15599
16594
|
if (["version", "description", "publisher"].includes(p)) {
|
|
15600
16595
|
return false;
|
|
15601
16596
|
}
|
|
15602
|
-
|
|
15603
|
-
return false;
|
|
15604
|
-
}
|
|
15605
|
-
return true;
|
|
16597
|
+
return !(queryObj.purlType !== "chrome-extension" && p === "name");
|
|
15606
16598
|
})) {
|
|
15607
16599
|
if (res[k] && res[k] !== "null") {
|
|
15608
16600
|
props.push({
|
|
@@ -15619,6 +16611,182 @@ export function convertOSQueryResults(
|
|
|
15619
16611
|
return pkgList;
|
|
15620
16612
|
}
|
|
15621
16613
|
|
|
16614
|
+
function createOsQueryComponentHashes(res) {
|
|
16615
|
+
const hashes = [];
|
|
16616
|
+
if (res?.md5) {
|
|
16617
|
+
hashes.push({ alg: "MD5", content: res.md5 });
|
|
16618
|
+
}
|
|
16619
|
+
if (res?.sha1) {
|
|
16620
|
+
hashes.push({ alg: "SHA-1", content: res.sha1 });
|
|
16621
|
+
}
|
|
16622
|
+
if (res?.sha256) {
|
|
16623
|
+
hashes.push({ alg: "SHA-256", content: res.sha256 });
|
|
16624
|
+
}
|
|
16625
|
+
return hashes.length ? hashes : undefined;
|
|
16626
|
+
}
|
|
16627
|
+
|
|
16628
|
+
function createOsQueryBomRef(
|
|
16629
|
+
queryCategory,
|
|
16630
|
+
componentType,
|
|
16631
|
+
res,
|
|
16632
|
+
name,
|
|
16633
|
+
version,
|
|
16634
|
+
purl,
|
|
16635
|
+
) {
|
|
16636
|
+
if (purl) {
|
|
16637
|
+
return decodeURIComponent(purl);
|
|
16638
|
+
}
|
|
16639
|
+
if (componentType === "cryptographic-asset") {
|
|
16640
|
+
return createOsQueryCryptoBomRef(queryCategory, res, name, version);
|
|
16641
|
+
}
|
|
16642
|
+
const identityEntry = [
|
|
16643
|
+
["path", res?.path],
|
|
16644
|
+
["key_file", res?.key_file],
|
|
16645
|
+
["history_file", res?.history_file],
|
|
16646
|
+
["fragment_path", res?.fragment_path],
|
|
16647
|
+
["source_path", res?.source_path],
|
|
16648
|
+
["source", res?.source],
|
|
16649
|
+
["key", res?.key],
|
|
16650
|
+
["label", res?.label],
|
|
16651
|
+
["identifier", res?.identifier],
|
|
16652
|
+
["uuid", res?.uuid],
|
|
16653
|
+
["device_id", res?.device_id],
|
|
16654
|
+
["sid", res?.sid],
|
|
16655
|
+
["logon_id", res?.logon_id],
|
|
16656
|
+
["pid", res?.pid],
|
|
16657
|
+
["uid", res?.uid],
|
|
16658
|
+
].find(
|
|
16659
|
+
([, value]) =>
|
|
16660
|
+
value !== undefined && value !== null && String(value).length,
|
|
16661
|
+
);
|
|
16662
|
+
return createOsQueryFallbackBomRef(
|
|
16663
|
+
queryCategory,
|
|
16664
|
+
componentType,
|
|
16665
|
+
name,
|
|
16666
|
+
version,
|
|
16667
|
+
identityEntry?.[0],
|
|
16668
|
+
identityEntry?.[1],
|
|
16669
|
+
);
|
|
16670
|
+
}
|
|
16671
|
+
|
|
16672
|
+
function createOsQueryCryptoBomRef(queryCategory, res, name, version) {
|
|
16673
|
+
const encodedName = encodeURIComponent(
|
|
16674
|
+
name || queryCategory || "crypto-asset",
|
|
16675
|
+
);
|
|
16676
|
+
switch (queryCategory) {
|
|
16677
|
+
case "trusted_gpg_keys":
|
|
16678
|
+
return `crypto/related-crypto-material/public-key/${encodedName}@sha256:${res?.sha256 || version || "unknown"}`;
|
|
16679
|
+
case "kernel_keys":
|
|
16680
|
+
return `crypto/related-crypto-material/key/${encodedName}@${version || res?.serial_number || "unknown"}`;
|
|
16681
|
+
case "certificates":
|
|
16682
|
+
case "secureboot_certificates":
|
|
16683
|
+
return `crypto/certificate/${encodedName}@${res?.sha256 ? `sha256:${res.sha256}` : version || "unknown"}`;
|
|
16684
|
+
default:
|
|
16685
|
+
return `crypto/related-crypto-material/unknown/${encodedName}@${version || "unknown"}`;
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16688
|
+
|
|
16689
|
+
function createOsQueryCryptoProperties(queryCategory, res, version) {
|
|
16690
|
+
switch (queryCategory) {
|
|
16691
|
+
case "trusted_gpg_keys":
|
|
16692
|
+
return {
|
|
16693
|
+
assetType: "related-crypto-material",
|
|
16694
|
+
relatedCryptoMaterialProperties: {
|
|
16695
|
+
type: "public-key",
|
|
16696
|
+
id: res?.sha256 || version || res?.path || res?.name || "unknown",
|
|
16697
|
+
state: "active",
|
|
16698
|
+
},
|
|
16699
|
+
};
|
|
16700
|
+
case "kernel_keys":
|
|
16701
|
+
return {
|
|
16702
|
+
assetType: "related-crypto-material",
|
|
16703
|
+
relatedCryptoMaterialProperties: {
|
|
16704
|
+
type: "key",
|
|
16705
|
+
id:
|
|
16706
|
+
res?.serial_number ||
|
|
16707
|
+
version ||
|
|
16708
|
+
res?.description ||
|
|
16709
|
+
res?.name ||
|
|
16710
|
+
"unknown",
|
|
16711
|
+
state: res?.timeout === "expd" ? "deactivated" : "active",
|
|
16712
|
+
},
|
|
16713
|
+
};
|
|
16714
|
+
case "certificates":
|
|
16715
|
+
case "secureboot_certificates": {
|
|
16716
|
+
const certificateFileExtension = extname(res?.path || "")
|
|
16717
|
+
.replace(/^\./, "")
|
|
16718
|
+
.toLowerCase();
|
|
16719
|
+
const certificateFormat =
|
|
16720
|
+
queryCategory === "secureboot_certificates"
|
|
16721
|
+
? "X.509"
|
|
16722
|
+
: deriveCertificateFormat(certificateFileExtension);
|
|
16723
|
+
return {
|
|
16724
|
+
assetType: "certificate",
|
|
16725
|
+
algorithmProperties: {
|
|
16726
|
+
executionEnvironment: "unknown",
|
|
16727
|
+
implementationPlatform: "unknown",
|
|
16728
|
+
},
|
|
16729
|
+
certificateProperties: {
|
|
16730
|
+
serialNumber: res?.serial || undefined,
|
|
16731
|
+
subjectName: res?.subject || res?.common_name || undefined,
|
|
16732
|
+
issuerName: res?.issuer || undefined,
|
|
16733
|
+
notValidBefore: normalizeCertificateDate(res?.not_valid_before),
|
|
16734
|
+
notValidAfter: normalizeCertificateDate(res?.not_valid_after),
|
|
16735
|
+
certificateFormat,
|
|
16736
|
+
certificateFileExtension: certificateFileExtension || undefined,
|
|
16737
|
+
fingerprint: res?.sha1
|
|
16738
|
+
? { alg: "SHA-1", content: res.sha1 }
|
|
16739
|
+
: undefined,
|
|
16740
|
+
},
|
|
16741
|
+
};
|
|
16742
|
+
}
|
|
16743
|
+
default:
|
|
16744
|
+
return {
|
|
16745
|
+
assetType: "related-crypto-material",
|
|
16746
|
+
relatedCryptoMaterialProperties: {
|
|
16747
|
+
type: "unknown",
|
|
16748
|
+
id: version || res?.path || res?.name || "unknown",
|
|
16749
|
+
},
|
|
16750
|
+
};
|
|
16751
|
+
}
|
|
16752
|
+
}
|
|
16753
|
+
|
|
16754
|
+
function deriveCertificateFormat(certificateFileExtension) {
|
|
16755
|
+
switch ((certificateFileExtension || "").toLowerCase()) {
|
|
16756
|
+
case "pem":
|
|
16757
|
+
return "PEM";
|
|
16758
|
+
case "der":
|
|
16759
|
+
return "DER";
|
|
16760
|
+
case "cer":
|
|
16761
|
+
case "crt":
|
|
16762
|
+
return "X.509";
|
|
16763
|
+
default:
|
|
16764
|
+
return undefined;
|
|
16765
|
+
}
|
|
16766
|
+
}
|
|
16767
|
+
|
|
16768
|
+
function normalizeCertificateDate(value) {
|
|
16769
|
+
if (value === undefined || value === null || value === "") {
|
|
16770
|
+
return undefined;
|
|
16771
|
+
}
|
|
16772
|
+
const stringValue = `${value}`.trim();
|
|
16773
|
+
if (!stringValue) {
|
|
16774
|
+
return undefined;
|
|
16775
|
+
}
|
|
16776
|
+
const numericValue = Number(stringValue);
|
|
16777
|
+
if (Number.isFinite(numericValue)) {
|
|
16778
|
+
const millis = stringValue.length > 10 ? numericValue : numericValue * 1000;
|
|
16779
|
+
const parsedDate = new Date(millis);
|
|
16780
|
+
return Number.isNaN(parsedDate.getTime())
|
|
16781
|
+
? undefined
|
|
16782
|
+
: parsedDate.toISOString();
|
|
16783
|
+
}
|
|
16784
|
+
const parsedDate = new Date(stringValue);
|
|
16785
|
+
return Number.isNaN(parsedDate.getTime())
|
|
16786
|
+
? undefined
|
|
16787
|
+
: parsedDate.toISOString();
|
|
16788
|
+
}
|
|
16789
|
+
|
|
15622
16790
|
/**
|
|
15623
16791
|
* Create a PackageURL object from a repository URL string, package type, and version.
|
|
15624
16792
|
*
|
|
@@ -17085,6 +18253,14 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
17085
18253
|
// continue regardless of error
|
|
17086
18254
|
}
|
|
17087
18255
|
gradleCmd = resolve(join(srcPath, findGradleFile));
|
|
18256
|
+
recordDecisionActivity(gradleCmd, {
|
|
18257
|
+
metadata: {
|
|
18258
|
+
decisionType: "path-resolution",
|
|
18259
|
+
selectedSource: "project-wrapper",
|
|
18260
|
+
tool: "gradle",
|
|
18261
|
+
},
|
|
18262
|
+
reason: `Selected project-local Gradle wrapper ${gradleCmd}.`,
|
|
18263
|
+
});
|
|
17088
18264
|
} else if (rootPath && safeExistsSync(join(rootPath, findGradleFile))) {
|
|
17089
18265
|
// Check if the root directory has a wrapper script
|
|
17090
18266
|
try {
|
|
@@ -17093,10 +18269,43 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
17093
18269
|
// continue regardless of error
|
|
17094
18270
|
}
|
|
17095
18271
|
gradleCmd = resolve(join(rootPath, findGradleFile));
|
|
18272
|
+
recordDecisionActivity(gradleCmd, {
|
|
18273
|
+
metadata: {
|
|
18274
|
+
decisionType: "path-resolution",
|
|
18275
|
+
selectedSource: "root-wrapper",
|
|
18276
|
+
tool: "gradle",
|
|
18277
|
+
},
|
|
18278
|
+
reason: `Selected root-level Gradle wrapper ${gradleCmd}.`,
|
|
18279
|
+
});
|
|
17096
18280
|
} else if (process.env.GRADLE_CMD) {
|
|
17097
18281
|
gradleCmd = process.env.GRADLE_CMD;
|
|
18282
|
+
recordDecisionActivity(gradleCmd, {
|
|
18283
|
+
metadata: {
|
|
18284
|
+
decisionType: "path-resolution",
|
|
18285
|
+
selectedSource: "GRADLE_CMD",
|
|
18286
|
+
tool: "gradle",
|
|
18287
|
+
},
|
|
18288
|
+
reason: `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
|
|
18289
|
+
});
|
|
17098
18290
|
} else if (process.env.GRADLE_HOME) {
|
|
17099
18291
|
gradleCmd = join(process.env.GRADLE_HOME, "bin", "gradle");
|
|
18292
|
+
recordDecisionActivity(gradleCmd, {
|
|
18293
|
+
metadata: {
|
|
18294
|
+
decisionType: "path-resolution",
|
|
18295
|
+
selectedSource: "GRADLE_HOME",
|
|
18296
|
+
tool: "gradle",
|
|
18297
|
+
},
|
|
18298
|
+
reason: `Selected Gradle command from GRADLE_HOME (${gradleCmd}).`,
|
|
18299
|
+
});
|
|
18300
|
+
} else {
|
|
18301
|
+
recordDecisionActivity(gradleCmd, {
|
|
18302
|
+
metadata: {
|
|
18303
|
+
decisionType: "path-resolution",
|
|
18304
|
+
selectedSource: "PATH",
|
|
18305
|
+
tool: "gradle",
|
|
18306
|
+
},
|
|
18307
|
+
reason: "Falling back to Gradle from PATH.",
|
|
18308
|
+
});
|
|
17100
18309
|
}
|
|
17101
18310
|
return gradleCmd;
|
|
17102
18311
|
}
|
|
@@ -17974,6 +19183,14 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
17974
19183
|
}
|
|
17975
19184
|
mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
|
|
17976
19185
|
isWrapperFound = true;
|
|
19186
|
+
recordDecisionActivity(mavenWrapperCmd, {
|
|
19187
|
+
metadata: {
|
|
19188
|
+
decisionType: "path-resolution",
|
|
19189
|
+
selectedSource: "project-wrapper-candidate",
|
|
19190
|
+
tool: "maven",
|
|
19191
|
+
},
|
|
19192
|
+
reason: `Found Maven wrapper candidate ${mavenWrapperCmd}.`,
|
|
19193
|
+
});
|
|
17977
19194
|
} else if (rootPath && safeExistsSync(join(rootPath, findMavenFile))) {
|
|
17978
19195
|
// Check if the root directory has a wrapper script
|
|
17979
19196
|
try {
|
|
@@ -17983,6 +19200,14 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
17983
19200
|
}
|
|
17984
19201
|
mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
|
|
17985
19202
|
isWrapperFound = true;
|
|
19203
|
+
recordDecisionActivity(mavenWrapperCmd, {
|
|
19204
|
+
metadata: {
|
|
19205
|
+
decisionType: "path-resolution",
|
|
19206
|
+
selectedSource: "root-wrapper-candidate",
|
|
19207
|
+
tool: "maven",
|
|
19208
|
+
},
|
|
19209
|
+
reason: `Found root-level Maven wrapper candidate ${mavenWrapperCmd}.`,
|
|
19210
|
+
});
|
|
17986
19211
|
}
|
|
17987
19212
|
if (isWrapperFound) {
|
|
17988
19213
|
if (DEBUG_MODE) {
|
|
@@ -17991,25 +19216,74 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
17991
19216
|
);
|
|
17992
19217
|
}
|
|
17993
19218
|
const result = safeSpawnSync(mavenWrapperCmd, ["wrapper:wrapper"], {
|
|
19219
|
+
cdxgenActivity: {
|
|
19220
|
+
kind: "probe",
|
|
19221
|
+
metadata: {
|
|
19222
|
+
tool: "maven",
|
|
19223
|
+
},
|
|
19224
|
+
probeType: "wrapper-readiness",
|
|
19225
|
+
},
|
|
17994
19226
|
cwd: rootPath,
|
|
17995
19227
|
shell: isWin,
|
|
17996
19228
|
});
|
|
17997
19229
|
if (!result.error && !result.status) {
|
|
17998
19230
|
isWrapperReady = true;
|
|
17999
19231
|
mavenCmd = mavenWrapperCmd;
|
|
19232
|
+
recordDecisionActivity(mavenCmd, {
|
|
19233
|
+
metadata: {
|
|
19234
|
+
decisionType: "path-resolution",
|
|
19235
|
+
selectedSource: "wrapper",
|
|
19236
|
+
tool: "maven",
|
|
19237
|
+
},
|
|
19238
|
+
reason: `Selected Maven wrapper ${mavenCmd} after readiness probe.`,
|
|
19239
|
+
});
|
|
18000
19240
|
} else {
|
|
18001
19241
|
if (DEBUG_MODE) {
|
|
18002
19242
|
console.log(
|
|
18003
19243
|
"Maven wrapper script test has failed. Will use the installed version of maven.",
|
|
18004
19244
|
);
|
|
18005
19245
|
}
|
|
19246
|
+
recordDecisionActivity(mavenWrapperCmd, {
|
|
19247
|
+
metadata: {
|
|
19248
|
+
decisionType: "fallback",
|
|
19249
|
+
selectedSource: "PATH",
|
|
19250
|
+
skippedSource: "wrapper",
|
|
19251
|
+
tool: "maven",
|
|
19252
|
+
},
|
|
19253
|
+
reason: `Maven wrapper readiness probe failed for ${mavenWrapperCmd}; falling back to installed Maven.`,
|
|
19254
|
+
});
|
|
18006
19255
|
}
|
|
18007
19256
|
}
|
|
18008
19257
|
if (!isWrapperFound || !isWrapperReady) {
|
|
18009
19258
|
if (process.env.MVN_CMD || process.env.MAVEN_CMD) {
|
|
18010
19259
|
mavenCmd = process.env.MVN_CMD || process.env.MAVEN_CMD;
|
|
19260
|
+
recordDecisionActivity(mavenCmd, {
|
|
19261
|
+
metadata: {
|
|
19262
|
+
decisionType: "path-resolution",
|
|
19263
|
+
selectedSource: process.env.MVN_CMD ? "MVN_CMD" : "MAVEN_CMD",
|
|
19264
|
+
tool: "maven",
|
|
19265
|
+
},
|
|
19266
|
+
reason: `Selected Maven command from environment (${mavenCmd}).`,
|
|
19267
|
+
});
|
|
18011
19268
|
} else if (process.env.MAVEN_HOME) {
|
|
18012
19269
|
mavenCmd = join(process.env.MAVEN_HOME, "bin", "mvn");
|
|
19270
|
+
recordDecisionActivity(mavenCmd, {
|
|
19271
|
+
metadata: {
|
|
19272
|
+
decisionType: "path-resolution",
|
|
19273
|
+
selectedSource: "MAVEN_HOME",
|
|
19274
|
+
tool: "maven",
|
|
19275
|
+
},
|
|
19276
|
+
reason: `Selected Maven command from MAVEN_HOME (${mavenCmd}).`,
|
|
19277
|
+
});
|
|
19278
|
+
} else {
|
|
19279
|
+
recordDecisionActivity(mavenCmd, {
|
|
19280
|
+
metadata: {
|
|
19281
|
+
decisionType: "path-resolution",
|
|
19282
|
+
selectedSource: "PATH",
|
|
19283
|
+
tool: "maven",
|
|
19284
|
+
},
|
|
19285
|
+
reason: "Falling back to Maven from PATH.",
|
|
19286
|
+
});
|
|
18013
19287
|
}
|
|
18014
19288
|
}
|
|
18015
19289
|
return mavenCmd;
|
|
@@ -19344,16 +20618,17 @@ export async function addEvidenceForImports(
|
|
|
19344
20618
|
const evidences = allImports[subevidence];
|
|
19345
20619
|
for (const evidence of evidences) {
|
|
19346
20620
|
if (evidence && Object.keys(evidence).length && evidence.fileName) {
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
20621
|
+
const occurrence = createOccurrenceEvidence(evidence.fileName, {
|
|
20622
|
+
...(evidence.lineNumber ? { line: evidence.lineNumber } : {}),
|
|
20623
|
+
});
|
|
20624
|
+
if (occurrence) {
|
|
20625
|
+
pkg.evidence = pkg.evidence || {};
|
|
20626
|
+
pkg.evidence.occurrences = pkg.evidence.occurrences || [];
|
|
20627
|
+
const occurrenceLocation = `${occurrence.location}${occurrence.line ? `#${occurrence.line}` : ""}`;
|
|
20628
|
+
if (!seenOccurrenceLocations.has(occurrenceLocation)) {
|
|
20629
|
+
pkg.evidence.occurrences.push(occurrence);
|
|
20630
|
+
seenOccurrenceLocations.add(occurrenceLocation);
|
|
20631
|
+
}
|
|
19357
20632
|
}
|
|
19358
20633
|
importedModules.add(evidence.importedAs);
|
|
19359
20634
|
for (const importedSm of evidence.importedModules || []) {
|
|
@@ -20578,14 +21853,16 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
20578
21853
|
purlLocationMap[apkg.purl],
|
|
20579
21854
|
).sort();
|
|
20580
21855
|
// Add the occurrences evidence
|
|
20581
|
-
apkg.evidence
|
|
20582
|
-
|
|
20583
|
-
|
|
21856
|
+
apkg.evidence = apkg.evidence || {};
|
|
21857
|
+
apkg.evidence.occurrences = locationOccurrences.map((l) =>
|
|
21858
|
+
parseOccurrenceEvidenceLocation(l),
|
|
21859
|
+
);
|
|
20584
21860
|
// Set the package scope
|
|
20585
21861
|
apkg.scope = "required";
|
|
20586
21862
|
}
|
|
20587
21863
|
// Add the imported modules to properties
|
|
20588
21864
|
if (purlModulesMap[apkg.purl]) {
|
|
21865
|
+
apkg.properties = apkg.properties || [];
|
|
20589
21866
|
apkg.properties.push({
|
|
20590
21867
|
name: "ImportedModules",
|
|
20591
21868
|
value: Array.from(purlModulesMap[apkg.purl]).sort().join(", "),
|
|
@@ -20593,6 +21870,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
20593
21870
|
}
|
|
20594
21871
|
// Add the called methods to properties
|
|
20595
21872
|
if (purlMethodsMap[apkg.purl]) {
|
|
21873
|
+
apkg.properties = apkg.properties || [];
|
|
20596
21874
|
apkg.properties.push({
|
|
20597
21875
|
name: "CalledMethods",
|
|
20598
21876
|
value: Array.from(purlMethodsMap[apkg.purl]).sort().join(", "),
|
|
@@ -20831,6 +22109,14 @@ export function extractPathEnv(envValues) {
|
|
|
20831
22109
|
expandedBinPaths.push(apath);
|
|
20832
22110
|
}
|
|
20833
22111
|
}
|
|
22112
|
+
recordObservedActivity("path-resolution", "PATH", {
|
|
22113
|
+
metadata: {
|
|
22114
|
+
capability: "path-lookup",
|
|
22115
|
+
pathCount: expandedBinPaths.length,
|
|
22116
|
+
},
|
|
22117
|
+
reasonBuilder: (count) =>
|
|
22118
|
+
`Expanded PATH into ${expandedBinPaths.length} executable search path(s)${buildReadCountSuffix(count)}.`,
|
|
22119
|
+
});
|
|
20834
22120
|
return expandedBinPaths;
|
|
20835
22121
|
}
|
|
20836
22122
|
|
|
@@ -20839,13 +22125,17 @@ export function extractPathEnv(envValues) {
|
|
|
20839
22125
|
*
|
|
20840
22126
|
* @param basePath Base directory
|
|
20841
22127
|
* @param binPaths {Array[String]} Paths containing potential binaries
|
|
22128
|
+
* @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
|
|
20842
22129
|
* @return {Array[String]} List of executables
|
|
20843
22130
|
*/
|
|
20844
|
-
export function collectExecutables(basePath, binPaths) {
|
|
22131
|
+
export function collectExecutables(basePath, binPaths, excludePaths = []) {
|
|
20845
22132
|
if (!binPaths) {
|
|
20846
22133
|
return [];
|
|
20847
22134
|
}
|
|
20848
22135
|
const executablesByResolvedPath = new Map();
|
|
22136
|
+
const excludedPathSet = new Set(
|
|
22137
|
+
(excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
|
|
22138
|
+
);
|
|
20849
22139
|
const ignoreList = [
|
|
20850
22140
|
"**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
|
|
20851
22141
|
"[",
|
|
@@ -20865,12 +22155,38 @@ export function collectExecutables(basePath, binPaths) {
|
|
|
20865
22155
|
let resolvedFile = file;
|
|
20866
22156
|
try {
|
|
20867
22157
|
resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
|
|
20868
|
-
|
|
22158
|
+
if (resolvedFile !== file) {
|
|
22159
|
+
recordSymlinkResolution(join(basePath, file), resolvedFile, {
|
|
22160
|
+
basePath,
|
|
22161
|
+
metadata: {
|
|
22162
|
+
resolutionKind: "executable",
|
|
22163
|
+
},
|
|
22164
|
+
reason: `Resolved executable candidate ${file} to ${resolvedFile}.`,
|
|
22165
|
+
});
|
|
22166
|
+
}
|
|
22167
|
+
} catch (err) {
|
|
22168
|
+
recordSymlinkResolution(join(basePath, file), undefined, {
|
|
22169
|
+
basePath,
|
|
22170
|
+
errorCode: err?.code || err?.name,
|
|
22171
|
+
metadata: {
|
|
22172
|
+
resolutionKind: "executable",
|
|
22173
|
+
},
|
|
22174
|
+
reason: `Failed to resolve executable candidate ${file}.`,
|
|
22175
|
+
status: "failed",
|
|
22176
|
+
});
|
|
20869
22177
|
// Broken symlinks or permission errors can prevent realpath resolution.
|
|
20870
22178
|
if (DEBUG_MODE) {
|
|
20871
22179
|
console.log(`Unable to resolve executable path alias for ${file}`);
|
|
20872
22180
|
}
|
|
20873
22181
|
}
|
|
22182
|
+
if (
|
|
22183
|
+
excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
|
|
22184
|
+
excludedPathSet.has(
|
|
22185
|
+
resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
|
|
22186
|
+
)
|
|
22187
|
+
) {
|
|
22188
|
+
continue;
|
|
22189
|
+
}
|
|
20874
22190
|
const existingFile = executablesByResolvedPath.get(resolvedFile);
|
|
20875
22191
|
if (shouldPreferUsrMergedExecutablePath(file, existingFile)) {
|
|
20876
22192
|
executablesByResolvedPath.set(resolvedFile, file);
|
|
@@ -20902,6 +22218,7 @@ function shouldPreferUsrMergedExecutablePath(file, existingFile) {
|
|
|
20902
22218
|
* @param libPaths {Array[String]} Paths containing potential libraries
|
|
20903
22219
|
* @param ldConf {String} Config file used by ldconfig to locate additional paths
|
|
20904
22220
|
* @param ldConfDirPattern {String} Config directory that can contain more .conf files for ldconfig
|
|
22221
|
+
* @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
|
|
20905
22222
|
*
|
|
20906
22223
|
* @return {Array[String]} List of executables
|
|
20907
22224
|
*/
|
|
@@ -20910,11 +22227,15 @@ export function collectSharedLibs(
|
|
|
20910
22227
|
libPaths,
|
|
20911
22228
|
ldConf,
|
|
20912
22229
|
ldConfDirPattern,
|
|
22230
|
+
excludePaths = [],
|
|
20913
22231
|
) {
|
|
20914
22232
|
if (!libPaths) {
|
|
20915
22233
|
return [];
|
|
20916
22234
|
}
|
|
20917
|
-
|
|
22235
|
+
const sharedLibs = [];
|
|
22236
|
+
const excludedPathSet = new Set(
|
|
22237
|
+
(excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
|
|
22238
|
+
);
|
|
20918
22239
|
const ignoreList = [
|
|
20919
22240
|
"**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
|
|
20920
22241
|
];
|
|
@@ -20946,7 +22267,39 @@ export function collectSharedLibs(
|
|
|
20946
22267
|
follow: true,
|
|
20947
22268
|
ignore: ignoreList,
|
|
20948
22269
|
});
|
|
20949
|
-
|
|
22270
|
+
for (const file of files) {
|
|
22271
|
+
let resolvedFile = file;
|
|
22272
|
+
try {
|
|
22273
|
+
resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
|
|
22274
|
+
recordSymlinkResolution(join(basePath, file), resolvedFile, {
|
|
22275
|
+
basePath,
|
|
22276
|
+
metadata: {
|
|
22277
|
+
resolutionKind: "shared-library",
|
|
22278
|
+
},
|
|
22279
|
+
reason: `Resolved shared library candidate ${file} to ${resolvedFile}.`,
|
|
22280
|
+
});
|
|
22281
|
+
} catch (err) {
|
|
22282
|
+
recordSymlinkResolution(join(basePath, file), undefined, {
|
|
22283
|
+
basePath,
|
|
22284
|
+
errorCode: err?.code || err?.name,
|
|
22285
|
+
metadata: {
|
|
22286
|
+
resolutionKind: "shared-library",
|
|
22287
|
+
},
|
|
22288
|
+
reason: `Failed to resolve shared library candidate ${file}.`,
|
|
22289
|
+
status: "failed",
|
|
22290
|
+
});
|
|
22291
|
+
// Broken symlinks or permission errors can prevent realpath resolution.
|
|
22292
|
+
}
|
|
22293
|
+
if (
|
|
22294
|
+
excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
|
|
22295
|
+
excludedPathSet.has(
|
|
22296
|
+
resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
|
|
22297
|
+
)
|
|
22298
|
+
) {
|
|
22299
|
+
continue;
|
|
22300
|
+
}
|
|
22301
|
+
sharedLibs.push(file);
|
|
22302
|
+
}
|
|
20950
22303
|
} catch (_err) {
|
|
20951
22304
|
// ignore
|
|
20952
22305
|
}
|
|
@@ -21080,11 +22433,7 @@ export function hasDangerousUnicode(str) {
|
|
|
21080
22433
|
|
|
21081
22434
|
// Check for control characters (except common ones like \n, \r, \t)
|
|
21082
22435
|
const controlChars = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/;
|
|
21083
|
-
|
|
21084
|
-
return true;
|
|
21085
|
-
}
|
|
21086
|
-
|
|
21087
|
-
return false;
|
|
22436
|
+
return controlChars.test(str);
|
|
21088
22437
|
}
|
|
21089
22438
|
// biome-ignore-end lint/suspicious/noControlCharactersInRegex: validation
|
|
21090
22439
|
|
|
@@ -21119,11 +22468,7 @@ export function isValidDriveRoot(root) {
|
|
|
21119
22468
|
}
|
|
21120
22469
|
|
|
21121
22470
|
// Backslash (optional) must be exactly ASCII backslash (0x5C)
|
|
21122
|
-
|
|
21123
|
-
return false;
|
|
21124
|
-
}
|
|
21125
|
-
|
|
21126
|
-
return true;
|
|
22471
|
+
return !(backslash && backslash.charCodeAt(0) !== 0x5c);
|
|
21127
22472
|
}
|
|
21128
22473
|
|
|
21129
22474
|
/**
|