@cyclonedx/cdxgen 12.2.0 → 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 (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  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 +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  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/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. 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
+ });