@cyclonedx/cdxgen 12.2.1 → 12.3.0

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.
Files changed (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,341 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import { validateSpdx } from "../../validator/bomValidator.js";
4
+ import {
5
+ convertCycloneDxToSpdx,
6
+ SPDX_JSONLD_CONTEXT,
7
+ } from "./spdxConverter.js";
8
+
9
+ function sampleBom() {
10
+ return {
11
+ bomFormat: "CycloneDX",
12
+ specVersion: 1.7,
13
+ serialNumber: "urn:uuid:1b671687-395b-41f5-a30f-a58921a69b79",
14
+ version: 1,
15
+ metadata: {
16
+ timestamp: "2024-02-02T00:00:00Z",
17
+ component: {
18
+ type: "application",
19
+ name: "demo-app",
20
+ version: "1.0.0",
21
+ "bom-ref": "pkg:generic/demo-app@1.0.0",
22
+ properties: [{ name: "cdx:app:tier", value: "backend" }],
23
+ },
24
+ properties: [{ name: "cdx:bom:componentTypes", value: "library" }],
25
+ },
26
+ components: [
27
+ {
28
+ type: "library",
29
+ name: "lodash",
30
+ version: "4.17.21",
31
+ purl: "pkg:npm/lodash@4.17.21",
32
+ "bom-ref": "pkg:npm/lodash@4.17.21",
33
+ hashes: [
34
+ { alg: "SHA-256", content: "abc123" },
35
+ { alg: "BLAKE2s", content: "def456" },
36
+ ],
37
+ properties: [{ name: "cdx:npm:hasInstallScript", value: "true" }],
38
+ externalReferences: [
39
+ { type: "website", url: "https://lodash.com" },
40
+ { type: "vcs", url: "https://github.com/lodash/lodash.git" },
41
+ ],
42
+ author: "Legacy Author",
43
+ authors: [{ name: "Lodash Author", email: "author@lodash.com" }],
44
+ publisher: "OpenJS Foundation",
45
+ maintainers: [{ name: "Lodash Maintainer" }],
46
+ tags: ["utility", "js"],
47
+ licenses: [{ license: { id: "MIT" } }],
48
+ },
49
+ ],
50
+ dependencies: [
51
+ {
52
+ ref: "pkg:generic/demo-app@1.0.0",
53
+ dependsOn: ["pkg:npm/lodash@4.17.21"],
54
+ },
55
+ { ref: "pkg:npm/lodash@4.17.21", dependsOn: [] },
56
+ ],
57
+ formulation: [
58
+ {
59
+ services: [
60
+ {
61
+ "bom-ref": "urn:example:service:api",
62
+ name: "api-service",
63
+ properties: [{ name: "cdx:service:httpMethod", value: "GET" }],
64
+ },
65
+ ],
66
+ workflows: [
67
+ {
68
+ "bom-ref": "urn:example:workflow:build",
69
+ name: "build-workflow",
70
+ tasks: [
71
+ {
72
+ "bom-ref": "urn:example:task:build",
73
+ name: "build-task",
74
+ properties: [
75
+ {
76
+ name: "cdx:github:workflow:hasWritePermissions",
77
+ value: "true",
78
+ },
79
+ ],
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ },
85
+ ],
86
+ };
87
+ }
88
+
89
+ function minimalBom() {
90
+ return {
91
+ bomFormat: "CycloneDX",
92
+ specVersion: 1.7,
93
+ metadata: {
94
+ timestamp: "2024-02-02T00:00:00Z",
95
+ component: {
96
+ type: "application",
97
+ name: "demo-app",
98
+ version: "1.0.0",
99
+ purl: "pkg:generic/demo-app@1.0.0",
100
+ "bom-ref": "pkg:generic/demo-app@1.0.0",
101
+ },
102
+ },
103
+ components: [
104
+ {
105
+ type: "library",
106
+ name: "left-pad",
107
+ version: "1.3.0",
108
+ purl: "pkg:npm/left-pad@1.3.0",
109
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
110
+ },
111
+ ],
112
+ dependencies: [
113
+ {
114
+ ref: "pkg:generic/demo-app@1.0.0",
115
+ dependsOn: ["pkg:npm/left-pad@1.3.0"],
116
+ },
117
+ { ref: "pkg:npm/left-pad@1.3.0", dependsOn: [] },
118
+ ],
119
+ };
120
+ }
121
+
122
+ function getExtensionPropertyMap(spdxElement) {
123
+ const propertyMap = new Map();
124
+ for (const extension of spdxElement?.extension || []) {
125
+ for (const entry of extension?.extension_cdxProperty || []) {
126
+ propertyMap.set(
127
+ entry.extension_cdxPropName,
128
+ entry.extension_cdxPropValue,
129
+ );
130
+ }
131
+ }
132
+ return propertyMap;
133
+ }
134
+
135
+ describe("convertCycloneDxToSpdx", () => {
136
+ it("converts a CycloneDX BOM into SPDX 3.0.1 JSON-LD", () => {
137
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
138
+ projectName: "demo-app",
139
+ });
140
+ assert.strictEqual(spdxJson["@context"], SPDX_JSONLD_CONTEXT);
141
+ assert.ok(Array.isArray(spdxJson["@graph"]));
142
+ assert.ok(
143
+ spdxJson["@graph"].some((element) => element.type === "SpdxDocument"),
144
+ );
145
+ assert.ok(
146
+ spdxJson["@graph"].some((element) => element.type === "Relationship"),
147
+ );
148
+ assert.deepStrictEqual(spdxJson["@graph"][0].createdBy, [
149
+ "https://github.com/cdxgen/cdxgen",
150
+ ]);
151
+ });
152
+
153
+ it("produces an export accepted by the bundled validator", () => {
154
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
155
+ projectName: "demo-app",
156
+ });
157
+ assert.strictEqual(validateSpdx(spdxJson), true);
158
+ });
159
+
160
+ it("converts CycloneDX 1.6 BOMs to valid SPDX 3.0.1 JSON-LD", () => {
161
+ const bom16 = sampleBom();
162
+ bom16.specVersion = 1.6;
163
+ const spdxJson = convertCycloneDxToSpdx(bom16, {
164
+ projectName: "demo-app",
165
+ });
166
+ assert.strictEqual(validateSpdx(spdxJson), true);
167
+ });
168
+
169
+ it("converts CycloneDX 1.7 BOMs to valid SPDX 3.0.1 JSON-LD", () => {
170
+ const bom17 = sampleBom();
171
+ bom17.specVersion = 1.7;
172
+ const spdxJson = convertCycloneDxToSpdx(bom17, {
173
+ projectName: "demo-app",
174
+ });
175
+ assert.strictEqual(validateSpdx(spdxJson), true);
176
+ });
177
+
178
+ it("preserves advanced CycloneDX data in SPDX extension fields", () => {
179
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
180
+ projectName: "demo-app",
181
+ });
182
+ const packageElement = spdxJson["@graph"].find(
183
+ (element) => element.software_packageUrl === "pkg:npm/lodash@4.17.21",
184
+ );
185
+ assert.ok(packageElement);
186
+ assert.ok(Array.isArray(packageElement.externalRef));
187
+ assert.strictEqual(
188
+ packageElement.externalRef[0].externalRefType,
189
+ "altWebPage",
190
+ );
191
+ const packageExtensionProperties = getExtensionPropertyMap(packageElement);
192
+ assert.strictEqual(
193
+ packageElement.extension[0].type,
194
+ "extension_CdxPropertiesExtension",
195
+ );
196
+ assert.strictEqual(
197
+ packageExtensionProperties.get("properties.cdx:npm:hasInstallScript"),
198
+ "true",
199
+ );
200
+ assert.strictEqual(
201
+ packageExtensionProperties.get("hashes"),
202
+ '[{"algorithm":"SHA-256","hashValue":"abc123","normalizedAlgorithm":"sha256"},{"algorithm":"BLAKE2s","hashValue":"def456"}]',
203
+ );
204
+ assert.strictEqual(
205
+ packageExtensionProperties.get("author"),
206
+ "Legacy Author",
207
+ );
208
+ assert.strictEqual(
209
+ packageExtensionProperties.get("authors"),
210
+ '[{"name":"Lodash Author","email":"author@lodash.com"}]',
211
+ );
212
+ assert.strictEqual(
213
+ packageExtensionProperties.get("publisher"),
214
+ "OpenJS Foundation",
215
+ );
216
+ assert.strictEqual(
217
+ packageExtensionProperties.get("maintainers"),
218
+ '[{"name":"Lodash Maintainer"}]',
219
+ );
220
+ assert.strictEqual(
221
+ packageExtensionProperties.get("tags"),
222
+ '["utility","js"]',
223
+ );
224
+ assert.strictEqual(
225
+ packageExtensionProperties.get("licenses"),
226
+ '[{"license":{"id":"MIT"}}]',
227
+ );
228
+ const documentElement = spdxJson["@graph"].find(
229
+ (element) => element.type === "SpdxDocument",
230
+ );
231
+ assert.ok(documentElement);
232
+ const documentExtensionProperties =
233
+ getExtensionPropertyMap(documentElement);
234
+ assert.strictEqual(
235
+ documentElement.profileConformance.includes("extension"),
236
+ true,
237
+ );
238
+ assert.strictEqual(
239
+ documentExtensionProperties.get(
240
+ "metadataProperties.cdx:bom:componentTypes",
241
+ ),
242
+ "library",
243
+ );
244
+ assert.strictEqual(
245
+ documentExtensionProperties.get("formulation"),
246
+ JSON.stringify(sampleBom().formulation),
247
+ );
248
+ });
249
+
250
+ it("omits document-level SPDX extensions while package-level metadata still enables the extension profile", () => {
251
+ const spdxJson = convertCycloneDxToSpdx(minimalBom(), {
252
+ projectName: "demo-app",
253
+ });
254
+ const packageElement = spdxJson["@graph"].find(
255
+ (element) => element.software_packageUrl === "pkg:npm/left-pad@1.3.0",
256
+ );
257
+ const documentElement = spdxJson["@graph"].find(
258
+ (element) => element.type === "SpdxDocument",
259
+ );
260
+ assert.ok(packageElement);
261
+ assert.ok(documentElement);
262
+ assert.strictEqual(documentElement.extension, undefined);
263
+ assert.strictEqual(
264
+ documentElement.profileConformance.includes("extension"),
265
+ true,
266
+ );
267
+ assert.strictEqual(
268
+ getExtensionPropertyMap(packageElement).get("bomRef"),
269
+ "pkg:npm/left-pad@1.3.0",
270
+ );
271
+ });
272
+
273
+ it("uses component bom-ref as document name fallback before version", () => {
274
+ const bom = sampleBom();
275
+ delete bom.metadata.component.name;
276
+ const spdxJson = convertCycloneDxToSpdx(bom);
277
+ const documentElement = spdxJson["@graph"].find(
278
+ (element) => element.type === "SpdxDocument",
279
+ );
280
+ assert.ok(documentElement);
281
+ assert.strictEqual(documentElement.name, "pkg:generic/demo-app@1.0.0");
282
+ });
283
+
284
+ it("rejects malformed SPDX exports", () => {
285
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
286
+ projectName: "demo-app",
287
+ });
288
+ spdxJson["@context"] = "https://example.com/not-spdx";
289
+ assert.strictEqual(validateSpdx(spdxJson), false);
290
+ });
291
+
292
+ it("rejects SPDX relationships with non-string from references", () => {
293
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
294
+ projectName: "demo-app",
295
+ });
296
+ const relationship = spdxJson["@graph"].find(
297
+ (element) => element.type === "Relationship",
298
+ );
299
+ relationship.from = [relationship.from];
300
+ assert.strictEqual(validateSpdx(spdxJson), false);
301
+ });
302
+
303
+ it("rejects SPDX exports with malformed extension entries", () => {
304
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
305
+ projectName: "demo-app",
306
+ });
307
+ const packageElement = spdxJson["@graph"].find(
308
+ (element) => element.software_packageUrl === "pkg:npm/lodash@4.17.21",
309
+ );
310
+ delete packageElement.extension[0].type;
311
+ assert.strictEqual(validateSpdx(spdxJson), false);
312
+ });
313
+
314
+ it("uses the official JSON-LD compact extension terms", () => {
315
+ const spdxJson = convertCycloneDxToSpdx(sampleBom(), {
316
+ projectName: "demo-app",
317
+ });
318
+ const documentElement = spdxJson["@graph"].find(
319
+ (element) => element.type === "SpdxDocument",
320
+ );
321
+ assert.ok(documentElement);
322
+ assert.strictEqual(
323
+ documentElement.extension[0].type,
324
+ "extension_CdxPropertiesExtension",
325
+ );
326
+ assert.strictEqual(
327
+ documentElement.extension[0].extension_cdxProperty[0].type,
328
+ "extension_CdxPropertyEntry",
329
+ );
330
+ assert.strictEqual(
331
+ typeof documentElement.extension[0].extension_cdxProperty[0]
332
+ .extension_cdxPropName,
333
+ "string",
334
+ );
335
+ assert.strictEqual(
336
+ typeof documentElement.extension[0].extension_cdxProperty[0]
337
+ .extension_cdxPropValue,
338
+ "string",
339
+ );
340
+ });
341
+ });
@@ -2,14 +2,55 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import Ajv from "ajv";
5
+ import Ajv2020 from "ajv/dist/2020.js";
5
6
  import addFormats from "ajv-formats";
