@cyclonedx/cdxgen 10.0.3 → 10.0.5
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 +3 -2
- package/analyzer.js +62 -24
- package/bin/cdxgen.js +1 -1
- package/docker.js +36 -1
- package/docker.test.js +85 -2
- package/index.js +35 -7
- package/package.json +1 -1
- package/postgen.js +23 -2
- package/postgen.test.js +3 -3
- package/utils.js +104 -5
- package/utils.test.js +6 -0
package/README.md
CHANGED
|
@@ -175,8 +175,9 @@ Options:
|
|
|
175
175
|
es. [boolean] [default: false]
|
|
176
176
|
--spec-version CycloneDX Specification version to use. Defaults
|
|
177
177
|
to 1.5 [default: 1.5]
|
|
178
|
-
--filter Filter components containing this word in purl
|
|
179
|
-
Multiple values
|
|
178
|
+
--filter Filter components containing this word in purl or
|
|
179
|
+
component.properties.value. Multiple values allo
|
|
180
|
+
wed. [array]
|
|
180
181
|
--only Include components only containing this word in
|
|
181
182
|
purl. Useful to generate BOM with first party co
|
|
182
183
|
mponents alone. Multiple values allowed. [array]
|
package/analyzer.js
CHANGED
|
@@ -8,7 +8,6 @@ import { basename, resolve, isAbsolute, relative } from "node:path";
|
|
|
8
8
|
const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS
|
|
9
9
|
? process.env.ASTGEN_IGNORE_DIRS.split(",")
|
|
10
10
|
: [
|
|
11
|
-
"node_modules",
|
|
12
11
|
"venv",
|
|
13
12
|
"docs",
|
|
14
13
|
"test",
|
|
@@ -37,7 +36,7 @@ const IGNORE_FILE_PATTERN = new RegExp(
|
|
|
37
36
|
"i"
|
|
38
37
|
);
|
|
39
38
|
|
|
40
|
-
const getAllFiles = (dir, extn, files, result, regex) => {
|
|
39
|
+
const getAllFiles = (deep, dir, extn, files, result, regex) => {
|
|
41
40
|
files = files || readdirSync(dir);
|
|
42
41
|
result = result || [];
|
|
43
42
|
regex = regex || new RegExp(`\\${extn}$`);
|
|
@@ -60,8 +59,20 @@ const getAllFiles = (dir, extn, files, result, regex) => {
|
|
|
60
59
|
) {
|
|
61
60
|
continue;
|
|
62
61
|
}
|
|
62
|
+
// We need to include node_modules in deep mode to track exports
|
|
63
|
+
// Ignore only for non-deep analysis
|
|
64
|
+
if (!deep && dirName === "node_modules") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
63
67
|
try {
|
|
64
|
-
result = getAllFiles(
|
|
68
|
+
result = getAllFiles(
|
|
69
|
+
deep,
|
|
70
|
+
file,
|
|
71
|
+
extn,
|
|
72
|
+
readdirSync(file),
|
|
73
|
+
result,
|
|
74
|
+
regex
|
|
75
|
+
);
|
|
65
76
|
} catch (error) {
|
|
66
77
|
continue;
|
|
67
78
|
}
|
|
@@ -101,7 +112,14 @@ const babelParserOptions = {
|
|
|
101
112
|
* Filter only references to (t|jsx?) or (less|scss) files for now.
|
|
102
113
|
* Opt to use our relative paths.
|
|
103
114
|
*/
|
|
104
|
-
const setFileRef = (
|
|
115
|
+
const setFileRef = (
|
|
116
|
+
allImports,
|
|
117
|
+
allExports,
|
|
118
|
+
src,
|
|
119
|
+
file,
|
|
120
|
+
pathnode,
|
|
121
|
+
specifiers = []
|
|
122
|
+
) => {
|
|
105
123
|
const pathway = pathnode.value || pathnode.name;
|
|
106
124
|
const sourceLoc = pathnode.loc?.start;
|
|
107
125
|
if (!pathway) {
|
|
@@ -115,9 +133,13 @@ const setFileRef = (allImports, src, file, pathnode, specifiers = []) => {
|
|
|
115
133
|
const importedModules = specifiers
|
|
116
134
|
.map((s) => s.imported?.name)
|
|
117
135
|
.filter((v) => v !== undefined);
|
|
136
|
+
const exportedModules = specifiers
|
|
137
|
+
.map((s) => s.exported?.name)
|
|
138
|
+
.filter((v) => v !== undefined);
|
|
118
139
|
const occurrence = {
|
|
119
140
|
importedAs: pathway,
|
|
120
141
|
importedModules,
|
|
142
|
+
exportedModules,
|
|
121
143
|
isExternal: true,
|
|
122
144
|
fileName: fileRelativeLoc,
|
|
123
145
|
lineNumber: sourceLoc && sourceLoc.line ? sourceLoc.line : undefined,
|
|
@@ -132,10 +154,13 @@ const setFileRef = (allImports, src, file, pathnode, specifiers = []) => {
|
|
|
132
154
|
moduleFullPath = relative(src, moduleFullPath);
|
|
133
155
|
wasAbsolute = true;
|
|
134
156
|
}
|
|
135
|
-
|
|
157
|
+
if (!moduleFullPath.startsWith("node_modules/")) {
|
|
158
|
+
occurrence.isExternal = false;
|
|
159
|
+
}
|
|
136
160
|
}
|
|
137
161
|
allImports[moduleFullPath] = allImports[moduleFullPath] || new Set();
|
|
138
162
|
allImports[moduleFullPath].add(occurrence);
|
|
163
|
+
|
|
139
164
|
// Handle module package name
|
|
140
165
|
// Eg: zone.js/dist/zone will be referred to as zone.js in package.json
|
|
141
166
|
if (!wasAbsolute && moduleFullPath.includes("/")) {
|
|
@@ -143,6 +168,16 @@ const setFileRef = (allImports, src, file, pathnode, specifiers = []) => {
|
|
|
143
168
|
allImports[modPkg] = allImports[modPkg] || new Set();
|
|
144
169
|
allImports[modPkg].add(occurrence);
|
|
145
170
|
}
|
|
171
|
+
if (exportedModules && exportedModules.length) {
|
|
172
|
+
moduleFullPath = moduleFullPath
|
|
173
|
+
.replace("node_modules/", "")
|
|
174
|
+
.replace("dist/", "")
|
|
175
|
+
.replace(/\.(js|ts|cjs|mjs)$/g, "")
|
|
176
|
+
.replace("src/", "");
|
|
177
|
+
allExports[moduleFullPath] = allExports[moduleFullPath] || new Set();
|
|
178
|
+
occurrence.exportedModules = exportedModules;
|
|
179
|
+
allExports[moduleFullPath].add(occurrence);
|
|
180
|
+
}
|
|
146
181
|
};
|
|
147
182
|
|
|
148
183
|
const vueCleaningRegex = /<\/*script.*>|<style[\s\S]*style>|<\/*br>/gi;
|
|
@@ -178,13 +213,14 @@ const fileToParseableCode = (file) => {
|
|
|
178
213
|
* Check AST tree for any (j|tsx?) files and set a file
|
|
179
214
|
* references for any import, require or dynamic import files.
|
|
180
215
|
*/
|
|
181
|
-
const parseFileASTTree = (src, file, allImports) => {
|
|
216
|
+
const parseFileASTTree = (src, file, allImports, allExports) => {
|
|
182
217
|
const ast = parse(fileToParseableCode(file), babelParserOptions);
|
|
183
218
|
traverse.default(ast, {
|
|
184
219
|
ImportDeclaration: (path) => {
|
|
185
220
|
if (path && path.node) {
|
|
186
221
|
setFileRef(
|
|
187
222
|
allImports,
|
|
223
|
+
allExports,
|
|
188
224
|
src,
|
|
189
225
|
file,
|
|
190
226
|
path.node.source,
|
|
@@ -200,24 +236,25 @@ const parseFileASTTree = (src, file, allImports) => {
|
|
|
200
236
|
path.node.name === "require" &&
|
|
201
237
|
path.parent.type === "CallExpression"
|
|
202
238
|
) {
|
|
203
|
-
setFileRef(allImports, src, file, path.parent.arguments[0]);
|
|
239
|
+
setFileRef(allImports, allExports, src, file, path.parent.arguments[0]);
|
|
204
240
|
}
|
|
205
241
|
},
|
|
206
242
|
// Use for dynamic imports like routes.jsx
|
|
207
243
|
CallExpression: (path) => {
|
|
208
244
|
if (path && path.node && path.node.callee.type === "Import") {
|
|
209
|
-
setFileRef(allImports, src, file, path.node.arguments[0]);
|
|
245
|
+
setFileRef(allImports, allExports, src, file, path.node.arguments[0]);
|
|
210
246
|
}
|
|
211
247
|
},
|
|
212
248
|
// Use for export barrells
|
|
213
249
|
ExportAllDeclaration: (path) => {
|
|
214
|
-
setFileRef(allImports, src, file, path.node.source);
|
|
250
|
+
setFileRef(allImports, allExports, src, file, path.node.source);
|
|
215
251
|
},
|
|
216
252
|
ExportNamedDeclaration: (path) => {
|
|
217
253
|
// ensure there is a path export
|
|
218
254
|
if (path && path.node && path.node.source) {
|
|
219
255
|
setFileRef(
|
|
220
256
|
allImports,
|
|
257
|
+
allExports,
|
|
221
258
|
src,
|
|
222
259
|
file,
|
|
223
260
|
path.node.source,
|
|
@@ -231,36 +268,37 @@ const parseFileASTTree = (src, file, allImports) => {
|
|
|
231
268
|
/**
|
|
232
269
|
* Return paths to all (j|tsx?) files.
|
|
233
270
|
*/
|
|
234
|
-
const getAllSrcJSAndTSFiles = (src) =>
|
|
271
|
+
const getAllSrcJSAndTSFiles = (src, deep) =>
|
|
235
272
|
Promise.all([
|
|
236
|
-
getAllFiles(src, ".js"),
|
|
237
|
-
getAllFiles(src, ".jsx"),
|
|
238
|
-
getAllFiles(src, ".cjs"),
|
|
239
|
-
getAllFiles(src, ".mjs"),
|
|
240
|
-
getAllFiles(src, ".ts"),
|
|
241
|
-
getAllFiles(src, ".tsx"),
|
|
242
|
-
getAllFiles(src, ".vue"),
|
|
243
|
-
getAllFiles(src, ".svelte")
|
|
273
|
+
getAllFiles(deep, src, ".js"),
|
|
274
|
+
getAllFiles(deep, src, ".jsx"),
|
|
275
|
+
getAllFiles(deep, src, ".cjs"),
|
|
276
|
+
getAllFiles(deep, src, ".mjs"),
|
|
277
|
+
getAllFiles(deep, src, ".ts"),
|
|
278
|
+
getAllFiles(deep, src, ".tsx"),
|
|
279
|
+
getAllFiles(deep, src, ".vue"),
|
|
280
|
+
getAllFiles(deep, src, ".svelte")
|
|
244
281
|
]);
|
|
245
282
|
|
|
246
283
|
/**
|
|
247
|
-
*
|
|
284
|
+
* Find all imports and exports
|
|
248
285
|
*/
|
|
249
|
-
export const
|
|
286
|
+
export const findJSImportsExports = async (src, deep) => {
|
|
250
287
|
const allImports = {};
|
|
288
|
+
const allExports = {};
|
|
251
289
|
const errFiles = [];
|
|
252
290
|
try {
|
|
253
|
-
const promiseMap = await getAllSrcJSAndTSFiles(src);
|
|
291
|
+
const promiseMap = await getAllSrcJSAndTSFiles(src, deep);
|
|
254
292
|
const srcFiles = promiseMap.flatMap((d) => d);
|
|
255
293
|
for (const file of srcFiles) {
|
|
256
294
|
try {
|
|
257
|
-
parseFileASTTree(src, file, allImports);
|
|
295
|
+
parseFileASTTree(src, file, allImports, allExports);
|
|
258
296
|
} catch (err) {
|
|
259
297
|
errFiles.push(file);
|
|
260
298
|
}
|
|
261
299
|
}
|
|
262
|
-
return allImports;
|
|
300
|
+
return { allImports, allExports };
|
|
263
301
|
} catch (err) {
|
|
264
|
-
return allImports;
|
|
302
|
+
return { allImports, allExports };
|
|
265
303
|
}
|
|
266
304
|
};
|
package/bin/cdxgen.js
CHANGED
|
@@ -193,7 +193,7 @@ const args = yargs(hideBin(process.argv))
|
|
|
193
193
|
})
|
|
194
194
|
.option("filter", {
|
|
195
195
|
description:
|
|
196
|
-
"Filter components containing this word in purl. Multiple values allowed."
|
|
196
|
+
"Filter components containing this word in purl or component.properties.value. Multiple values allowed."
|
|
197
197
|
})
|
|
198
198
|
.option("only", {
|
|
199
199
|
description:
|
package/docker.js
CHANGED
|
@@ -426,7 +426,14 @@ export const parseImageName = (fullImageName) => {
|
|
|
426
426
|
return nameObj;
|
|
427
427
|
}
|
|
428
428
|
// ensure it's lowercased
|
|
429
|
-
fullImageName = fullImageName.toLowerCase();
|
|
429
|
+
fullImageName = fullImageName.toLowerCase().trim();
|
|
430
|
+
|
|
431
|
+
// Extract platform
|
|
432
|
+
if (fullImageName.startsWith("--platform=")) {
|
|
433
|
+
const tmpName = fullImageName.replace("--platform=", "").split(" ");
|
|
434
|
+
nameObj.platform = tmpName[0];
|
|
435
|
+
fullImageName = tmpName[1];
|
|
436
|
+
}
|
|
430
437
|
|
|
431
438
|
// Extract registry name
|
|
432
439
|
if (
|
|
@@ -1183,3 +1190,31 @@ export const getCredsFromHelper = (exeSuffix, serverAddress) => {
|
|
|
1183
1190
|
}
|
|
1184
1191
|
return undefined;
|
|
1185
1192
|
};
|
|
1193
|
+
|
|
1194
|
+
export const addSkippedSrcFiles = (skippedImageSrcs, components) => {
|
|
1195
|
+
for (const skippedImage of skippedImageSrcs) {
|
|
1196
|
+
for (const co of components) {
|
|
1197
|
+
let srcFileValues = [];
|
|
1198
|
+
let srcImageValue;
|
|
1199
|
+
co.properties.forEach(function (property) {
|
|
1200
|
+
if (property.name === "oci:SrcImage") {
|
|
1201
|
+
srcImageValue = property.value;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (property.name === "SrcFile") {
|
|
1205
|
+
srcFileValues.push(property.value);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
if (
|
|
1210
|
+
srcImageValue === skippedImage.image &&
|
|
1211
|
+
!srcFileValues.includes(skippedImage.src)
|
|
1212
|
+
) {
|
|
1213
|
+
co.properties = co.properties.concat({
|
|
1214
|
+
name: "SrcFile",
|
|
1215
|
+
value: skippedImage.src
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
};
|
package/docker.test.js
CHANGED
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
getImage,
|
|
5
5
|
removeImage,
|
|
6
6
|
exportImage,
|
|
7
|
-
isWin
|
|
7
|
+
isWin,
|
|
8
|
+
addSkippedSrcFiles
|
|
8
9
|
} from "./docker.js";
|
|
9
|
-
import { expect, test } from "@jest/globals";
|
|
10
|
+
import { expect, test, describe, beforeEach } from "@jest/globals";
|
|
10
11
|
|
|
11
12
|
test("docker connection", async () => {
|
|
12
13
|
if (!(isWin && process.env.CI === "true")) {
|
|
@@ -84,6 +85,19 @@ test("parseImageName tests", () => {
|
|
|
84
85
|
group: "docker/library",
|
|
85
86
|
name: "eclipse-temurin"
|
|
86
87
|
});
|
|
88
|
+
expect(
|
|
89
|
+
parseImageName(
|
|
90
|
+
"--platform=linux/amd64 foocorp.jfrog.io/docker/library/eclipse-temurin:latest"
|
|
91
|
+
)
|
|
92
|
+
).toEqual({
|
|
93
|
+
registry: "foocorp.jfrog.io",
|
|
94
|
+
repo: "docker/library/eclipse-temurin",
|
|
95
|
+
tag: "latest",
|
|
96
|
+
digest: "",
|
|
97
|
+
platform: "linux/amd64",
|
|
98
|
+
group: "docker/library",
|
|
99
|
+
name: "eclipse-temurin"
|
|
100
|
+
});
|
|
87
101
|
expect(
|
|
88
102
|
parseImageName(
|
|
89
103
|
"quay.io/shiftleft/scan-java@sha256:5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1"
|
|
@@ -117,3 +131,72 @@ test("docker getImage", async () => {
|
|
|
117
131
|
const imageData = await exportImage("hello-world:latest");
|
|
118
132
|
expect(imageData);
|
|
119
133
|
}, 120000);
|
|
134
|
+
|
|
135
|
+
describe("addSkippedSrcFiles tests", () => {
|
|
136
|
+
let testComponents;
|
|
137
|
+
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
testComponents = [
|
|
140
|
+
{
|
|
141
|
+
name: "node",
|
|
142
|
+
version: "20",
|
|
143
|
+
component: "node:20",
|
|
144
|
+
purl: "pkg:oci/node@20?tag=20",
|
|
145
|
+
type: "container",
|
|
146
|
+
"bom-ref": "pkg:oci/node@20?tag=20",
|
|
147
|
+
properties: [
|
|
148
|
+
{
|
|
149
|
+
name: "SrcFile",
|
|
150
|
+
value: "/some/project/Dockerfile"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "oci:SrcImage",
|
|
154
|
+
value: "node:20"
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
];
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("no matching additional src files", () => {
|
|
162
|
+
addSkippedSrcFiles(
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
image: "node:18",
|
|
166
|
+
src: "/some/project/bitbucket-pipeline.yml"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
testComponents
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(testComponents[0].properties).toHaveLength(2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("adds additional src files", () => {
|
|
176
|
+
addSkippedSrcFiles(
|
|
177
|
+
[
|
|
178
|
+
{
|
|
179
|
+
image: "node:20",
|
|
180
|
+
src: "/some/project/bitbucket-pipeline.yml"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
testComponents
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(testComponents[0].properties).toHaveLength(3);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("skips if same src file", () => {
|
|
190
|
+
addSkippedSrcFiles(
|
|
191
|
+
[
|
|
192
|
+
{
|
|
193
|
+
image: "node:20",
|
|
194
|
+
src: "/some/project/Dockerfile"
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
testComponents
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(testComponents[0].properties).toHaveLength(2);
|
|
201
|
+
});
|
|
202
|
+
}, 120000);
|
package/index.js
CHANGED
|
@@ -133,13 +133,14 @@ const selfPJson = JSON.parse(
|
|
|
133
133
|
readFileSync(join(dirName, "package.json"), "utf-8")
|
|
134
134
|
);
|
|
135
135
|
const _version = selfPJson.version;
|
|
136
|
-
import {
|
|
136
|
+
import { findJSImportsExports } from "./analyzer.js";
|
|
137
137
|
import { gte, lte } from "semver";
|
|
138
138
|
import {
|
|
139
139
|
getPkgPathList,
|
|
140
140
|
parseImageName,
|
|
141
141
|
exportArchive,
|
|
142
|
-
exportImage
|
|
142
|
+
exportImage,
|
|
143
|
+
addSkippedSrcFiles
|
|
143
144
|
} from "./docker.js";
|
|
144
145
|
import {
|
|
145
146
|
getGoBuildInfo,
|
|
@@ -1795,6 +1796,7 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1795
1796
|
}
|
|
1796
1797
|
}
|
|
1797
1798
|
let allImports = {};
|
|
1799
|
+
let allExports = {};
|
|
1798
1800
|
if (
|
|
1799
1801
|
!["docker", "oci", "container", "os"].includes(options.projectType) &&
|
|
1800
1802
|
!options.noBabel
|
|
@@ -1804,7 +1806,9 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1804
1806
|
`Performing babel-based package usage analysis with source code at ${path}`
|
|
1805
1807
|
);
|
|
1806
1808
|
}
|
|
1807
|
-
|
|
1809
|
+
const retData = await findJSImportsExports(path, options.deep);
|
|
1810
|
+
allImports = retData.allImports;
|
|
1811
|
+
allExports = retData.allExports;
|
|
1808
1812
|
}
|
|
1809
1813
|
const yarnLockFiles = getAllFiles(
|
|
1810
1814
|
path,
|
|
@@ -1976,7 +1980,12 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1976
1980
|
if (existsSync(swFile)) {
|
|
1977
1981
|
let pkgList = await parseNodeShrinkwrap(swFile);
|
|
1978
1982
|
if (allImports && Object.keys(allImports).length) {
|
|
1979
|
-
pkgList = addEvidenceForImports(
|
|
1983
|
+
pkgList = await addEvidenceForImports(
|
|
1984
|
+
pkgList,
|
|
1985
|
+
allImports,
|
|
1986
|
+
allExports,
|
|
1987
|
+
options.deep
|
|
1988
|
+
);
|
|
1980
1989
|
}
|
|
1981
1990
|
return buildBomNSData(options, pkgList, "npm", {
|
|
1982
1991
|
allImports,
|
|
@@ -1986,10 +1995,16 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1986
1995
|
} else if (existsSync(pnpmLock)) {
|
|
1987
1996
|
let pkgList = await parsePnpmLock(pnpmLock);
|
|
1988
1997
|
if (allImports && Object.keys(allImports).length) {
|
|
1989
|
-
pkgList = addEvidenceForImports(
|
|
1998
|
+
pkgList = await addEvidenceForImports(
|
|
1999
|
+
pkgList,
|
|
2000
|
+
allImports,
|
|
2001
|
+
allExports,
|
|
2002
|
+
options.deep
|
|
2003
|
+
);
|
|
1990
2004
|
}
|
|
1991
2005
|
return buildBomNSData(options, pkgList, "npm", {
|
|
1992
2006
|
allImports,
|
|
2007
|
+
allExports,
|
|
1993
2008
|
src: path,
|
|
1994
2009
|
filename: "pnpm-lock.yaml"
|
|
1995
2010
|
});
|
|
@@ -2140,7 +2155,12 @@ export const createNodejsBom = async (path, options) => {
|
|
|
2140
2155
|
// We need to set this to force our version to be used rather than the directory name based one.
|
|
2141
2156
|
options.parentComponent = parentComponent;
|
|
2142
2157
|
if (allImports && Object.keys(allImports).length) {
|
|
2143
|
-
pkgList = addEvidenceForImports(
|
|
2158
|
+
pkgList = await addEvidenceForImports(
|
|
2159
|
+
pkgList,
|
|
2160
|
+
allImports,
|
|
2161
|
+
allExports,
|
|
2162
|
+
options.deep
|
|
2163
|
+
);
|
|
2144
2164
|
}
|
|
2145
2165
|
return buildBomNSData(options, pkgList, "npm", {
|
|
2146
2166
|
src: path,
|
|
@@ -3640,6 +3660,7 @@ export const createContainerSpecLikeBom = async (path, options) => {
|
|
|
3640
3660
|
let parentComponent = {};
|
|
3641
3661
|
let dependencies = [];
|
|
3642
3662
|
const doneimages = [];
|
|
3663
|
+
const skippedImageSrcs = [];
|
|
3643
3664
|
const doneservices = [];
|
|
3644
3665
|
const origProjectType = options.projectType;
|
|
3645
3666
|
let dcFiles = getAllFiles(
|
|
@@ -3772,6 +3793,9 @@ export const createContainerSpecLikeBom = async (path, options) => {
|
|
|
3772
3793
|
if (DEBUG_MODE) {
|
|
3773
3794
|
console.log("Skipping", img.image);
|
|
3774
3795
|
}
|
|
3796
|
+
|
|
3797
|
+
skippedImageSrcs.push({ image: img.image, src: f });
|
|
3798
|
+
|
|
3775
3799
|
continue;
|
|
3776
3800
|
}
|
|
3777
3801
|
if (DEBUG_MODE) {
|
|
@@ -3790,7 +3814,8 @@ export const createContainerSpecLikeBom = async (path, options) => {
|
|
|
3790
3814
|
type: "container"
|
|
3791
3815
|
};
|
|
3792
3816
|
if (imageObj.registry) {
|
|
3793
|
-
pkg["qualifiers"]["repository_url"] =
|
|
3817
|
+
pkg["qualifiers"]["repository_url"] =
|
|
3818
|
+
`${imageObj.registry}/${imageObj.repo}`;
|
|
3794
3819
|
}
|
|
3795
3820
|
if (imageObj.platform) {
|
|
3796
3821
|
pkg["qualifiers"]["platform"] = imageObj.platform;
|
|
@@ -3826,6 +3851,9 @@ export const createContainerSpecLikeBom = async (path, options) => {
|
|
|
3826
3851
|
} // for img
|
|
3827
3852
|
}
|
|
3828
3853
|
} // for
|
|
3854
|
+
|
|
3855
|
+
// Add additional SrcFile property to skipped image components
|
|
3856
|
+
addSkippedSrcFiles(skippedImageSrcs, components);
|
|
3829
3857
|
} // if
|
|
3830
3858
|
// Parse openapi files
|
|
3831
3859
|
if (oapiFiles.length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyclonedx/cdxgen",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.5",
|
|
4
4
|
"description": "Creates CycloneDX Software Bill of Materials (SBOM) from source or container image",
|
|
5
5
|
"homepage": "http://github.com/cyclonedx/cdxgen",
|
|
6
6
|
"author": "Prabhu Subramanian <prabhu@appthreat.com>",
|
package/postgen.js
CHANGED
|
@@ -27,7 +27,10 @@ export const filterBom = (bomJson, options) => {
|
|
|
27
27
|
}
|
|
28
28
|
let purlfiltered = false;
|
|
29
29
|
for (const filterstr of options.only) {
|
|
30
|
-
if (
|
|
30
|
+
if (
|
|
31
|
+
filterstr.length &&
|
|
32
|
+
!comp.purl.toLowerCase().includes(filterstr.toLowerCase())
|
|
33
|
+
) {
|
|
31
34
|
filtered = true;
|
|
32
35
|
purlfiltered = true;
|
|
33
36
|
continue;
|
|
@@ -42,11 +45,29 @@ export const filterBom = (bomJson, options) => {
|
|
|
42
45
|
}
|
|
43
46
|
let purlfiltered = false;
|
|
44
47
|
for (const filterstr of options.filter) {
|
|
45
|
-
|
|
48
|
+
// Check the purl
|
|
49
|
+
if (
|
|
50
|
+
filterstr.length &&
|
|
51
|
+
comp.purl.toLowerCase().includes(filterstr.toLowerCase())
|
|
52
|
+
) {
|
|
46
53
|
filtered = true;
|
|
47
54
|
purlfiltered = true;
|
|
48
55
|
continue;
|
|
49
56
|
}
|
|
57
|
+
// Look for any properties value matching the string
|
|
58
|
+
const properties = comp.properties || [];
|
|
59
|
+
for (const aprop of properties) {
|
|
60
|
+
if (
|
|
61
|
+
filterstr.length &&
|
|
62
|
+
aprop &&
|
|
63
|
+
aprop.value &&
|
|
64
|
+
aprop.value.toLowerCase().includes(filterstr.toLowerCase())
|
|
65
|
+
) {
|
|
66
|
+
filtered = true;
|
|
67
|
+
purlfiltered = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
50
71
|
}
|
|
51
72
|
if (!purlfiltered) {
|
|
52
73
|
newPkgMap[comp["bom-ref"]] = comp;
|
package/postgen.test.js
CHANGED
|
@@ -41,14 +41,14 @@ test("filter bom tests2", () => {
|
|
|
41
41
|
throw new Error(`${comp.purl} is unexpected`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
expect(newBom.components.length).toEqual(
|
|
44
|
+
expect(newBom.components.length).toEqual(158);
|
|
45
45
|
newBom = filterBom(bomJson, { filter: ["apache", "json"] });
|
|
46
46
|
for (const comp of newBom.components) {
|
|
47
47
|
if (comp.purl.includes("apache") || comp.purl.includes("json")) {
|
|
48
48
|
throw new Error(`${comp.purl} is unexpected`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
expect(newBom.components.length).toEqual(
|
|
51
|
+
expect(newBom.components.length).toEqual(135);
|
|
52
52
|
expect(newBom.compositions).toBeUndefined();
|
|
53
53
|
newBom = filterBom(bomJson, {
|
|
54
54
|
only: ["org.springframework"],
|
|
@@ -60,7 +60,7 @@ test("filter bom tests2", () => {
|
|
|
60
60
|
throw new Error(`${comp.purl} is unexpected`);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
expect(newBom.components.length).toEqual(
|
|
63
|
+
expect(newBom.components.length).toEqual(29);
|
|
64
64
|
expect(newBom.compositions).toEqual([
|
|
65
65
|
{
|
|
66
66
|
aggregate: "incomplete_first_party_only",
|
package/utils.js
CHANGED
|
@@ -559,13 +559,29 @@ export const parsePkgJson = async (pkgJsonFile, simple = false) => {
|
|
|
559
559
|
null,
|
|
560
560
|
null
|
|
561
561
|
).toString();
|
|
562
|
+
const author = pkgData.author;
|
|
563
|
+
const authorString =
|
|
564
|
+
author instanceof Object
|
|
565
|
+
? `${author.name}${author.email ? ` <${author.email}>` : ""}${
|
|
566
|
+
author.url ? ` (${author.url})` : ""
|
|
567
|
+
}`
|
|
568
|
+
: author;
|
|
562
569
|
const apkg = {
|
|
563
570
|
name,
|
|
564
571
|
group,
|
|
565
572
|
version: pkgData.version,
|
|
573
|
+
description: pkgData.description,
|
|
566
574
|
purl: purl,
|
|
567
|
-
"bom-ref": decodeURIComponent(purl)
|
|
575
|
+
"bom-ref": decodeURIComponent(purl),
|
|
576
|
+
author: authorString,
|
|
577
|
+
license: pkgData.license
|
|
568
578
|
};
|
|
579
|
+
if (pkgData.homepage) {
|
|
580
|
+
apkg.homepage = { url: pkgData.homepage };
|
|
581
|
+
}
|
|
582
|
+
if (pkgData.repository && pkgData.repository.url) {
|
|
583
|
+
apkg.repository = { url: pkgData.repository.url };
|
|
584
|
+
}
|
|
569
585
|
if (!simple) {
|
|
570
586
|
apkg.properties = [
|
|
571
587
|
{
|
|
@@ -720,6 +736,12 @@ export const parsePkgLock = async (pkgLockFile, options = {}) => {
|
|
|
720
736
|
value: node.resolved
|
|
721
737
|
});
|
|
722
738
|
}
|
|
739
|
+
if (node.location) {
|
|
740
|
+
pkg.properties.push({
|
|
741
|
+
name: "LocalNodeModulesPath",
|
|
742
|
+
value: node.location
|
|
743
|
+
});
|
|
744
|
+
}
|
|
723
745
|
}
|
|
724
746
|
const packageLicense = node.package.license;
|
|
725
747
|
if (packageLicense) {
|
|
@@ -8135,9 +8157,16 @@ export const parsePackageJsonName = (name) => {
|
|
|
8135
8157
|
*
|
|
8136
8158
|
* @param {array} pkgList List of package
|
|
8137
8159
|
* @param {object} allImports Import statements object with package name as key and an object with file and location details
|
|
8160
|
+
* @param {object} allExports Exported modules if available from node_modules
|
|
8138
8161
|
*/
|
|
8139
|
-
export const addEvidenceForImports = (
|
|
8162
|
+
export const addEvidenceForImports = async (
|
|
8163
|
+
pkgList,
|
|
8164
|
+
allImports,
|
|
8165
|
+
allExports,
|
|
8166
|
+
deep
|
|
8167
|
+
) => {
|
|
8140
8168
|
const impPkgs = Object.keys(allImports);
|
|
8169
|
+
const exportedPkgs = Object.keys(allExports);
|
|
8141
8170
|
for (const pkg of pkgList) {
|
|
8142
8171
|
if (impPkgs && impPkgs.length) {
|
|
8143
8172
|
// Assume that all packages are optional until we see an evidence
|
|
@@ -8158,6 +8187,47 @@ export const addEvidenceForImports = (pkgList, allImports) => {
|
|
|
8158
8187
|
find_pkg.startsWith(alias) &&
|
|
8159
8188
|
(find_pkg.length === alias.length || find_pkg[alias.length] === "/")
|
|
8160
8189
|
);
|
|
8190
|
+
const all_exports = exportedPkgs.filter((find_pkg) =>
|
|
8191
|
+
find_pkg.startsWith(alias)
|
|
8192
|
+
);
|
|
8193
|
+
if (all_exports && all_exports.length) {
|
|
8194
|
+
let exportedModules = new Set(all_exports);
|
|
8195
|
+
pkg.properties = pkg.properties || [];
|
|
8196
|
+
for (const subevidence of all_exports) {
|
|
8197
|
+
const evidences = allExports[subevidence];
|
|
8198
|
+
for (const evidence of evidences) {
|
|
8199
|
+
if (evidence && Object.keys(evidence).length) {
|
|
8200
|
+
if (evidence.exportedModules.length > 1) {
|
|
8201
|
+
for (const aexpsubm of evidence.exportedModules) {
|
|
8202
|
+
// Be selective on the submodule names
|
|
8203
|
+
if (
|
|
8204
|
+
!evidence.importedAs
|
|
8205
|
+
.toLowerCase()
|
|
8206
|
+
.includes(aexpsubm.toLowerCase()) &&
|
|
8207
|
+
!alias.endsWith(aexpsubm)
|
|
8208
|
+
) {
|
|
8209
|
+
// Store both the short and long form of the exported sub modules
|
|
8210
|
+
if (aexpsubm.length > 3) {
|
|
8211
|
+
exportedModules.add(aexpsubm);
|
|
8212
|
+
}
|
|
8213
|
+
exportedModules.add(
|
|
8214
|
+
`${evidence.importedAs.replace("./", "")}/${aexpsubm}`
|
|
8215
|
+
);
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
8221
|
+
}
|
|
8222
|
+
exportedModules = Array.from(exportedModules);
|
|
8223
|
+
if (exportedModules.length) {
|
|
8224
|
+
pkg.properties.push({
|
|
8225
|
+
name: "ExportedModules",
|
|
8226
|
+
value: exportedModules.join(",")
|
|
8227
|
+
});
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
// Identify all the imported modules of a component
|
|
8161
8231
|
if (impPkgs.includes(alias) || all_includes.length) {
|
|
8162
8232
|
let importedModules = new Set();
|
|
8163
8233
|
pkg.scope = "required";
|
|
@@ -8178,7 +8248,9 @@ export const addEvidenceForImports = (pkgList, allImports) => {
|
|
|
8178
8248
|
continue;
|
|
8179
8249
|
}
|
|
8180
8250
|
// Store both the short and long form of the imported sub modules
|
|
8181
|
-
|
|
8251
|
+
if (importedSm.length > 3) {
|
|
8252
|
+
importedModules.add(importedSm);
|
|
8253
|
+
}
|
|
8182
8254
|
importedModules.add(`${evidence.importedAs}/${importedSm}`);
|
|
8183
8255
|
}
|
|
8184
8256
|
}
|
|
@@ -8194,8 +8266,35 @@ export const addEvidenceForImports = (pkgList, allImports) => {
|
|
|
8194
8266
|
}
|
|
8195
8267
|
break;
|
|
8196
8268
|
}
|
|
8197
|
-
|
|
8198
|
-
|
|
8269
|
+
// Capture metadata such as description from local node_modules in deep mode
|
|
8270
|
+
if (deep && !pkg.description && pkg.properties) {
|
|
8271
|
+
let localNodeModulesPath = undefined;
|
|
8272
|
+
for (const aprop of pkg.properties) {
|
|
8273
|
+
if (aprop.name === "LocalNodeModulesPath") {
|
|
8274
|
+
localNodeModulesPath = resolve(join(aprop.value, "package.json"));
|
|
8275
|
+
break;
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
if (localNodeModulesPath && existsSync(localNodeModulesPath)) {
|
|
8279
|
+
const lnmPkgList = await parsePkgJson(localNodeModulesPath, true);
|
|
8280
|
+
if (lnmPkgList && lnmPkgList.length === 1) {
|
|
8281
|
+
const lnmMetadata = lnmPkgList[0];
|
|
8282
|
+
if (lnmMetadata && Object.keys(lnmMetadata).length) {
|
|
8283
|
+
pkg.description = lnmMetadata.description;
|
|
8284
|
+
pkg.author = lnmMetadata.author;
|
|
8285
|
+
pkg.license = lnmMetadata.license;
|
|
8286
|
+
pkg.homepage = lnmMetadata.homepage;
|
|
8287
|
+
pkg.repository = lnmMetadata.repository;
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
}
|
|
8291
|
+
}
|
|
8292
|
+
} // for alias
|
|
8293
|
+
// Trim the properties
|
|
8294
|
+
pkg.properties = pkg.properties.filter(
|
|
8295
|
+
(p) => p.name !== "LocalNodeModulesPath"
|
|
8296
|
+
);
|
|
8297
|
+
} // for pkg
|
|
8199
8298
|
return pkgList;
|
|
8200
8299
|
};
|
|
8201
8300
|
|
package/utils.test.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
parsePom,
|
|
39
39
|
getMvnMetadata,
|
|
40
40
|
getLicenses,
|
|
41
|
+
parsePkgJson,
|
|
41
42
|
parsePkgLock,
|
|
42
43
|
parseBowerJson,
|
|
43
44
|
parseNodeShrinkwrap,
|
|
@@ -1719,6 +1720,11 @@ test("get licenses", () => {
|
|
|
1719
1720
|
]);
|
|
1720
1721
|
});
|
|
1721
1722
|
|
|
1723
|
+
test("parsePkgJson", async () => {
|
|
1724
|
+
const pkgList = await parsePkgJson("./package.json", true);
|
|
1725
|
+
expect(pkgList.length).toEqual(1);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1722
1728
|
test("parsePkgLock v1", async () => {
|
|
1723
1729
|
const parsedList = await parsePkgLock(
|
|
1724
1730
|
"./test/data/package-json/v1/package-lock.json"
|