@cyclonedx/cdxgen 12.3.3 → 12.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -3,27 +3,51 @@ import { join } from "node:path";
3
3
 
4
4
  import { assert, it } from "poku";
5
5
 
6
- import { readBinary, writeBinary } from "./protobom.js";
6
+ import {
7
+ assertProtoSupportedSpecVersion,
8
+ isProtoBomFile,
9
+ readBinary,
10
+ writeBinary,
11
+ } from "./protobom.js";
7
12
  import { getTmpDir } from "./utils.js";
8
13
 
9
- const tempDir = mkdtempSync(join(getTmpDir(), "bin-tests-"));
10
14
  const testBom = JSON.parse(
11
15
  readFileSync("./test/data/bom-java.json", { encoding: "utf-8" }),
12
16
  );
17
+ const cbomFixture = JSON.parse(
18
+ readFileSync("./test/data/bom-cbom-js-fixture.json", { encoding: "utf-8" }),
19
+ );
20
+
21
+ const createTempDir = () => mkdtempSync(join(getTmpDir(), "bin-tests-"));
22
+
23
+ const cleanupTempDir = (tempDir) => {
24
+ if (tempDir?.startsWith(getTmpDir()) && rmSync) {
25
+ rmSync(tempDir, { recursive: true, force: true });
26
+ }
27
+ };
13
28
 
