Skip to content

Commit b2b77f8

Browse files
authored
Expand optional tree (CycloneDX#1523)
* Expand optional tree Signed-off-by: Prabhu Subramanian <[email protected]> * Collect more properties Signed-off-by: Prabhu Subramanian <[email protected]> * Bump version Signed-off-by: Prabhu Subramanian <[email protected]> * More juice-shop edge cases Signed-off-by: Prabhu Subramanian <[email protected]> --------- Signed-off-by: Prabhu Subramanian <[email protected]>
1 parent ad6b102 commit b2b77f8

File tree

7 files changed

+165
-18
lines changed

7 files changed

+165
-18
lines changed

.github/workflows/snapshot-tests.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,12 @@ name: Test BOM Snapshots
22

33
on:
44
workflow_dispatch:
5-
pull_request:
6-
branches:
7-
- master
8-
push:
9-
branches:
10-
- master
115

126

137
concurrency:
148
group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}"
159
cancel-in-progress: true
1610

17-
1811
jobs:
1912

2013
test_non_dotnet:

bin/cdxgen.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,15 @@ const applyAdvancedOptions = (options) => {
510510
options.installDeps = true;
511511
break;
512512
}
513+
// When the user specifies source-code-analysis as a technique, then enable deep and evidence mode.
514+
if (
515+
options?.technique &&
516+
Array.isArray(options.technique) &&
517+
options?.technique?.includes("source-code-analysis")
518+
) {
519+
options.deep = true;
520+
options.evidence = true;
521+
}
513522
return options;
514523
};
515524
applyAdvancedOptions(options);

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "11.0.8",
3+
"version": "11.0.9",
44
"exports": "./lib/cli/index.js",
55
"compilerOptions": {
66
"lib": ["deno.window"],

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "11.0.8",
3+
"version": "11.0.9",
44
"exports": "./lib/cli/index.js",
55
"include": ["*.js", "lib/**", "bin/**", "data/**", "types/**"],
66
"exclude": [

lib/helpers/utils.js

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
10381038
if (!options) {
10391039
options = {};
10401040
}
1041-
1041+
const pkgSpecVersionCache = {};
10421042
if (!existsSync(pkgLockFile)) {
10431043
return {
10441044
pkgList,
@@ -1051,6 +1051,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
10511051
rootNode,
10521052
parentRef = null,
10531053
visited = new Set(),
1054+
pkgSpecVersionCache = {},
10541055
options = {},
10551056
) => {
10561057
if (visited.has(node)) {
@@ -1119,6 +1120,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
11191120
author: authorString,
11201121
scope: scope,
11211122
_integrity: integrity,
1123+
externalReferences: [],
11221124
properties: [
11231125
{
11241126
name: "SrcFile",
@@ -1155,11 +1157,88 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
11551157
value: node.location,
11561158
});
11571159
}
1160+
if (node?.installLinks) {
1161+
pkg.properties.push({
1162+
name: "cdx:npm:installLinks",
1163+
value: "true",
1164+
});
1165+
}
1166+
if (node?.binPaths?.length) {
1167+
pkg.properties.push({
1168+
name: "cdx:npm:binPaths",
1169+
value: node.binPaths.join(", "),
1170+
});
1171+
}
1172+
if (node?.hasInstallScript) {
1173+
pkg.properties.push({
1174+
name: "cdx:npm:hasInstallScript",
1175+
value: "true",
1176+
});
1177+
}
1178+
if (node?.isLink) {
1179+
pkg.properties.push({
1180+
name: "cdx:npm:isLink",
1181+
value: "true",
1182+
});
1183+
}
1184+
// This getter method could fail with errors at times.
1185+
// Example Error: Invalid tag name "^>=6.0.0" of package "^>=6.0.0": Tags may not have any characters that encodeURIComponent encodes.
1186+
try {
1187+
if (!node?.isRegistryDependency) {
1188+
pkg.properties.push({
1189+
name: "cdx:npm:isRegistryDependency",
1190+
value: "false",
1191+
});
1192+
}
1193+
} catch (err) {
1194+
// ignore
1195+
}
1196+
if (node?.isWorkspace) {
1197+
pkg.properties.push({
1198+
name: "cdx:npm:isWorkspace",
1199+
value: "true",
1200+
});
1201+
}
1202+
if (node?.inBundle) {
1203+
pkg.properties.push({
1204+
name: "cdx:npm:inBundle",
1205+
value: "true",
1206+
});
1207+
}
1208+
if (node?.inDepBundle) {
1209+
pkg.properties.push({
1210+
name: "cdx:npm:inDepBundle",
1211+
value: "true",
1212+
});
1213+
}
1214+
if (node.package?.repository?.url) {
1215+
pkg.externalReferences.push({
1216+
type: "vcs",
1217+
url: node.package.repository.url,
1218+
});
1219+
}
1220+
if (node.package?.bugs?.url) {
1221+
pkg.externalReferences.push({
1222+
type: "issue-tracker",
1223+
url: node.package.bugs.url,
1224+
});
1225+
}
1226+
if (node?.package?.keywords?.length) {
1227+
pkg.tags = Array.isArray(node.package.keywords)
1228+
? node.package.keywords.sort()
1229+
: node.package.keywords.split(",");
1230+
}
11581231
}
1159-
const packageLicense = node.package.license;
1160-
if (packageLicense) {
1232+
if (node.package?.license) {
11611233
// License will be overridden if shouldFetchLicense() is enabled
1162-
pkg.license = packageLicense;
1234+
pkg.license = node.package.license;
1235+
}
1236+
const deprecatedMessage = node.package?.deprecated;
1237+
if (deprecatedMessage) {
1238+
pkg.properties.push({
1239+
name: "cdx:npm:deprecated",
1240+
value: deprecatedMessage,
1241+
});
11631242
}
11641243
pkgList.push(pkg);
11651244

@@ -1170,7 +1249,14 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
11701249
const {
11711250
pkgList: childPkgList,
11721251
dependenciesList: childDependenciesList,
1173-
} = parseArboristNode(workspaceNode, rootNode, purlString, visited);
1252+
} = parseArboristNode(
1253+
workspaceNode,
1254+
rootNode,
1255+
purlString,
1256+
visited,
1257+
pkgSpecVersionCache,
1258+
options,
1259+
);
11741260
pkgList = pkgList.concat(childPkgList);
11751261
dependenciesList = dependenciesList.concat(childDependenciesList);
11761262
const depWorkspacePurlString = decodeURIComponent(
@@ -1193,8 +1279,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
11931279

11941280
// this handles the case when a node has ["dependencies"] key in a package-lock.json
11951281
// for a node. We exclude the root node because it's already been handled
1282+
// If the node has "requires", we don't have to track the "dependencies"
11961283
const childrenDependsOn = [];
1197-
if (node !== rootNode) {
1284+
if (node !== rootNode && !node.edgesOut.size) {
11981285
for (const child of node.children) {
11991286
const childNode = child[1];
12001287
const {
@@ -1205,6 +1292,8 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
12051292
rootNode,
12061293
decodeURIComponent(purlString),
12071294
visited,
1295+
pkgSpecVersionCache,
1296+
options,
12081297
);
12091298
pkgList = pkgList.concat(childPkgList);
12101299
dependenciesList = dependenciesList.concat(childDependenciesList);
@@ -1232,6 +1321,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
12321321
let targetVersion;
12331322
let targetName;
12341323
let foundMatch = false;
1324+
// This cache is required to help us down the line.
1325+
if (edge?.to?.version && edge?.spec) {
1326+
pkgSpecVersionCache[`${edge.name}-${edge.spec}`] = edge.to.version;
1327+
}
12351328
// if the edge doesn't have an integrity, it's likely a peer dependency
12361329
// which isn't installed
12371330
// Bug #795. At times, npm loses the integrity node completely and such packages are getting missed out
@@ -1285,11 +1378,43 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
12851378
break;
12861379
}
12871380
}
1381+
if (!targetVersion || !targetName) {
1382+
if (pkgSpecVersionCache[`${edge.name}-${edge.spec}`]) {
1383+
targetVersion = pkgSpecVersionCache[`${edge.name}-${edge.spec}`];
1384+
targetName = edge.name;
1385+
}
1386+
}
12881387
}
12891388

12901389
// if we can't find the version of the edge, continue
12911390
// it may be an optional peer dependency
12921391
if (!targetVersion || !targetName) {
1392+
if (
1393+
DEBUG_MODE &&
1394+
!options.deep &&
1395+
!["optional", "peer", "peerOptional"].includes(edge?.type)
1396+
) {
1397+
if (!targetVersion) {
1398+
console.log(
1399+
`Unable to determine the version for the dependency ${edge.name} from the path ${edge?.from?.path}. This is likely an edge case that is not handled.`,
1400+
edge,
1401+
);
1402+
} else if (!targetName) {
1403+
console.log(
1404+
`Unable to determine the name for the dependency from the edge from the path ${edge?.from?.path}. This is likely an edge case that is not handled.`,
1405+
edge,
1406+
);
1407+
}
1408+
}
1409+
// juice-shop
1410+
// Lock files created with --legacy-peer-deps will have certain peer dependencies missing
1411+
// This flags any non-missing peers
1412+
if (DEBUG_MODE && edge?.type === "peer" && edge?.error !== "MISSING") {
1413+
console.log(
1414+
`Unable to determine the version for the dependency ${edge.name} from the path ${edge?.from?.path}. This is likely an edge case that is not handled.`,
1415+
edge,
1416+
);
1417+
}
12931418
continue;
12941419
}
12951420
const depPurlString = decodeURIComponent(
@@ -1309,6 +1434,8 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
13091434
rootNode,
13101435
decodeURIComponent(purlString),
13111436
visited,
1437+
pkgSpecVersionCache,
1438+
options,
13121439
);
13131440
pkgList = pkgList.concat(childPkgList);
13141441
dependenciesList = dependenciesList.concat(childDependenciesList);
@@ -1332,7 +1459,24 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
13321459
});
13331460
let tree = undefined;
13341461
try {
1335-
tree = await arb.loadVirtual();
1462+
const rootNodeModulesDir = join(path.dirname(pkgLockFile), "node_modules");
1463+
if (existsSync(rootNodeModulesDir)) {
1464+
if (options.deep) {
1465+
console.log(
1466+
`Constructing the actual dependency hierarchy from ${rootNodeModulesDir}.`,
1467+
);
1468+
tree = await arb.loadActual();
1469+
} else {
1470+
if (DEBUG_MODE) {
1471+
console.log(
1472+
"Constructing virtual dependency tree based on the lock file. Pass --deep argument to construct the actual dependency tree from disk.",
1473+
);
1474+
}
1475+
tree = await arb.loadVirtual();
1476+
}
1477+
} else {
1478+
tree = await arb.loadVirtual();
1479+
}
13361480
} catch (e) {
13371481
console.log(
13381482
`Unable to parse ${pkgLockFile} without legacy peer dependencies. Retrying ...`,
@@ -1364,6 +1508,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
13641508
tree,
13651509
null,
13661510
new Set(),
1511+
pkgSpecVersionCache,
13671512
options,
13681513
));
13691514

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cyclonedx/cdxgen",
3-
"version": "11.0.8",
3+
"version": "11.0.9",
44
"description": "Creates CycloneDX Software Bill of Materials (SBOM) from source or container image",
55
"homepage": "http://github.com/cyclonedx/cdxgen",
66
"author": "Prabhu Subramanian <[email protected]>",

0 commit comments

Comments
 (0)