@cyclonedx/cdxgen 9.9.4 → 9.9.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 CHANGED
@@ -445,7 +445,7 @@ This feature is powered by osquery, which is [installed](https://github.com/cycl
445
445
 
446
446
  See [evinse mode](./ADVANCED.md) in the advanced documentation.
447
447
 
448
- ## BoM signing
448
+ ## BOM signing
449
449
 
450
450
  cdxgen can sign the generated BOM json file to increase authenticity and non-repudiation capabilities. To enable this, set the following environment variables.
451
451
 
package/analyzer.js CHANGED
@@ -108,7 +108,7 @@ const setFileRef = (allImports, src, file, pathnode, specifiers = []) => {
108
108
  }
109
109
  const fileRelativeLoc = relative(src, file);
110
110
  // remove unexpected extension imports
111
- if (/\.(svg|png|jpg|d\.ts)/.test(pathway)) {
111
+ if (/\.(svg|png|jpg|json|d\.ts)/.test(pathway)) {
112
112
  return;
113
113
  }
114
114
  const importedModules = specifiers
package/docker.js CHANGED
@@ -30,9 +30,25 @@ let dockerConn = undefined;
30
30
  let isPodman = false;
31
31
  let isPodmanRootless = true;
32
32
  let isDockerRootless = false;
33
+ // https://github.com/containerd/containerd
34
+ let isContainerd = !!process.env.CONTAINERD_ADDRESS;
33
35
  const WIN_LOCAL_TLS = "http://localhost:2375";
34
36
  let isWinLocalTLS = false;
35
37
 
38
+ if (
39
+ !process.env.DOCKER_HOST &&
40
+ (process.env.CONTAINERD_ADDRESS ||
41
+ (process.env.XDG_RUNTIME_DIR &&
42
+ existsSync(
43
+ join(process.env.XDG_RUNTIME_DIR, "containerd-rootless", "api.sock")
44
+ )))
45
+ ) {
46
+ isContainerd = true;
47
+ }
48
+
49
+ // Cache the registry auth keys
50
+ const registry_auth_keys = {};
51
+
36
52
  /**
37
53
  * Method to get all dirs matching a name
38
54
  *
@@ -94,7 +110,17 @@ export const getOnlyDirs = (srcpath, dirName) => {
94
110
  ].filter((d) => d.endsWith(dirName));
95
111
  };
96
112
 
97
- const getDefaultOptions = () => {
113
+ const getDefaultOptions = (forRegistry) => {
114
+ let authTokenSet = false;
115
+ if (!forRegistry && process.env.DOCKER_SERVER_ADDRESS) {
116
+ forRegistry = process.env.DOCKER_SERVER_ADDRESS;
117
+ }
118
+ if (forRegistry) {
119
+ forRegistry = forRegistry.replace("http://", "").replace("https://", "");
120
+ if (forRegistry.includes("/")) {
121
+ forRegistry = forRegistry.split("/")[0];
122
+ }
123
+ }
98
124
  const opts = {
99
125
  enableUnixSockets: true,
100
126
  throwHttpErrors: true,
@@ -102,6 +128,97 @@ const getDefaultOptions = () => {
102
128
  hooks: { beforeError: [] },
103
129
  mutableDefaults: true
104
130
  };
131
+ const DOCKER_CONFIG = process.env.DOCKER_CONFIG || join(homedir(), ".docker");
132
+ // Support for private registry
133
+ if (process.env.DOCKER_AUTH_CONFIG) {
134
+ opts.headers = {
135
+ "X-Registry-Auth": process.env.DOCKER_AUTH_CONFIG
136
+ };
137
+ authTokenSet = true;
138
+ }
139
+ if (
140
+ !authTokenSet &&
141
+ process.env.DOCKER_USER &&
142
+ process.env.DOCKER_PASSWORD &&
143
+ process.env.DOCKER_EMAIL &&
144
+ forRegistry
145
+ ) {
146
+ const authPayload = {
147
+ username: process.env.DOCKER_USER,
148
+ email: process.env.DOCKER_EMAIL,
149
+ serveraddress: forRegistry
150
+ };
151
+ if (process.env.DOCKER_USER === "<token>") {
152
+ authPayload.IdentityToken = process.env.DOCKER_PASSWORD;
153
+ } else {
154
+ authPayload.password = process.env.DOCKER_PASSWORD;
155
+ }
156
+ opts.headers = {
157
+ "X-Registry-Auth": Buffer.from(JSON.stringify(authPayload)).toString(
158
+ "base64"
159
+ )
160
+ };
161
+ }
162
+ if (!authTokenSet && existsSync(join(DOCKER_CONFIG, "config.json"))) {
163
+ const configData = readFileSync(
164
+ join(DOCKER_CONFIG, "config.json"),
165
+ "utf-8"
166
+ );
167
+ if (configData) {
168
+ try {
169
+ const configJson = JSON.parse(configData);
170
+ if (configJson.auths) {
171
+ // Check if there are hardcoded tokens
172
+ for (const serverAddress of Object.keys(configJson.auths)) {
173
+ if (forRegistry && !serverAddress.includes(forRegistry)) {
174
+ continue;
175
+ }
176
+ if (configJson.auths[serverAddress].auth) {
177
+ opts.headers = {
178
+ "X-Registry-Auth": configJson.auths[serverAddress].auth
179
+ };
180
+ authTokenSet = true;
181
+ break;
182
+ } else if (configJson.credsStore) {
183
+ const helperAuthToken = getCredsFromHelper(
184
+ configJson.credsStore,
185
+ serverAddress
186
+ );
187
+ if (helperAuthToken) {
188
+ opts.headers = {
189
+ "X-Registry-Auth": helperAuthToken
190
+ };
191
+ authTokenSet = true;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ } else if (configJson.credHelpers) {
197
+ // Support for credential helpers
198
+ for (const serverAddress of Object.keys(configJson.credHelpers)) {
199
+ if (forRegistry && !serverAddress.includes(forRegistry)) {
200
+ continue;
201
+ }
202
+ if (configJson.credHelpers[serverAddress]) {
203
+ const helperAuthToken = getCredsFromHelper(
204
+ configJson.credHelpers[serverAddress],
205
+ serverAddress
206
+ );
207
+ if (helperAuthToken) {
208
+ opts.headers = {
209
+ "X-Registry-Auth": helperAuthToken
210
+ };
211
+ authTokenSet = true;
212
+ break;
213
+ }
214
+ }
215
+ }
216
+ }
217
+ } catch (err) {
218
+ // pass
219
+ }
220
+ }
221
+ }
105
222
  const userInfo = _userInfo();
106
223
  opts.podmanPrefixUrl = isWin ? "" : `http://unix:/run/podman/podman.sock:`;
107
224
  opts.podmanRootlessPrefixUrl = isWin
@@ -148,22 +265,34 @@ const getDefaultOptions = () => {
148
265
  ),
149
266
  key: readFileSync(join(process.env.DOCKER_CERT_PATH, "key.pem"), "utf8")
150
267
  };
268
+ // Disable tls on empty values
269
+ // From the docker docs: Setting the DOCKER_TLS_VERIFY environment variable to any value other than the empty string is equivalent to setting the --tlsverify flag
270
+ if (
271
+ process.env.DOCKER_TLS_VERIFY &&
272
+ process.env.DOCKER_TLS_VERIFY === ""
273
+ ) {
274
+ opts.https.rejectUnauthorized = false;
275
+ console.log("TLS Verification disabled for", hostStr);
276
+ }
151
277
  }
152
278
  }
153
279
 
154
280
  return opts;
155
281
  };
156
282
 
157
- export const getConnection = async (options) => {
158
- if (!dockerConn) {
159
- const defaultOptions = getDefaultOptions();
283
+ export const getConnection = async (options, forRegistry) => {
284
+ if (isContainerd) {
285
+ return undefined;
286
+ } else if (!dockerConn) {
287
+ const defaultOptions = getDefaultOptions(forRegistry);
160
288
  const opts = Object.assign(
161
289
  {},
162
290
  {
163
291
  enableUnixSockets: defaultOptions.enableUnixSockets,
164
292
  throwHttpErrors: defaultOptions.throwHttpErrors,
165
293
  method: defaultOptions.method,
166
- prefixUrl: defaultOptions.prefixUrl
294
+ prefixUrl: defaultOptions.prefixUrl,
295
+ headers: defaultOptions.headers
167
296
  },
168
297
  options
169
298
  );
@@ -247,8 +376,8 @@ export const getConnection = async (options) => {
247
376
  return dockerConn;
248
377
  };
249
378
 
250
- export const makeRequest = async (path, method = "GET") => {
251
- const client = await getConnection();
379
+ export const makeRequest = async (path, method = "GET", forRegistry) => {
380
+ const client = await getConnection({}, forRegistry);
252
381
  if (!client) {
253
382
  return undefined;
254
383
  }
@@ -258,14 +387,15 @@ export const makeRequest = async (path, method = "GET") => {
258
387
  enableUnixSockets: true,
259
388
  method
260
389
  };
261
- const defaultOptions = getDefaultOptions();
390
+ const defaultOptions = getDefaultOptions(forRegistry);
262
391
  const opts = Object.assign(
263
392
  {},
264
393
  {
265
394
  enableUnixSockets: defaultOptions.enableUnixSockets,
266
395
  throwHttpErrors: defaultOptions.throwHttpErrors,
267
396
  method: defaultOptions.method,
268
- prefixUrl: defaultOptions.prefixUrl
397
+ prefixUrl: defaultOptions.prefixUrl,
398
+ headers: defaultOptions.headers
269
399
  },
270
400
  extraOptions
271
401
  );
@@ -331,20 +461,44 @@ export const parseImageName = (fullImageName) => {
331
461
  return nameObj;
332
462
  };
333
463
 
464
+ /**
465
+ * Prefer cli on windows or when using tcp/ssh based host.
466
+ *
467
+ * @returns boolean true if we should use the cli. false otherwise
468
+ */
469
+ const needsCliFallback = () => {
470
+ return (
471
+ isWin ||
472
+ (process.env.DOCKER_HOST &&
473
+ (process.env.DOCKER_HOST.startsWith("tcp://") ||
474
+ process.env.DOCKER_HOST.startsWith("ssh://")))
475
+ );
476
+ };
477
+
334
478
  /**
335
479
  * Method to get image to the local registry by pulling from the remote if required
336
480
  */
337
481
  export const getImage = async (fullImageName) => {
338
482
  let localData = undefined;
339
483
  let pullData = undefined;
340
- const { repo, tag, digest } = parseImageName(fullImageName);
341
- let repoWithTag = `${repo}:${tag !== "" ? tag : ":latest"}`;
484
+ const { registry, repo, tag, digest } = parseImageName(fullImageName);
485
+ let repoWithTag =
486
+ registry && registry !== "docker.io"
487
+ ? fullImageName
488
+ : `${repo}:${tag !== "" ? tag : ":latest"}`;
342
489
  // Fetch only the latest tag if none is specified
343
490
  if (tag === "" && digest === "") {
344
491
  fullImageName = fullImageName + ":latest";
345
492
  }
346
- if (isWin) {
347
- let result = spawnSync("docker", ["pull", fullImageName], {
493
+ if (isContainerd) {
494
+ console.log(
495
+ "containerd/nerdctl is currently unsupported. Export the image manually and run cdxgen against the tar image."
496
+ );
497
+ return undefined;
498
+ }
499
+ if (needsCliFallback()) {
500
+ const dockerCmd = process.env.DOCKER_CMD || "docker";
501
+ let result = spawnSync(dockerCmd, ["pull", fullImageName], {
348
502
  encoding: "utf-8"
349
503
  });
350
504
  if (result.status !== 0 || result.error) {
@@ -355,12 +509,16 @@ export const getImage = async (fullImageName) => {
355
509
  console.log(
356
510
  "Ensure Docker for Desktop is running as an administrator with 'Exposing daemon on TCP without TLS' setting turned on."
357
511
  );
512
+ } else if (result.stderr && result.stderr.includes("not found")) {
513
+ console.log(
514
+ "Set the environment variable DOCKER_CMD to use an alternative command such as nerdctl or podman."
515
+ );
358
516
  } else {
359
517
  console.log(result.stderr);
360
518
  }
361
519
  return localData;
362
520
  } else {
363
- result = spawnSync("docker", ["inspect", fullImageName], {
521
+ result = spawnSync(dockerCmd, ["inspect", fullImageName], {
364
522
  encoding: "utf-8"
365
523
  });
366
524
  if (result.status !== 0 || result.error) {
@@ -385,7 +543,11 @@ export const getImage = async (fullImageName) => {
385
543
  }
386
544
  }
387
545
  try {
388
- localData = await makeRequest(`images/${repoWithTag}/json`);
546
+ localData = await makeRequest(
547
+ `images/${repoWithTag}/json`,
548
+ "GET",
549
+ registry
550
+ );
389
551
  if (localData) {
390
552
  return localData;
391
553
  }
@@ -393,10 +555,14 @@ export const getImage = async (fullImageName) => {
393
555
  // ignore
394
556
  }
395
557
  try {
396
- localData = await makeRequest(`images/${repo}/json`);
558
+ localData = await makeRequest(`images/${repo}/json`, "GET", registry);
397
559
  } catch (err) {
398
560
  try {
399
- localData = await makeRequest(`images/${fullImageName}/json`);
561
+ localData = await makeRequest(
562
+ `images/${fullImageName}/json`,
563
+ "GET",
564
+ registry
565
+ );
400
566
  if (localData) {
401
567
  return localData;
402
568
  }
@@ -412,7 +578,8 @@ export const getImage = async (fullImageName) => {
412
578
  try {
413
579
  pullData = await makeRequest(
414
580
  `images/create?fromImage=${fullImageName}`,
415
- "POST"
581
+ "POST",
582
+ registry
416
583
  );
417
584
  if (
418
585
  pullData &&
@@ -434,7 +601,8 @@ export const getImage = async (fullImageName) => {
434
601
  }
435
602
  pullData = await makeRequest(
436
603
  `images/create?fromImage=${repoWithTag}`,
437
- "POST"
604
+ "POST",
605
+ registry
438
606
  );
439
607
  } catch (err) {
440
608
  // continue regardless of error
@@ -444,7 +612,11 @@ export const getImage = async (fullImageName) => {
444
612
  if (DEBUG_MODE) {
445
613
  console.log(`Trying with ${repoWithTag}`);
446
614
  }
447
- localData = await makeRequest(`images/${repoWithTag}/json`);
615
+ localData = await makeRequest(
616
+ `images/${repoWithTag}/json`,
617
+ "GET",
618
+ registry
619
+ );
448
620
  if (localData) {
449
621
  return localData;
450
622
  }
@@ -453,7 +625,7 @@ export const getImage = async (fullImageName) => {
453
625
  if (DEBUG_MODE) {
454
626
  console.log(`Trying with ${repo}`);
455
627
  }
456
- localData = await makeRequest(`images/${repo}/json`);
628
+ localData = await makeRequest(`images/${repo}/json`, "GET", registry);
457
629
  if (localData) {
458
630
  return localData;
459
631
  }
@@ -464,7 +636,11 @@ export const getImage = async (fullImageName) => {
464
636
  if (DEBUG_MODE) {
465
637
  console.log(`Trying with ${fullImageName}`);
466
638
  }
467
- localData = await makeRequest(`images/${fullImageName}/json`);
639
+ localData = await makeRequest(
640
+ `images/${fullImageName}/json`,
641
+ "GET",
642
+ registry
643
+ );
468
644
  } catch (err) {
469
645
  // continue regardless of error
470
646
  }
@@ -684,7 +860,7 @@ export const exportImage = async (fullImageName) => {
684
860
  if (!localData) {
685
861
  return undefined;
686
862
  }
687
- const { tag, digest } = parseImageName(fullImageName);
863
+ const { registry, tag, digest } = parseImageName(fullImageName);
688
864
  // Fetch only the latest tag if none is specified
689
865
  if (tag === "" && digest === "") {
690
866
  fullImageName = fullImageName + ":latest";
@@ -695,7 +871,7 @@ export const exportImage = async (fullImageName) => {
695
871
  // Windows containers use index.json
696
872
  const manifestIndexFile = join(tempDir, "index.json");
697
873
  // On Windows, fallback to invoking cli
698
- if (isWin) {
874
+ if (needsCliFallback()) {
699
875
  const imageTarFile = join(tempDir, "image.tar");
700
876
  console.log(
701
877
  `About to export image ${fullImageName} to ${imageTarFile} using docker cli`
@@ -722,10 +898,16 @@ export const exportImage = async (fullImageName) => {
722
898
  }
723
899
  }
724
900
  } else {
725
- const client = await getConnection();
901
+ const client = await getConnection({}, registry);
726
902
  try {
727
903
  if (DEBUG_MODE) {
728
- console.log(`About to export image ${fullImageName} to ${tempDir}`);
904
+ if (registry && registry.trim().length) {
905
+ console.log(
906
+ `About to export image ${fullImageName} from ${registry} to ${tempDir}`
907
+ );
908
+ } else {
909
+ console.log(`About to export image ${fullImageName} to ${tempDir}`);
910
+ }
729
911
  }
730
912
  await stream.pipeline(
731
913
  client.stream(`images/${fullImageName}/get`),
@@ -896,3 +1078,46 @@ export const removeImage = async (fullImageName, force = false) => {
896
1078
  );
897
1079
  return removeData;
898
1080
  };
1081
+
1082
+ export const getCredsFromHelper = (exeSuffix, serverAddress) => {
1083
+ if (registry_auth_keys[serverAddress]) {
1084
+ return registry_auth_keys[serverAddress];
1085
+ }
1086
+ let credHelperExe = `docker-credential-${exeSuffix}`;
1087
+ if (isWin) {
1088
+ credHelperExe = credHelperExe + ".exe";
1089
+ }
1090
+ const result = spawnSync(credHelperExe, ["get"], {
1091
+ input: serverAddress,
1092
+ encoding: "utf-8"
1093
+ });
1094
+ if (result.status !== 0 || result.error) {
1095
+ console.log(result.stdout, result.stderr);
1096
+ } else if (result.stdout) {
1097
+ const cmdOutput = Buffer.from(result.stdout).toString();
1098
+ try {
1099
+ const authPayload = JSON.parse(cmdOutput);
1100
+ const fixedAuthPayload = {
1101
+ username:
1102
+ authPayload.username ||
1103
+ authPayload.Username ||
1104
+ process.env.DOCKER_USER,
1105
+ password:
1106
+ authPayload.password ||
1107
+ authPayload.Secret ||
1108
+ process.env.DOCKER_PASSWORD,
1109
+ email:
1110
+ authPayload.email || authPayload.username || process.env.DOCKER_USER,
1111
+ serveraddress: serverAddress
1112
+ };
1113
+ const authKey = Buffer.from(JSON.stringify(fixedAuthPayload)).toString(
1114
+ "base64"
1115
+ );
1116
+ registry_auth_keys[serverAddress] = authKey;
1117
+ return authKey;
1118
+ } catch (err) {
1119
+ return undefined;
1120
+ }
1121
+ }
1122
+ return undefined;
1123
+ };
package/docker.test.js CHANGED
@@ -54,6 +54,15 @@ test("parseImageName tests", () => {
54
54
  digest: "",
55
55
  platform: ""
56
56
  });
57
+ expect(
58
+ parseImageName("foocorp.jfrog.io/docker/library/eclipse-temurin:latest")
59
+ ).toEqual({
60
+ registry: "foocorp.jfrog.io",
61
+ repo: "docker/library/eclipse-temurin",
62
+ tag: "latest",
63
+ digest: "",
64
+ platform: ""
65
+ });
57
66
  expect(
58
67
  parseImageName(
59
68
  "quay.io/shiftleft/scan-java@sha256:5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1"
package/index.js CHANGED
@@ -1860,7 +1860,7 @@ export const createNodejsBom = async (path, options) => {
1860
1860
  const parentSubComponents = [];
1861
1861
  let ppurl = "";
1862
1862
  // Docker mode requires special handling
1863
- if (["docker", "oci", "os"].includes(options.projectType)) {
1863
+ if (["docker", "oci", "container", "os"].includes(options.projectType)) {
1864
1864
  const pkgJsonFiles = getAllFiles(path, "**/package.json", options);
1865
1865
  // Are there any package.json files in the container?
1866
1866
  if (pkgJsonFiles.length) {
@@ -1880,7 +1880,7 @@ export const createNodejsBom = async (path, options) => {
1880
1880
  }
1881
1881
  let allImports = {};
1882
1882
  if (
1883
- !["docker", "oci", "os"].includes(options.projectType) &&
1883
+ !["docker", "oci", "container", "os"].includes(options.projectType) &&
1884
1884
  !options.noBabel
1885
1885
  ) {
1886
1886
  if (DEBUG_MODE) {
@@ -2753,7 +2753,7 @@ export const createGoBom = async (path, options) => {
2753
2753
  if (gomodFiles.length) {
2754
2754
  let shouldManuallyParse = false;
2755
2755
  // Use the go list -deps and go mod why commands to generate a good quality BOM for non-docker invocations
2756
- if (!["docker", "oci", "os"].includes(options.projectType)) {
2756
+ if (!["docker", "oci", "container", "os"].includes(options.projectType)) {
2757
2757
  for (const f of gomodFiles) {
2758
2758
  const basePath = dirname(f);
2759
2759
  // Ignore vendor packages
@@ -2865,7 +2865,7 @@ export const createGoBom = async (path, options) => {
2865
2865
  }
2866
2866
  }
2867
2867
  // Parse the gomod files manually. The resultant BOM would be incomplete
2868
- if (!["docker", "oci", "os"].includes(options.projectType)) {
2868
+ if (!["docker", "oci", "container", "os"].includes(options.projectType)) {
2869
2869
  console.log(
2870
2870
  "Manually parsing go.mod files. The resultant BOM would be incomplete."
2871
2871
  );
@@ -3154,7 +3154,7 @@ export const createCppBom = (path, options) => {
3154
3154
  // inside of other project types. So we currently limit this analyis only when -t argument
3155
3155
  // is used.
3156
3156
  if (
3157
- !["docker", "oci", "os"].includes(options.projectType) &&
3157
+ !["docker", "oci", "container", "os"].includes(options.projectType) &&
3158
3158
  (!options.createMultiXBom || options.deep)
3159
3159
  ) {
3160
3160
  let osPkgsList = [];
@@ -5296,6 +5296,7 @@ export const createBom = async (path, options) => {
5296
5296
  projectType === "docker" ||
5297
5297
  projectType === "podman" ||
5298
5298
  projectType === "oci" ||
5299
+ projectType === "container" ||
5299
5300
  path.startsWith("docker.io") ||
5300
5301
  path.startsWith("quay.io") ||
5301
5302
  path.startsWith("ghcr.io") ||
@@ -5544,19 +5545,36 @@ export async function submitBom(args, bomContents) {
5544
5545
  if (encodedBomContents.startsWith("77u/")) {
5545
5546
  encodedBomContents = encodedBomContents.substring(4);
5546
5547
  }
5547
- let projectVersion = args.projectVersion || "master";
5548
- if (projectVersion == true) {
5549
- projectVersion = "master";
5550
- }
5551
5548
  const bomPayload = {
5552
- project: args.projectId,
5553
- projectName: args.projectName,
5554
- projectVersion: projectVersion,
5555
5549
  autoCreate: "true",
5556
5550
  bom: encodedBomContents
5557
5551
  };
5558
- if (typeof args.parentProjectId !== "undefined") {
5559
- bomPayload.parentUUID = args.parentProjectId;
5552
+ let projectVersion = args.projectVersion || "master";
5553
+ if (
5554
+ typeof args.projectId !== "undefined" ||
5555
+ (typeof args.projectName !== "undefined" &&
5556
+ typeof projectVersion !== "undefined")
5557
+ ) {
5558
+ if (typeof args.projectId !== "undefined") {
5559
+ bomPayload.project = args.projectId;
5560
+ }
5561
+ if (typeof args.projectName !== "undefined") {
5562
+ bomPayload.projectName = args.projectName;
5563
+ }
5564
+ if (typeof projectVersion !== "undefined") {
5565
+ bomPayload.projectVersion = projectVersion;
5566
+ }
5567
+ } else {
5568
+ console.log(
5569
+ "projectId, projectName and projectVersion, or all three must be provided."
5570
+ );
5571
+ return;
5572
+ }
5573
+ if (
5574
+ typeof args.parentProjectId !== "undefined" ||
5575
+ typeof args.parentUUID !== "undefined"
5576
+ ) {
5577
+ bomPayload.parentUUID = args.parentProjectId || args.parentUUID;
5560
5578
  }
5561
5579
  if (DEBUG_MODE) {
5562
5580
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyclonedx/cdxgen",
3
- "version": "9.9.4",
3
+ "version": "9.9.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>",
@@ -83,7 +83,7 @@
83
83
  "yargs": "^17.7.2"
84
84
  },
85
85
  "optionalDependencies": {
86
- "@appthreat/atom": "1.6.3",
86
+ "@appthreat/atom": "1.6.4",
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",
package/server.js CHANGED
@@ -72,7 +72,7 @@ const parseQueryString = (q, body, options = {}) => {
72
72
  "requiredOnly",
73
73
  "noBabel",
74
74
  "installDeps",
75
- "project",
75
+ "projectId",
76
76
  "projectName",
77
77
  "projectGroup",
78
78
  "projectVersion",
package/utils.js CHANGED
@@ -19,7 +19,8 @@ import {
19
19
  readFileSync,
20
20
  rmSync,
21
21
  unlinkSync,
22
- writeFileSync
22
+ writeFileSync,
23
+ readdirSync
23
24
  } from "node:fs";
24
25
  import got from "got";
25
26
  import Arborist from "@npmcli/arborist";
@@ -6514,12 +6515,67 @@ export const parseJarManifest = function (jarMetadata) {
6514
6515
  return metadata;
6515
6516
  };
6516
6517
 
6518
+ export const parsePomProperties = function (pomProperties) {
6519
+ const properties = {};
6520
+ if (!pomProperties) {
6521
+ return properties;
6522
+ }
6523
+ pomProperties.split("\n").forEach((l) => {
6524
+ l = l.replace("\r", "");
6525
+ if (l.includes("=")) {
6526
+ const tmpA = l.split("=");
6527
+ if (tmpA && tmpA.length === 2) {
6528
+ properties[tmpA[0]] = tmpA[1].replace("\r", "");
6529
+ }
6530
+ }
6531
+ });
6532
+ return properties;
6533
+ };
6534
+
6517
6535
  export const encodeForPurl = (s) => {
6518
6536
  return s && !s.includes("%40")
6519
6537
  ? encodeURIComponent(s).replace(/%3A/g, ":").replace(/%2F/g, "/")
6520
6538
  : s;
6521
6539
  };
6522
6540
 
6541
+ /**
6542
+ * Method to get pom properties from maven directory
6543
+ *
6544
+ * @param {string} mavenDir Path to maven directory
6545
+ *
6546
+ * @return array with pom properties
6547
+ */
6548
+ export const getPomPropertiesFromMavenDir = function (mavenDir) {
6549
+ let pomProperties = {};
6550
+ if (existsSync(mavenDir) && lstatSync(mavenDir).isDirectory()) {
6551
+ let mavenDirEntries = readdirSync(mavenDir, { withFileTypes: true });
6552
+ mavenDirEntries.forEach((mavenDirEntry) => {
6553
+ if (mavenDirEntry.isDirectory()) {
6554
+ let groupDirEntries = readdirSync(
6555
+ join(mavenDirEntry.path, mavenDirEntry.name),
6556
+ { withFileTypes: true }
6557
+ );
6558
+ groupDirEntries.forEach((groupDirEntry) => {
6559
+ if (groupDirEntry.isDirectory()) {
6560
+ let pomPropertiesFile = join(
6561
+ groupDirEntry.path,
6562
+ groupDirEntry.name,
6563
+ "pom.properties"
6564
+ );
6565
+ if (existsSync(pomPropertiesFile)) {
6566
+ const pomPropertiesString = readFileSync(pomPropertiesFile, {
6567
+ encoding: "utf-8"
6568
+ });
6569
+ pomProperties = parsePomProperties(pomPropertiesString);
6570
+ }
6571
+ }
6572
+ });
6573
+ }
6574
+ });
6575
+ }
6576
+ return pomProperties;
6577
+ };
6578
+
6523
6579
  /**
6524
6580
  * Method to extract a war or ear file
6525
6581
  *
@@ -6601,13 +6657,14 @@ export const extractJarArchive = function (
6601
6657
  }
6602
6658
  const manifestDir = join(tempDir, "META-INF");
6603
6659
  const manifestFile = join(manifestDir, "MANIFEST.MF");
6660
+ const mavenDir = join(manifestDir, "maven");
6604
6661
  let jarResult = {
6605
6662
  status: 1
6606
6663
  };
6607
6664
  if (existsSync(pomname)) {
6608
6665
  jarResult = { status: 0 };
6609
6666
  } else {
6610
- jarResult = spawnSync("jar", ["-xf", jf], {
6667
+ jarResult = spawnSync("jar", ["-xf", jf, "META-INF"], {
6611
6668
  encoding: "utf-8",
6612
6669
  cwd: tempDir,
6613
6670
  shell: isWin,
@@ -6617,29 +6674,42 @@ export const extractJarArchive = function (
6617
6674
  if (jarResult.status !== 0) {
6618
6675
  console.error(jarResult.stdout, jarResult.stderr);
6619
6676
  } else {
6620
- if (existsSync(manifestFile)) {
6677
+ // When maven descriptor is available take group, name and version from pom.properties
6678
+ // META-INF/maven/${groupId}/${artifactId}/pom.properties
6679
+ // see https://maven.apache.org/shared/maven-archiver/index.html
6680
+ const pomProperties = getPomPropertiesFromMavenDir(mavenDir);
6681
+ let group = pomProperties["groupId"],
6682
+ name = pomProperties["artifactId"],
6683
+ version = pomProperties["version"],
6684
+ confidence = 1,
6685
+ technique = "manifest-analysis";
6686
+ if ((!group || !name || !version) && existsSync(manifestFile)) {
6687
+ confidence = 0.8;
6621
6688
  const jarMetadata = parseJarManifest(
6622
6689
  readFileSync(manifestFile, {
6623
6690
  encoding: "utf-8"
6624
6691
  })
6625
6692
  );
6626
- let group =
6693
+ group =
6694
+ group ||
6627
6695
  jarMetadata["Extension-Name"] ||
6628
6696
  jarMetadata["Implementation-Vendor-Id"] ||
6629
6697
  jarMetadata["Bundle-SymbolicName"] ||
6630
6698
  jarMetadata["Bundle-Vendor"] ||
6631
6699
  jarMetadata["Automatic-Module-Name"] ||
6632
6700
  "";
6633
- let version =
6701
+ version =
6702
+ version ||
6634
6703
  jarMetadata["Bundle-Version"] ||
6635
6704
  jarMetadata["Implementation-Version"] ||
6636
6705
  jarMetadata["Specification-Version"];
6637
6706
  if (version && version.includes(" ")) {
6638
6707
  version = version.split(" ")[0];
6639
6708
  }
6640
- let name = "";
6641
6709
  // Prefer jar filename to construct name and version
6642
6710
  if (!name || !version || name === "" || version === "") {
6711
+ confidence = 0.5;
6712
+ technique = "filename";
6643
6713
  const tmpA = jarname.split("-");
6644
6714
  if (tmpA && tmpA.length > 1) {
6645
6715
  const lastPart = tmpA[tmpA.length - 1];
@@ -6688,56 +6758,56 @@ export const extractJarArchive = function (
6688
6758
  break;
6689
6759
  }
6690
6760
  }
6691
- if (name && version) {
6692
- // if group is empty use name as group
6693
- group = encodeForPurl(group === "." ? name : group || name) || "";
6694
- let apkg = {
6761
+ // if group is empty use name as group
6762
+ group = group === "." ? name : group || name;
6763
+ }
6764
+ if (name && version) {
6765
+ let apkg = {
6766
+ group: group ? encodeForPurl(group) : "",
6767
+ name: name ? encodeForPurl(name) : "",
6768
+ version,
6769
+ purl: new PackageURL(
6770
+ "maven",
6695
6771
  group,
6696
- name: name ? encodeForPurl(name) : "",
6772
+ name,
6697
6773
  version,
6698
- purl: new PackageURL(
6699
- "maven",
6700
- group,
6701
- name,
6702
- version,
6703
- { type: "jar" },
6704
- null
6705
- ).toString(),
6706
- evidence: {
6707
- identity: {
6708
- field: "purl",
6709
- confidence: 0.5,
6710
- methods: [
6711
- {
6712
- technique: "filename",
6713
- confidence: 0.5,
6714
- value: jarname
6715
- }
6716
- ]
6717
- }
6718
- },
6719
- properties: [
6720
- {
6721
- name: "SrcFile",
6722
- value: jarname
6723
- }
6724
- ]
6725
- };
6726
- if (
6727
- jarNSMapping &&
6728
- jarNSMapping[apkg.purl] &&
6729
- jarNSMapping[apkg.purl].namespaces
6730
- ) {
6731
- apkg.properties.push({
6732
- name: "Namespaces",
6733
- value: jarNSMapping[apkg.purl].namespaces.join("\n")
6734
- });
6735
- }
6736
- pkgList.push(apkg);
6737
- } else {
6738
- if (DEBUG_MODE) {
6739
- console.log(`Ignored jar ${jarname}`, jarMetadata, name, version);
6740
- }
6774
+ { type: "jar" },
6775
+ null
6776
+ ).toString(),
6777
+ evidence: {
6778
+ identity: {
6779
+ field: "purl",
6780
+ confidence: confidence,
6781
+ methods: [
6782
+ {
6783
+ technique: technique,
6784
+ confidence: confidence,
6785
+ value: jarname
6786
+ }
6787
+ ]
6788
+ }
6789
+ },
6790
+ properties: [
6791
+ {
6792
+ name: "SrcFile",
6793
+ value: jarname
6794
+ }
6795
+ ]
6796
+ };
6797
+ if (
6798
+ jarNSMapping &&
6799
+ jarNSMapping[apkg.purl] &&
6800
+ jarNSMapping[apkg.purl].namespaces
6801
+ ) {
6802
+ apkg.properties.push({
6803
+ name: "Namespaces",
6804
+ value: jarNSMapping[apkg.purl].namespaces.join("\n")
6805
+ });
6806
+ }
6807
+ pkgList.push(apkg);
6808
+ } else {
6809
+ if (DEBUG_MODE) {
6810
+ console.log(`Ignored jar ${jarname}`, name, version);
6741
6811
  }
6742
6812
  }
6743
6813
  try {
@@ -6929,6 +6999,7 @@ export const getMavenCommand = (srcPath, rootPath) => {
6929
6999
  let isWrapperReady = false;
6930
7000
  let isWrapperFound = false;
6931
7001
  let findMavenFile = "mvnw";
7002
+ let mavenWrapperCmd = null;
6932
7003
  if (platform() == "win32") {
6933
7004
  findMavenFile = "mvnw.bat";
6934
7005
  if (
@@ -6947,7 +7018,7 @@ export const getMavenCommand = (srcPath, rootPath) => {
6947
7018
  } catch (e) {
6948
7019
  // continue regardless of error
6949
7020
  }
6950
- mavenCmd = resolve(join(srcPath, findMavenFile));
7021
+ mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
6951
7022
  isWrapperFound = true;
6952
7023
  } else if (rootPath && existsSync(join(rootPath, findMavenFile))) {
6953
7024
  // Check if the root directory has a wrapper script
@@ -6956,7 +7027,7 @@ export const getMavenCommand = (srcPath, rootPath) => {
6956
7027
  } catch (e) {
6957
7028
  // continue regardless of error
6958
7029
  }
6959
- mavenCmd = resolve(join(rootPath, findMavenFile));
7030
+ mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
6960
7031
  isWrapperFound = true;
6961
7032
  }
6962
7033
  if (isWrapperFound) {
@@ -6965,14 +7036,15 @@ export const getMavenCommand = (srcPath, rootPath) => {
6965
7036
  "Testing the wrapper script by invoking wrapper:wrapper task"
6966
7037
  );
6967
7038
  }
6968
- const result = spawnSync(mavenCmd, ["wrapper:wrapper"], {
7039
+ const result = spawnSync(mavenWrapperCmd, ["wrapper:wrapper"], {
6969
7040
  encoding: "utf-8",
6970
7041
  cwd: rootPath,
6971
7042
  timeout: TIMEOUT_MS,
6972
7043
  shell: isWin
6973
7044
  });
6974
- if (!result.error) {
7045
+ if (!result.error && !result.status) {
6975
7046
  isWrapperReady = true;
7047
+ mavenCmd = mavenWrapperCmd;
6976
7048
  } else {
6977
7049
  if (DEBUG_MODE) {
6978
7050
  console.log(