14
29
  it("proto binary tests", () => {
30
+ const tempDir = createTempDir();
15
31
  const binFile = join(tempDir, "test.cdx.bin");
16
32
  writeBinary({}, binFile);
17
33
  assert.deepStrictEqual(existsSync(binFile), true);
18
34
  writeBinary(testBom, binFile);
19
35
  assert.deepStrictEqual(existsSync(binFile), true);
36
+ assert.equal(isProtoBomFile(binFile), true);
37
+ assert.equal(isProtoBomFile("test.proto"), true);
38
+ assert.equal(isProtoBomFile("bom.json"), false);
20
39
  let bomObject = readBinary(binFile);
21
40
  assert.ok(bomObject);
22
41
  assert.deepStrictEqual(
23
42
  bomObject.serialNumber,
24
43
  "urn:uuid:cc8b5a04-2698-4375-b04c-cedfa4317fee",
25
44
  );
45
+ assert.deepStrictEqual(bomObject.bomFormat, "CycloneDX");
26
46
  assert.deepStrictEqual(bomObject.specVersion, "1.5");
47
+ assert.equal(
48
+ bomObject.metadata.component.type.startsWith("CLASSIFICATION_"),
49
+ false,
50
+ );
27
51
  bomObject = readBinary(binFile, false, 1.5);
28
52
  assert.ok(bomObject);
29
53
  assert.deepStrictEqual(
@@ -31,7 +55,161 @@ it("proto binary tests", () => {
31
55
  "urn:uuid:cc8b5a04-2698-4375-b04c-cedfa4317fee",
32
56
  );
33
57
  assert.deepStrictEqual(bomObject.specVersion, "1.5");
34
- if (tempDir?.startsWith(getTmpDir()) && rmSync) {
35
- rmSync(tempDir, { recursive: true, force: true });
36
- }
58
+ const modernBinFile = join(tempDir, "test-1.7.cdx");
59
+ writeBinary(
60
+ {
61
+ bomFormat: "CycloneDX",
62
+ metadata: {
63
+ component: {
64
+ name: "cdxgen",
65
+ type: "application",
66
+ },
67
+ },
68
+ serialNumber: "urn:uuid:11111111-1111-1111-1111-111111111111",
69
+ specVersion: "1.7",
70
+ version: 1,
71
+ },
72
+ modernBinFile,
73
+ );
74
+ const modernBomObject = readBinary(modernBinFile);
75
+ assert.ok(modernBomObject);
76
+ assert.deepStrictEqual(modernBomObject.bomFormat, "CycloneDX");
77
+ assert.deepStrictEqual(modernBomObject.specVersion, "1.7");
78
+ assert.deepStrictEqual(
79
+ modernBomObject.metadata.component.type,
80
+ "application",
81
+ );
82
+ assert.deepStrictEqual(modernBomObject.metadata.component.name, "cdxgen");
83
+ cleanupTempDir(tempDir);
84
+ });
85
+
86
+ it("keeps canonical definitions and declarations as objects during proto round-trip", () => {
87
+ const tempDir = createTempDir();
88
+ const binFile = join(tempDir, "standard-sections.cdx");
89
+ writeBinary(
90
+ {
91
+ bomFormat: "CycloneDX",
92
+ declarations: {
93
+ affirmation: {
94
+ statement: "verified",
95
+ },
96
+ claims: [
97
+ {
98
+ predicate: "meets-control",
99
+ target: "pkg:npm/demo-app@1.0.0",
100
+ },
101
+ ],
102
+ },
103
+ definitions: {
104
+ standards: [
105
+ {
106
+ name: "ASVS",
107
+ requirements: [
108
+ {
109
+ identifier: "V1.1",
110
+ title: "Authenticate requests",
111
+ },
112
+ ],
113
+ version: "5.0",
114
+ },
115
+ ],
116
+ },
117
+ metadata: {
118
+ component: {
119
+ name: "demo-app",
120
+ type: "application",
121
+ version: "1.0.0",
122
+ },
123
+ },
124
+ serialNumber: "urn:uuid:22222222-2222-2222-2222-222222222222",
125
+ specVersion: "1.7",
126
+ version: 1,
127
+ },
128
+ binFile,
129
+ );
130
+
131
+ const bomObject = readBinary(binFile);
132
+ assert.ok(bomObject);
133
+ assert.equal(Array.isArray(bomObject.definitions), false);
134
+ assert.equal(Array.isArray(bomObject.declarations), false);
135
+ assert.equal(bomObject.definitions.standards[0].name, "ASVS");
136
+ assert.equal(
137
+ bomObject.definitions.standards[0].requirements[0].identifier,
138
+ "V1.1",
139
+ );
140
+ assert.equal(bomObject.declarations.claims[0].predicate, "meets-control");
141
+ assert.equal(bomObject.declarations.affirmation.statement, "verified");
142
+ cleanupTempDir(tempDir);
143
+ });
144
+
145
+ it("rejects unsupported CycloneDX 2.0 protobuf operations with a clear error", () => {
146
+ const tempDir = createTempDir();
147
+ const binFile = join(tempDir, "unsupported-2.0.cdx");
148
+ assert.throws(
149
+ () =>
150
+ writeBinary(
151
+ {
152
+ specFormat: "CycloneDX",
153
+ specVersion: "2.0",
154
+ version: 1,
155
+ },
156
+ binFile,
157
+ ),
158
+ /CycloneDX 2\.0 is not currently supported for protobuf serialization/,
159
+ );
160
+ assert.throws(
161
+ () => assertProtoSupportedSpecVersion("2.0", "protobuf export"),
162
+ /@appthreat\/cdx-proto supports 1\.5, 1\.6, 1\.7 only/,
163
+ );
164
+ assert.throws(
165
+ () =>
166
+ writeBinary(
167
+ {
168
+ bomFormat: "CycloneDX",
169
+ specVersion: "2.0.1",
170
+ version: 1,
171
+ },
172
+ binFile,
173
+ ),
174
+ /CycloneDX 2\.0\.1 is not currently supported for protobuf serialization/,
175
+ );
176
+ assert.throws(
177
+ () => assertProtoSupportedSpecVersion("2.0.1", "protobuf export"),
178
+ /CycloneDX 2\.0\.1 is not currently supported for protobuf export/,
179
+ );
180
+ cleanupTempDir(tempDir);
181
+ });
182
+
183
+ it("round-trips real CBOM fixture data with cryptographic assets intact", () => {
184
+ const tempDir = createTempDir();
185
+ const binFile = join(tempDir, "cbom-fixture.cdx");
186
+ writeBinary(cbomFixture, binFile);
187
+
188
+ const bomObject = readBinary(binFile);
189
+ const cryptoComponents = (bomObject.components || []).filter(
190
+ (component) => component.type === "cryptographic-asset",
191
+ );
192
+
193
+ assert.ok(bomObject);
194
+ assert.equal(bomObject.specVersion, "1.7");
195
+ assert.ok(cryptoComponents.length >= 3);
196
+ assert.equal(
197
+ cryptoComponents.some(
198
+ (component) => component.cryptoProperties?.assetType === "algorithm",
199
+ ),
200
+ true,
201
+ );
202
+ assert.equal(
203
+ cryptoComponents.some((component) => component.purl !== undefined),
204
+ false,
205
+ );
206
+ assert.equal(
207
+ cryptoComponents.some(
208
+ (component) =>
209
+ component.name === "sha-512" &&
210
+ component.cryptoProperties?.oid === "2.16.840.1.101.3.4.2.3",
211
+ ),
212
+ true,
213
+ );
214
+ cleanupTempDir(tempDir);
37
215
  });
@@ -0,0 +1,43 @@
1
+ const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
2
+
3
+ /**
4
+ * Determine whether a path looks like a CycloneDX protobuf BOM file.
5
+ *
6
+ * @param {string} filePath File path
7
+ * @returns {boolean} true when the path uses a protobuf BOM extension
8
+ */
9
+ export function isProtoBomPath(filePath) {
10
+ const normalizedPath = `${filePath || ""}`.toLowerCase();
11
+ return PROTO_BOM_FILE_EXTENSIONS.some((extension) =>
12
+ normalizedPath.endsWith(extension),
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Import protobuf BOM helpers and replace optional-dependency loader failures
18
+ * with actionable command-specific messages.
19
+ *
20
+ * @param {string} [commandName="cdxgen"] CLI command name
21
+ * @param {string} [featureDescription="protobuf support"] Feature being used
22
+ * @returns {Promise<object>} Loaded protobom module namespace
23
+ */
24
+ export async function importProtobomModule(
25
+ commandName = "cdxgen",
26
+ featureDescription = "protobuf support",
27
+ ) {
28
+ try {
29
+ return await import("./protobom.js");
30
+ } catch (error) {
31
+ const message = `${error?.message || ""}`;
32
+ if (
33
+ error?.code === "ERR_MODULE_NOT_FOUND" ||
34
+ message.includes("@appthreat/cdx-proto") ||
35
+ message.includes("@bufbuild/protobuf")
36
+ ) {
37
+ throw new Error(
38
+ `${commandName} ${featureDescription} requires the optional '@appthreat/cdx-proto' and '@bufbuild/protobuf' dependencies. Install optional dependencies or use a binary that bundles protobuf support.`,
39
+ );
40
+ }
41
+ throw error;
42
+ }
43
+ }
@@ -0,0 +1,31 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import { importProtobomModule, isProtoBomPath } from "./protobomLoader.js";
4
+
5
+ describe("protobomLoader", () => {
6
+ it("detects protobuf BOM file extensions", () => {
7
+ assert.strictEqual(isProtoBomPath("bom.cdx"), true);
8
+ assert.strictEqual(isProtoBomPath("bom.CDX.BIN"), true);
9
+ assert.strictEqual(isProtoBomPath("bom.proto"), true);
10
+ assert.strictEqual(isProtoBomPath("bom.json"), false);
11
+ assert.strictEqual(isProtoBomPath(""), false);
12
+ });
13
+
14
+ it("imports the protobuf BOM helper when optional support is installed", async () => {
15
+ let protobomModule;
16
+ try {
17
+ protobomModule = await importProtobomModule(
18
+ "cdx-test",
19
+ "protobuf BOM input",
20
+ );
21
+ } catch (error) {
22
+ assert.match(
23
+ error.message,
24
+ /requires the optional '@appthreat\/cdx-proto' and '@bufbuild\/protobuf' dependencies/u,
25
+ );
26
+ return;
27
+ }
28
+ assert.strictEqual(typeof protobomModule.readBinary, "function");
29
+ assert.strictEqual(typeof protobomModule.writeBinary, "function");
30
+ });
31
+ });
@@ -1,13 +1,46 @@
1
1
  import { Buffer } from "node:buffer";
2
2
 
3
+ import { hasDangerousUnicode } from "../utils.js";
4
+
5
+ /**
6
+ * Returns the Dependency-Track BOM API URL as a sanitized URL object.
7
+ *
8
+ * @param {string} serverUrl Dependency-Track server URL
9
+ * @returns {URL | undefined} API URL to submit BOM payload
10
+ */
11
+ export function getDependencyTrackBomApiUrl(serverUrl) {
12
+ const rawServerUrl = `${serverUrl || ""}`.trim();
13
+ if (!rawServerUrl || hasDangerousUnicode(rawServerUrl)) {
14
+ return undefined;
15
+ }
16
+ let parsedUrl;
17
+ try {
18
+ parsedUrl = new URL(rawServerUrl);
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
23
+ return undefined;
24
+ }
25
+ if (!parsedUrl.hostname || hasDangerousUnicode(parsedUrl.hostname)) {
26
+ return undefined;
27
+ }
28
+ parsedUrl.username = "";
29
+ parsedUrl.password = "";
30
+ parsedUrl.search = "";
31
+ parsedUrl.hash = "";
32
+ parsedUrl.pathname = `${parsedUrl.pathname.replace(/\/+$/, "")}/api/v1/bom`;
33
+ return parsedUrl;
34
+ }
35
+
3
36
  /**
4
- * Returns the Dependency-Track BOM API URL.
37
+ * Returns the Dependency-Track BOM API URL string.
5
38
  *
6
39
  * @param {string} serverUrl Dependency-Track server URL
7
- * @returns {string} API URL to submit BOM payload
40
+ * @returns {string | undefined} API URL to submit BOM payload
8
41
  */
9
42
  export function getDependencyTrackBomUrl(serverUrl) {
10
- return `${serverUrl.replace(/\/$/, "")}/api/v1/bom`;
43
+ return getDependencyTrackBomApiUrl(serverUrl)?.toString();
11
44
  }
12
45
 
13
46
  /**
@@ -2,6 +2,7 @@ import { assert, describe, it } from "poku";
2
2
 
3
3
  import {
4
4
  buildDependencyTrackBomPayload,
5
+ getDependencyTrackBomApiUrl,
5
6
  getDependencyTrackBomUrl,
6
7
  } from "./dependency-track.js";
7
8
 
@@ -17,6 +18,49 @@ describe("Dependency-Track helper tests", () => {
17
18
  );
18
19
  });
19
20
 
21
+ it("removes credentials, query strings, and fragments from the submission URL", () => {
22
+ assert.strictEqual(
23
+ getDependencyTrackBomUrl(
24
+ "https://user:pass@dtrack.example.com/base/?token=secret#frag",
25
+ ),
26
+ "https://dtrack.example.com/base/api/v1/bom",
27
+ );
28
+ });
29
+
30
+ it("returns a sanitized URL object for Dependency-Track requests", () => {
31
+ const apiUrl = getDependencyTrackBomApiUrl(
32
+ "https://user:pass@dtrack.example.com/base/?token=secret#frag",
33
+ );
34
+ assert.ok(apiUrl instanceof URL);
35
+ assert.strictEqual(apiUrl?.hostname, "dtrack.example.com");
36
+ assert.strictEqual(apiUrl?.pathname, "/base/api/v1/bom");
37
+ assert.strictEqual(apiUrl?.username, "");
38
+ assert.strictEqual(apiUrl?.password, "");
39
+ assert.strictEqual(apiUrl?.search, "");
40
+ assert.strictEqual(apiUrl?.hash, "");
41
+ });
42
+
43
+ it("rejects malformed or unsupported submission URLs", () => {
44
+ assert.strictEqual(
45
+ getDependencyTrackBomUrl("file:///tmp/dtrack"),
46
+ undefined,
47
+ );
48
+ assert.strictEqual(
49
+ getDependencyTrackBomApiUrl("file:///tmp/dtrack"),
50
+ undefined,
51
+ );
52
+ assert.strictEqual(
53
+ getDependencyTrackBomUrl("javascript:alert(1)"),
54
+ undefined,
55
+ );
56
+ assert.strictEqual(
57
+ getDependencyTrackBomApiUrl("javascript:alert(1)"),
58
+ undefined,
59
+ );
60
+ assert.strictEqual(getDependencyTrackBomUrl("not a url"), undefined);
61
+ assert.strictEqual(getDependencyTrackBomApiUrl("not a url"), undefined);
62
+ });
63
+
20
64
  it("builds payload with parentUUID and tags", () => {
21
65
  const payload = buildDependencyTrackBomPayload(
22
66
  {
@@ -77,6 +77,21 @@ function isSafeGitRefName(refName) {
77
77
  return /^[A-Za-z0-9._/@+-]+$/.test(refName);
78
78
  }
79
79
 
80
+ function inferGitOperation(args = []) {
81
+ for (let index = 0; index < args.length; index += 1) {
82
+ const arg = args[index];
83
+ if (arg === "-c" || arg === "--config-env") {
84
+ index += 1;
85
+ continue;
86
+ }
87
+ if (typeof arg === "string" && arg.startsWith("-")) {
88
+ continue;
89
+ }
90
+ return arg || "command";
91
+ }
92
+ return "command";
93
+ }
94
+
80
95
  /**
81
96
  * Execute git with hardened defaults.
82
97
  *
@@ -86,6 +101,7 @@ function isSafeGitRefName(refName) {
86
101
  * @returns {Object} spawn result
87
102
  */
88
103
  export function hardenedGitCommand(args, options = {}) {
104
+ const gitOperation = inferGitOperation(args);
89
105
  const gitAllowProtocol = getGitAllowProtocol();
90
106
  const envConfigs = {
91
107
  GIT_CONFIG_COUNT: "2",
@@ -109,6 +125,14 @@ export function hardenedGitCommand(args, options = {}) {
109
125
  GIT_ALLOW_PROTOCOL: gitAllowProtocol,
110
126
  };
111
127
  return safeSpawnSync("git", args, {
128
+ cdxgenActivity: {
129
+ blockedReason: `Dry run mode blocks git ${gitOperation} operations.`,
130
+ gitOperation,
131
+ kind: `git-${gitOperation}`,
132
+ metadata: {
133
+ capability: "git-operation",
134
+ },
135
+ },
112
136
  shell: false,
113
137
  cwd: options.cwd,
114
138
  env,
@@ -45,6 +45,38 @@ describe("source helper purl resolution", () => {
45
45
  assert.strictEqual(recordActivity.firstCall.args[0].status, "blocked");
46
46
  });
47
47
 
48
+ it("hardenedGitCommand() tags git operations with the specific subcommand", async () => {
49
+ const safeSpawnSync = sinon
50
+ .stub()
51
+ .returns({ status: 0, stdout: "", stderr: "" });
52
+ const { hardenedGitCommand } = await esmock("./source.js", {
53
+ "./utils.js": {
54
+ cdxgenAgent: { get: sinon.stub() },
55
+ DEBUG_MODE: false,
56
+ fetchPomXmlAsJson: sinon.stub(),
57
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
58
+ hasDangerousUnicode: sinon.stub().returns(false),
59
+ isDryRun: false,
60
+ isSecureMode: false,
61
+ isValidDriveRoot: sinon.stub().returns(true),
62
+ isWin: false,
63
+ safeMkdtempSync: sinon.stub(),
64
+ safeRmSync: sinon.stub(),
65
+ safeSpawnSync,
66
+ },
67
+ });
68
+
69
+ hardenedGitCommand(["fetch", "--tags"], { cwd: "/tmp/repo" });
70
+
71
+ sinon.assert.calledWithMatch(safeSpawnSync, "git", ["fetch", "--tags"], {
72
+ cdxgenActivity: sinon.match({
73
+ gitOperation: "fetch",
74
+ kind: "git-fetch",
75
+ }),
76
+ cwd: "/tmp/repo",
77
+ });
78
+ });
79
+
48
80
  it("resolves npm purl to repository URL", async () => {
49
81
  const getStub = sinon.stub().resolves({
50
82
  body: {