@cyclonedx/cdxgen 12.2.1 → 12.3.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 +239 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +513 -167
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +154 -11
- package/lib/cli/index.poku.js +251 -0
- package/lib/helpers/analyzer.js +446 -2
- package/lib/helpers/analyzer.poku.js +72 -1
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/display.js +241 -59
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +700 -128
- package/lib/helpers/utils.poku.js +877 -80
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +17 -0
- package/lib/server/server.js +225 -336
- package/lib/server/server.poku.js +16 -10
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +19 -3
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +11 -0
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -7
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +29 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -36
- 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.map +1 -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/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- package/types/bin/licenses.d.ts.map +0 -1
package/lib/helpers/utils.js
CHANGED
|
@@ -54,10 +54,32 @@ import { parse as _load, parseAllDocuments } from "yaml";
|
|
|
54
54
|
import { getTreeWithPlugin } from "../managers/piptree.js";
|
|
55
55
|
import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
|
|
56
56
|
import Arborist from "../third-party/arborist/lib/index.js";
|
|
57
|
+
import { analyzeSuspiciousJsFile } from "./analyzer.js";
|
|
57
58
|
import { parseWorkflowFile } from "./ciParsers/githubActions.js";
|
|
58
59
|
import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
|
|
59
60
|
import { thoughtLog, traceLog } from "./logger.js";
|
|
61
|
+
import { createLolbasProperties } from "./lolbas.js";
|
|
62
|
+
import {
|
|
63
|
+
createOsQueryPurl,
|
|
64
|
+
deriveOsQueryDescription,
|
|
65
|
+
deriveOsQueryName,
|
|
66
|
+
deriveOsQueryPublisher,
|
|
67
|
+
deriveOsQueryVersion,
|
|
68
|
+
sanitizeOsQueryIdentity,
|
|
69
|
+
} from "./osqueryTransform.js";
|
|
70
|
+
import {
|
|
71
|
+
collectPyLockFileComponents,
|
|
72
|
+
collectPyLockPackageProperties,
|
|
73
|
+
collectPyLockTopLevelProperties,
|
|
74
|
+
getPyLockPackages,
|
|
75
|
+
isDefaultPypiRegistry,
|
|
76
|
+
isPyLockObject,
|
|
77
|
+
} from "./pylockutils.js";
|
|
60
78
|
import { get_python_command_from_env, getVenvMetadata } from "./pythonutils.js";
|
|
79
|
+
import {
|
|
80
|
+
collectNpmRegistryProvenanceProperties,
|
|
81
|
+
collectPypiRegistryProvenanceProperties,
|
|
82
|
+
} from "./registryProvenance.js";
|
|
61
83
|
|
|
62
84
|
let url = import.meta?.url;
|
|
63
85
|
if (url && !url.startsWith("file://")) {
|
|
@@ -229,16 +251,37 @@ export function safeSpawnSync(command, args, options) {
|
|
|
229
251
|
if (!options.timeout) {
|
|
230
252
|
options.timeout = TIMEOUT_MS;
|
|
231
253
|
}
|
|
254
|
+
// Emit certain operational warnings only once per process to keep audit logs readable.
|
|
255
|
+
const emitNoticeOnce = (noticeKey, message, level = "warn") => {
|
|
256
|
+
if (!globalThis.__cdxgenNoticeCache) {
|
|
257
|
+
globalThis.__cdxgenNoticeCache = new Set();
|
|
258
|
+
}
|
|
259
|
+
if (globalThis.__cdxgenNoticeCache.has(noticeKey)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
globalThis.__cdxgenNoticeCache.add(noticeKey);
|
|
263
|
+
if (level === "log") {
|
|
264
|
+
console.log(message);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
console.warn(message);
|
|
268
|
+
};
|
|
232
269
|
// Check for -S for python invocations in secure mode
|
|
233
270
|
if (command.includes("python") && (!args?.length || args[0] !== "-S")) {
|
|
234
271
|
if (isSecureMode) {
|
|
235
|
-
|
|
272
|
+
emitNoticeOnce(
|
|
273
|
+
"python-without-S-secure",
|
|
236
274
|
"\x1b[1;35mNotice: Running python command without '-S' argument. This is a bug in cdxgen. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
|
|
237
275
|
);
|
|
238
276
|
} else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
|
|
239
|
-
|
|
277
|
+
emitNoticeOnce(
|
|
278
|
+
"python-without-S-container",
|
|
279
|
+
"Running python command without '-S' argument.",
|
|
280
|
+
"log",
|
|
281
|
+
);
|
|
240
282
|
} else {
|
|
241
|
-
|
|
283
|
+
emitNoticeOnce(
|
|
284
|
+
"python-without-S-host",
|
|
242
285
|
"\x1b[1;35mNotice: Running python command without '-S' argument. Only run cdxgen in trusted directories to prevent auto-executing local scripts.\x1b[0m",
|
|
243
286
|
);
|
|
244
287
|
}
|
|
@@ -265,14 +308,20 @@ export function safeSpawnSync(command, args, options) {
|
|
|
265
308
|
);
|
|
266
309
|
if (!hasOnlyBinary) {
|
|
267
310
|
if (isSecureMode) {
|
|
268
|
-
|
|
311
|
+
emitNoticeOnce(
|
|
312
|
+
"pip-without-only-binary-secure",
|
|
269
313
|
"\x1b[1;31mSecurity Alert: pip/uv install invoked without '--only-binary' argument in secure mode. This is a bug in cdxgen and introduces Arbitrary Code Execution (ACE) risks. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
|
|
270
314
|
);
|
|
271
315
|
} else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
|
|
272
|
-
|
|
316
|
+
emitNoticeOnce(
|
|
317
|
+
"pip-without-only-binary-container",
|
|
318
|
+
"Running pip/uv install without '--only-binary' argument.",
|
|
319
|
+
"log",
|
|
320
|
+
);
|
|
273
321
|
} else {
|
|
274
|
-
|
|
275
|
-
"
|
|
322
|
+
emitNoticeOnce(
|
|
323
|
+
"pip-without-only-binary-host",
|
|
324
|
+
"\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'. This allows executing untrusted setup.py scripts. Only run cdxgen in trusted directories.\x1b[0m",
|
|
276
325
|
);
|
|
277
326
|
}
|
|
278
327
|
}
|
|
@@ -337,6 +386,8 @@ export const DEBUG_MODE =
|
|
|
337
386
|
["debug", "verbose"].includes(process.env.CDXGEN_DEBUG_MODE) ||
|
|
338
387
|
process.env.SCAN_DEBUG_MODE === "debug";
|
|
339
388
|
|
|
389
|
+
export const CDXGEN_SPDX_CREATED_BY = process.env.CDXGEN_SPDX_CREATED_BY;
|
|
390
|
+
|
|
340
391
|
// Table border style for console output.
|
|
341
392
|
export const TABLE_BORDER_STYLE = ["ascii", "unicode", "auto"].includes(
|
|
342
393
|
`${process.env.CDXGEN_TABLE_BORDER || ""}`.toLowerCase(),
|
|
@@ -390,6 +441,19 @@ export function shouldFetchLicense() {
|
|
|
390
441
|
);
|
|
391
442
|
}
|
|
392
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Determines whether remote package metadata should be fetched for enrichment.
|
|
446
|
+
*
|
|
447
|
+
* @returns {boolean} True when registry metadata enrichment is enabled.
|
|
448
|
+
*/
|
|
449
|
+
export function shouldFetchPackageMetadata() {
|
|
450
|
+
return (
|
|
451
|
+
shouldFetchLicense() ||
|
|
452
|
+
(process.env.CDXGEN_FETCH_PKG_METADATA &&
|
|
453
|
+
["true", "1"].includes(process.env.CDXGEN_FETCH_PKG_METADATA))
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
393
457
|
/**
|
|
394
458
|
* Determines whether VCS (version control system) information should be fetched
|
|
395
459
|
* for Go packages, based on the GO_FETCH_VCS environment variable.
|
|
@@ -686,6 +750,12 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
686
750
|
"vscode-extensions",
|
|
687
751
|
"ide-extensions",
|
|
688
752
|
],
|
|
753
|
+
"chrome-extension": [
|
|
754
|
+
"chrome-extension",
|
|
755
|
+
"chrome-extensions",
|
|
756
|
+
"chromium-extension",
|
|
757
|
+
"chromium-extensions",
|
|
758
|
+
],
|
|
689
759
|
};
|
|
690
760
|
|
|
691
761
|
// Package manager aliases
|
|
@@ -1375,6 +1445,10 @@ export async function getNpmMetadata(pkgList) {
|
|
|
1375
1445
|
if (body.homepage) {
|
|
1376
1446
|
p.homepage = { url: body.homepage };
|
|
1377
1447
|
}
|
|
1448
|
+
p.properties = p.properties || [];
|
|
1449
|
+
p.properties.push(
|
|
1450
|
+
...collectNpmRegistryProvenanceProperties(body, p.version),
|
|
1451
|
+
);
|
|
1378
1452
|
cdepList.push(p);
|
|
1379
1453
|
} catch (_err) {
|
|
1380
1454
|
cdepList.push(p);
|
|
@@ -1393,6 +1467,382 @@ export async function getNpmMetadata(pkgList) {
|
|
|
1393
1467
|
* @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
|
|
1394
1468
|
* @param {boolean} securityProps Collect security-related properties
|
|
1395
1469
|
*/
|
|
1470
|
+
const NPM_INSTALL_HOOK_NAMES = [
|
|
1471
|
+
"preinstall",
|
|
1472
|
+
"install",
|
|
1473
|
+
"postinstall",
|
|
1474
|
+
"prepublish",
|
|
1475
|
+
"prepare",
|
|
1476
|
+
];
|
|
1477
|
+
|
|
1478
|
+
const NPM_LIFECYCLE_OBFUSCATION_PATTERNS = [
|
|
1479
|
+
[
|
|
1480
|
+
"base64-decode",
|
|
1481
|
+
/\b(?:base64(?:\s+--decode|\s+-d)?|openssl\s+enc\s+-base64\s+-d)\b/i,
|
|
1482
|
+
],
|
|
1483
|
+
["buffer-base64", /Buffer\.from\s*\([^)]*,\s*["']base64["']\s*\)/i],
|
|
1484
|
+
["atob", /\batob\s*\(/i],
|
|
1485
|
+
["string-from-char-code", /\bString\.fromCharCode\s*\(/i],
|
|
1486
|
+
["long-base64-literal", /\b[A-Za-z0-9+/]{80,}={0,2}\b/],
|
|
1487
|
+
];
|
|
1488
|
+
|
|
1489
|
+
const NPM_LIFECYCLE_EXECUTION_PATTERNS = [
|
|
1490
|
+
["node-eval", /\bnode\b[^\n]*\s-[ep]\b/i],
|
|
1491
|
+
["eval", /\beval\s*\(/i],
|
|
1492
|
+
["function-constructor", /\b(?:new\s+Function|Function\s*\()/i],
|
|
1493
|
+
[
|
|
1494
|
+
"child-process",
|
|
1495
|
+
/\b(?:child_process|node:child_process|execSync|execFileSync|spawnSync|execFile|spawn|exec)\b/i,
|
|
1496
|
+
],
|
|
1497
|
+
[
|
|
1498
|
+
"shell-inline",
|
|
1499
|
+
/\b(?:sh|bash|cmd|powershell|pwsh)\b[^\n]*\s-(?:c|Command|EncodedCommand)\b/i,
|
|
1500
|
+
],
|
|
1501
|
+
];
|
|
1502
|
+
|
|
1503
|
+
const NPM_LIFECYCLE_NETWORK_PATTERNS = [
|
|
1504
|
+
["curl", /\bcurl\b/i],
|
|
1505
|
+
["wget", /\bwget\b/i],
|
|
1506
|
+
["invoke-webrequest", /\b(?:invoke-webrequest|iwr)\b/i],
|
|
1507
|
+
["http-url", /https?:\/\//i],
|
|
1508
|
+
];
|
|
1509
|
+
|
|
1510
|
+
const NPM_LIFECYCLE_JS_RUNNERS = new Set([
|
|
1511
|
+
"babel-node",
|
|
1512
|
+
"node",
|
|
1513
|
+
"ts-node",
|
|
1514
|
+
"tsx",
|
|
1515
|
+
"bun",
|
|
1516
|
+
"deno",
|
|
1517
|
+
]);
|
|
1518
|
+
|
|
1519
|
+
const NPM_LIFECYCLE_JS_RUNNER_VALUE_OPTIONS = new Set([
|
|
1520
|
+
"-c",
|
|
1521
|
+
"-e",
|
|
1522
|
+
"-p",
|
|
1523
|
+
"-r",
|
|
1524
|
+
"--config",
|
|
1525
|
+
"--conditions",
|
|
1526
|
+
"--cwd-file",
|
|
1527
|
+
"--compilerOptions",
|
|
1528
|
+
"--cwd",
|
|
1529
|
+
"--env-file",
|
|
1530
|
+
"--env-file-if-exists",
|
|
1531
|
+
"--eval",
|
|
1532
|
+
"--experimental-loader",
|
|
1533
|
+
"--ignore",
|
|
1534
|
+
"--import",
|
|
1535
|
+
"--import-map",
|
|
1536
|
+
"--input-type",
|
|
1537
|
+
"--inspect",
|
|
1538
|
+
"--inspect-brk",
|
|
1539
|
+
"--inspect-port",
|
|
1540
|
+
"--loader",
|
|
1541
|
+
"--print",
|
|
1542
|
+
"--preload",
|
|
1543
|
+
"--project",
|
|
1544
|
+
"--require",
|
|
1545
|
+
"--test-name-pattern",
|
|
1546
|
+
"--test-reporter",
|
|
1547
|
+
"--test-reporter-destination",
|
|
1548
|
+
"--test-shard",
|
|
1549
|
+
"--title",
|
|
1550
|
+
"--tsconfig",
|
|
1551
|
+
"--watch-path",
|
|
1552
|
+
]);
|
|
1553
|
+
|
|
1554
|
+
const NPM_LIFECYCLE_JS_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/i;
|
|
1555
|
+
const SHELL_ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
|
|
1556
|
+
|
|
1557
|
+
function splitLifecycleScriptCommands(scriptValue) {
|
|
1558
|
+
const commands = [];
|
|
1559
|
+
let current = "";
|
|
1560
|
+
let escaped = false;
|
|
1561
|
+
let quoteChar = "";
|
|
1562
|
+
|
|
1563
|
+
for (let index = 0; index < scriptValue.length; index++) {
|
|
1564
|
+
const character = scriptValue[index];
|
|
1565
|
+
if (escaped) {
|
|
1566
|
+
current += character;
|
|
1567
|
+
escaped = false;
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
if (character === "\\") {
|
|
1571
|
+
current += character;
|
|
1572
|
+
escaped = true;
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (quoteChar) {
|
|
1576
|
+
current += character;
|
|
1577
|
+
if (character === quoteChar) {
|
|
1578
|
+
quoteChar = "";
|
|
1579
|
+
}
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (character === '"' || character === "'") {
|
|
1583
|
+
current += character;
|
|
1584
|
+
quoteChar = character;
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (character === ";" || character === "|") {
|
|
1588
|
+
if (current.trim()) {
|
|
1589
|
+
commands.push(current.trim());
|
|
1590
|
+
}
|
|
1591
|
+
current = "";
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if (character === "&") {
|
|
1595
|
+
if (current.trim()) {
|
|
1596
|
+
commands.push(current.trim());
|
|
1597
|
+
}
|
|
1598
|
+
current = "";
|
|
1599
|
+
if (scriptValue[index + 1] === "&") {
|
|
1600
|
+
index += 1;
|
|
1601
|
+
}
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
current += character;
|
|
1605
|
+
}
|
|
1606
|
+
if (current.trim()) {
|
|
1607
|
+
commands.push(current.trim());
|
|
1608
|
+
}
|
|
1609
|
+
return commands;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function lifecycleRunnerOptionConsumesValue(token) {
|
|
1613
|
+
if (!token?.startsWith("-") || token === "--") {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
if (token.includes("=")) {
|
|
1617
|
+
return false;
|
|
1618
|
+
}
|
|
1619
|
+
if (token.startsWith("--")) {
|
|
1620
|
+
return NPM_LIFECYCLE_JS_RUNNER_VALUE_OPTIONS.has(token);
|
|
1621
|
+
}
|
|
1622
|
+
if (token.length > 2) {
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
return NPM_LIFECYCLE_JS_RUNNER_VALUE_OPTIONS.has(token);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function isLifecycleScriptSourceFile(token) {
|
|
1629
|
+
if (!token || token.startsWith("-")) {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
const fileToken = token.split(/[?#]/u, 1)[0];
|
|
1633
|
+
return NPM_LIFECYCLE_JS_SOURCE_FILE_PATTERN.test(fileToken);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function findLifecycleScriptSourceArg(tokens, startIndex) {
|
|
1637
|
+
for (let index = startIndex; index < tokens.length; index++) {
|
|
1638
|
+
const token = tokens[index]?.trim();
|
|
1639
|
+
if (!token) {
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (token === "--" || SHELL_ENV_ASSIGNMENT_PATTERN.test(token)) {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (token.startsWith("-")) {
|
|
1646
|
+
if (
|
|
1647
|
+
lifecycleRunnerOptionConsumesValue(token) &&
|
|
1648
|
+
index + 1 < tokens.length
|
|
1649
|
+
) {
|
|
1650
|
+
index += 1;
|
|
1651
|
+
}
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (isLifecycleScriptSourceFile(token)) {
|
|
1655
|
+
return token;
|
|
1656
|
+
}
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
return undefined;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function findLifecycleScriptSourceStartIndex(tokens, runnerIndex) {
|
|
1663
|
+
const runnerToken = tokens[runnerIndex];
|
|
1664
|
+
for (let index = runnerIndex + 1; index < tokens.length; index++) {
|
|
1665
|
+
const token = tokens[index]?.trim();
|
|
1666
|
+
if (!token || token === "--" || SHELL_ENV_ASSIGNMENT_PATTERN.test(token)) {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
if (token.startsWith("-")) {
|
|
1670
|
+
if (
|
|
1671
|
+
lifecycleRunnerOptionConsumesValue(token) &&
|
|
1672
|
+
index + 1 < tokens.length
|
|
1673
|
+
) {
|
|
1674
|
+
index += 1;
|
|
1675
|
+
}
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (runnerToken === "deno") {
|
|
1679
|
+
return token === "run" ? index + 1 : undefined;
|
|
1680
|
+
}
|
|
1681
|
+
if (runnerToken === "bun" && token === "run") {
|
|
1682
|
+
return index + 1;
|
|
1683
|
+
}
|
|
1684
|
+
return index;
|
|
1685
|
+
}
|
|
1686
|
+
return undefined;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function isResolvedPathWithinDirectory(baseDir, resolvedFile) {
|
|
1690
|
+
const relativePath = relative(baseDir, resolvedFile);
|
|
1691
|
+
if (!relativePath) {
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
return (
|
|
1695
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
1696
|
+
relativePath !== ".." &&
|
|
1697
|
+
!path.isAbsolute(relativePath)
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function collectLifecyclePatternIndicators(scriptValue, patterns) {
|
|
1702
|
+
const indicators = [];
|
|
1703
|
+
patterns.forEach(([name, pattern]) => {
|
|
1704
|
+
if (pattern.test(scriptValue)) {
|
|
1705
|
+
indicators.push(name);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
return indicators;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function extractLifecycleScriptSourceFiles(pkgJsonFile, scriptValue) {
|
|
1712
|
+
const sourceFiles = [];
|
|
1713
|
+
const seen = new Set();
|
|
1714
|
+
if (!scriptValue || typeof scriptValue !== "string") {
|
|
1715
|
+
return sourceFiles;
|
|
1716
|
+
}
|
|
1717
|
+
const pkgJsonDir = resolve(dirname(pkgJsonFile));
|
|
1718
|
+
const commandSegments = splitLifecycleScriptCommands(scriptValue);
|
|
1719
|
+
for (const commandSegment of commandSegments) {
|
|
1720
|
+
const tokens = splitCommandArgs(commandSegment);
|
|
1721
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
1722
|
+
if (!NPM_LIFECYCLE_JS_RUNNERS.has(tokens[index])) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const scriptStartIndex = findLifecycleScriptSourceStartIndex(
|
|
1726
|
+
tokens,
|
|
1727
|
+
index,
|
|
1728
|
+
);
|
|
1729
|
+
if (scriptStartIndex === undefined) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const relativeFile = findLifecycleScriptSourceArg(
|
|
1733
|
+
tokens,
|
|
1734
|
+
scriptStartIndex,
|
|
1735
|
+
);
|
|
1736
|
+
if (!relativeFile) {
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
const resolvedFile = resolve(pkgJsonDir, relativeFile);
|
|
1740
|
+
if (
|
|
1741
|
+
!isResolvedPathWithinDirectory(pkgJsonDir, resolvedFile) ||
|
|
1742
|
+
!safeExistsSync(resolvedFile) ||
|
|
1743
|
+
seen.has(resolvedFile)
|
|
1744
|
+
) {
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
seen.add(resolvedFile);
|
|
1748
|
+
sourceFiles.push(resolvedFile);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return sourceFiles;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function analyzeNpmLifecycleScripts(pkgJsonFile, scripts) {
|
|
1755
|
+
const executionIndicators = new Set();
|
|
1756
|
+
const networkIndicators = new Set();
|
|
1757
|
+
const obfuscationIndicators = new Set();
|
|
1758
|
+
const obfuscatedScripts = [];
|
|
1759
|
+
const suspiciousScripts = [];
|
|
1760
|
+
const scriptIndicatorMap = [];
|
|
1761
|
+
const riskyScripts = NPM_INSTALL_HOOK_NAMES.filter(
|
|
1762
|
+
(scriptName) => scripts?.[scriptName],
|
|
1763
|
+
);
|
|
1764
|
+
riskyScripts.forEach((scriptName) => {
|
|
1765
|
+
const scriptValue = String(scripts[scriptName] || "");
|
|
1766
|
+
const scriptExecutionIndicators = new Set(
|
|
1767
|
+
collectLifecyclePatternIndicators(
|
|
1768
|
+
scriptValue,
|
|
1769
|
+
NPM_LIFECYCLE_EXECUTION_PATTERNS,
|
|
1770
|
+
),
|
|
1771
|
+
);
|
|
1772
|
+
const scriptNetworkIndicators = new Set(
|
|
1773
|
+
collectLifecyclePatternIndicators(
|
|
1774
|
+
scriptValue,
|
|
1775
|
+
NPM_LIFECYCLE_NETWORK_PATTERNS,
|
|
1776
|
+
),
|
|
1777
|
+
);
|
|
1778
|
+
const scriptObfuscationIndicators = new Set(
|
|
1779
|
+
collectLifecyclePatternIndicators(
|
|
1780
|
+
scriptValue,
|
|
1781
|
+
NPM_LIFECYCLE_OBFUSCATION_PATTERNS,
|
|
1782
|
+
),
|
|
1783
|
+
);
|
|
1784
|
+
extractLifecycleScriptSourceFiles(pkgJsonFile, scriptValue).forEach(
|
|
1785
|
+
(sourceFile) => {
|
|
1786
|
+
const astIndicators = analyzeSuspiciousJsFile(sourceFile);
|
|
1787
|
+
astIndicators.executionIndicators.forEach((indicator) => {
|
|
1788
|
+
scriptExecutionIndicators.add(`ast:${indicator}`);
|
|
1789
|
+
});
|
|
1790
|
+
astIndicators.networkIndicators.forEach((indicator) => {
|
|
1791
|
+
scriptNetworkIndicators.add(`ast:${indicator}`);
|
|
1792
|
+
});
|
|
1793
|
+
astIndicators.obfuscationIndicators.forEach((indicator) => {
|
|
1794
|
+
scriptObfuscationIndicators.add(`ast:${indicator}`);
|
|
1795
|
+
});
|
|
1796
|
+
},
|
|
1797
|
+
);
|
|
1798
|
+
if (scriptObfuscationIndicators.size) {
|
|
1799
|
+
scriptExecutionIndicators.forEach((indicator) => {
|
|
1800
|
+
executionIndicators.add(indicator);
|
|
1801
|
+
});
|
|
1802
|
+
scriptObfuscationIndicators.forEach((indicator) => {
|
|
1803
|
+
obfuscationIndicators.add(indicator);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
if (
|
|
1807
|
+
scriptObfuscationIndicators.size &&
|
|
1808
|
+
(scriptExecutionIndicators.size || scriptNetworkIndicators.size)
|
|
1809
|
+
) {
|
|
1810
|
+
obfuscatedScripts.push(scriptName);
|
|
1811
|
+
}
|
|
1812
|
+
if (
|
|
1813
|
+
scriptObfuscationIndicators.size ||
|
|
1814
|
+
(scriptExecutionIndicators.size && scriptNetworkIndicators.size)
|
|
1815
|
+
) {
|
|
1816
|
+
suspiciousScripts.push(scriptName);
|
|
1817
|
+
}
|
|
1818
|
+
scriptNetworkIndicators.forEach((indicator) => {
|
|
1819
|
+
networkIndicators.add(indicator);
|
|
1820
|
+
});
|
|
1821
|
+
if (
|
|
1822
|
+
scriptExecutionIndicators.size ||
|
|
1823
|
+
scriptNetworkIndicators.size ||
|
|
1824
|
+
scriptObfuscationIndicators.size
|
|
1825
|
+
) {
|
|
1826
|
+
scriptIndicatorMap.push(
|
|
1827
|
+
`${scriptName}:${[
|
|
1828
|
+
...scriptObfuscationIndicators,
|
|
1829
|
+
...scriptExecutionIndicators,
|
|
1830
|
+
...scriptNetworkIndicators,
|
|
1831
|
+
].join("+")}`,
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
return {
|
|
1836
|
+
executionIndicators: Array.from(executionIndicators).sort(),
|
|
1837
|
+
networkIndicators: Array.from(networkIndicators).sort(),
|
|
1838
|
+
obfuscatedScripts: [...new Set(obfuscatedScripts)].sort(),
|
|
1839
|
+
obfuscationIndicators: Array.from(obfuscationIndicators).sort(),
|
|
1840
|
+
riskyScripts,
|
|
1841
|
+
scriptIndicatorMap: scriptIndicatorMap.sort(),
|
|
1842
|
+
suspiciousScripts: [...new Set(suspiciousScripts)].sort(),
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1396
1846
|
export async function parsePkgJson(
|
|
1397
1847
|
pkgJsonFile,
|
|
1398
1848
|
simple = false,
|
|
@@ -1482,24 +1932,70 @@ export async function parsePkgJson(
|
|
|
1482
1932
|
// Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
|
|
1483
1933
|
if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
|
|
1484
1934
|
const scriptNames = Object.keys(pkgData.scripts).join(", ");
|
|
1935
|
+
const lifecycleAnalysis = analyzeNpmLifecycleScripts(
|
|
1936
|
+
pkgJsonFile,
|
|
1937
|
+
pkgData.scripts,
|
|
1938
|
+
);
|
|
1485
1939
|
apkg.properties.push({
|
|
1486
1940
|
name: "cdx:npm:scripts",
|
|
1487
1941
|
value: scriptNames,
|
|
1488
1942
|
});
|
|
1489
1943
|
// Flag high-risk scripts specifically
|
|
1490
|
-
const riskyScripts =
|
|
1491
|
-
"preinstall",
|
|
1492
|
-
"install",
|
|
1493
|
-
"postinstall",
|
|
1494
|
-
"prepublish",
|
|
1495
|
-
"prepare",
|
|
1496
|
-
].filter((script) => pkgData.scripts[script]);
|
|
1944
|
+
const riskyScripts = lifecycleAnalysis.riskyScripts;
|
|
1497
1945
|
if (riskyScripts.length) {
|
|
1946
|
+
apkg.properties.push({
|
|
1947
|
+
name: "cdx:npm:hasInstallScript",
|
|
1948
|
+
value: "true",
|
|
1949
|
+
});
|
|
1498
1950
|
apkg.properties.push({
|
|
1499
1951
|
name: "cdx:npm:risky_scripts",
|
|
1500
1952
|
value: riskyScripts.join(", "),
|
|
1501
1953
|
});
|
|
1502
1954
|
}
|
|
1955
|
+
if (lifecycleAnalysis.suspiciousScripts.length) {
|
|
1956
|
+
apkg.properties.push({
|
|
1957
|
+
name: "cdx:npm:hasSuspiciousLifecycleScript",
|
|
1958
|
+
value: "true",
|
|
1959
|
+
});
|
|
1960
|
+
apkg.properties.push({
|
|
1961
|
+
name: "cdx:npm:suspiciousLifecycleScripts",
|
|
1962
|
+
value: lifecycleAnalysis.suspiciousScripts.join(", "),
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
if (lifecycleAnalysis.obfuscatedScripts.length) {
|
|
1966
|
+
apkg.properties.push({
|
|
1967
|
+
name: "cdx:npm:hasObfuscatedLifecycleScript",
|
|
1968
|
+
value: "true",
|
|
1969
|
+
});
|
|
1970
|
+
apkg.properties.push({
|
|
1971
|
+
name: "cdx:npm:obfuscatedLifecycleScripts",
|
|
1972
|
+
value: lifecycleAnalysis.obfuscatedScripts.join(", "),
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
if (lifecycleAnalysis.obfuscationIndicators.length) {
|
|
1976
|
+
apkg.properties.push({
|
|
1977
|
+
name: "cdx:npm:lifecycleObfuscationIndicators",
|
|
1978
|
+
value: lifecycleAnalysis.obfuscationIndicators.join(", "),
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
if (lifecycleAnalysis.executionIndicators.length) {
|
|
1982
|
+
apkg.properties.push({
|
|
1983
|
+
name: "cdx:npm:lifecycleExecutionIndicators",
|
|
1984
|
+
value: lifecycleAnalysis.executionIndicators.join(", "),
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
if (lifecycleAnalysis.networkIndicators.length) {
|
|
1988
|
+
apkg.properties.push({
|
|
1989
|
+
name: "cdx:npm:lifecycleNetworkIndicators",
|
|
1990
|
+
value: lifecycleAnalysis.networkIndicators.join(", "),
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (lifecycleAnalysis.scriptIndicatorMap.length) {
|
|
1994
|
+
apkg.properties.push({
|
|
1995
|
+
name: "cdx:npm:lifecycleIndicatorMap",
|
|
1996
|
+
value: lifecycleAnalysis.scriptIndicatorMap.join(" | "),
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1503
1999
|
}
|
|
1504
2000
|
// Track platform/architecture constraints
|
|
1505
2001
|
if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
|
|
@@ -1566,10 +2062,10 @@ export async function parsePkgJson(
|
|
|
1566
2062
|
// continue regardless of error
|
|
1567
2063
|
}
|
|
1568
2064
|
}
|
|
1569
|
-
if (!simple &&
|
|
2065
|
+
if (!simple && shouldFetchPackageMetadata() && pkgList?.length) {
|
|
1570
2066
|
if (DEBUG_MODE) {
|
|
1571
2067
|
console.log(
|
|
1572
|
-
`About to fetch
|
|
2068
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgJson`,
|
|
1573
2069
|
);
|
|
1574
2070
|
}
|
|
1575
2071
|
return await getNpmMetadata(pkgList);
|
|
@@ -1616,8 +2112,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
1616
2112
|
const srcFilePath = node.path.includes(`${_sep}node_modules`)
|
|
1617
2113
|
? node.path.split(`${_sep}node_modules`)[0]
|
|
1618
2114
|
: node.path;
|
|
2115
|
+
const isDevelopmentNode = node.dev === true || node.devOptional === true;
|
|
1619
2116
|
const scope =
|
|
1620
|
-
|
|
2117
|
+
isDevelopmentNode || node.optional === true ? "optional" : undefined;
|
|
1621
2118
|
const integrity = node.integrity ? node.integrity : undefined;
|
|
1622
2119
|
|
|
1623
2120
|
let pkg;
|
|
@@ -1690,7 +2187,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
1690
2187
|
purl: purlString,
|
|
1691
2188
|
"bom-ref": decodeURIComponent(purlString),
|
|
1692
2189
|
};
|
|
1693
|
-
if (
|
|
2190
|
+
if (isDevelopmentNode) {
|
|
1694
2191
|
_setNpmDevelopmentProperty(pkg);
|
|
1695
2192
|
}
|
|
1696
2193
|
if (node.resolved) {
|
|
@@ -2128,10 +2625,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
2128
2625
|
options,
|
|
2129
2626
|
));
|
|
2130
2627
|
|
|
2131
|
-
if (
|
|
2628
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
2132
2629
|
if (DEBUG_MODE) {
|
|
2133
2630
|
console.log(
|
|
2134
|
-
`About to fetch
|
|
2631
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgLock`,
|
|
2135
2632
|
);
|
|
2136
2633
|
}
|
|
2137
2634
|
pkgList = await getNpmMetadata(pkgList);
|
|
@@ -2761,10 +3258,10 @@ export async function parseYarnLock(
|
|
|
2761
3258
|
}
|
|
2762
3259
|
}
|
|
2763
3260
|
|
|
2764
|
-
if (
|
|
3261
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
2765
3262
|
if (DEBUG_MODE) {
|
|
2766
3263
|
console.log(
|
|
2767
|
-
`About to fetch
|
|
3264
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseYarnLock`,
|
|
2768
3265
|
);
|
|
2769
3266
|
}
|
|
2770
3267
|
pkgList = await getNpmMetadata(pkgList);
|
|
@@ -2838,10 +3335,10 @@ export async function parseNodeShrinkwrap(swFile) {
|
|
|
2838
3335
|
}
|
|
2839
3336
|
}
|
|
2840
3337
|
}
|
|
2841
|
-
if (
|
|
3338
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
2842
3339
|
if (DEBUG_MODE) {
|
|
2843
3340
|
console.log(
|
|
2844
|
-
`About to fetch
|
|
3341
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseNodeShrinkwrap`,
|
|
2845
3342
|
);
|
|
2846
3343
|
}
|
|
2847
3344
|
return await getNpmMetadata(pkgList);
|
|
@@ -4157,10 +4654,10 @@ export async function parsePnpmLock(
|
|
|
4157
4654
|
pkgList = await pnpmMetadata(pkgList, pnpmLock);
|
|
4158
4655
|
}
|
|
4159
4656
|
|
|
4160
|
-
if (
|
|
4657
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
4161
4658
|
if (DEBUG_MODE) {
|
|
4162
4659
|
console.log(
|
|
4163
|
-
`About to fetch
|
|
4660
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parsePnpmLock`,
|
|
4164
4661
|
);
|
|
4165
4662
|
}
|
|
4166
4663
|
pkgList = await getNpmMetadata(pkgList);
|
|
@@ -4218,10 +4715,10 @@ export async function parseBowerJson(bowerJsonFile) {
|
|
|
4218
4715
|
// continue regardless of error
|
|
4219
4716
|
}
|
|
4220
4717
|
}
|
|
4221
|
-
if (
|
|
4718
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
4222
4719
|
if (DEBUG_MODE) {
|
|
4223
4720
|
console.log(
|
|
4224
|
-
`About to fetch
|
|
4721
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseBowerJson`,
|
|
4225
4722
|
);
|
|
4226
4723
|
}
|
|
4227
4724
|
return await getNpmMetadata(pkgList);
|
|
@@ -4316,10 +4813,10 @@ export async function parseMinJs(minJsFile) {
|
|
|
4316
4813
|
// continue regardless of error
|
|
4317
4814
|
}
|
|
4318
4815
|
}
|
|
4319
|
-
if (
|
|
4816
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
4320
4817
|
if (DEBUG_MODE) {
|
|
4321
4818
|
console.log(
|
|
4322
|
-
`About to fetch
|
|
4819
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseMinJs`,
|
|
4323
4820
|
);
|
|
4324
4821
|
}
|
|
4325
4822
|
return await getNpmMetadata(pkgList);
|
|
@@ -5811,7 +6308,7 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
|
|
|
5811
6308
|
* @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
|
|
5812
6309
|
*/
|
|
5813
6310
|
export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
5814
|
-
if (!
|
|
6311
|
+
if (!shouldFetchPackageMetadata() && !fetchDepsInfo) {
|
|
5815
6312
|
return pkgList;
|
|
5816
6313
|
}
|
|
5817
6314
|
const PYPI_URL = process.env.PYPI_URL || "https://pypi.org/pypi/";
|
|
@@ -5987,6 +6484,10 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
5987
6484
|
null,
|
|
5988
6485
|
null,
|
|
5989
6486
|
).toString();
|
|
6487
|
+
p.properties = p.properties || [];
|
|
6488
|
+
p.properties.push(
|
|
6489
|
+
...collectPypiRegistryProvenanceProperties(body, p.version),
|
|
6490
|
+
);
|
|
5990
6491
|
p.purl = purlString;
|
|
5991
6492
|
p["bom-ref"] = decodeURIComponent(purlString);
|
|
5992
6493
|
cdepList.push(p);
|
|
@@ -6389,9 +6890,9 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
6389
6890
|
}
|
|
6390
6891
|
|
|
6391
6892
|
/**
|
|
6392
|
-
* Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock.
|
|
6893
|
+
* Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
|
|
6393
6894
|
*
|
|
6394
|
-
* @param {
|
|
6895
|
+
* @param {string} lockData Raw TOML text from poetry.lock, pdm.lock, uv.lock, or pylock.toml
|
|
6395
6896
|
* @param {string} lockFile Lock file name for evidence
|
|
6396
6897
|
* @param {string} pyProjectFile pyproject.toml file
|
|
6397
6898
|
*/
|
|
@@ -6408,6 +6909,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6408
6909
|
let workspacePaths;
|
|
6409
6910
|
let workspaceWarningShown = false;
|
|
6410
6911
|
let hasWorkspaces = false;
|
|
6912
|
+
let pyLockProperties = [];
|
|
6411
6913
|
// Keep track of any workspace components to be added to the parent component
|
|
6412
6914
|
const workspaceComponentMap = {};
|
|
6413
6915
|
const workspacePyProjMap = {};
|
|
@@ -6536,7 +7038,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6536
7038
|
}
|
|
6537
7039
|
}
|
|
6538
7040
|
}
|
|
6539
|
-
|
|
7041
|
+
const pyLockMode = isPyLockObject(lockTomlObj);
|
|
7042
|
+
if (pyLockMode) {
|
|
7043
|
+
pyLockProperties = collectPyLockTopLevelProperties(lockTomlObj);
|
|
7044
|
+
if (parentComponent) {
|
|
7045
|
+
parentComponent.properties = parentComponent.properties || [];
|
|
7046
|
+
parentComponent.properties =
|
|
7047
|
+
parentComponent.properties.concat(pyLockProperties);
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
const packageEntries = getPyLockPackages(lockTomlObj);
|
|
7051
|
+
for (const apkg of packageEntries) {
|
|
6540
7052
|
// This avoids validation errors with uv.lock
|
|
6541
7053
|
if (parentComponent?.name && parentComponent.name === apkg.name) {
|
|
6542
7054
|
continue;
|
|
@@ -6556,10 +7068,19 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6556
7068
|
if (apkg.optional) {
|
|
6557
7069
|
pkg.scope = "optional";
|
|
6558
7070
|
}
|
|
6559
|
-
|
|
7071
|
+
// poetry/pdm/uv use "python-versions", while pylock (PEP 751) uses "requires-python".
|
|
7072
|
+
// Prefer the existing lock-family field when both are present.
|
|
7073
|
+
const requiresPython = apkg["python-versions"] || apkg["requires-python"];
|
|
7074
|
+
if (requiresPython) {
|
|
6560
7075
|
pkg.properties.push({
|
|
6561
7076
|
name: "cdx:pypi:requiresPython",
|
|
6562
|
-
value:
|
|
7077
|
+
value: requiresPython,
|
|
7078
|
+
});
|
|
7079
|
+
}
|
|
7080
|
+
if (apkg.index && !isDefaultPypiRegistry(apkg.index)) {
|
|
7081
|
+
pkg.properties.push({
|
|
7082
|
+
name: "cdx:pypi:registry",
|
|
7083
|
+
value: apkg.index,
|
|
6563
7084
|
});
|
|
6564
7085
|
}
|
|
6565
7086
|
if (apkg?.source) {
|
|
@@ -6585,6 +7106,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6585
7106
|
});
|
|
6586
7107
|
}
|
|
6587
7108
|
}
|
|
7109
|
+
if (pyLockMode) {
|
|
7110
|
+
pkg.properties = pkg.properties.concat(
|
|
7111
|
+
collectPyLockPackageProperties(apkg),
|
|
7112
|
+
);
|
|
7113
|
+
}
|
|
6588
7114
|
// Is this component a module?
|
|
6589
7115
|
if (workspaceComponentMap[pkg.name]) {
|
|
6590
7116
|
pkg.properties.push({
|
|
@@ -6672,6 +7198,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6672
7198
|
});
|
|
6673
7199
|
}
|
|
6674
7200
|
}
|
|
7201
|
+
if (pyLockMode) {
|
|
7202
|
+
const pylockFileComponents = collectPyLockFileComponents(apkg, lockFile);
|
|
7203
|
+
if (pylockFileComponents.length) {
|
|
7204
|
+
pkg.components = (pkg.components || []).concat(pylockFileComponents);
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
6675
7207
|
if (
|
|
6676
7208
|
directDepsKeys[pkg.name] ||
|
|
6677
7209
|
(hasWorkspaces && !Object.keys(workspaceComponentMap).length)
|
|
@@ -6728,13 +7260,14 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6728
7260
|
// Example: "msgpack>=0.5.2"
|
|
6729
7261
|
const nameStr =
|
|
6730
7262
|
apkgDep.name || apkgDep.split(/(==|<=|~=|>=)/)[0].split(" ")[0];
|
|
6731
|
-
|
|
7263
|
+
// Python package names are normalized/case-insensitive; support both forms for lookup.
|
|
7264
|
+
const nameLower = nameStr.toLowerCase();
|
|
7265
|
+
const depPkgRef =
|
|
7266
|
+
existingPkgMap[nameLower] || existingPkgMap[nameStr];
|
|
7267
|
+
depsMap[pkg["bom-ref"]].add(depPkgRef || nameStr);
|
|
6732
7268
|
// Propagate the workspace properties to the child components
|
|
6733
|
-
if (
|
|
6734
|
-
|
|
6735
|
-
pkgBomRefMap[existingPkgMap[nameStr]]
|
|
6736
|
-
) {
|
|
6737
|
-
const dependentPkg = pkgBomRefMap[existingPkgMap[nameStr]];
|
|
7269
|
+
if (depPkgRef && pkgBomRefMap[depPkgRef]) {
|
|
7270
|
+
const dependentPkg = pkgBomRefMap[depPkgRef];
|
|
6738
7271
|
dependentPkg.properties = dependentPkg.properties || [];
|
|
6739
7272
|
const addedValue = {};
|
|
6740
7273
|
// Is the parent a workspace
|
|
@@ -6776,6 +7309,8 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6776
7309
|
depRef = adep;
|
|
6777
7310
|
} else if (existingPkgMap[adep]) {
|
|
6778
7311
|
depRef = existingPkgMap[adep];
|
|
7312
|
+
} else if (existingPkgMap[adep.toLowerCase()]) {
|
|
7313
|
+
depRef = existingPkgMap[adep.toLowerCase()];
|
|
6779
7314
|
} else if (existingPkgMap[`py${adep}`]) {
|
|
6780
7315
|
depRef = existingPkgMap[`py${adep}`];
|
|
6781
7316
|
} else if (existingPkgMap[adep.replace(/-/g, "_")]) {
|
|
@@ -6845,6 +7380,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6845
7380
|
pkgList,
|
|
6846
7381
|
rootList,
|
|
6847
7382
|
dependenciesList,
|
|
7383
|
+
pyLockProperties,
|
|
6848
7384
|
workspaceWarningShown,
|
|
6849
7385
|
};
|
|
6850
7386
|
}
|
|
@@ -12956,38 +13492,14 @@ export function convertOSQueryResults(
|
|
|
12956
13492
|
const pkgList = [];
|
|
12957
13493
|
if (results?.length) {
|
|
12958
13494
|
for (const res of results) {
|
|
12959
|
-
const version =
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
res.
|
|
12963
|
-
|
|
12964
|
-
res.pid ||
|
|
12965
|
-
res.subject_key_id ||
|
|
12966
|
-
res.interface ||
|
|
12967
|
-
res.instance_id;
|
|
12968
|
-
let name =
|
|
12969
|
-
res.name ||
|
|
12970
|
-
res.device_id ||
|
|
12971
|
-
res.hotfix_id ||
|
|
12972
|
-
res.uuid ||
|
|
12973
|
-
res.serial ||
|
|
12974
|
-
res.pid ||
|
|
12975
|
-
res.address ||
|
|
12976
|
-
res.ami_id ||
|
|
12977
|
-
res.interface ||
|
|
12978
|
-
res.client_app_id;
|
|
13495
|
+
const version = deriveOsQueryVersion(res);
|
|
13496
|
+
let name = deriveOsQueryName(res, results.length === 1, queryObj.name);
|
|
13497
|
+
if (queryObj.purlType === "chrome-extension") {
|
|
13498
|
+
name = (res.identifier || res.extension_id || name || "").toLowerCase();
|
|
13499
|
+
}
|
|
12979
13500
|
let group = "";
|
|
12980
13501
|
const subpath = res.path || res.admindir || res.source;
|
|
12981
|
-
|
|
12982
|
-
res.publisher ||
|
|
12983
|
-
res.maintainer ||
|
|
12984
|
-
res.creator ||
|
|
12985
|
-
res.manufacturer ||
|
|
12986
|
-
res.provider ||
|
|
12987
|
-
"";
|
|
12988
|
-
if (publisher === "null") {
|
|
12989
|
-
publisher = "";
|
|
12990
|
-
}
|
|
13502
|
+
const publisher = deriveOsQueryPublisher(res);
|
|
12991
13503
|
// For vscode-extension purl type, the publisher is used as the namespace
|
|
12992
13504
|
if (queryObj.purlType === "vscode-extension" && publisher) {
|
|
12993
13505
|
group = publisher.toLowerCase();
|
|
@@ -12998,20 +13510,8 @@ export function convertOSQueryResults(
|
|
|
12998
13510
|
scope = compScope;
|
|
12999
13511
|
}
|
|
13000
13512
|
const description =
|
|
13001
|
-
res
|
|
13002
|
-
res.
|
|
13003
|
-
res.arguments ||
|
|
13004
|
-
res.device ||
|
|
13005
|
-
res.codename ||
|
|
13006
|
-
res.section ||
|
|
13007
|
-
res.status ||
|
|
13008
|
-
res.identifier ||
|
|
13009
|
-
res.components ||
|
|
13010
|
-
"";
|
|
13011
|
-
// Re-use the name from query obj
|
|
13012
|
-
if (!name && results.length === 1 && queryObj.name) {
|
|
13013
|
-
name = queryObj.name;
|
|
13014
|
-
}
|
|
13513
|
+
deriveOsQueryDescription(res) ||
|
|
13514
|
+
(queryObj.purlType === "chrome-extension" ? res.name || "" : "");
|
|
13015
13515
|
let qualifiers;
|
|
13016
13516
|
if (res.identifying_number?.length) {
|
|
13017
13517
|
qualifiers = {
|
|
@@ -13019,25 +13519,18 @@ export function convertOSQueryResults(
|
|
|
13019
13519
|
};
|
|
13020
13520
|
}
|
|
13021
13521
|
if (name) {
|
|
13022
|
-
name = name
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
.
|
|
13026
|
-
.replace(/[}]$/g, "");
|
|
13027
|
-
group = group
|
|
13028
|
-
.replace(/ /g, "+")
|
|
13029
|
-
.replace(/[:%]/g, "-")
|
|
13030
|
-
.replace(/^[@{]/g, "")
|
|
13031
|
-
.replace(/[}]$/g, "");
|
|
13032
|
-
const purl = new PackageURL(
|
|
13033
|
-
queryObj.purlType || "swid",
|
|
13522
|
+
name = sanitizeOsQueryIdentity(name);
|
|
13523
|
+
group = sanitizeOsQueryIdentity(group);
|
|
13524
|
+
const purl = createOsQueryPurl(
|
|
13525
|
+
queryObj.purlType,
|
|
13034
13526
|
group,
|
|
13035
13527
|
name,
|
|
13036
|
-
version
|
|
13528
|
+
version,
|
|
13037
13529
|
qualifiers,
|
|
13038
13530
|
subpath,
|
|
13039
|
-
)
|
|
13531
|
+
);
|
|
13040
13532
|
const props = [{ name: "cdx:osquery:category", value: queryCategory }];
|
|
13533
|
+
props.push(...createLolbasProperties(queryCategory, res));
|
|
13041
13534
|
let providesList;
|
|
13042
13535
|
if (enhance) {
|
|
13043
13536
|
switch (queryObj.purlType) {
|
|
@@ -13074,9 +13567,15 @@ export function convertOSQueryResults(
|
|
|
13074
13567
|
scope,
|
|
13075
13568
|
type: queryObj.componentType,
|
|
13076
13569
|
};
|
|
13077
|
-
for (const k of Object.keys(res).filter(
|
|
13078
|
-
(
|
|
13079
|
-
|
|
13570
|
+
for (const k of Object.keys(res).filter((p) => {
|
|
13571
|
+
if (["version", "description", "publisher"].includes(p)) {
|
|
13572
|
+
return false;
|
|
13573
|
+
}
|
|
13574
|
+
if (queryObj.purlType !== "chrome-extension" && p === "name") {
|
|
13575
|
+
return false;
|
|
13576
|
+
}
|
|
13577
|
+
return true;
|
|
13578
|
+
})) {
|
|
13080
13579
|
if (res[k] && res[k] !== "null") {
|
|
13081
13580
|
props.push({
|
|
13082
13581
|
name: k,
|
|
@@ -13758,6 +14257,76 @@ export function parseJarManifest(jarMetadata) {
|
|
|
13758
14257
|
return metadata;
|
|
13759
14258
|
}
|
|
13760
14259
|
|
|
14260
|
+
/**
|
|
14261
|
+
* Determine whether a manifest candidate looks like a namespace-qualified identifier.
|
|
14262
|
+
*
|
|
14263
|
+
* @param {string} candidate Manifest field value
|
|
14264
|
+
* @returns {boolean} True when candidate appears namespace-qualified
|
|
14265
|
+
*/
|
|
14266
|
+
function isQualifiedJarNamespace(candidate) {
|
|
14267
|
+
return (
|
|
14268
|
+
!!candidate &&
|
|
14269
|
+
!candidate.includes(" ") &&
|
|
14270
|
+
(candidate.includes(".") || candidate.includes("-"))
|
|
14271
|
+
);
|
|
14272
|
+
}
|
|
14273
|
+
|
|
14274
|
+
/**
|
|
14275
|
+
* Select the most reliable group candidate from JAR manifest metadata.
|
|
14276
|
+
*
|
|
14277
|
+
* @param {Object} jarMetadata Parsed MANIFEST.MF key-value map
|
|
14278
|
+
* @returns {string} Best group candidate, or empty string if none exists
|
|
14279
|
+
*/
|
|
14280
|
+
export function inferJarGroupFromManifest(jarMetadata = {}) {
|
|
14281
|
+
// Keep this ordered from most to least namespace-qualified manifest fields.
|
|
14282
|
+
// Extension-Name is intentionally lower priority due to inconsistent usage.
|
|
14283
|
+
const qualifiedCandidates = [
|
|
14284
|
+
jarMetadata["Bundle-SymbolicName"],
|
|
14285
|
+
jarMetadata["Automatic-Module-Name"],
|
|
14286
|
+
jarMetadata["Implementation-Title"],
|
|
14287
|
+
jarMetadata["Extension-Name"],
|
|
14288
|
+
];
|
|
14289
|
+
for (const candidate of qualifiedCandidates) {
|
|
14290
|
+
if (isQualifiedJarNamespace(candidate)) {
|
|
14291
|
+
return candidate;
|
|
14292
|
+
}
|
|
14293
|
+
}
|
|
14294
|
+
return (
|
|
14295
|
+
jarMetadata["Implementation-Vendor-Id"] ||
|
|
14296
|
+
jarMetadata["Bundle-Vendor"] ||
|
|
14297
|
+
jarMetadata["Extension-Name"] ||
|
|
14298
|
+
""
|
|
14299
|
+
);
|
|
14300
|
+
}
|
|
14301
|
+
|
|
14302
|
+
/**
|
|
14303
|
+
* Trim group suffix that duplicates the artifact name for compound artifact names.
|
|
14304
|
+
*
|
|
14305
|
+
* @param {string} group Group candidate
|
|
14306
|
+
* @param {string} name Artifact name candidate
|
|
14307
|
+
* @returns {string} Adjusted group
|
|
14308
|
+
*/
|
|
14309
|
+
export function trimJarGroupSuffix(group, name) {
|
|
14310
|
+
if (!group || !name || group.startsWith("javax")) {
|
|
14311
|
+
return group;
|
|
14312
|
+
}
|
|
14313
|
+
// Only trim when the artifact name contains a separator (hyphen or dot).
|
|
14314
|
+
if (!name.includes("-") && !name.includes(".")) {
|
|
14315
|
+
return group;
|
|
14316
|
+
}
|
|
14317
|
+
const lowerName = name.toLowerCase();
|
|
14318
|
+
const dottedName = lowerName.replace(/-/g, ".");
|
|
14319
|
+
const dottedSuffix = `.${dottedName}`;
|
|
14320
|
+
if (group.endsWith(dottedSuffix)) {
|
|
14321
|
+
return group.slice(0, -dottedSuffix.length);
|
|
14322
|
+
}
|
|
14323
|
+
const lowerSuffix = `.${lowerName}`;
|
|
14324
|
+
if (group.endsWith(lowerSuffix)) {
|
|
14325
|
+
return group.slice(0, -lowerSuffix.length);
|
|
14326
|
+
}
|
|
14327
|
+
return group;
|
|
14328
|
+
}
|
|
14329
|
+
|
|
13761
14330
|
/**
|
|
13762
14331
|
* Parse a Maven pom.properties file and return its key-value pairs as an object.
|
|
13763
14332
|
*
|
|
@@ -14048,14 +14617,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
14048
14617
|
.split(";")[0]
|
|
14049
14618
|
.trim();
|
|
14050
14619
|
}
|
|
14051
|
-
group =
|
|
14052
|
-
group ||
|
|
14053
|
-
jarMetadata["Extension-Name"] ||
|
|
14054
|
-
jarMetadata["Implementation-Vendor-Id"] ||
|
|
14055
|
-
jarMetadata["Bundle-SymbolicName"] ||
|
|
14056
|
-
jarMetadata["Bundle-Vendor"] ||
|
|
14057
|
-
jarMetadata["Automatic-Module-Name"] ||
|
|
14058
|
-
"";
|
|
14620
|
+
group = group || inferJarGroupFromManifest(jarMetadata);
|
|
14059
14621
|
version =
|
|
14060
14622
|
version ||
|
|
14061
14623
|
jarMetadata["Bundle-Version"] ||
|
|
@@ -14111,16 +14673,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
14111
14673
|
}
|
|
14112
14674
|
// Sometimes the group might already contain the name
|
|
14113
14675
|
// Eg: group: org.checkerframework.checker.qual name: checker-qual
|
|
14114
|
-
|
|
14115
|
-
if (group.includes(`.${name.toLowerCase().replace(/-/g, ".")}`)) {
|
|
14116
|
-
group = group.replace(
|
|
14117
|
-
new RegExp(`.${name.toLowerCase().replace(/-/g, ".")}$`),
|
|
14118
|
-
"",
|
|
14119
|
-
);
|
|
14120
|
-
} else if (group.includes(`.${name.toLowerCase()}`)) {
|
|
14121
|
-
group = group.replace(new RegExp(`.${name.toLowerCase()}$`), "");
|
|
14122
|
-
}
|
|
14123
|
-
}
|
|
14676
|
+
group = trimJarGroupSuffix(group, name);
|
|
14124
14677
|
// Patch the group string
|
|
14125
14678
|
if (vendorAliases[name]) {
|
|
14126
14679
|
group = vendorAliases[name];
|
|
@@ -17948,6 +18501,29 @@ export function parseMakeDFile(dfile) {
|
|
|
17948
18501
|
return pkgFilesMap;
|
|
17949
18502
|
}
|
|
17950
18503
|
|
|
18504
|
+
const isAsciiHexCode = (code) => {
|
|
18505
|
+
return (
|
|
18506
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
18507
|
+
(code >= 0x41 && code <= 0x46) ||
|
|
18508
|
+
(code >= 0x61 && code <= 0x66)
|
|
18509
|
+
);
|
|
18510
|
+
};
|
|
18511
|
+
|
|
18512
|
+
const hasValidPercentEncoding = (value) => {
|
|
18513
|
+
for (let index = 0; index < value.length; index++) {
|
|
18514
|
+
if (value.charCodeAt(index) !== 0x25) {
|
|
18515
|
+
continue;
|
|
18516
|
+
}
|
|
18517
|
+
const firstHex = value.charCodeAt(index + 1);
|
|
18518
|
+
const secondHex = value.charCodeAt(index + 2);
|
|
18519
|
+
if (!isAsciiHexCode(firstHex) || !isAsciiHexCode(secondHex)) {
|
|
18520
|
+
return false;
|
|
18521
|
+
}
|
|
18522
|
+
index += 2;
|
|
18523
|
+
}
|
|
18524
|
+
return true;
|
|
18525
|
+
};
|
|
18526
|
+
|
|
17951
18527
|
/**
|
|
17952
18528
|
* Function to validate an externalReference URL for conforming to the JSON schema or bomLink
|
|
17953
18529
|
* https://github.com/CycloneDX/cyclonedx-core-java/blob/75575318b268dda9e2a290761d7db11b4f414255/src/main/resources/bom-1.5.schema.json#L1140
|
|
@@ -17972,13 +18548,9 @@ export function isValidIriReference(iri) {
|
|
|
17972
18548
|
return false;
|
|
17973
18549
|
}
|
|
17974
18550
|
|
|
17975
|
-
//
|
|
17976
|
-
//
|
|
17977
|
-
|
|
17978
|
-
// - The end of the string ($)
|
|
17979
|
-
// - Or a character that is NOT a hex digit ([^0-9A-Fa-f])
|
|
17980
|
-
// This catches %ab, %ab%, %abZ, %abc, etc.
|
|
17981
|
-
if (/%(?!([0-9A-Fa-f]{2})($|[^0-9A-Fa-f]))/.test(iri)) {
|
|
18551
|
+
// Validate percent-encoding with a linear scan to avoid regex backtracking
|
|
18552
|
+
// issues on very long attacker-controlled inputs.
|
|
18553
|
+
if (!hasValidPercentEncoding(iri)) {
|
|
17982
18554
|
return false;
|
|
17983
18555
|
}
|
|
17984
18556
|
|