@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,771 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import esmock from "esmock";
6
+ import { assert, describe, it } from "poku";
7
+ import sinon from "sinon";
8
+
9
+ describe("source helper purl resolution", () => {
10
+ it("resolves npm purl to repository URL", async () => {
11
+ const getStub = sinon.stub().resolves({
12
+ body: {
13
+ repository: {
14
+ url: "git+https://github.com/cdxgen/cdxgen.git#main",
15
+ },
16
+ },
17
+ });
18
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
19
+ "./utils.js": {
20
+ cdxgenAgent: { get: getStub },
21
+ DEBUG_MODE: false,
22
+ fetchPomXmlAsJson: sinon.stub(),
23
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
24
+ hasDangerousUnicode: sinon.stub().returns(false),
25
+ isSecureMode: false,
26
+ isValidDriveRoot: sinon.stub().returns(true),
27
+ isWin: false,
28
+ safeSpawnSync: sinon.stub(),
29
+ },
30
+ });
31
+
32
+ const result = await resolveGitUrlFromPurl("pkg:npm/cdxgen@12.3.0");
33
+
34
+ assert.strictEqual(result.repoUrl, "https://github.com/cdxgen/cdxgen.git");
35
+ });
36
+
37
+ it("resolves pypi purl using project_urls source fields", async () => {
38
+ const getStub = sinon.stub().resolves({
39
+ body: {
40
+ info: {
41
+ project_urls: {
42
+ Source: "https://github.com/pallets/flask",
43
+ },
44
+ },
45
+ },
46
+ });
47
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
48
+ "./utils.js": {
49
+ cdxgenAgent: { get: getStub },
50
+ DEBUG_MODE: false,
51
+ fetchPomXmlAsJson: sinon.stub(),
52
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
53
+ hasDangerousUnicode: sinon.stub().returns(false),
54
+ isSecureMode: false,
55
+ isValidDriveRoot: sinon.stub().returns(true),
56
+ isWin: false,
57
+ safeSpawnSync: sinon.stub(),
58
+ },
59
+ });
60
+
61
+ const result = await resolveGitUrlFromPurl("pkg:pypi/flask@3.1.2");
62
+
63
+ assert.strictEqual(result.repoUrl, "https://github.com/pallets/flask");
64
+ });
65
+
66
+ it("returns undefined for unsupported purl type", async () => {
67
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
68
+ "./utils.js": {
69
+ cdxgenAgent: { get: sinon.stub() },
70
+ DEBUG_MODE: false,
71
+ fetchPomXmlAsJson: sinon.stub(),
72
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
73
+ hasDangerousUnicode: sinon.stub().returns(false),
74
+ isSecureMode: false,
75
+ isValidDriveRoot: sinon.stub().returns(true),
76
+ isWin: false,
77
+ safeSpawnSync: sinon.stub(),
78
+ },
79
+ });
80
+
81
+ const result = await resolveGitUrlFromPurl("pkg:hex/phoenix@1.7.14");
82
+
83
+ assert.strictEqual(result, undefined);
84
+ });
85
+
86
+ it("validates unsupported purl type explicitly", async () => {
87
+ const { validatePurlSource } = await esmock("./source.js", {
88
+ "./utils.js": {
89
+ cdxgenAgent: { get: sinon.stub() },
90
+ DEBUG_MODE: false,
91
+ fetchPomXmlAsJson: sinon.stub(),
92
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
93
+ hasDangerousUnicode: sinon.stub().returns(false),
94
+ isSecureMode: false,
95
+ isValidDriveRoot: sinon.stub().returns(true),
96
+ isWin: false,
97
+ safeSpawnSync: sinon.stub(),
98
+ },
99
+ });
100
+
101
+ const result = validatePurlSource("pkg:hex/phoenix@1.7.14");
102
+
103
+ assert.strictEqual(result.error, "Unsupported purl source type");
104
+ });
105
+
106
+ it("resolves github purl to repository URL without registry lookup", async () => {
107
+ const getStub = sinon.stub();
108
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
109
+ "./utils.js": {
110
+ cdxgenAgent: { get: getStub },
111
+ DEBUG_MODE: false,
112
+ fetchPomXmlAsJson: sinon.stub(),
113
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
114
+ hasDangerousUnicode: sinon.stub().returns(false),
115
+ isSecureMode: false,
116
+ isValidDriveRoot: sinon.stub().returns(true),
117
+ isWin: false,
118
+ safeSpawnSync: sinon.stub(),
119
+ },
120
+ });
121
+
122
+ const result = await resolveGitUrlFromPurl("pkg:github/cdxgen/cdxgen");
123
+
124
+ assert.strictEqual(result.repoUrl, "https://github.com/cdxgen/cdxgen");
125
+ assert.strictEqual(getStub.callCount, 0);
126
+ });
127
+
128
+ it("resolves bitbucket purl to repository URL without registry lookup", async () => {
129
+ const getStub = sinon.stub();
130
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
131
+ "./utils.js": {
132
+ cdxgenAgent: { get: getStub },
133
+ DEBUG_MODE: false,
134
+ fetchPomXmlAsJson: sinon.stub(),
135
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
136
+ hasDangerousUnicode: sinon.stub().returns(false),
137
+ isSecureMode: false,
138
+ isValidDriveRoot: sinon.stub().returns(true),
139
+ isWin: false,
140
+ safeSpawnSync: sinon.stub(),
141
+ },
142
+ });
143
+
144
+ const result = await resolveGitUrlFromPurl("pkg:bitbucket/acme/team-lib");
145
+
146
+ assert.strictEqual(result.repoUrl, "https://bitbucket.org/acme/team-lib");
147
+ assert.strictEqual(getStub.callCount, 0);
148
+ });
149
+
150
+ it("resolves maven purl from pom scm metadata", async () => {
151
+ const getStub = sinon.stub();
152
+ const fetchPomXmlAsJson = sinon.stub().resolves({
153
+ scm: {
154
+ url: {
155
+ _: "scm:git:https://github.com/apache/commons-lang.git",
156
+ },
157
+ },
158
+ });
159
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
160
+ "./utils.js": {
161
+ cdxgenAgent: { get: getStub },
162
+ DEBUG_MODE: false,
163
+ fetchPomXmlAsJson,
164
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
165
+ hasDangerousUnicode: sinon.stub().returns(false),
166
+ isSecureMode: false,
167
+ isValidDriveRoot: sinon.stub().returns(true),
168
+ isWin: false,
169
+ safeSpawnSync: sinon.stub(),
170
+ },
171
+ });
172
+
173
+ const result = await resolveGitUrlFromPurl(
174
+ "pkg:maven/org.apache.commons/commons-lang3@3.17.0",
175
+ );
176
+
177
+ assert.strictEqual(
178
+ result.repoUrl,
179
+ "https://github.com/apache/commons-lang.git",
180
+ );
181
+ assert.strictEqual(
182
+ fetchPomXmlAsJson.firstCall.args[0].urlPrefix,
183
+ "https://repo1.maven.org/maven2/",
184
+ );
185
+ assert.strictEqual(
186
+ fetchPomXmlAsJson.firstCall.args[0].group,
187
+ "org.apache.commons",
188
+ );
189
+ assert.strictEqual(
190
+ fetchPomXmlAsJson.firstCall.args[0].name,
191
+ "commons-lang3",
192
+ );
193
+ assert.strictEqual(fetchPomXmlAsJson.firstCall.args[0].version, "3.17.0");
194
+ assert.strictEqual(getStub.callCount, 0);
195
+ });
196
+
197
+ it("resolves maven purl from pom scm connection metadata", async () => {
198
+ const fetchPomXmlAsJson = sinon.stub().resolves({
199
+ scm: {
200
+ connection: {
201
+ _: "scm:git:git://github.com/apache/commons-lang.git",
202
+ },
203
+ },
204
+ });
205
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
206
+ "./utils.js": {
207
+ cdxgenAgent: { get: sinon.stub() },
208
+ DEBUG_MODE: false,
209
+ fetchPomXmlAsJson,
210
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
211
+ hasDangerousUnicode: sinon.stub().returns(false),
212
+ isSecureMode: false,
213
+ isValidDriveRoot: sinon.stub().returns(true),
214
+ isWin: false,
215
+ safeSpawnSync: sinon.stub(),
216
+ },
217
+ });
218
+
219
+ const result = await resolveGitUrlFromPurl(
220
+ "pkg:maven/org.apache.commons/commons-lang3@3.17.0",
221
+ );
222
+
223
+ assert.strictEqual(
224
+ result.repoUrl,
225
+ "git://github.com/apache/commons-lang.git",
226
+ );
227
+ });
228
+
229
+ it("resolves composer purl from packagist source metadata", async () => {
230
+ const getStub = sinon.stub().resolves({
231
+ body: {
232
+ packages: {
233
+ "laravel/framework": [
234
+ {
235
+ version: "v11.36.0",
236
+ source: {
237
+ type: "git",
238
+ url: "https://github.com/laravel/framework.git",
239
+ },
240
+ },
241
+ ],
242
+ },
243
+ },
244
+ });
245
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
246
+ "./utils.js": {
247
+ cdxgenAgent: { get: getStub },
248
+ DEBUG_MODE: false,
249
+ fetchPomXmlAsJson: sinon.stub(),
250
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
251
+ hasDangerousUnicode: sinon.stub().returns(false),
252
+ isSecureMode: false,
253
+ isValidDriveRoot: sinon.stub().returns(true),
254
+ isWin: false,
255
+ safeSpawnSync: sinon.stub(),
256
+ },
257
+ });
258
+
259
+ const result = await resolveGitUrlFromPurl(
260
+ "pkg:composer/laravel/framework@v11.36.0",
261
+ );
262
+
263
+ assert.strictEqual(
264
+ result.repoUrl,
265
+ "https://github.com/laravel/framework.git",
266
+ );
267
+ assert.strictEqual(
268
+ getStub.firstCall.args[0],
269
+ "https://repo.packagist.org/p2/laravel/framework.json",
270
+ );
271
+ });
272
+
273
+ it("logs underlying registry lookup errors for purl resolution", async () => {
274
+ const originalNpmUrl = process.env.NPM_URL;
275
+ process.env.NPM_URL = "https://user:secret@example.com/repository/npm/";
276
+ const consoleErrorStub = sinon.stub(console, "error");
277
+ const lookupError = new Error("connect ECONNREFUSED");
278
+ lookupError.code = "ECONNREFUSED";
279
+ lookupError.hostname = "example.com";
280
+ const getStub = sinon.stub().rejects(lookupError);
281
+ try {
282
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
283
+ "./utils.js": {
284
+ cdxgenAgent: { get: getStub },
285
+ DEBUG_MODE: false,
286
+ fetchPomXmlAsJson: sinon.stub(),
287
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
288
+ hasDangerousUnicode: sinon.stub().returns(false),
289
+ isSecureMode: false,
290
+ isValidDriveRoot: sinon.stub().returns(true),
291
+ isWin: false,
292
+ safeSpawnSync: sinon.stub(),
293
+ },
294
+ });
295
+
296
+ const result = await resolveGitUrlFromPurl("pkg:npm/lodash@4.17.21");
297
+
298
+ assert.strictEqual(result, undefined);
299
+ sinon.assert.calledOnce(consoleErrorStub);
300
+ sinon.assert.calledWithMatch(
301
+ consoleErrorStub,
302
+ sinon.match(
303
+ /Unable to resolve repository URL for purl 'pkg:npm\/lodash@4\.17\.21' using registry 'https:\/\/\*\*\*:\*\*\*@example\.com\/repository\/npm\/': connect ECONNREFUSED \(code=ECONNREFUSED, host=example\.com\)/,
304
+ ),
305
+ );
306
+ } finally {
307
+ consoleErrorStub.restore();
308
+ if (originalNpmUrl === undefined) {
309
+ delete process.env.NPM_URL;
310
+ } else {
311
+ process.env.NPM_URL = originalNpmUrl;
312
+ }
313
+ }
314
+ });
315
+
316
+ it("cleans up temp directories even when the provided path uses a symlinked temp alias", async () => {
317
+ const realTmpRoot = fs.mkdtempSync(
318
+ path.join(os.tmpdir(), "cdxgen-real-tmp-"),
319
+ );
320
+ const realTarget = path.join(realTmpRoot, "checkout");
321
+ const aliasRoot = path.join(os.tmpdir(), `cdxgen-tmp-alias-${Date.now()}`);
322
+ const aliasTarget = path.join(aliasRoot, "checkout");
323
+ fs.mkdirSync(realTarget, { recursive: true });
324
+ fs.symlinkSync(realTmpRoot, aliasRoot, "dir");
325
+ const { cleanupSourceDir } = await esmock("./source.js", {
326
+ "./logger.js": {
327
+ thoughtLog: sinon.stub(),
328
+ },
329
+ "./utils.js": {
330
+ cdxgenAgent: { get: sinon.stub() },
331
+ DEBUG_MODE: false,
332
+ fetchPomXmlAsJson: sinon.stub(),
333
+ getTmpDir: sinon.stub().returns(realTmpRoot),
334
+ hasDangerousUnicode: sinon.stub().returns(false),
335
+ isSecureMode: false,
336
+ isValidDriveRoot: sinon.stub().returns(true),
337
+ isWin: false,
338
+ safeSpawnSync: sinon.stub(),
339
+ },
340
+ });
341
+
342
+ try {
343
+ assert.ok(fs.existsSync(aliasTarget));
344
+ cleanupSourceDir(aliasTarget);
345
+ assert.strictEqual(fs.existsSync(aliasTarget), false);
346
+ assert.strictEqual(fs.existsSync(realTarget), false);
347
+ assert.strictEqual(fs.existsSync(realTmpRoot), true);
348
+ } finally {
349
+ fs.rmSync(aliasRoot, { force: true, recursive: true });
350
+ fs.rmSync(realTmpRoot, { force: true, recursive: true });
351
+ }
352
+ });
353
+
354
+ it("requires version for maven purl sources", async () => {
355
+ const { validatePurlSource } = await esmock("./source.js", {
356
+ "./utils.js": {
357
+ cdxgenAgent: { get: sinon.stub() },
358
+ DEBUG_MODE: false,
359
+ fetchPomXmlAsJson: sinon.stub(),
360
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
361
+ hasDangerousUnicode: sinon.stub().returns(false),
362
+ isSecureMode: false,
363
+ isValidDriveRoot: sinon.stub().returns(true),
364
+ isWin: false,
365
+ safeSpawnSync: sinon.stub(),
366
+ },
367
+ });
368
+
369
+ const result = validatePurlSource(
370
+ "pkg:maven/org.apache.commons/commons-lang3",
371
+ );
372
+
373
+ assert.strictEqual(result.error, "Invalid purl source");
374
+ assert.strictEqual(
375
+ result.details,
376
+ "The provided maven package URL must include a version.",
377
+ );
378
+ });
379
+
380
+ it("treats docker purl as unsupported source type", async () => {
381
+ const { validatePurlSource } = await esmock("./source.js", {
382
+ "./utils.js": {
383
+ cdxgenAgent: { get: sinon.stub() },
384
+ DEBUG_MODE: false,
385
+ fetchPomXmlAsJson: sinon.stub(),
386
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
387
+ hasDangerousUnicode: sinon.stub().returns(false),
388
+ isSecureMode: false,
389
+ isValidDriveRoot: sinon.stub().returns(true),
390
+ isWin: false,
391
+ safeSpawnSync: sinon.stub(),
392
+ },
393
+ });
394
+
395
+ const result = validatePurlSource("pkg:docker/cdxgen/cdxgen@1.0.0");
396
+
397
+ assert.strictEqual(result.error, "Unsupported purl source type");
398
+ });
399
+
400
+ it("resolves generic purl from vcs_url qualifier", async () => {
401
+ const { resolveGitUrlFromPurl } = await esmock("./source.js", {
402
+ "./utils.js": {
403
+ cdxgenAgent: { get: sinon.stub() },
404
+ DEBUG_MODE: false,
405
+ fetchPomXmlAsJson: sinon.stub(),
406
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
407
+ hasDangerousUnicode: sinon.stub().returns(false),
408
+ isSecureMode: false,
409
+ isValidDriveRoot: sinon.stub().returns(true),
410
+ isWin: false,
411
+ safeSpawnSync: sinon.stub(),
412
+ },
413
+ });
414
+
415
+ const result = await resolveGitUrlFromPurl(
416
+ "pkg:generic/example@1.0.0?vcs_url=git+https://github.com/cdxgen/cdxgen.git",
417
+ );
418
+
419
+ assert.strictEqual(result.repoUrl, "https://github.com/cdxgen/cdxgen.git");
420
+ });
421
+
422
+ it("requires vcs_url or download_url qualifier for generic purl", async () => {
423
+ const { validatePurlSource } = await esmock("./source.js", {
424
+ "./utils.js": {
425
+ cdxgenAgent: { get: sinon.stub() },
426
+ DEBUG_MODE: false,
427
+ fetchPomXmlAsJson: sinon.stub(),
428
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
429
+ hasDangerousUnicode: sinon.stub().returns(false),
430
+ isSecureMode: false,
431
+ isValidDriveRoot: sinon.stub().returns(true),
432
+ isWin: false,
433
+ safeSpawnSync: sinon.stub(),
434
+ },
435
+ });
436
+
437
+ const result = validatePurlSource("pkg:generic/example@1.0.0");
438
+
439
+ assert.strictEqual(result.error, "Unsupported generic purl source");
440
+ });
441
+
442
+ it("finds matching git ref for npm package version", async () => {
443
+ const safeSpawnSync = sinon.stub().returns({
444
+ status: 0,
445
+ stdout: `a refs/tags/v1.2.3
446
+ b refs/tags/other
447
+ `,
448
+ });
449
+ const { findGitRefForPurlVersion } = await esmock("./source.js", {
450
+ "./utils.js": {
451
+ cdxgenAgent: { get: sinon.stub() },
452
+ DEBUG_MODE: false,
453
+ fetchPomXmlAsJson: sinon.stub(),
454
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
455
+ hasDangerousUnicode: sinon.stub().returns(false),
456
+ isSecureMode: false,
457
+ isValidDriveRoot: sinon.stub().returns(true),
458
+ isWin: false,
459
+ safeSpawnSync,
460
+ },
461
+ });
462
+ const result = findGitRefForPurlVersion(
463
+ "https://github.com/cdxgen/cdxgen",
464
+ {
465
+ type: "npm",
466
+ namespace: "cdxgen",
467
+ name: "cdxgen",
468
+ version: "1.2.3",
469
+ },
470
+ );
471
+ assert.strictEqual(result, "v1.2.3");
472
+ });
473
+
474
+ it("hardens git ls-remote invocation in secure mode", async () => {
475
+ const safeSpawnSync = sinon.stub().returns({
476
+ status: 0,
477
+ stdout: "a refs/tags/v1.2.3\n",
478
+ });
479
+ const { findGitRefForPurlVersion } = await esmock("./source.js", {
480
+ "./utils.js": {
481
+ cdxgenAgent: { get: sinon.stub() },
482
+ DEBUG_MODE: false,
483
+ fetchPomXmlAsJson: sinon.stub(),
484
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
485
+ hasDangerousUnicode: sinon.stub().returns(false),
486
+ isSecureMode: true,
487
+ isValidDriveRoot: sinon.stub().returns(true),
488
+ isWin: false,
489
+ safeSpawnSync,
490
+ },
491
+ });
492
+
493
+ const result = findGitRefForPurlVersion(
494
+ "https://github.com/cdxgen/cdxgen",
495
+ {
496
+ type: "npm",
497
+ namespace: "cdxgen",
498
+ name: "cdxgen",
499
+ version: "1.2.3",
500
+ },
501
+ );
502
+
503
+ assert.strictEqual(result, "v1.2.3");
504
+ assert.strictEqual(safeSpawnSync.firstCall.args[0], "git");
505
+ assert.deepStrictEqual(safeSpawnSync.firstCall.args[1].slice(0, 8), [
506
+ "-c",
507
+ "alias.ls-remote=",
508
+ "-c",
509
+ "core.fsmonitor=false",
510
+ "-c",
511
+ "safe.bareRepository=explicit",
512
+ "-c",
513
+ "core.hooksPath=/dev/null",
514
+ ]);
515
+ assert.strictEqual(
516
+ safeSpawnSync.firstCall.args[2].env.GIT_ALLOW_PROTOCOL,
517
+ "https:ssh",
518
+ );
519
+ assert.strictEqual(
520
+ safeSpawnSync.firstCall.args[2].env.GIT_CONFIG_NOSYSTEM,
521
+ "1",
522
+ );
523
+ assert.strictEqual(
524
+ safeSpawnSync.firstCall.args[2].env.GIT_CONFIG_GLOBAL,
525
+ "/dev/null",
526
+ );
527
+ });
528
+
529
+ it("selects npm monorepo directory based on package.json name", async () => {
530
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cdxgen-purl-test-"));
531
+ const pkgDir = path.join(tmpRoot, "packages", "core");
532
+ fs.mkdirSync(pkgDir, { recursive: true });
533
+ fs.writeFileSync(
534
+ path.join(pkgDir, "package.json"),
535
+ JSON.stringify({ name: "@scope/pkg" }),
536
+ "utf-8",
537
+ );
538
+ const { resolvePurlSourceDirectory } = await esmock("./source.js", {
539
+ "./utils.js": {
540
+ cdxgenAgent: { get: sinon.stub() },
541
+ DEBUG_MODE: false,
542
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
543
+ hasDangerousUnicode: sinon.stub().returns(false),
544
+ isSecureMode: false,
545
+ isValidDriveRoot: sinon.stub().returns(true),
546
+ isWin: false,
547
+ safeSpawnSync: sinon.stub(),
548
+ },
549
+ });
550
+ const result = resolvePurlSourceDirectory(tmpRoot, {
551
+ type: "npm",
552
+ namespace: "scope",
553
+ name: "pkg",
554
+ });
555
+ assert.strictEqual(result, pkgDir);
556
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
557
+ });
558
+
559
+ it("builds release notes from provided tags", async () => {
560
+ const { buildReleaseNotesFromGit } = await esmock("./source.js", {
561
+ "./utils.js": {
562
+ cdxgenAgent: { get: sinon.stub() },
563
+ DEBUG_MODE: false,
564
+ fetchPomXmlAsJson: sinon.stub(),
565
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
566
+ hasDangerousUnicode: sinon.stub().returns(false),
567
+ isSecureMode: false,
568
+ isValidDriveRoot: sinon.stub().returns(true),
569
+ isWin: false,
570
+ safeSpawnSync: sinon
571
+ .stub()
572
+ .returns({ status: 1, stdout: "", stderr: "" }),
573
+ },
574
+ });
575
+ const releaseNotes = buildReleaseNotesFromGit(undefined, {
576
+ releaseNotesCurrentTag: "v1.2.3",
577
+ releaseNotesPreviousTag: "v1.2.2",
578
+ });
579
+ assert.strictEqual(releaseNotes.type, "patch");
580
+ assert.strictEqual(releaseNotes.title, "Release notes for v1.2.3");
581
+ assert.deepStrictEqual(releaseNotes.tags, ["v1.2.3", "v1.2.2"]);
582
+ assert.ok(Array.isArray(releaseNotes.resolves));
583
+ assert.strictEqual(releaseNotes.resolves.length, 0);
584
+ });
585
+
586
+ it("returns undefined for unsafe current tag from options", async () => {
587
+ const safeSpawnSync = sinon
588
+ .stub()
589
+ .returns({ status: 1, stdout: "", stderr: "" });
590
+ const { buildReleaseNotesFromGit } = await esmock("./source.js", {
591
+ "./utils.js": {
592
+ cdxgenAgent: { get: sinon.stub() },
593
+ DEBUG_MODE: false,
594
+ fetchPomXmlAsJson: sinon.stub(),
595
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
596
+ hasDangerousUnicode: sinon.stub().returns(false),
597
+ isSecureMode: false,
598
+ isValidDriveRoot: sinon.stub().returns(true),
599
+ isWin: false,
600
+ safeSpawnSync,
601
+ },
602
+ });
603
+ const releaseNotes = buildReleaseNotesFromGit(undefined, {
604
+ releaseNotesCurrentTag: "-bad-tag",
605
+ releaseNotesPreviousTag: "v1.2.2",
606
+ });
607
+ assert.strictEqual(releaseNotes, undefined);
608
+ assert.strictEqual(safeSpawnSync.callCount, 0);
609
+ });
610
+
611
+ it("auto-detects local tags and commit resolves using hardened git command", async () => {
612
+ const safeSpawnSync = sinon.stub().callsFake((_cmd, args) => {
613
+ if (args.includes("rev-parse")) {
614
+ return { status: 0, stdout: "true\n", stderr: "" };
615
+ }
616
+ if (args.includes("tag")) {
617
+ return { status: 0, stdout: "v2.0.0\nv1.9.0\n", stderr: "" };
618
+ }
619
+ if (args.includes("config")) {
620
+ return {
621
+ status: 0,
622
+ stdout: "https://github.com/cdxgen/cdxgen.git\n",
623
+ stderr: "",
624
+ };
625
+ }
626
+ if (args.includes("--format=%cI")) {
627
+ return { status: 0, stdout: "2026-04-01T12:00:00Z\n", stderr: "" };
628
+ }
629
+ if (args.includes("--pretty=format:%H%x09%s")) {
630
+ return {
631
+ status: 0,
632
+ stdout: "abcdef123456\tFix parser bug\n",
633
+ stderr: "",
634
+ };
635
+ }
636
+ return { status: 1, stdout: "", stderr: "" };
637
+ });
638
+ const { buildReleaseNotesFromGit } = await esmock("./source.js", {
639
+ "./utils.js": {
640
+ cdxgenAgent: { get: sinon.stub() },
641
+ DEBUG_MODE: false,
642
+ fetchPomXmlAsJson: sinon.stub(),
643
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
644
+ hasDangerousUnicode: sinon.stub().returns(false),
645
+ isSecureMode: false,
646
+ isValidDriveRoot: sinon.stub().returns(true),
647
+ isWin: false,
648
+ safeSpawnSync,
649
+ },
650
+ });
651
+ const releaseNotes = buildReleaseNotesFromGit("/tmp/repo", {});
652
+ assert.strictEqual(releaseNotes.type, "major");
653
+ assert.strictEqual(releaseNotes.timestamp, "2026-04-01T12:00:00Z");
654
+ assert.deepStrictEqual(releaseNotes.tags, ["v2.0.0", "v1.9.0"]);
655
+ assert.strictEqual(releaseNotes.resolves[0].type, "defect");
656
+ assert.strictEqual(releaseNotes.resolves[0].id, "abcdef123456");
657
+ assert.strictEqual(releaseNotes.resolves[0].name, "Fix parser bug");
658
+ assert.strictEqual(releaseNotes.resolves[0].description, "Fix parser bug");
659
+ assert.strictEqual(safeSpawnSync.firstCall.args[0], "git");
660
+ });
661
+
662
+ it("ignores unsafe previous tag and skips git log range", async () => {
663
+ const safeSpawnSync = sinon.stub().callsFake((_cmd, args) => {
664
+ if (args.includes("rev-parse")) {
665
+ return { status: 0, stdout: "true\n", stderr: "" };
666
+ }
667
+ if (args.includes("tag")) {
668
+ return { status: 0, stdout: "v2.0.0\n-bad-prev\n", stderr: "" };
669
+ }
670
+ if (args.includes("--format=%cI")) {
671
+ return { status: 0, stdout: "2026-04-01T12:00:00Z\n", stderr: "" };
672
+ }
673
+ return { status: 1, stdout: "", stderr: "" };
674
+ });
675
+ const { buildReleaseNotesFromGit } = await esmock("./source.js", {
676
+ "./utils.js": {
677
+ cdxgenAgent: { get: sinon.stub() },
678
+ DEBUG_MODE: false,
679
+ fetchPomXmlAsJson: sinon.stub(),
680
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
681
+ hasDangerousUnicode: sinon.stub().returns(false),
682
+ isSecureMode: false,
683
+ isValidDriveRoot: sinon.stub().returns(true),
684
+ isWin: false,
685
+ safeSpawnSync,
686
+ },
687
+ });
688
+ const releaseNotes = buildReleaseNotesFromGit("/tmp/repo", {});
689
+ assert.strictEqual(releaseNotes.title, "Release notes for v2.0.0");
690
+ assert.deepStrictEqual(releaseNotes.tags, ["v2.0.0"]);
691
+ assert.strictEqual(releaseNotes.resolves.length, 0);
692
+ assert.strictEqual(
693
+ safeSpawnSync
694
+ .getCalls()
695
+ .some((call) => call.args[1].includes("--pretty=format:%H%x09%s")),
696
+ false,
697
+ );
698
+ });
699
+
700
+ it("skips remote tag discovery for invalid releaseNotesGitUrl", async () => {
701
+ const safeSpawnSync = sinon.stub().callsFake((_cmd, args) => {
702
+ if (args.includes("ls-remote")) {
703
+ return {
704
+ status: 0,
705
+ stdout: "a refs/tags/v2.0.0\nb refs/tags/v1.9.0\n",
706
+ stderr: "",
707
+ };
708
+ }
709
+ return { status: 1, stdout: "", stderr: "" };
710
+ });
711
+ const { buildReleaseNotesFromGit } = await esmock("./source.js", {
712
+ "./utils.js": {
713
+ cdxgenAgent: { get: sinon.stub() },
714
+ DEBUG_MODE: false,
715
+ fetchPomXmlAsJson: sinon.stub(),
716
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
717
+ hasDangerousUnicode: sinon.stub().returns(false),
718
+ isSecureMode: false,
719
+ isValidDriveRoot: sinon.stub().returns(true),
720
+ isWin: false,
721
+ safeSpawnSync,
722
+ },
723
+ });
724
+ const releaseNotes = buildReleaseNotesFromGit(undefined, {
725
+ releaseNotesGitUrl: "ext::https://github.com/cdxgen/cdxgen.git",
726
+ });
727
+ assert.strictEqual(releaseNotes, undefined);
728
+ assert.strictEqual(
729
+ safeSpawnSync
730
+ .getCalls()
731
+ .some((call) => call.args[1].includes("ls-remote")),
732
+ false,
733
+ );
734
+ });
735
+
736
+ it("skips remote tag discovery for leading-dash releaseNotesGitUrl", async () => {
737
+ const safeSpawnSync = sinon.stub().callsFake((_cmd, args) => {
738
+ if (args.includes("ls-remote")) {
739
+ return {
740
+ status: 0,
741
+ stdout: "a refs/tags/v2.0.0\nb refs/tags/v1.9.0\n",
742
+ stderr: "",
743
+ };
744
+ }
745
+ return { status: 1, stdout: "", stderr: "" };
746
+ });
747
+ const { buildReleaseNotesFromGit } = await esmock("./source.js", {
748
+ "./utils.js": {
749
+ cdxgenAgent: { get: sinon.stub() },
750
+ DEBUG_MODE: false,
751
+ fetchPomXmlAsJson: sinon.stub(),
752
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
753
+ hasDangerousUnicode: sinon.stub().returns(false),
754
+ isSecureMode: false,
755
+ isValidDriveRoot: sinon.stub().returns(true),
756
+ isWin: false,
757
+ safeSpawnSync,
758
+ },
759
+ });
760
+ const releaseNotes = buildReleaseNotesFromGit(undefined, {
761
+ releaseNotesGitUrl: "-https://github.com/cdxgen/cdxgen.git",
762
+ });
763
+ assert.strictEqual(releaseNotes, undefined);
764
+ assert.strictEqual(
765
+ safeSpawnSync
766
+ .getCalls()
767
+ .some((call) => call.args[1].includes("ls-remote")),
768
+ false,
769
+ );
770
+ });
771
+ });