6
7
  import { PackageURL } from "packageurl-js";
7
8
 
8
9
  import { thoughtLog } from "../helpers/logger.js";
9
10
  import { DEBUG_MODE, dirNameStr, isPartialTree } from "../helpers/utils.js";
11
+ import {
12
+ SPDX_JSONLD_CONTEXT,
13
+ SPDX_SPEC_VERSION,
14
+ } from "../stages/postgen/spdxConverter.js";
10
15
 
11
16
  const dirName = dirNameStr;
12
17
  const PLACEHOLDER_COMPONENT_NAMES = new Set(["app", "application", "project"]);
18
+ const SPDX_EXPORT_TYPES = new Set([
19
+ "CreationInfo",
20
+ "Relationship",
21
+ "SpdxDocument",
22
+ "software_File",
23
+ "software_Package",
24
+ ]);
25
+ let spdxExportSchemaValidator;
26
+
27
+ const getSpdxElementId = (element) => element?.spdxId || element?.["@id"];
28
+
29
+ const getSpdxExportSchemaValidator = () => {
30
+ if (spdxExportSchemaValidator !== undefined) {
31
+ return spdxExportSchemaValidator;
32
+ }
33
+ try {
34
+ const spdxExportSchema = JSON.parse(
35
+ readFileSync(join(dirName, "data", "spdx-export.schema.json"), "utf-8"),
36
+ );
37
+ const ajv = new Ajv2020({
38
+ strict: false,
39
+ logger: false,
40
+ verbose: true,
41
+ code: {
42
+ source: true,
43
+ lines: true,
44
+ optimize: true,
45
+ },
46
+ });
47
+ addFormats(ajv);
48
+ spdxExportSchemaValidator = ajv.compile(spdxExportSchema);
49
+ } catch (_error) {
50
+ spdxExportSchemaValidator = null;
51
+ }
52
+ return spdxExportSchemaValidator;
53
+ };
13
54
 
