@cyclonedx/cdxgen 9.9.3 → 9.9.4
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 +4 -2
- package/bin/cdxgen.js +2 -3
- package/docker.js +2 -2
- package/evinser.js +25 -8
- package/index.js +45 -8
- package/package.json +7 -7
- package/utils.js +38 -1
- package/utils.test.js +27 -1
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ Most SBOM tools are like barcode scanners. They can scan a few package manifest
|
|
|
49
49
|
| Gradle Cache | $HOME/caches/modules-2/files-2.1/\*\*/\*.jar | N/A | |
|
|
50
50
|
| Helm Index | $HOME/.cache/helm/repository/\*\*/\*.yaml | N/A | |
|
|
51
51
|
| Docker compose | docker-compose\*.yml. Images would also be scanned. | N/A | |
|
|
52
|
+
| Dockerfile | `*Dockerfile*` Images would also be scanned. | N/A | |
|
|
53
|
+
| Containerfile | `*Containerfile*`. Images would also be scanned. | N/A | |
|
|
52
54
|
| Google CloudBuild configuration | cloudbuild.yaml | N/A | |
|
|
53
55
|
| OpenAPI | openapi\*.json, openapi\*.yaml | N/A | |
|
|
54
56
|
|
|
@@ -173,9 +175,9 @@ Options:
|
|
|
173
175
|
es. [boolean] [default: false]
|
|
174
176
|
--spec-version CycloneDX Specification version to use. Defaults
|
|
175
177
|
to 1.5 [default: 1.5]
|
|
176
|
-
--filter Filter components
|
|
178
|
+
--filter Filter components containing this word in purl.
|
|
177
179
|
Multiple values allowed. [array]
|
|
178
|
-
--only Include components only
|
|
180
|
+
--only Include components only containing this word in
|
|
179
181
|
purl. Useful to generate BOM with first party co
|
|
180
182
|
mponents alone. Multiple values allowed. [array]
|
|
181
183
|
--author The person(s) who created the BOM. Set this value
|
package/bin/cdxgen.js
CHANGED
|
@@ -193,11 +193,11 @@ const args = yargs(hideBin(process.argv))
|
|
|
193
193
|
})
|
|
194
194
|
.option("filter", {
|
|
195
195
|
description:
|
|
196
|
-
"Filter components
|
|
196
|
+
"Filter components containing this word in purl. Multiple values allowed."
|
|
197
197
|
})
|
|
198
198
|
.option("only", {
|
|
199
199
|
description:
|
|
200
|
-
"Include components only
|
|
200
|
+
"Include components only containing this word in purl. Useful to generate BOM with first party components alone. Multiple values allowed."
|
|
201
201
|
})
|
|
202
202
|
.option("author", {
|
|
203
203
|
description:
|
|
@@ -520,7 +520,6 @@ const checkPermissions = (filePath) => {
|
|
|
520
520
|
if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) {
|
|
521
521
|
const nsFile = jsonFile + ".map";
|
|
522
522
|
fs.writeFileSync(nsFile, JSON.stringify(bomNSData.nsMapping));
|
|
523
|
-
console.log("Namespace mapping file written to", nsFile);
|
|
524
523
|
}
|
|
525
524
|
}
|
|
526
525
|
} else if (!options.print) {
|
package/docker.js
CHANGED
|
@@ -126,8 +126,8 @@ const getDefaultOptions = () => {
|
|
|
126
126
|
opts.prefixUrl = isWin
|
|
127
127
|
? WIN_LOCAL_TLS
|
|
128
128
|
: isDockerRootless
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
? `http://unix:${homedir()}/.docker/run/docker.sock:`
|
|
130
|
+
: "http://unix:/var/run/docker.sock:";
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
} else {
|
package/evinser.js
CHANGED
|
@@ -498,14 +498,16 @@ export const parseSliceUsages = async (
|
|
|
498
498
|
!isFilterableType(language, userDefinedTypesMap, atype[1])
|
|
499
499
|
) {
|
|
500
500
|
if (!atype[1].includes("(") && !atype[1].includes(".py")) {
|
|
501
|
-
typesToLookup.add(atype[1]);
|
|
501
|
+
typesToLookup.add(simplifyType(atype[1]));
|
|
502
502
|
// Javascript calls can be resolved to a precise line number only from the call nodes
|
|
503
503
|
if (
|
|
504
504
|
["javascript", "js", "ts", "typescript"].includes(language) &&
|
|
505
505
|
ausageLine
|
|
506
506
|
) {
|
|
507
507
|
if (atype[1].includes(":")) {
|
|
508
|
-
typesToLookup.add(
|
|
508
|
+
typesToLookup.add(
|
|
509
|
+
simplifyType(atype[1].split("::")[0].replace(/:/g, "/"))
|
|
510
|
+
);
|
|
509
511
|
}
|
|
510
512
|
addToOverrides(lKeyOverrides, atype[1], fileName, ausageLine);
|
|
511
513
|
}
|
|
@@ -532,7 +534,7 @@ export const parseSliceUsages = async (
|
|
|
532
534
|
!acall?.resolvedMethod.includes("(") &&
|
|
533
535
|
!acall?.resolvedMethod.includes(".py")
|
|
534
536
|
) {
|
|
535
|
-
typesToLookup.add(acall?.resolvedMethod);
|
|
537
|
+
typesToLookup.add(simplifyType(acall?.resolvedMethod));
|
|
536
538
|
// Javascript calls can be resolved to a precise line number only from the call nodes
|
|
537
539
|
if (acall.lineNumber) {
|
|
538
540
|
addToOverrides(
|
|
@@ -560,10 +562,12 @@ export const parseSliceUsages = async (
|
|
|
560
562
|
for (const aparamType of acall?.paramTypes || []) {
|
|
561
563
|
if (!isFilterableType(language, userDefinedTypesMap, aparamType)) {
|
|
562
564
|
if (!aparamType.includes("(") && !aparamType.includes(".py")) {
|
|
563
|
-
typesToLookup.add(aparamType);
|
|
565
|
+
typesToLookup.add(simplifyType(aparamType));
|
|
564
566
|
if (acall.lineNumber) {
|
|
565
567
|
if (aparamType.includes(":")) {
|
|
566
|
-
typesToLookup.add(
|
|
568
|
+
typesToLookup.add(
|
|
569
|
+
simplifyType(aparamType.split("::")[0].replace(/:/g, "/"))
|
|
570
|
+
);
|
|
567
571
|
}
|
|
568
572
|
addToOverrides(
|
|
569
573
|
lKeyOverrides,
|
|
@@ -609,7 +613,7 @@ export const parseSliceUsages = async (
|
|
|
609
613
|
} else {
|
|
610
614
|
// Check the namespaces db
|
|
611
615
|
let nsHits = typePurlsCache[atype];
|
|
612
|
-
if (["java", "jar"].includes(language)) {
|
|
616
|
+
if (!nsHits && ["java", "jar"].includes(language)) {
|
|
613
617
|
nsHits = await dbObjMap.Namespaces.findAll({
|
|
614
618
|
attributes: ["purl"],
|
|
615
619
|
where: {
|
|
@@ -629,6 +633,9 @@ export const parseSliceUsages = async (
|
|
|
629
633
|
}
|
|
630
634
|
}
|
|
631
635
|
typePurlsCache[atype] = nsHits;
|
|
636
|
+
} else {
|
|
637
|
+
// Avoid persistent lookups
|
|
638
|
+
typePurlsCache[atype] = [];
|
|
632
639
|
}
|
|
633
640
|
}
|
|
634
641
|
}
|
|
@@ -834,7 +841,7 @@ export const extractEndpoints = (language, code) => {
|
|
|
834
841
|
case "jar":
|
|
835
842
|
if (
|
|
836
843
|
code.startsWith("@") &&
|
|
837
|
-
code.includes("Mapping") &&
|
|
844
|
+
(code.includes("Mapping") || code.includes("Path")) &&
|
|
838
845
|
code.includes("(")
|
|
839
846
|
) {
|
|
840
847
|
const matches = code.match(/['"](.*?)['"]/gi) || [];
|
|
@@ -1190,6 +1197,16 @@ export const framePicker = (dfFrames) => {
|
|
|
1190
1197
|
return aframe;
|
|
1191
1198
|
};
|
|
1192
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Method to simplify types. For example, arrays ending with [] could be simplified.
|
|
1202
|
+
*
|
|
1203
|
+
* @param {string} typeFullName Full name of the type to simplify
|
|
1204
|
+
* @returns Simplified type string
|
|
1205
|
+
*/
|
|
1206
|
+
export const simplifyType = (typeFullName) => {
|
|
1207
|
+
return typeFullName.replace("[]", "");
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1193
1210
|
export const getClassTypeFromSignature = (language, typeFullName) => {
|
|
1194
1211
|
if (["java", "jar"].includes(language) && typeFullName.includes(":")) {
|
|
1195
1212
|
typeFullName = typeFullName.split(":")[0];
|
|
@@ -1225,7 +1242,7 @@ export const getClassTypeFromSignature = (language, typeFullName) => {
|
|
|
1225
1242
|
if (typeFullName.includes("$")) {
|
|
1226
1243
|
typeFullName = typeFullName.split("$")[0];
|
|
1227
1244
|
}
|
|
1228
|
-
return typeFullName;
|
|
1245
|
+
return simplifyType(typeFullName);
|
|
1229
1246
|
};
|
|
1230
1247
|
|
|
1231
1248
|
const addToOverrides = (lKeyOverrides, atype, fileName, ausageLineNumber) => {
|
package/index.js
CHANGED
|
@@ -104,7 +104,8 @@ import {
|
|
|
104
104
|
TIMEOUT_MS,
|
|
105
105
|
MAX_BUFFER,
|
|
106
106
|
getNugetMetadata,
|
|
107
|
-
frameworksList
|
|
107
|
+
frameworksList,
|
|
108
|
+
parseContainerFile
|
|
108
109
|
} from "./utils.js";
|
|
109
110
|
import { spawnSync } from "node:child_process";
|
|
110
111
|
import { fileURLToPath } from "node:url";
|
|
@@ -3713,6 +3714,16 @@ export const createContainerSpecLikeBom = async (path, options) => {
|
|
|
3713
3714
|
(options.multiProject ? "**/" : "") + "*.yml",
|
|
3714
3715
|
options
|
|
3715
3716
|
);
|
|
3717
|
+
const dfFiles = getAllFiles(
|
|
3718
|
+
path,
|
|
3719
|
+
(options.multiProject ? "**/" : "") + "*Dockerfile*",
|
|
3720
|
+
options
|
|
3721
|
+
);
|
|
3722
|
+
const cfFiles = getAllFiles(
|
|
3723
|
+
path,
|
|
3724
|
+
(options.multiProject ? "**/" : "") + "*Containerfile*",
|
|
3725
|
+
options
|
|
3726
|
+
);
|
|
3716
3727
|
const yamlFiles = getAllFiles(
|
|
3717
3728
|
path,
|
|
3718
3729
|
(options.multiProject ? "**/" : "") + "*.yaml",
|
|
@@ -3736,14 +3747,22 @@ export const createContainerSpecLikeBom = async (path, options) => {
|
|
|
3736
3747
|
}
|
|
3737
3748
|
// Privado.ai json files
|
|
3738
3749
|
const privadoFiles = getAllFiles(path, ".privado/" + "*.json", options);
|
|
3739
|
-
//
|
|
3740
|
-
if (dcFiles.length) {
|
|
3741
|
-
for (const f of dcFiles) {
|
|
3750
|
+
// Parse yaml manifest files, dockerfiles or containerfiles
|
|
3751
|
+
if (dcFiles.length || dfFiles.length || cfFiles.length) {
|
|
3752
|
+
for (const f of [...dcFiles, ...dfFiles, ...cfFiles]) {
|
|
3742
3753
|
if (DEBUG_MODE) {
|
|
3743
3754
|
console.log(`Parsing ${f}`);
|
|
3744
3755
|
}
|
|
3745
|
-
|
|
3746
|
-
const
|
|
3756
|
+
|
|
3757
|
+
const dData = readFileSync(f, { encoding: "utf-8" });
|
|
3758
|
+
let imglist = [];
|
|
3759
|
+
// parse yaml manifest files
|
|
3760
|
+
if (f.endsWith(".yml") || f.endsWith(".yaml")) {
|
|
3761
|
+
imglist = parseContainerSpecData(dData);
|
|
3762
|
+
} else {
|
|
3763
|
+
imglist = parseContainerFile(dData);
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3747
3766
|
if (imglist && imglist.length) {
|
|
3748
3767
|
if (DEBUG_MODE) {
|
|
3749
3768
|
console.log("Images identified in", f, "are", imglist);
|
|
@@ -5186,12 +5205,22 @@ export const createXBom = async (path, options) => {
|
|
|
5186
5205
|
return createHelmBom(path, options);
|
|
5187
5206
|
}
|
|
5188
5207
|
|
|
5189
|
-
// Docker compose, kubernetes and skaffold
|
|
5208
|
+
// Docker compose, dockerfile, containerfile, kubernetes and skaffold
|
|
5190
5209
|
const dcFiles = getAllFiles(
|
|
5191
5210
|
path,
|
|
5192
5211
|
(options.multiProject ? "**/" : "") + "docker-compose*.yml",
|
|
5193
5212
|
options
|
|
5194
5213
|
);
|
|
5214
|
+
const dfFiles = getAllFiles(
|
|
5215
|
+
path,
|
|
5216
|
+
(options.multiProject ? "**/" : "") + "*Dockerfile*",
|
|
5217
|
+
options
|
|
5218
|
+
);
|
|
5219
|
+
const cfFiles = getAllFiles(
|
|
5220
|
+
path,
|
|
5221
|
+
(options.multiProject ? "**/" : "") + "*Containerfile*",
|
|
5222
|
+
options
|
|
5223
|
+
);
|
|
5195
5224
|
const skFiles = getAllFiles(
|
|
5196
5225
|
path,
|
|
5197
5226
|
(options.multiProject ? "**/" : "") + "skaffold.yaml",
|
|
@@ -5202,7 +5231,13 @@ export const createXBom = async (path, options) => {
|
|
|
5202
5231
|
(options.multiProject ? "**/" : "") + "deployment.yaml",
|
|
5203
5232
|
options
|
|
5204
5233
|
);
|
|
5205
|
-
if (
|
|
5234
|
+
if (
|
|
5235
|
+
dcFiles.length ||
|
|
5236
|
+
dfFiles.length ||
|
|
5237
|
+
cfFiles.length ||
|
|
5238
|
+
skFiles.length ||
|
|
5239
|
+
deplFiles.length
|
|
5240
|
+
) {
|
|
5206
5241
|
return await createContainerSpecLikeBom(path, options);
|
|
5207
5242
|
}
|
|
5208
5243
|
|
|
@@ -5468,7 +5503,9 @@ export const createBom = async (path, options) => {
|
|
|
5468
5503
|
options
|
|
5469
5504
|
);
|
|
5470
5505
|
case "universal":
|
|
5506
|
+
case "containerfile":
|
|
5471
5507
|
case "docker-compose":
|
|
5508
|
+
case "dockerfile":
|
|
5472
5509
|
case "swarm":
|
|
5473
5510
|
case "tekton":
|
|
5474
5511
|
case "kustomize":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyclonedx/cdxgen",
|
|
3
|
-
"version": "9.9.
|
|
3
|
+
"version": "9.9.4",
|
|
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>",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"url": "https://github.com/cyclonedx/cdxgen/issues"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@babel/parser": "^7.23.
|
|
59
|
-
"@babel/traverse": "^7.23.
|
|
58
|
+
"@babel/parser": "^7.23.3",
|
|
59
|
+
"@babel/traverse": "^7.23.3",
|
|
60
60
|
"@npmcli/arborist": "7.2.0",
|
|
61
61
|
"ajv": "^8.12.0",
|
|
62
62
|
"ajv-formats": "^2.1.1",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"yargs": "^17.7.2"
|
|
84
84
|
},
|
|
85
85
|
"optionalDependencies": {
|
|
86
|
-
"@appthreat/atom": "1.
|
|
86
|
+
"@appthreat/atom": "1.6.3",
|
|
87
87
|
"@cyclonedx/cdxgen-plugins-bin": "^1.4.0",
|
|
88
88
|
"@cyclonedx/cdxgen-plugins-bin-arm64": "^1.4.0",
|
|
89
89
|
"@cyclonedx/cdxgen-plugins-bin-ppc64": "^1.4.0",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"compression": "^1.7.4",
|
|
92
92
|
"connect": "^3.7.0",
|
|
93
93
|
"jsonata": "^2.0.3",
|
|
94
|
-
"sequelize": "^6.
|
|
94
|
+
"sequelize": "^6.35.0",
|
|
95
95
|
"sqlite3": "^5.1.6"
|
|
96
96
|
},
|
|
97
97
|
"files": [
|
|
@@ -102,10 +102,10 @@
|
|
|
102
102
|
"devDependencies": {
|
|
103
103
|
"caxa": "^3.0.1",
|
|
104
104
|
"docsify-cli": "^4.4.4",
|
|
105
|
-
"eslint": "^8.
|
|
105
|
+
"eslint": "^8.53.0",
|
|
106
106
|
"eslint-config-prettier": "^9.0.0",
|
|
107
107
|
"eslint-plugin-prettier": "^5.0.1",
|
|
108
108
|
"jest": "^29.7.0",
|
|
109
|
-
"prettier": "3.0
|
|
109
|
+
"prettier": "3.1.0"
|
|
110
110
|
}
|
|
111
111
|
}
|
package/utils.js
CHANGED
|
@@ -4388,6 +4388,43 @@ export const recurseImageNameLookup = (keyValueObj, pkgList, imgList) => {
|
|
|
4388
4388
|
return imgList;
|
|
4389
4389
|
};
|
|
4390
4390
|
|
|
4391
|
+
export const parseContainerFile = function (fileContents) {
|
|
4392
|
+
const imgList = [];
|
|
4393
|
+
|
|
4394
|
+
let buildStageNames = [];
|
|
4395
|
+
for (const line of fileContents.split("\n")) {
|
|
4396
|
+
if (line.trim().startsWith("#")) {
|
|
4397
|
+
continue; // skip commented out lines
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
if (line.includes("FROM")) {
|
|
4401
|
+
const fromStatement = line.split("FROM")[1].split("AS");
|
|
4402
|
+
|
|
4403
|
+
const imageStatement = fromStatement[0].trim();
|
|
4404
|
+
const buildStageName = fromStatement[1]?.trim();
|
|
4405
|
+
|
|
4406
|
+
if (buildStageNames.includes(imageStatement)) {
|
|
4407
|
+
if (DEBUG_MODE) {
|
|
4408
|
+
console.log(
|
|
4409
|
+
`Skipping image ${imageStatement} which uses previously seen build stage name.`
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
imgList.push({
|
|
4416
|
+
image: imageStatement
|
|
4417
|
+
});
|
|
4418
|
+
|
|
4419
|
+
if (buildStageName) {
|
|
4420
|
+
buildStageNames.push(buildStageName);
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
return imgList;
|
|
4426
|
+
};
|
|
4427
|
+
|
|
4391
4428
|
export const parseContainerSpecData = function (dcData) {
|
|
4392
4429
|
const pkgList = [];
|
|
4393
4430
|
const imgList = [];
|
|
@@ -7690,7 +7727,7 @@ export const parseCmakeLikeFile = (cmakeListFile, pkgType, options = {}) => {
|
|
|
7690
7727
|
.split(")")[0]
|
|
7691
7728
|
.split(",")
|
|
7692
7729
|
.filter((v) => v.length > 1);
|
|
7693
|
-
const parentName = tmpB[0];
|
|
7730
|
+
const parentName = tmpB[0].replace(":", "");
|
|
7694
7731
|
let parentVersion = undefined;
|
|
7695
7732
|
// In case of meson.build we can find the version number after the word version
|
|
7696
7733
|
// thanks to our replaces and splits
|
package/utils.test.js
CHANGED
|
@@ -73,7 +73,8 @@ import {
|
|
|
73
73
|
parsePyProjectToml,
|
|
74
74
|
parseSbtTree,
|
|
75
75
|
parseCmakeDotFile,
|
|
76
|
-
parseCmakeLikeFile
|
|
76
|
+
parseCmakeLikeFile,
|
|
77
|
+
parseContainerFile
|
|
77
78
|
} from "./utils.js";
|
|
78
79
|
import { readFileSync } from "node:fs";
|
|
79
80
|
import { parse } from "ssri";
|
|
@@ -2727,6 +2728,31 @@ test("parse container spec like files", async () => {
|
|
|
2727
2728
|
});
|
|
2728
2729
|
});
|
|
2729
2730
|
|
|
2731
|
+
test("parse containerfiles / dockerfiles", async () => {
|
|
2732
|
+
let dep_list = parseContainerFile(
|
|
2733
|
+
readFileSync("./test/data/Dockerfile", { encoding: "utf-8" })
|
|
2734
|
+
);
|
|
2735
|
+
expect(dep_list.length).toEqual(5);
|
|
2736
|
+
expect(dep_list[0]).toEqual({
|
|
2737
|
+
image: "hello-world"
|
|
2738
|
+
});
|
|
2739
|
+
expect(dep_list[0]).toEqual({
|
|
2740
|
+
image: "hello-world"
|
|
2741
|
+
});
|
|
2742
|
+
expect(dep_list[1]).toEqual({
|
|
2743
|
+
image: "hello-world"
|
|
2744
|
+
});
|
|
2745
|
+
expect(dep_list[2]).toEqual({
|
|
2746
|
+
image: "hello-world:latest"
|
|
2747
|
+
});
|
|
2748
|
+
expect(dep_list[3]).toEqual({
|
|
2749
|
+
image: "hello-world@sha256:1234567890abcdef"
|
|
2750
|
+
});
|
|
2751
|
+
expect(dep_list[4]).toEqual({
|
|
2752
|
+
image: "hello-world:latest@sha256:1234567890abcdef"
|
|
2753
|
+
});
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2730
2756
|
test("parse cloudbuild data", async () => {
|
|
2731
2757
|
expect(parseCloudBuildData(null)).toEqual([]);
|
|
2732
2758
|
const dep_list = parseCloudBuildData(
|