@cyclonedx/cdxgen 12.2.0 → 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 +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- 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 +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- 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/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- 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/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -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/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- 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 +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- 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 +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- 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/depsUtils.d.ts.map +1 -1
- 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/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.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/table.d.ts +6 -0
- package/types/lib/helpers/table.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 +30 -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 -35
- 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,15 @@ 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
|
+
|
|
391
|
+
// Table border style for console output.
|
|
392
|
+
export const TABLE_BORDER_STYLE = ["ascii", "unicode", "auto"].includes(
|
|
393
|
+
`${process.env.CDXGEN_TABLE_BORDER || ""}`.toLowerCase(),
|
|
394
|
+
)
|
|
395
|
+
? `${process.env.CDXGEN_TABLE_BORDER}`.toLowerCase()
|
|
396
|
+
: "auto";
|
|
397
|
+
|
|
340
398
|
// Timeout milliseconds. Default 20 mins
|
|
341
399
|
export const TIMEOUT_MS =
|
|
342
400
|
Number.parseInt(process.env.CDXGEN_TIMEOUT_MS, 10) || 20 * 60 * 1000;
|
|
@@ -383,6 +441,19 @@ export function shouldFetchLicense() {
|
|
|
383
441
|
);
|
|
384
442
|
}
|
|
385
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
|
+
|
|
386
457
|
/**
|
|
387
458
|
* Determines whether VCS (version control system) information should be fetched
|
|
388
459
|
* for Go packages, based on the GO_FETCH_VCS environment variable.
|
|
@@ -679,6 +750,12 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
679
750
|
"vscode-extensions",
|
|
680
751
|
"ide-extensions",
|
|
681
752
|
],
|
|
753
|
+
"chrome-extension": [
|
|
754
|
+
"chrome-extension",
|
|
755
|
+
"chrome-extensions",
|
|
756
|
+
"chromium-extension",
|
|
757
|
+
"chromium-extensions",
|
|
758
|
+
],
|
|
682
759
|
};
|
|
683
760
|
|
|
684
761
|
// Package manager aliases
|
|
@@ -1120,7 +1197,44 @@ export function getLicenses(pkg) {
|
|
|
1120
1197
|
licenseContent.name = l;
|
|
1121
1198
|
}
|
|
1122
1199
|
} else if (Object.keys(l).length) {
|
|
1123
|
-
licenseContent = l;
|
|
1200
|
+
licenseContent = { ...l };
|
|
1201
|
+
if (
|
|
1202
|
+
licenseContent.type &&
|
|
1203
|
+
!licenseContent.id &&
|
|
1204
|
+
!licenseContent.name &&
|
|
1205
|
+
!licenseContent.expression
|
|
1206
|
+
) {
|
|
1207
|
+
if (spdxLicenses.includes(licenseContent.type)) {
|
|
1208
|
+
licenseContent.id = licenseContent.type;
|
|
1209
|
+
} else if (isSpdxLicenseExpression(licenseContent.type)) {
|
|
1210
|
+
licenseContent.expression = licenseContent.type;
|
|
1211
|
+
} else {
|
|
1212
|
+
licenseContent.name = licenseContent.type;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (
|
|
1216
|
+
!licenseContent.id &&
|
|
1217
|
+
!licenseContent.name &&
|
|
1218
|
+
!licenseContent.expression &&
|
|
1219
|
+
licenseContent.url?.startsWith("http")
|
|
1220
|
+
) {
|
|
1221
|
+
const knownLicense = getKnownLicense(licenseContent.url, pkg);
|
|
1222
|
+
if (knownLicense) {
|
|
1223
|
+
if (knownLicense.id) {
|
|
1224
|
+
licenseContent.id = knownLicense.id;
|
|
1225
|
+
} else if (knownLicense.name) {
|
|
1226
|
+
licenseContent.name = knownLicense.name;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (
|
|
1231
|
+
!licenseContent.id &&
|
|
1232
|
+
!licenseContent.name &&
|
|
1233
|
+
!licenseContent.expression
|
|
1234
|
+
) {
|
|
1235
|
+
licenseContent.name = "CUSTOM";
|
|
1236
|
+
}
|
|
1237
|
+
delete licenseContent.type;
|
|
1124
1238
|
} else {
|
|
1125
1239
|
return undefined;
|
|
1126
1240
|
}
|
|
@@ -1331,6 +1445,10 @@ export async function getNpmMetadata(pkgList) {
|
|
|
1331
1445
|
if (body.homepage) {
|
|
1332
1446
|
p.homepage = { url: body.homepage };
|
|
1333
1447
|
}
|
|
1448
|
+
p.properties = p.properties || [];
|
|
1449
|
+
p.properties.push(
|
|
1450
|
+
...collectNpmRegistryProvenanceProperties(body, p.version),
|
|
1451
|
+
);
|
|
1334
1452
|
cdepList.push(p);
|
|
1335
1453
|
} catch (_err) {
|
|
1336
1454
|
cdepList.push(p);
|
|
@@ -1349,6 +1467,382 @@ export async function getNpmMetadata(pkgList) {
|
|
|
1349
1467
|
* @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
|
|
1350
1468
|
* @param {boolean} securityProps Collect security-related properties
|
|
1351
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
|
+
|
|
1352
1846
|
export async function parsePkgJson(
|
|
1353
1847
|
pkgJsonFile,
|
|
1354
1848
|
simple = false,
|
|
@@ -1438,24 +1932,70 @@ export async function parsePkgJson(
|
|
|
1438
1932
|
// Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
|
|
1439
1933
|
if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
|
|
1440
1934
|
const scriptNames = Object.keys(pkgData.scripts).join(", ");
|
|
1935
|
+
const lifecycleAnalysis = analyzeNpmLifecycleScripts(
|
|
1936
|
+
pkgJsonFile,
|
|
1937
|
+
pkgData.scripts,
|
|
1938
|
+
);
|
|
1441
1939
|
apkg.properties.push({
|
|
1442
1940
|
name: "cdx:npm:scripts",
|
|
1443
1941
|
value: scriptNames,
|
|
1444
1942
|
});
|
|
1445
1943
|
// Flag high-risk scripts specifically
|
|
1446
|
-
const riskyScripts =
|
|
1447
|
-
"preinstall",
|
|
1448
|
-
"install",
|
|
1449
|
-
"postinstall",
|
|
1450
|
-
"prepublish",
|
|
1451
|
-
"prepare",
|
|
1452
|
-
].filter((script) => pkgData.scripts[script]);
|
|
1944
|
+
const riskyScripts = lifecycleAnalysis.riskyScripts;
|
|
1453
1945
|
if (riskyScripts.length) {
|
|
1946
|
+
apkg.properties.push({
|
|
1947
|
+
name: "cdx:npm:hasInstallScript",
|
|
1948
|
+
value: "true",
|
|
1949
|
+
});
|
|
1454
1950
|
apkg.properties.push({
|
|
1455
1951
|
name: "cdx:npm:risky_scripts",
|
|
1456
1952
|
value: riskyScripts.join(", "),
|
|
1457
1953
|
});
|
|
1458
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
|
+
}
|
|
1459
1999
|
}
|
|
1460
2000
|
// Track platform/architecture constraints
|
|
1461
2001
|
if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
|
|
@@ -1522,10 +2062,10 @@ export async function parsePkgJson(
|
|
|
1522
2062
|
// continue regardless of error
|
|
1523
2063
|
}
|
|
1524
2064
|
}
|
|
1525
|
-
if (!simple &&
|
|
2065
|
+
if (!simple && shouldFetchPackageMetadata() && pkgList?.length) {
|
|
1526
2066
|
if (DEBUG_MODE) {
|
|
1527
2067
|
console.log(
|
|
1528
|
-
`About to fetch
|
|
2068
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgJson`,
|
|
1529
2069
|
);
|
|
1530
2070
|
}
|
|
1531
2071
|
return await getNpmMetadata(pkgList);
|
|
@@ -1572,7 +2112,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
1572
2112
|
const srcFilePath = node.path.includes(`${_sep}node_modules`)
|
|
1573
2113
|
? node.path.split(`${_sep}node_modules`)[0]
|
|
1574
2114
|
: node.path;
|
|
1575
|
-
const
|
|
2115
|
+
const isDevelopmentNode = node.dev === true || node.devOptional === true;
|
|
2116
|
+
const scope =
|
|
2117
|
+
isDevelopmentNode || node.optional === true ? "optional" : undefined;
|
|
1576
2118
|
const integrity = node.integrity ? node.integrity : undefined;
|
|
1577
2119
|
|
|
1578
2120
|
let pkg;
|
|
@@ -1645,6 +2187,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
1645
2187
|
purl: purlString,
|
|
1646
2188
|
"bom-ref": decodeURIComponent(purlString),
|
|
1647
2189
|
};
|
|
2190
|
+
if (isDevelopmentNode) {
|
|
2191
|
+
_setNpmDevelopmentProperty(pkg);
|
|
2192
|
+
}
|
|
1648
2193
|
if (node.resolved) {
|
|
1649
2194
|
if (node.resolved.startsWith("file:")) {
|
|
1650
2195
|
pkg.properties.push({
|
|
@@ -2080,10 +2625,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
2080
2625
|
options,
|
|
2081
2626
|
));
|
|
2082
2627
|
|
|
2083
|
-
if (
|
|
2628
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
2084
2629
|
if (DEBUG_MODE) {
|
|
2085
2630
|
console.log(
|
|
2086
|
-
`About to fetch
|
|
2631
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgLock`,
|
|
2087
2632
|
);
|
|
2088
2633
|
}
|
|
2089
2634
|
pkgList = await getNpmMetadata(pkgList);
|
|
@@ -2713,10 +3258,10 @@ export async function parseYarnLock(
|
|
|
2713
3258
|
}
|
|
2714
3259
|
}
|
|
2715
3260
|
|
|
2716
|
-
if (
|
|
3261
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
2717
3262
|
if (DEBUG_MODE) {
|
|
2718
3263
|
console.log(
|
|
2719
|
-
`About to fetch
|
|
3264
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseYarnLock`,
|
|
2720
3265
|
);
|
|
2721
3266
|
}
|
|
2722
3267
|
pkgList = await getNpmMetadata(pkgList);
|
|
@@ -2790,10 +3335,10 @@ export async function parseNodeShrinkwrap(swFile) {
|
|
|
2790
3335
|
}
|
|
2791
3336
|
}
|
|
2792
3337
|
}
|
|
2793
|
-
if (
|
|
3338
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
2794
3339
|
if (DEBUG_MODE) {
|
|
2795
3340
|
console.log(
|
|
2796
|
-
`About to fetch
|
|
3341
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseNodeShrinkwrap`,
|
|
2797
3342
|
);
|
|
2798
3343
|
}
|
|
2799
3344
|
return await getNpmMetadata(pkgList);
|
|
@@ -2826,6 +3371,51 @@ function _markTreeOptional(
|
|
|
2826
3371
|
}
|
|
2827
3372
|
}
|
|
2828
3373
|
|
|
3374
|
+
function _markTreeDevelopment(
|
|
3375
|
+
dbomRef,
|
|
3376
|
+
dependenciesMap,
|
|
3377
|
+
possibleDevelopmentDeps,
|
|
3378
|
+
visited,
|
|
3379
|
+
) {
|
|
3380
|
+
// Production-required packages set this map entry to false, and that wins
|
|
3381
|
+
// over any later attempt to propagate a development-only marking.
|
|
3382
|
+
if (possibleDevelopmentDeps[dbomRef] === undefined) {
|
|
3383
|
+
possibleDevelopmentDeps[dbomRef] = true;
|
|
3384
|
+
}
|
|
3385
|
+
if (dependenciesMap[dbomRef] && !visited[dbomRef]) {
|
|
3386
|
+
visited[dbomRef] = true;
|
|
3387
|
+
for (const eachDep of dependenciesMap[dbomRef]) {
|
|
3388
|
+
// Undefined means we have not classified this dependency yet, so we
|
|
3389
|
+
// continue propagating the dev-only marking unless it was already proven
|
|
3390
|
+
// to be non-development via a false entry.
|
|
3391
|
+
if (possibleDevelopmentDeps[eachDep] !== false) {
|
|
3392
|
+
_markTreeDevelopment(
|
|
3393
|
+
eachDep,
|
|
3394
|
+
dependenciesMap,
|
|
3395
|
+
possibleDevelopmentDeps,
|
|
3396
|
+
visited,
|
|
3397
|
+
);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
function _setNpmDevelopmentProperty(pkg) {
|
|
3404
|
+
if (!pkg.properties) {
|
|
3405
|
+
pkg.properties = [];
|
|
3406
|
+
}
|
|
3407
|
+
if (
|
|
3408
|
+
!pkg.properties.some((property) => {
|
|
3409
|
+
return property.name === "cdx:npm:package:development";
|
|
3410
|
+
})
|
|
3411
|
+
) {
|
|
3412
|
+
pkg.properties.push({
|
|
3413
|
+
name: "cdx:npm:package:development",
|
|
3414
|
+
value: "true",
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
|
|
2829
3419
|
function _setTreeWorkspaceRef(
|
|
2830
3420
|
dependenciesMap,
|
|
2831
3421
|
depref,
|
|
@@ -3201,6 +3791,7 @@ export async function parsePnpmLock(
|
|
|
3201
3791
|
// See: #1163
|
|
3202
3792
|
// Moreover, we have changed >= 9 for >= 6
|
|
3203
3793
|
// See: discussion #1359
|
|
3794
|
+
const possibleDevelopmentDeps = {};
|
|
3204
3795
|
const possibleOptionalDeps = {};
|
|
3205
3796
|
const dependenciesMap = {};
|
|
3206
3797
|
let ppurl = "";
|
|
@@ -3293,7 +3884,7 @@ export async function parsePnpmLock(
|
|
|
3293
3884
|
null,
|
|
3294
3885
|
null,
|
|
3295
3886
|
).toString();
|
|
3296
|
-
|
|
3887
|
+
possibleDevelopmentDeps[decodeURIComponent(dpurl)] = true;
|
|
3297
3888
|
}
|
|
3298
3889
|
// Find the root optional and peer dependencies
|
|
3299
3890
|
for (const rdk of Object.keys({ ...rootOptionalDeps, ...rootPeerDeps })) {
|
|
@@ -3317,6 +3908,7 @@ export async function parsePnpmLock(
|
|
|
3317
3908
|
null,
|
|
3318
3909
|
).toString();
|
|
3319
3910
|
possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
|
|
3911
|
+
possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
|
|
3320
3912
|
}
|
|
3321
3913
|
// Find the root direct dependencies
|
|
3322
3914
|
for (const dk of Object.keys(rootDirectDeps)) {
|
|
@@ -3344,6 +3936,7 @@ export async function parsePnpmLock(
|
|
|
3344
3936
|
// These are direct dependencies so cannot be optional
|
|
3345
3937
|
possibleOptionalDeps[decodeURIComponent(dpurl)] = false;
|
|
3346
3938
|
}
|
|
3939
|
+
possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
|
|
3347
3940
|
}
|
|
3348
3941
|
// pnpm-lock.yaml contains more than root dependencies in importers
|
|
3349
3942
|
// we do what we did above but for all the other components
|
|
@@ -3469,6 +4062,7 @@ export async function parsePnpmLock(
|
|
|
3469
4062
|
// This is a definite dependency of this component
|
|
3470
4063
|
comDepList.add(depRef);
|
|
3471
4064
|
possibleOptionalDeps[depRef] = false;
|
|
4065
|
+
possibleDevelopmentDeps[depRef] = false;
|
|
3472
4066
|
// Track the package.json files
|
|
3473
4067
|
if (pkgSrcFile) {
|
|
3474
4068
|
if (!srcFilesMap[depRef]) {
|
|
@@ -3488,7 +4082,7 @@ export async function parsePnpmLock(
|
|
|
3488
4082
|
null,
|
|
3489
4083
|
).toString();
|
|
3490
4084
|
const devDpRef = decodeURIComponent(dpurl);
|
|
3491
|
-
|
|
4085
|
+
possibleDevelopmentDeps[devDpRef] = true;
|
|
3492
4086
|
// This is also a dependency of this component
|
|
3493
4087
|
comDepList.add(devDpRef);
|
|
3494
4088
|
}
|
|
@@ -3506,6 +4100,7 @@ export async function parsePnpmLock(
|
|
|
3506
4100
|
null,
|
|
3507
4101
|
).toString();
|
|
3508
4102
|
possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
|
|
4103
|
+
possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
|
|
3509
4104
|
}
|
|
3510
4105
|
dependenciesList.push({
|
|
3511
4106
|
ref: decodeURIComponent(compPurl),
|
|
@@ -3708,7 +4303,21 @@ export async function parsePnpmLock(
|
|
|
3708
4303
|
null,
|
|
3709
4304
|
).toString();
|
|
3710
4305
|
const bomRef = decodeURIComponent(purlString);
|
|
4306
|
+
if (
|
|
4307
|
+
packageNode.dev === true &&
|
|
4308
|
+
possibleDevelopmentDeps[bomRef] === undefined
|
|
4309
|
+
) {
|
|
4310
|
+
possibleDevelopmentDeps[bomRef] = true;
|
|
4311
|
+
}
|
|
3711
4312
|
const isBaseOptional = possibleOptionalDeps[bomRef];
|
|
4313
|
+
// optionalDependencies are tracked separately because they may still
|
|
4314
|
+
// be runtime-relevant and should keep the CycloneDX optional scope.
|
|
4315
|
+
// packageNode.dev captures explicit dev-only packages from the lock
|
|
4316
|
+
// entry, while possibleDevelopmentDeps lets that marking propagate to
|
|
4317
|
+
// transitive dependencies discovered through the dependency graph.
|
|
4318
|
+
const isBaseDevelopment =
|
|
4319
|
+
packageNode.dev === true ||
|
|
4320
|
+
possibleDevelopmentDeps[bomRef] === true;
|
|
3712
4321
|
const deplist = [];
|
|
3713
4322
|
for (let dpkgName of Object.keys(deps)) {
|
|
3714
4323
|
let vers = deps[dpkgName];
|
|
@@ -3751,6 +4360,18 @@ export async function parsePnpmLock(
|
|
|
3751
4360
|
{},
|
|
3752
4361
|
);
|
|
3753
4362
|
}
|
|
4363
|
+
if (
|
|
4364
|
+
isBaseDevelopment &&
|
|
4365
|
+
possibleDevelopmentDeps[dbomRef] === undefined
|
|
4366
|
+
) {
|
|
4367
|
+
possibleDevelopmentDeps[dbomRef] = true;
|
|
4368
|
+
_markTreeDevelopment(
|
|
4369
|
+
dbomRef,
|
|
4370
|
+
dependenciesMap,
|
|
4371
|
+
possibleDevelopmentDeps,
|
|
4372
|
+
{},
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
3754
4375
|
}
|
|
3755
4376
|
if (!dependenciesMap[bomRef]) {
|
|
3756
4377
|
dependenciesMap[bomRef] = [];
|
|
@@ -3910,6 +4531,19 @@ export async function parsePnpmLock(
|
|
|
3910
4531
|
}
|
|
3911
4532
|
}
|
|
3912
4533
|
}
|
|
4534
|
+
// Repeat development dependency detection after the dependency graph is fully
|
|
4535
|
+
// built, since a single package iteration can encounter a dev-only component
|
|
4536
|
+
// before its own dependency list has been captured in dependenciesMap.
|
|
4537
|
+
for (const dependencyRef of Object.keys(possibleDevelopmentDeps)) {
|
|
4538
|
+
if (possibleDevelopmentDeps[dependencyRef] === true) {
|
|
4539
|
+
_markTreeDevelopment(
|
|
4540
|
+
dependencyRef,
|
|
4541
|
+
dependenciesMap,
|
|
4542
|
+
possibleDevelopmentDeps,
|
|
4543
|
+
{},
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
3913
4547
|
|
|
3914
4548
|
// Problem: We might have over aggressively marked a package as optional even it is both required and optional
|
|
3915
4549
|
// The below loops ensure required packages continue to stay required
|
|
@@ -3940,6 +4574,15 @@ export async function parsePnpmLock(
|
|
|
3940
4574
|
if (requiredDependencies[apkg["bom-ref"]]) {
|
|
3941
4575
|
apkg.scope = undefined;
|
|
3942
4576
|
}
|
|
4577
|
+
if (
|
|
4578
|
+
!requiredDependencies[apkg["bom-ref"]] &&
|
|
4579
|
+
possibleDevelopmentDeps[apkg["bom-ref"]]
|
|
4580
|
+
) {
|
|
4581
|
+
if (!apkg.scope) {
|
|
4582
|
+
apkg.scope = "optional";
|
|
4583
|
+
}
|
|
4584
|
+
_setNpmDevelopmentProperty(apkg);
|
|
4585
|
+
}
|
|
3943
4586
|
if (possibleAliasesRefs[apkg["bom-ref"]]) {
|
|
3944
4587
|
apkg.properties.push({
|
|
3945
4588
|
name: "cdx:pnpm:alias",
|
|
@@ -4011,10 +4654,10 @@ export async function parsePnpmLock(
|
|
|
4011
4654
|
pkgList = await pnpmMetadata(pkgList, pnpmLock);
|
|
4012
4655
|
}
|
|
4013
4656
|
|
|
4014
|
-
if (
|
|
4657
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
4015
4658
|
if (DEBUG_MODE) {
|
|
4016
4659
|
console.log(
|
|
4017
|
-
`About to fetch
|
|
4660
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parsePnpmLock`,
|
|
4018
4661
|
);
|
|
4019
4662
|
}
|
|
4020
4663
|
pkgList = await getNpmMetadata(pkgList);
|
|
@@ -4072,10 +4715,10 @@ export async function parseBowerJson(bowerJsonFile) {
|
|
|
4072
4715
|
// continue regardless of error
|
|
4073
4716
|
}
|
|
4074
4717
|
}
|
|
4075
|
-
if (
|
|
4718
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
4076
4719
|
if (DEBUG_MODE) {
|
|
4077
4720
|
console.log(
|
|
4078
|
-
`About to fetch
|
|
4721
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseBowerJson`,
|
|
4079
4722
|
);
|
|
4080
4723
|
}
|
|
4081
4724
|
return await getNpmMetadata(pkgList);
|
|
@@ -4170,10 +4813,10 @@ export async function parseMinJs(minJsFile) {
|
|
|
4170
4813
|
// continue regardless of error
|
|
4171
4814
|
}
|
|
4172
4815
|
}
|
|
4173
|
-
if (
|
|
4816
|
+
if (shouldFetchPackageMetadata() && pkgList?.length) {
|
|
4174
4817
|
if (DEBUG_MODE) {
|
|
4175
4818
|
console.log(
|
|
4176
|
-
`About to fetch
|
|
4819
|
+
`About to fetch npm registry metadata for ${pkgList.length} packages in parseMinJs`,
|
|
4177
4820
|
);
|
|
4178
4821
|
}
|
|
4179
4822
|
return await getNpmMetadata(pkgList);
|
|
@@ -5665,7 +6308,7 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
|
|
|
5665
6308
|
* @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
|
|
5666
6309
|
*/
|
|
5667
6310
|
export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
5668
|
-
if (!
|
|
6311
|
+
if (!shouldFetchPackageMetadata() && !fetchDepsInfo) {
|
|
5669
6312
|
return pkgList;
|
|
5670
6313
|
}
|
|
5671
6314
|
const PYPI_URL = process.env.PYPI_URL || "https://pypi.org/pypi/";
|
|
@@ -5841,6 +6484,10 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
5841
6484
|
null,
|
|
5842
6485
|
null,
|
|
5843
6486
|
).toString();
|
|
6487
|
+
p.properties = p.properties || [];
|
|
6488
|
+
p.properties.push(
|
|
6489
|
+
...collectPypiRegistryProvenanceProperties(body, p.version),
|
|
6490
|
+
);
|
|
5844
6491
|
p.purl = purlString;
|
|
5845
6492
|
p["bom-ref"] = decodeURIComponent(purlString);
|
|
5846
6493
|
cdepList.push(p);
|
|
@@ -6243,9 +6890,9 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
6243
6890
|
}
|
|
6244
6891
|
|
|
6245
6892
|
/**
|
|
6246
|
-
* 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.
|
|
6247
6894
|
*
|
|
6248
|
-
* @param {
|
|
6895
|
+
* @param {string} lockData Raw TOML text from poetry.lock, pdm.lock, uv.lock, or pylock.toml
|
|
6249
6896
|
* @param {string} lockFile Lock file name for evidence
|
|
6250
6897
|
* @param {string} pyProjectFile pyproject.toml file
|
|
6251
6898
|
*/
|
|
@@ -6262,6 +6909,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6262
6909
|
let workspacePaths;
|
|
6263
6910
|
let workspaceWarningShown = false;
|
|
6264
6911
|
let hasWorkspaces = false;
|
|
6912
|
+
let pyLockProperties = [];
|
|
6265
6913
|
// Keep track of any workspace components to be added to the parent component
|
|
6266
6914
|
const workspaceComponentMap = {};
|
|
6267
6915
|
const workspacePyProjMap = {};
|
|
@@ -6390,7 +7038,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6390
7038
|
}
|
|
6391
7039
|
}
|
|
6392
7040
|
}
|
|
6393
|
-
|
|
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) {
|
|
6394
7052
|
// This avoids validation errors with uv.lock
|
|
6395
7053
|
if (parentComponent?.name && parentComponent.name === apkg.name) {
|
|
6396
7054
|
continue;
|
|
@@ -6410,10 +7068,19 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6410
7068
|
if (apkg.optional) {
|
|
6411
7069
|
pkg.scope = "optional";
|
|
6412
7070
|
}
|
|
6413
|
-
|
|
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) {
|
|
6414
7075
|
pkg.properties.push({
|
|
6415
7076
|
name: "cdx:pypi:requiresPython",
|
|
6416
|
-
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,
|
|
6417
7084
|
});
|
|
6418
7085
|
}
|
|
6419
7086
|
if (apkg?.source) {
|
|
@@ -6439,6 +7106,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6439
7106
|
});
|
|
6440
7107
|
}
|
|
6441
7108
|
}
|
|
7109
|
+
if (pyLockMode) {
|
|
7110
|
+
pkg.properties = pkg.properties.concat(
|
|
7111
|
+
collectPyLockPackageProperties(apkg),
|
|
7112
|
+
);
|
|
7113
|
+
}
|
|
6442
7114
|
// Is this component a module?
|
|
6443
7115
|
if (workspaceComponentMap[pkg.name]) {
|
|
6444
7116
|
pkg.properties.push({
|
|
@@ -6526,6 +7198,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6526
7198
|
});
|
|
6527
7199
|
}
|
|
6528
7200
|
}
|
|
7201
|
+
if (pyLockMode) {
|
|
7202
|
+
const pylockFileComponents = collectPyLockFileComponents(apkg, lockFile);
|
|
7203
|
+
if (pylockFileComponents.length) {
|
|
7204
|
+
pkg.components = (pkg.components || []).concat(pylockFileComponents);
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
6529
7207
|
if (
|
|
6530
7208
|
directDepsKeys[pkg.name] ||
|
|
6531
7209
|
(hasWorkspaces && !Object.keys(workspaceComponentMap).length)
|
|
@@ -6582,13 +7260,14 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6582
7260
|
// Example: "msgpack>=0.5.2"
|
|
6583
7261
|
const nameStr =
|
|
6584
7262
|
apkgDep.name || apkgDep.split(/(==|<=|~=|>=)/)[0].split(" ")[0];
|
|
6585
|
-
|
|
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);
|
|
6586
7268
|
// Propagate the workspace properties to the child components
|
|
6587
|
-
if (
|
|
6588
|
-
|
|
6589
|
-
pkgBomRefMap[existingPkgMap[nameStr]]
|
|
6590
|
-
) {
|
|
6591
|
-
const dependentPkg = pkgBomRefMap[existingPkgMap[nameStr]];
|
|
7269
|
+
if (depPkgRef && pkgBomRefMap[depPkgRef]) {
|
|
7270
|
+
const dependentPkg = pkgBomRefMap[depPkgRef];
|
|
6592
7271
|
dependentPkg.properties = dependentPkg.properties || [];
|
|
6593
7272
|
const addedValue = {};
|
|
6594
7273
|
// Is the parent a workspace
|
|
@@ -6630,6 +7309,8 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6630
7309
|
depRef = adep;
|
|
6631
7310
|
} else if (existingPkgMap[adep]) {
|
|
6632
7311
|
depRef = existingPkgMap[adep];
|
|
7312
|
+
} else if (existingPkgMap[adep.toLowerCase()]) {
|
|
7313
|
+
depRef = existingPkgMap[adep.toLowerCase()];
|
|
6633
7314
|
} else if (existingPkgMap[`py${adep}`]) {
|
|
6634
7315
|
depRef = existingPkgMap[`py${adep}`];
|
|
6635
7316
|
} else if (existingPkgMap[adep.replace(/-/g, "_")]) {
|
|
@@ -6699,6 +7380,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
6699
7380
|
pkgList,
|
|
6700
7381
|
rootList,
|
|
6701
7382
|
dependenciesList,
|
|
7383
|
+
pyLockProperties,
|
|
6702
7384
|
workspaceWarningShown,
|
|
6703
7385
|
};
|
|
6704
7386
|
}
|
|
@@ -12810,38 +13492,14 @@ export function convertOSQueryResults(
|
|
|
12810
13492
|
const pkgList = [];
|
|
12811
13493
|
if (results?.length) {
|
|
12812
13494
|
for (const res of results) {
|
|
12813
|
-
const version =
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
res.
|
|
12817
|
-
|
|
12818
|
-
res.pid ||
|
|
12819
|
-
res.subject_key_id ||
|
|
12820
|
-
res.interface ||
|
|
12821
|
-
res.instance_id;
|
|
12822
|
-
let name =
|
|
12823
|
-
res.name ||
|
|
12824
|
-
res.device_id ||
|
|
12825
|
-
res.hotfix_id ||
|
|
12826
|
-
res.uuid ||
|
|
12827
|
-
res.serial ||
|
|
12828
|
-
res.pid ||
|
|
12829
|
-
res.address ||
|
|
12830
|
-
res.ami_id ||
|
|
12831
|
-
res.interface ||
|
|
12832
|
-
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
|
+
}
|
|
12833
13500
|
let group = "";
|
|
12834
13501
|
const subpath = res.path || res.admindir || res.source;
|
|
12835
|
-
|
|
12836
|
-
res.publisher ||
|
|
12837
|
-
res.maintainer ||
|
|
12838
|
-
res.creator ||
|
|
12839
|
-
res.manufacturer ||
|
|
12840
|
-
res.provider ||
|
|
12841
|
-
"";
|
|
12842
|
-
if (publisher === "null") {
|
|
12843
|
-
publisher = "";
|
|
12844
|
-
}
|
|
13502
|
+
const publisher = deriveOsQueryPublisher(res);
|
|
12845
13503
|
// For vscode-extension purl type, the publisher is used as the namespace
|
|
12846
13504
|
if (queryObj.purlType === "vscode-extension" && publisher) {
|
|
12847
13505
|
group = publisher.toLowerCase();
|
|
@@ -12852,20 +13510,8 @@ export function convertOSQueryResults(
|
|
|
12852
13510
|
scope = compScope;
|
|
12853
13511
|
}
|
|
12854
13512
|
const description =
|
|
12855
|
-
res
|
|
12856
|
-
res.
|
|
12857
|
-
res.arguments ||
|
|
12858
|
-
res.device ||
|
|
12859
|
-
res.codename ||
|
|
12860
|
-
res.section ||
|
|
12861
|
-
res.status ||
|
|
12862
|
-
res.identifier ||
|
|
12863
|
-
res.components ||
|
|
12864
|
-
"";
|
|
12865
|
-
// Re-use the name from query obj
|
|
12866
|
-
if (!name && results.length === 1 && queryObj.name) {
|
|
12867
|
-
name = queryObj.name;
|
|
12868
|
-
}
|
|
13513
|
+
deriveOsQueryDescription(res) ||
|
|
13514
|
+
(queryObj.purlType === "chrome-extension" ? res.name || "" : "");
|
|
12869
13515
|
let qualifiers;
|
|
12870
13516
|
if (res.identifying_number?.length) {
|
|
12871
13517
|
qualifiers = {
|
|
@@ -12873,25 +13519,18 @@ export function convertOSQueryResults(
|
|
|
12873
13519
|
};
|
|
12874
13520
|
}
|
|
12875
13521
|
if (name) {
|
|
12876
|
-
name = name
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
.
|
|
12880
|
-
.replace(/[}]$/g, "");
|
|
12881
|
-
group = group
|
|
12882
|
-
.replace(/ /g, "+")
|
|
12883
|
-
.replace(/[:%]/g, "-")
|
|
12884
|
-
.replace(/^[@{]/g, "")
|
|
12885
|
-
.replace(/[}]$/g, "");
|
|
12886
|
-
const purl = new PackageURL(
|
|
12887
|
-
queryObj.purlType || "swid",
|
|
13522
|
+
name = sanitizeOsQueryIdentity(name);
|
|
13523
|
+
group = sanitizeOsQueryIdentity(group);
|
|
13524
|
+
const purl = createOsQueryPurl(
|
|
13525
|
+
queryObj.purlType,
|
|
12888
13526
|
group,
|
|
12889
13527
|
name,
|
|
12890
|
-
version
|
|
13528
|
+
version,
|
|
12891
13529
|
qualifiers,
|
|
12892
13530
|
subpath,
|
|
12893
|
-
)
|
|
13531
|
+
);
|
|
12894
13532
|
const props = [{ name: "cdx:osquery:category", value: queryCategory }];
|
|
13533
|
+
props.push(...createLolbasProperties(queryCategory, res));
|
|
12895
13534
|
let providesList;
|
|
12896
13535
|
if (enhance) {
|
|
12897
13536
|
switch (queryObj.purlType) {
|
|
@@ -12928,9 +13567,15 @@ export function convertOSQueryResults(
|
|
|
12928
13567
|
scope,
|
|
12929
13568
|
type: queryObj.componentType,
|
|
12930
13569
|
};
|
|
12931
|
-
for (const k of Object.keys(res).filter(
|
|
12932
|
-
(
|
|
12933
|
-
|
|
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
|
+
})) {
|
|
12934
13579
|
if (res[k] && res[k] !== "null") {
|
|
12935
13580
|
props.push({
|
|
12936
13581
|
name: k,
|
|
@@ -13612,6 +14257,76 @@ export function parseJarManifest(jarMetadata) {
|
|
|
13612
14257
|
return metadata;
|
|
13613
14258
|
}
|
|
13614
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
|
+
|
|
13615
14330
|
/**
|
|
13616
14331
|
* Parse a Maven pom.properties file and return its key-value pairs as an object.
|
|
13617
14332
|
*
|
|
@@ -13902,14 +14617,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
13902
14617
|
.split(";")[0]
|
|
13903
14618
|
.trim();
|
|
13904
14619
|
}
|
|
13905
|
-
group =
|
|
13906
|
-
group ||
|
|
13907
|
-
jarMetadata["Extension-Name"] ||
|
|
13908
|
-
jarMetadata["Implementation-Vendor-Id"] ||
|
|
13909
|
-
jarMetadata["Bundle-SymbolicName"] ||
|
|
13910
|
-
jarMetadata["Bundle-Vendor"] ||
|
|
13911
|
-
jarMetadata["Automatic-Module-Name"] ||
|
|
13912
|
-
"";
|
|
14620
|
+
group = group || inferJarGroupFromManifest(jarMetadata);
|
|
13913
14621
|
version =
|
|
13914
14622
|
version ||
|
|
13915
14623
|
jarMetadata["Bundle-Version"] ||
|
|
@@ -13965,16 +14673,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
13965
14673
|
}
|
|
13966
14674
|
// Sometimes the group might already contain the name
|
|
13967
14675
|
// Eg: group: org.checkerframework.checker.qual name: checker-qual
|
|
13968
|
-
|
|
13969
|
-
if (group.includes(`.${name.toLowerCase().replace(/-/g, ".")}`)) {
|
|
13970
|
-
group = group.replace(
|
|
13971
|
-
new RegExp(`.${name.toLowerCase().replace(/-/g, ".")}$`),
|
|
13972
|
-
"",
|
|
13973
|
-
);
|
|
13974
|
-
} else if (group.includes(`.${name.toLowerCase()}`)) {
|
|
13975
|
-
group = group.replace(new RegExp(`.${name.toLowerCase()}$`), "");
|
|
13976
|
-
}
|
|
13977
|
-
}
|
|
14676
|
+
group = trimJarGroupSuffix(group, name);
|
|
13978
14677
|
// Patch the group string
|
|
13979
14678
|
if (vendorAliases[name]) {
|
|
13980
14679
|
group = vendorAliases[name];
|
|
@@ -16447,6 +17146,7 @@ export async function addEvidenceForImports(
|
|
|
16447
17146
|
: [name];
|
|
16448
17147
|
let isImported = false;
|
|
16449
17148
|
for (const alias of aliases) {
|
|
17149
|
+
const isWasmAlias = /\.wasm([?#].*)?$/i.test(alias);
|
|
16450
17150
|
const all_includes = impPkgs.filter(
|
|
16451
17151
|
(find_pkg) =>
|
|
16452
17152
|
find_pkg.startsWith(alias) &&
|
|
@@ -16456,12 +17156,20 @@ export async function addEvidenceForImports(
|
|
|
16456
17156
|
find_pkg.startsWith(alias),
|
|
16457
17157
|
);
|
|
16458
17158
|
if (all_exports?.length) {
|
|
16459
|
-
let exportedModules = new Set(all_exports);
|
|
17159
|
+
let exportedModules = new Set(isWasmAlias ? [] : all_exports);
|
|
16460
17160
|
pkg.properties = pkg.properties || [];
|
|
16461
17161
|
for (const subevidence of all_exports) {
|
|
16462
17162
|
const evidences = allExports[subevidence];
|
|
16463
17163
|
for (const evidence of evidences) {
|
|
16464
17164
|
if (evidence && Object.keys(evidence).length) {
|
|
17165
|
+
if (isWasmAlias) {
|
|
17166
|
+
for (const wasmImportedModule of evidence.importedModules ||
|
|
17167
|
+
[]) {
|
|
17168
|
+
if (wasmImportedModule?.length) {
|
|
17169
|
+
exportedModules.add(wasmImportedModule);
|
|
17170
|
+
}
|
|
17171
|
+
}
|
|
17172
|
+
}
|
|
16465
17173
|
if (evidence.exportedModules.length > 1) {
|
|
16466
17174
|
for (const aexpsubm of evidence.exportedModules) {
|
|
16467
17175
|
// Be selective on the submodule names
|
|
@@ -16496,6 +17204,8 @@ export async function addEvidenceForImports(
|
|
|
16496
17204
|
if (impPkgs.includes(alias) || all_includes.length) {
|
|
16497
17205
|
isImported = true;
|
|
16498
17206
|
let importedModules = new Set();
|
|
17207
|
+
let wasmExportedModules = new Set();
|
|
17208
|
+
const seenOccurrenceLocations = new Set();
|
|
16499
17209
|
pkg.scope = "required";
|
|
16500
17210
|
for (const subevidence of all_includes) {
|
|
16501
17211
|
const evidences = allImports[subevidence];
|
|
@@ -16503,16 +17213,23 @@ export async function addEvidenceForImports(
|
|
|
16503
17213
|
if (evidence && Object.keys(evidence).length && evidence.fileName) {
|
|
16504
17214
|
pkg.evidence = pkg.evidence || {};
|
|
16505
17215
|
pkg.evidence.occurrences = pkg.evidence.occurrences || [];
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
|
|
17216
|
+
const occurrenceLocation = `${evidence.fileName}${
|
|
17217
|
+
evidence.lineNumber ? `#${evidence.lineNumber}` : ""
|
|
17218
|
+
}`;
|
|
17219
|
+
if (!seenOccurrenceLocations.has(occurrenceLocation)) {
|
|
17220
|
+
pkg.evidence.occurrences.push({
|
|
17221
|
+
location: occurrenceLocation,
|
|
17222
|
+
});
|
|
17223
|
+
seenOccurrenceLocations.add(occurrenceLocation);
|
|
17224
|
+
}
|
|
16511
17225
|
importedModules.add(evidence.importedAs);
|
|
16512
17226
|
for (const importedSm of evidence.importedModules || []) {
|
|
16513
17227
|
if (!importedSm) {
|
|
16514
17228
|
continue;
|
|
16515
17229
|
}
|
|
17230
|
+
if (isWasmAlias) {
|
|
17231
|
+
wasmExportedModules.add(importedSm);
|
|
17232
|
+
}
|
|
16516
17233
|
// Store both the short and long form of the imported sub modules
|
|
16517
17234
|
if (importedSm.length > 3) {
|
|
16518
17235
|
importedModules.add(importedSm);
|
|
@@ -16523,6 +17240,7 @@ export async function addEvidenceForImports(
|
|
|
16523
17240
|
}
|
|
16524
17241
|
}
|
|
16525
17242
|
importedModules = Array.from(importedModules);
|
|
17243
|
+
wasmExportedModules = Array.from(wasmExportedModules);
|
|
16526
17244
|
if (importedModules.length) {
|
|
16527
17245
|
pkg.properties = pkg.properties || [];
|
|
16528
17246
|
pkg.properties.push({
|
|
@@ -16530,6 +17248,15 @@ export async function addEvidenceForImports(
|
|
|
16530
17248
|
value: importedModules.join(","),
|
|
16531
17249
|
});
|
|
16532
17250
|
}
|
|
17251
|
+
if (isWasmAlias && wasmExportedModules.length) {
|
|
17252
|
+
pkg.properties = pkg.properties || [];
|
|
17253
|
+
if (!pkg.properties.some((p) => p.name === "ExportedModules")) {
|
|
17254
|
+
pkg.properties.push({
|
|
17255
|
+
name: "ExportedModules",
|
|
17256
|
+
value: wasmExportedModules.join(","),
|
|
17257
|
+
});
|
|
17258
|
+
}
|
|
17259
|
+
}
|
|
16533
17260
|
break;
|
|
16534
17261
|
}
|
|
16535
17262
|
if (
|
|
@@ -17774,6 +18501,29 @@ export function parseMakeDFile(dfile) {
|
|
|
17774
18501
|
return pkgFilesMap;
|
|
17775
18502
|
}
|
|
17776
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
|
+
|
|
17777
18527
|
/**
|
|
17778
18528
|
* Function to validate an externalReference URL for conforming to the JSON schema or bomLink
|
|
17779
18529
|
* https://github.com/CycloneDX/cyclonedx-core-java/blob/75575318b268dda9e2a290761d7db11b4f414255/src/main/resources/bom-1.5.schema.json#L1140
|
|
@@ -17798,13 +18548,9 @@ export function isValidIriReference(iri) {
|
|
|
17798
18548
|
return false;
|
|
17799
18549
|
}
|
|
17800
18550
|
|
|
17801
|
-
//
|
|
17802
|
-
//
|
|
17803
|
-
|
|
17804
|
-
// - The end of the string ($)
|
|
17805
|
-
// - Or a character that is NOT a hex digit ([^0-9A-Fa-f])
|
|
17806
|
-
// This catches %ab, %ab%, %abZ, %abc, etc.
|
|
17807
|
-
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)) {
|
|
17808
18554
|
return false;
|
|
17809
18555
|
}
|
|
17810
18556
|
|