14
55
  /**
15
56
  * Validate the generated bom using jsonschema
@@ -81,6 +122,197 @@ export const validateBom = (bomJson) => {
81
122
  );
82
123
  };
83
124
 
125
+ /**
126
+ * Validate the generated SPDX export.
127
+ *
128
+ * @param {object|string} spdxJson SPDX json object
129
+ * @returns {boolean} true if the SPDX export is valid
130
+ */
131
+ export const validateSpdx = (spdxJson) => {
132
+ if (!spdxJson) {
133
+ return true;
134
+ }
135
+ if (typeof spdxJson === "string" || spdxJson instanceof String) {
136
+ spdxJson = JSON.parse(spdxJson);
137
+ }
138
+ const spdxDocument = (spdxJson?.["@graph"] || []).find(
139
+ (element) => element?.type === "SpdxDocument",
140
+ );
141
+ const validateSchema = getSpdxExportSchemaValidator();
142
+ if (!validateSchema) {
143
+ console.log("The bundled SPDX export schema is empty or malformed.");
144
+ return false;
145
+ }
146
+ if (!validateSchema(spdxJson)) {
147
+ console.log(
148
+ `SPDX 3.0.1 schema validation failed${spdxDocument?.name ? ` for ${spdxDocument.name}` : ""}`,
149
+ );
150
+ console.log(validateSchema.errors);
151
+ return false;
152
+ }
153
+ const errorList = [];
154
+ if (spdxJson?.["@context"] !== SPDX_JSONLD_CONTEXT) {
155
+ errorList.push(`@context must be '${SPDX_JSONLD_CONTEXT}'.`);
156
+ }
157
+ if (!Array.isArray(spdxJson?.["@graph"]) || !spdxJson["@graph"].length) {
158
+ errorList.push("@graph must be a non-empty array.");
159
+ }
160
+ const ids = new Set();
161
+ const knownRefs = new Set();
162
+ let creationInfoCount = 0;
163
+ let documentCount = 0;
164
+ let usesExtensionProfile = false;
165
+ for (const element of spdxJson?.["@graph"] || []) {
166
+ if (!SPDX_EXPORT_TYPES.has(element?.type)) {
167
+ errorList.push(`Unsupported SPDX export type '${element?.type}'.`);
168
+ }
169
+ const elementId = getSpdxElementId(element);
170
+ if (!elementId || typeof elementId !== "string") {
171
+ errorList.push(
172
+ `Missing SPDX identifier for type '${element?.type || "unknown"}'.`,
173
+ );
174
+ continue;
175
+ }
176
+ if (ids.has(elementId)) {
177
+ errorList.push(`Duplicate SPDX identifier '${elementId}'.`);
178
+ }
179
+ ids.add(elementId);
180
+ knownRefs.add(elementId);
181
+ if (Array.isArray(element?.extension) && element.extension.length) {
182
+ usesExtensionProfile = true;
183
+ }
184
+ switch (element.type) {
185
+ case "CreationInfo":
186
+ creationInfoCount += 1;
187
+ if (element.specVersion !== SPDX_SPEC_VERSION) {
188
+ errorList.push(
189
+ `CreationInfo '${elementId}' has unexpected specVersion '${element.specVersion}'.`,
190
+ );
191
+ }
192
+ if (!Array.isArray(element.createdBy) || !element.createdBy.length) {
193
+ errorList.push(`CreationInfo '${elementId}' must include createdBy.`);
194
+ }
195
+ if (!element.created) {
196
+ errorList.push(`CreationInfo '${elementId}' must include created.`);
197
+ }
198
+ break;
199
+ case "SpdxDocument":
200
+ documentCount += 1;
201
+ if (!element.creationInfo) {
202
+ errorList.push(
203
+ `SpdxDocument '${elementId}' must include creationInfo.`,
204
+ );
205
+ }
206
+ if (!Array.isArray(element.element) || !element.element.length) {
207
+ errorList.push(
208
+ `SpdxDocument '${elementId}' must include element refs.`,
209
+ );
210
+ }
211
+ if (
212
+ element.rootElement &&
213
+ (!Array.isArray(element.rootElement) || !element.rootElement.length)
214
+ ) {
215
+ errorList.push(
216
+ `SpdxDocument '${elementId}' rootElement must be a non-empty array when present.`,
217
+ );
218
+ }
219
+ break;
220
+ case "Relationship":
221
+ if (
222
+ !element.creationInfo ||
223
+ !element.from ||
224
+ typeof element.from !== "string"
225
+ ) {
226
+ errorList.push(
227
+ `Relationship '${elementId}' must include creationInfo and from.`,
228
+ );
229
+ }
230
+ if (!element.to || !Array.isArray(element.to) || !element.to.length) {
231
+ errorList.push(`Relationship '${elementId}' must include to refs.`);
232
+ }
233
+ if (element.relationshipType !== "dependsOn") {
234
+ errorList.push(
235
+ `Relationship '${elementId}' has unsupported relationshipType '${element.relationshipType}'.`,
236
+ );
237
+ }
238
+ break;
239
+ case "software_File":
240
+ case "software_Package":
241
+ if (!element.creationInfo || !element.name) {
242
+ errorList.push(
243
+ `${element.type} '${elementId}' must include creationInfo and name.`,
244
+ );
245
+ }
246
+ break;
247
+ default:
248
+ break;
249
+ }
250
+ }
251
+ if (creationInfoCount !== 1) {
252
+ errorList.push(
253
+ `Expected exactly one CreationInfo, found ${creationInfoCount}.`,
254
+ );
255
+ }
256
+ if (documentCount !== 1) {
257
+ errorList.push(
258
+ `Expected exactly one SpdxDocument, found ${documentCount}.`,
259
+ );
260
+ }
261
+ for (const element of spdxJson?.["@graph"] || []) {
262
+ const elementId = getSpdxElementId(element);
263
+ if (element.creationInfo && !knownRefs.has(element.creationInfo)) {
264
+ errorList.push(
265
+ `Element '${elementId || "unknown"}' references unknown creationInfo '${element.creationInfo}'.`,
266
+ );
267
+ }
268
+ if (Array.isArray(element.element)) {
269
+ for (const ref of element.element) {
270
+ if (!knownRefs.has(ref)) {
271
+ errorList.push(
272
+ `SpdxDocument '${elementId || "unknown"}' references unknown element '${ref}'.`,
273
+ );
274
+ }
275
+ }
276
+ }
277
+ if (Array.isArray(element.rootElement)) {
278
+ for (const ref of element.rootElement) {
279
+ if (!knownRefs.has(ref)) {
280
+ errorList.push(
281
+ `SpdxDocument '${elementId || "unknown"}' references unknown rootElement '${ref}'.`,
282
+ );
283
+ }
284
+ }
285
+ }
286
+ if (typeof element.from === "string" && !knownRefs.has(element.from)) {
287
+ errorList.push(
288
+ `Relationship '${elementId || "unknown"}' references unknown from '${element.from}'.`,
289
+ );
290
+ }
291
+ if (Array.isArray(element.to)) {
292
+ for (const ref of element.to) {
293
+ if (!knownRefs.has(ref)) {
294
+ errorList.push(
295
+ `Relationship '${elementId || "unknown"}' references unknown to '${ref}'.`,
296
+ );
297
+ }
298
+ }
299
+ }
300
+ }
301
+ if (usesExtensionProfile) {
302
+ if (!spdxDocument?.profileConformance?.includes("extension")) {
303
+ errorList.push(
304
+ `SpdxDocument '${spdxDocument?.spdxId || "unknown"}' must declare the extension profile when extension data is present.`,
305
+ );
306
+ }
307
+ }
308
+ if (errorList.length > 0) {
309
+ console.log("SPDX 3.0.1 validation failed");
310
+ console.log(errorList);
311
+ return false;
312
+ }
313
+ return true;
314
+ };
315
+
84
316
  /**
85
317
  * Validate the metadata object
86
318
  *
@@ -0,0 +1,70 @@
1
+ import esmock from "esmock";
2
+ import { assert, describe, it } from "poku";
3
+ import sinon from "sinon";
4
+
5
+ describe("validateSpdx()", () => {
6
+ it("lazy-loads the bundled SPDX export schema on first validation call", async () => {
7
+ const readFileSyncStub = sinon
8
+ .stub()
9
+ .returns(
10
+ '{"type":"object","properties":{"@context":{"const":"https://spdx.org/rdf/3.0.1/spdx-context.jsonld"},"@graph":{"type":"array"}}}',
11
+ );
12
+ const { validateSpdx } = await esmock("./bomValidator.js", {
13
+ "node:fs": {
14
+ readFileSync: readFileSyncStub,
15
+ },
16
+ "../helpers/utils.js": {
17
+ DEBUG_MODE: false,
18
+ dirNameStr: "/tmp",
19
+ isPartialTree: sinon.stub().returns(false),
20
+ },
21
+ "../stages/postgen/spdxConverter.js": {
22
+ SPDX_JSONLD_CONTEXT: "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
23
+ SPDX_SPEC_VERSION: "3.0.1",
24
+ },
25
+ });
26
+
27
+ sinon.assert.notCalled(readFileSyncStub);
28
+ assert.strictEqual(
29
+ validateSpdx({
30
+ "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
31
+ "@graph": [],
32
+ }),
33
+ false,
34
+ );
35
+ sinon.assert.calledOnce(readFileSyncStub);
36
+ });
37
+
38
+ it("caches the bundled SPDX export schema between validation calls", async () => {
39
+ const readFileSyncStub = sinon
40
+ .stub()
41
+ .returns(
42
+ '{"type":"object","properties":{"@context":{"const":"https://spdx.org/rdf/3.0.1/spdx-context.jsonld"},"@graph":{"type":"array"}}}',
43
+ );
44
+ const { validateSpdx } = await esmock("./bomValidator.js", {
45
+ "node:fs": {
46
+ readFileSync: readFileSyncStub,
47
+ },
48
+ "../helpers/utils.js": {
49
+ DEBUG_MODE: false,
50
+ dirNameStr: "/tmp",
51
+ isPartialTree: sinon.stub().returns(false),
52
+ },
53
+ "../stages/postgen/spdxConverter.js": {
54
+ SPDX_JSONLD_CONTEXT: "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
55
+ SPDX_SPEC_VERSION: "3.0.1",
56
+ },
57
+ });
58
+
59
+ validateSpdx({
60
+ "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
61
+ "@graph": [],
62
+ });
63
+ validateSpdx({
64
+ "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
65
+ "@graph": [],
66
+ });
67
+
68
+ sinon.assert.calledOnce(readFileSyncStub);
69
+ });
70
+ });