@cyclonedx/cdxgen 11.3.2 → 11.4.1

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.
@@ -1,5 +1,4 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { spawnSync } from "node:child_process";
3
2
  import {
4
3
  createReadStream,
5
4
  lstatSync,
@@ -25,6 +24,7 @@ import {
25
24
  getTmpDir,
26
25
  safeExistsSync,
27
26
  safeMkdirSync,
27
+ safeSpawnSync,
28
28
  } from "../helpers/utils.js";
29
29
 
30
30
  export const isWin = _platform() === "win32";
@@ -88,7 +88,7 @@ export function detectColima() {
88
88
  return true;
89
89
  }
90
90
  if (_platform() === "darwin") {
91
- const result = spawnSync("colima", ["version"], {
91
+ const result = safeSpawnSync("colima", ["version"], {
92
92
  encoding: "utf-8",
93
93
  });
94
94
  if (result.status !== 0 || result.error) {
@@ -133,7 +133,7 @@ export function detectRancherDesktop() {
133
133
  );
134
134
  // Is Rancher Desktop running
135
135
  if (safeExistsSync(limactl) || safeExistsSync(limaHome)) {
136
- const result = spawnSync("rdctl", ["list-settings"], {
136
+ const result = safeSpawnSync("rdctl", ["list-settings"], {
137
137
  encoding: "utf-8",
138
138
  });
139
139
  if (result.status !== 0 || result.error) {
@@ -616,7 +616,10 @@ export const parseImageName = (fullImageName) => {
616
616
  * @returns boolean true if we should use the cli. false otherwise
617
617
  */
618
618
  const needsCliFallback = () => {
619
- if (_platform() === "darwin" && (detectRancherDesktop() || detectColima())) {
619
+ if (
620
+ ["true", "1"].includes(process.env.DOCKER_USE_CLI) ||
621
+ (_platform() === "darwin" && (detectRancherDesktop() || detectColima()))
622
+ ) {
620
623
  return true;
621
624
  }
622
625
  return (
@@ -658,17 +661,14 @@ export const getImage = async (fullImageName) => {
658
661
  }
659
662
  let needsPull = true;
660
663
  // Let's check the local cache first
661
- let result = spawnSync(dockerCmd, ["images", "--format=json"], {
664
+ let result = safeSpawnSync(dockerCmd, ["images", "--format=json"], {
662
665
  encoding: "utf-8",
663
666
  });
664
667
  if (result.status === 0 && result.stdout) {
665
668
  for (const imgLine of result.stdout.split("\n")) {
666
669
  try {
667
670
  const imgObj = JSON.parse(Buffer.from(imgLine).toString());
668
- if (
669
- imgObj.Repository === fullImageName ||
670
- imgObj?.Name?.endsWith(fullImageName)
671
- ) {
671
+ if (`${imgObj.Repository}:${imgObj.Tag}` === fullImageName) {
672
672
  needsPull = false;
673
673
  break;
674
674
  }
@@ -678,7 +678,7 @@ export const getImage = async (fullImageName) => {
678
678
  }
679
679
  }
680
680
  if (needsPull) {
681
- result = spawnSync(dockerCmd, ["pull", fullImageName], {
681
+ result = safeSpawnSync(dockerCmd, ["pull", fullImageName], {
682
682
  encoding: "utf-8",
683
683
  timeout: TIMEOUT_MS,
684
684
  });
@@ -696,7 +696,7 @@ export const getImage = async (fullImageName) => {
696
696
  }
697
697
  }
698
698
  }
699
- result = spawnSync(dockerCmd, ["inspect", fullImageName], {
699
+ result = safeSpawnSync(dockerCmd, ["inspect", fullImageName], {
700
700
  encoding: "utf-8",
701
701
  });
702
702
  if (result.status !== 0 || result.error) {
@@ -1160,7 +1160,7 @@ export const exportImage = async (fullImageName, options) => {
1160
1160
  console.log(
1161
1161
  `About to export image ${fullImageName} to ${imageTarFile} using ${dockerCmd} cli`,
1162
1162
  );
1163
- const result = spawnSync(
1163
+ const result = safeSpawnSync(
1164
1164
  dockerCmd,
1165
1165
  ["save", "-o", imageTarFile, fullImageName],
1166
1166
  {
@@ -1392,7 +1392,7 @@ export const getCredsFromHelper = (exeSuffix, serverAddress) => {
1392
1392
  if (isWin) {
1393
1393
  credHelperExe = `${credHelperExe}.exe`;
1394
1394
  }
1395
- const result = spawnSync(credHelperExe, ["get"], {
1395
+ const result = safeSpawnSync(credHelperExe, ["get"], {
1396
1396
  input: serverAddress,
1397
1397
  encoding: "utf-8",
1398
1398
  });
@@ -1,25 +1,29 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { spawnSync } from "node:child_process";
3
2
  import fs from "node:fs";
4
- import { MAX_BUFFER, getAllFiles, getTmpDir, isWin } from "../helpers/utils.js";
3
+ import {
4
+ MAX_BUFFER,
5
+ getAllFiles,
6
+ getTmpDir,
7
+ isWin,
8
+ safeSpawnSync,
9
+ } from "../helpers/utils.js";
5
10
 
6
- export function getBomWithOras(image) {
7
- let result = spawnSync(
8
- "oras",
9
- [
10
- "discover",
11
- "--format",
12
- "json",
13
- "--artifact-type",
14
- "sbom/cyclonedx",
15
- image,
16
- ],
17
- {
18
- encoding: "utf-8",
19
- shell: isWin,
20
- maxBuffer: MAX_BUFFER,
21
- },
22
- );
11
+ export function getBomWithOras(image, platform = undefined) {
12
+ let parameters = [
13
+ "discover",
14
+ "--format",
15
+ "json",
16
+ "--artifact-type",
17
+ "sbom/cyclonedx",
18
+ ];
19
+ if (platform) {
20
+ parameters = parameters.concat(["--platform", platform]);
21
+ }
22
+ let result = safeSpawnSync("oras", parameters.concat([image]), {
23
+ encoding: "utf-8",
24
+ shell: isWin,
25
+ maxBuffer: MAX_BUFFER,
26
+ });
23
27
  if (result.status !== 0 || result.error) {
24
28
  console.log(
25
29
  "Install oras by following the instructions at: https://oras.land/docs/installation",
@@ -40,7 +44,7 @@ export function getBomWithOras(image) {
40
44
  ) {
41
45
  const imageRef = manifestObj.manifests[0].reference;
42
46
  const tmpDir = getTmpDir();
43
- result = spawnSync("oras", ["pull", imageRef, "-o", tmpDir], {
47
+ result = safeSpawnSync("oras", ["pull", imageRef, "-o", tmpDir], {
44
48
  encoding: "utf-8",
45
49
  shell: isWin,
46
50
  maxBuffer: MAX_BUFFER,
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  /**
3
2
  * The idea behind this plugin came from the excellent pipdeptree package
4
3
  * https://github.com/tox-dev/pipdeptree
@@ -13,7 +12,7 @@ import {
13
12
  writeFileSync,
14
13
  } from "node:fs";
15
14
  import { delimiter, join } from "node:path";
16
- import { getTmpDir } from "../helpers/utils.js";
15
+ import { getTmpDir, safeSpawnSync } from "../helpers/utils.js";
17
16
 
18
17
  const PIP_TREE_PLUGIN_CONTENT = `
19
18
  import importlib.metadata as importlib_metadata
@@ -22,6 +21,16 @@ import sys
22
21
 
23
22
  from pip._internal.metadata import pkg_resources
24
23
 
24
+ REQUIREMENT_MODULE_FOUND = False
25
+ try:
26
+ from packaging.requirements import Requirement
27
+ REQUIREMENT_MODULE_FOUND = True
28
+ except ImportError:
29
+ try:
30
+ from pip._vendor.packaging.requirements import Requirement
31
+ REQUIREMENT_MODULE_FOUND = True
32
+ except ImportError:
33
+ pass
25
34
 
26
35
  def frozen_req_from_dist(dist):
27
36
  try:
@@ -41,8 +50,8 @@ def frozen_req_from_dist(dist):
41
50
  pass
42
51
 
43
52
 
44
- def get_installed_distributions():
45
- dists = pkg_resources.Environment.from_paths(None).iter_installed_distributions(
53
+ def get_installed_distributions(python_path=None):
54
+ dists = pkg_resources.Environment.from_paths(python_path).iter_installed_distributions(
46
55
  local_only=False,
47
56
  skip=(),
48
57
  user_only=False,
@@ -50,7 +59,81 @@ def get_installed_distributions():
50
59
  return [d._dist for d in dists]
51
60
 
52
61
 
53
- def find_deps(idx, path, reqs, traverse_count):
62
+ def _get_extra_deps_from_dist(dist):
63
+ extra_deps = {}
64
+ if not dist:
65
+ return extra_deps
66
+ # all requirements, some of which may be extra-only:
67
+ reqs = dist.metadata.get_all('Requires-Dist') or []
68
+ # extras this package defines:
69
+ extras = dist.metadata.get_all('Provides-Extra') or []
70
+ for req_str in reqs:
71
+ req = Requirement(req_str)
72
+ if req.marker and 'extra' in str(req.marker):
73
+ # evaluate marker for each declared extra
74
+ for extra in extras:
75
+ if req.marker.evaluate({'extra': extra}):
76
+ extra_deps.setdefault(extra, []).append({"name": str(req.name), "versionSpecifiers": str(req.specifier), "url": str(req.url) if req.url else None})
77
+ return extra_deps
78
+
79
+
80
+ def _get_deps_from_extras(name_version_cache, name_dist_cache, extra_deps):
81
+ dependencies = []
82
+ if not extra_deps:
83
+ return dependencies
84
+ # Treat an extra with the name all as dependencies
85
+ all_deps = extra_deps.get("all", [])
86
+ for dep in all_deps:
87
+ dversion = name_version_cache.get(dep["name"])
88
+ if not dversion:
89
+ continue
90
+ dversionSpecifiers = dep.get("versionSpecifiers")
91
+ dpurl = f"""pkg:pypi/{dep["name"].lower()}@{dversion}"""
92
+ dextra_deps = _get_extra_deps_from_dist(name_dist_cache.get(dep["name"]))
93
+ ddependencies = _get_deps_from_extras(name_version_cache, name_dist_cache, dextra_deps)
94
+ dependencies.append({
95
+ "name": dep["name"],
96
+ "version": dversion,
97
+ "versionSpecifiers": dversionSpecifiers,
98
+ "purl": dpurl,
99
+ "extra_deps": dextra_deps,
100
+ "dependencies": ddependencies
101
+ })
102
+ return dependencies
103
+
104
+
105
+ def get_installed_with_extras():
106
+ result = {}
107
+ if not REQUIREMENT_MODULE_FOUND:
108
+ return result
109
+ name_version_cache = {}
110
+ name_dist_cache = {}
111
+ for dist in importlib_metadata.distributions():
112
+ name = dist.metadata['Name']
113
+ version = dist.version or ""
114
+ name_version_cache[name] = version
115
+ name_dist_cache[name] = dist
116
+ for dist in importlib_metadata.distributions():
117
+ name = dist.metadata['Name']
118
+ version = dist.version or ""
119
+ # extras this package defines:
120
+ extras = dist.metadata.get_all('Provides-Extra') or []
121
+ # map each extra → its extra-only dependencies
122
+ extra_deps = _get_extra_deps_from_dist(dist)
123
+ purl = f"pkg:pypi/{name.lower()}@{version}"
124
+ dependencies = _get_deps_from_extras(name_version_cache, name_dist_cache, extra_deps)
125
+ result[purl] = {
126
+ 'name': name,
127
+ 'version': version,
128
+ 'extras': extras,
129
+ 'purl': purl,
130
+ 'extra_deps': extra_deps,
131
+ "dependencies": dependencies
132
+ }
133
+ return result
134
+
135
+
136
+ def find_deps(idx, path, purl, reqs, global_installed, traverse_count):
54
137
  freqs = []
55
138
  for r in reqs:
56
139
  d = idx.get(r.key)
@@ -58,17 +141,26 @@ def find_deps(idx, path, reqs, traverse_count):
58
141
  continue
59
142
  r.project_name = d.project_name if d is not None else r.project_name
60
143
  if r.key in path:
144
+ print(f"Cycle detected: {' -> '.join(current_path)}")
61
145
  continue
62
146
  current_path = path + [r.key]
63
147
  specs = sorted(r.specs, reverse=True)
64
148
  specs_str = ",".join(["".join(sp) for sp in specs]) if specs else ""
65
149
  dreqs = d.requires()
150
+ name = r.project_name
151
+ version = importlib_metadata.version(r.key)
152
+ purl = f"pkg:pypi/{name.lower()}@{version}"
153
+ extra_deps = global_installed.get(purl, {}).get("extra_deps", {})
154
+ dependencies = find_deps(idx, current_path, purl, dreqs, global_installed, traverse_count + 1) if dreqs and traverse_count < 200 else []
155
+ all_dependencies = global_installed.get(purl, {}).get("dependencies", [])
66
156
  freqs.append(
67
157
  {
68
- "name": r.project_name,
69
- "version": importlib_metadata.version(r.key),
158
+ "name": name,
159
+ "version": version,
70
160
  "versionSpecifiers": specs_str,
71
- "dependencies": find_deps(idx, current_path, dreqs, traverse_count + 1) if dreqs and traverse_count < 200 else [],
161
+ 'purl': purl,
162
+ "extra_deps": extra_deps,
163
+ "dependencies": dependencies + all_dependencies,
72
164
  }
73
165
  )
74
166
  return freqs
@@ -77,7 +169,8 @@ def find_deps(idx, path, reqs, traverse_count):
77
169
  def main(argv):
78
170
  out_file = "piptree.json" if len(argv) < 2 else argv[-1]
79
171
  tree = []
80
- pkgs = get_installed_distributions()
172
+ global_installed = get_installed_with_extras()
173
+ pkgs = get_installed_distributions(python_path=None)
81
174
  idx = {p.key: p for p in pkgs}
82
175
  traverse_count = 0
83
176
  for p in pkgs:
@@ -93,22 +186,22 @@ def main(argv):
93
186
  version = "latest"
94
187
  if len(tmpA) == 2:
95
188
  version = tmpA[1]
189
+ pkgName = name.split(" ")[0]
190
+ purl = f"pkg:pypi/{pkgName.lower()}@{version}"
191
+ extra_deps = global_installed.get(purl, {}).get("extra_deps", "")
192
+ all_dependencies = global_installed.get(purl, {}).get("dependencies", [])
193
+ dependencies = find_deps(idx, [p.key], purl, p.requires(), global_installed, traverse_count + 1)
96
194
  tree.append(
97
195
  {
98
- "name": name.split(" ")[0],
196
+ "name": pkgName,
99
197
  "version": version,
100
- "dependencies": find_deps(idx, [p.key], p.requires(), traverse_count + 1),
198
+ "purl": purl,
199
+ "extra_deps": extra_deps,
200
+ "dependencies": dependencies + all_dependencies,
101
201
  }
102
202
  )
103
- all_deps = {}
104
- for t in tree:
105
- for d in t["dependencies"]:
106
- all_deps[d["name"]] = True
107
- trimmed_tree = [
108
- t for t in tree if t["name"] not in all_deps
109
- ]
110
203
  with open(out_file, mode="w", encoding="utf-8") as fp:
111
- json.dump(trimmed_tree, fp)
204
+ json.dump(tree, fp)
112
205
 
113
206
 
114
207
  if __name__ == "__main__":
@@ -141,7 +234,7 @@ export const getTreeWithPlugin = (env, python_cmd, basePath) => {
141
234
  env.PYTHONPATH = `${env.PYTHONPATH}${delimiter}${env.PIP_TARGET}`;
142
235
  }
143
236
  }
144
- const result = spawnSync(python_cmd, pipPluginArgs, {
237
+ const result = safeSpawnSync(python_cmd, pipPluginArgs, {
145
238
  cwd: basePath,
146
239
  encoding: "utf-8",
147
240
  env,
@@ -216,12 +216,22 @@ components:
216
216
  type: object
217
217
  properties:
218
218
  type:
219
- type: string
220
- description: Project Type
219
+ type: array
220
+ items:
221
+ type: string
222
+ description: Project Types
221
223
  default: "universal"
222
224
  externalDocs:
223
225
  description: Single or comma separated values. See supported project types
224
226
  url: https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES
227
+ excludeType:
228
+ type: array
229
+ items:
230
+ type: string
231
+ description: Exclude Types
232
+ externalDocs:
233
+ description: Project types to exclude
234
+ url: https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES
225
235
  multiProject:
226
236
  type: boolean
227
237
  requiredOnly:
@@ -259,7 +269,7 @@ components:
259
269
  specVersion:
260
270
  type: string
261
271
  description: CycloneDX Specification version to use
262
- default: "1.5"
272
+ default: "1.6"
263
273
  filter:
264
274
  type: array
265
275
  items:
@@ -304,6 +314,14 @@ components:
304
314
  standard:
305
315
  type: string
306
316
  description: The list of standards which may consist of regulations, industry or organizational-specific standards, maturity models, best practices, or any other requirements which can be evaluated against or attested to. Choices are asvs-4.0.3, bsimm-v13, masvs-2.0.0, nist_ssdf-1.1, pcissc-secure-slc-1.1, scvs-1.0.0, ssaf-DRAFT-2023-11
317
+ minConfidence:
318
+ type: number
319
+ description: Minimum confidence needed for the identity of a component from 0 - 1, where 1 is 100% confidence.
320
+ technique:
321
+ type: array
322
+ items:
323
+ type: string
324
+ description: Analysis technique to use
307
325
  CycloneDXSBOM:
308
326
  type: object
309
327
  externalDocs:
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import fs from "node:fs";
3
2
  import http from "node:http";
4
3
  import path from "node:path";
@@ -7,7 +6,7 @@ import { URL } from "node:url";
7
6
  import bodyParser from "body-parser";
8
7
  import connect from "connect";
9
8
  import { createBom, submitBom } from "../cli/index.js";
10
- import { getTmpDir, isSecureMode } from "../helpers/utils.js";
9
+ import { getTmpDir, isSecureMode, safeSpawnSync } from "../helpers/utils.js";
11
10
  import { postProcess } from "../stages/postgen/postgen.js";
12
11
 
13
12
  import compression from "compression";
@@ -16,6 +15,37 @@ import compression from "compression";
16
15
  const TIMEOUT_MS =
17
16
  Number.parseInt(process.env.CDXGEN_SERVER_TIMEOUT_MS) || 10 * 60 * 1000;
18
17
 
18
+ const ALLOWED_PARAMS = [
19
+ "type",
20
+ "excludeType",
21
+ "multiProject",
22
+ "requiredOnly",
23
+ "noBabel",
24
+ "installDeps",
25
+ "projectId",
26
+ "projectName",
27
+ "projectGroup",
28
+ "projectVersion",
29
+ "parentUUID",
30
+ "serverUrl",
31
+ "apiKey",
32
+ "specVersion",
33
+ "filter",
34
+ "only",
35
+ "autoCompositions",
36
+ "gitBranch",
37
+ "lifecycle",
38
+ "deep",
39
+ "profile",
40
+ "exclude",
41
+ "includeFormulation",
42
+ "includeCrypto",
43
+ "standard",
44
+ "minConfidence",
45
+ "technique",
46
+ "tlpClassification",
47
+ ];
48
+
19
49
  const app = connect();
20
50
 
21
51
  app.use(
@@ -58,7 +88,7 @@ const gitClone = (repoUrl, branch = null) => {
58
88
  `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
59
89
  );
60
90
 
61
- const result = spawnSync("git", gitArgs, {
91
+ const result = safeSpawnSync("git", gitArgs, {
62
92
  encoding: "utf-8",
63
93
  shell: false,
64
94
  });
@@ -69,41 +99,11 @@ const gitClone = (repoUrl, branch = null) => {
69
99
  return tempDir;
70
100
  };
71
101
 
72
- const parseQueryString = (q, body, options = {}) => {
73
- if (body && Object.keys(body).length) {
74
- options = Object.assign(options, body);
75
- }
76
-
77
- const queryParams = [
78
- "type",
79
- "multiProject",
80
- "requiredOnly",
81
- "noBabel",
82
- "installDeps",
83
- "projectId",
84
- "projectName",
85
- "projectGroup",
86
- "projectVersion",
87
- "parentUUID",
88
- "serverUrl",
89
- "apiKey",
90
- "specVersion",
91
- "filter",
92
- "only",
93
- "autoCompositions",
94
- "gitBranch",
95
- "lifecycle",
96
- "deep",
97
- "profile",
98
- "exclude",
99
- "includeFormulation",
100
- "includeCrypto",
101
- "standard",
102
- ];
103
-
104
- for (const param of queryParams) {
105
- if (q[param]) {
106
- let value = q[param];
102
+ const parseQueryString = (q, body = {}, options = {}) => {
103
+ // Priority is query params followed by body
104
+ for (const param of ALLOWED_PARAMS) {
105
+ if (q[param] || body[param]) {
106
+ let value = q[param] || body[param];
107
107
  // Convert string to boolean
108
108
  if (value === "true") {
109
109
  value = true;
@@ -99,6 +99,7 @@ export function textualMetadata(bomJson) {
99
99
  const { bomType, bomTypeDescription } = findBomType(bomJson);
100
100
  const metadata = bomJson.metadata;
101
101
  const lifecycles = metadata?.lifecycles || [];
102
+ const tlpClassification = metadata.distribution;
102
103
  const cryptoAssetsCount = bomJson?.components?.filter(
103
104
  (c) => c.type === "cryptographic-asset",
104
105
  ).length;
@@ -122,6 +123,9 @@ export function textualMetadata(bomJson) {
122
123
  }
123
124
  }
124
125
  }
126
+ if (tlpClassification) {
127
+ text = `${text} The Traffic Light Protocol (TLP) classification for this document is '${tlpClassification}'.`;
128
+ }
125
129
  if (lifecycles && Array.isArray(lifecycles)) {
126
130
  if (lifecycles.length === 1) {
127
131
  const thePhase = lifecycles[0].phase;
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, rmSync } from "node:fs";
1
+ import { readFileSync, rmSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
3
  import process from "node:process";
4
4
  import { PackageURL } from "packageurl-js";
@@ -9,6 +9,7 @@ import {
9
9
  getTimestamp,
10
10
  getTmpDir,
11
11
  hasAnyProjectType,
12
+ safeExistsSync,
12
13
  } from "../../helpers/utils.js";
13
14
  import { extractTags, findBomType, textualMetadata } from "./annotator.js";
14
15
 
@@ -31,7 +32,7 @@ function relativeDir(d, options) {
31
32
  return rd.includes("all-layers") ? rd.split("all-layers").pop() : rd;
32
33
  }
33
34
  const baseDir = options.filePath || process.cwd();
34
- if (existsSync(baseDir)) {
35
+ if (safeExistsSync(baseDir)) {
35
36
  const rdir = relative(baseDir, d);
36
37
  return rdir.startsWith(join("..", "..")) ? d : rdir;
37
38
  }
@@ -200,7 +201,7 @@ export function applyStandards(bomJson, options) {
200
201
  "templates",
201
202
  `${astandard}.cdx.json`,
202
203
  );
203
- if (existsSync(templateFile)) {
204
+ if (safeExistsSync(templateFile)) {
204
205
  const templateData = JSON.parse(readFileSync(templateFile, "utf-8"));
205
206
  if (templateData?.metadata?.licenses) {
206
207
  if (!bomJson.metadata.licenses) {
@@ -224,6 +225,26 @@ export function applyStandards(bomJson, options) {
224
225
  return bomJson;
225
226
  }
226
227
 
228
+ /**
229
+ * Method to normalize the identity field from a component's evidence block.
230
+ *
231
+ * In different versions of CycloneDX, the `identity` field can be either a single object or an array of objects.
232
+ * This function ensures that the result is always an array for consistent processing.
233
+ *
234
+ * @param {Object} comp - The component object potentially containing evidence.identity.
235
+ * @returns {Array} An array of identity objects (empty if none are present).
236
+ */
237
+ function normalizeIdentities(comp) {
238
+ const identity = comp?.evidence?.identity;
239
+ if (Array.isArray(identity)) {
240
+ return identity;
241
+ }
242
+ if (identity) {
243
+ return [identity];
244
+ }
245
+ return [];
246
+ }
247
+
227
248
  /**
228
249
  * Method to get the purl identity confidence.
229
250
  *
@@ -235,7 +256,7 @@ function getIdentityConfidence(comp) {
235
256
  return undefined;
236
257
  }
237
258
  let confidence;
238
- for (const aidentity of comp?.evidence?.identity || []) {
259
+ for (const aidentity of normalizeIdentities(comp)) {
239
260
  if (aidentity?.field === "purl") {
240
261
  if (confidence === undefined) {
241
262
  confidence = aidentity.confidence || 0;
@@ -258,7 +279,7 @@ function getIdentityTechniques(comp) {
258
279
  return undefined;
259
280
  }
260
281
  const techniques = new Set();
261
- for (const aidentity of comp?.evidence?.identity || []) {
282
+ for (const aidentity of normalizeIdentities(comp)) {
262
283
  if (aidentity?.field === "purl") {
263
284
  for (const amethod of aidentity.methods || []) {
264
285
  techniques.add(amethod?.technique);
@@ -304,7 +325,7 @@ export function filterBom(bomJson, options) {
304
325
  // Set.intersection is only available in node >= 22. See Bug# 1651
305
326
  if (
306
327
  usedTechniques &&
307
- !new Set([...usedTechniques].filter((i) => allowedTechniques.has(i)))
328
+ ![...usedTechniques].some((i) => allowedTechniques.has(i))
308
329
  ) {
309
330
  filtered = true;
310
331
  continue;
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { existsSync, mkdtempSync, readFileSync, readdirSync } from "node:fs";
3
2
  import { arch, platform } from "node:os";
4
3
  import { delimiter, dirname, join, resolve } from "node:path";
@@ -27,6 +26,7 @@ import {
27
26
  isMac,
28
27
  isSecureMode,
29
28
  isWin,
29
+ safeSpawnSync,
30
30
  } from "../../helpers/utils.js";
31
31
 
32
32
  /**
@@ -207,7 +207,7 @@ export function tryLoadNvmAndInstallTool(nodeVersion) {
207
207
  fi
208
208
  `;
209
209
 
210
- const result = spawnSync(process.env.SHELL || "bash", ["-c", command], {
210
+ const result = safeSpawnSync(process.env.SHELL || "bash", ["-c", command], {
211
211
  encoding: "utf-8",
212
212
  shell: process.env.SHELL || true,
213
213
  });
@@ -232,7 +232,7 @@ export function doNpmInstall(filePath, nvmNodePath) {
232
232
  if (isSecureMode) {
233
233
  installArgs = `${installArgs} --ignore-scripts --no-audit`;
234
234
  }
235
- const resultNpmInstall = spawnSync(
235
+ const resultNpmInstall = safeSpawnSync(
236
236
  process.env.SHELL || "bash",
237
237
  [
238
238
  "-i",