@cyclonedx/cdxgen 12.1.5 → 12.2.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 (193) hide show
  1. package/README.md +51 -40
  2. package/bin/cdxgen.js +194 -97
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +449 -429
  14. package/lib/cli/index.poku.js +117 -0
  15. package/lib/evinser/db.js +137 -0
  16. package/lib/{helpers → evinser}/db.poku.js +2 -6
  17. package/lib/evinser/evinser.js +2 -14
  18. package/lib/helpers/analyzer.js +606 -3
  19. package/lib/helpers/analyzer.poku.js +230 -0
  20. package/lib/helpers/bomSigner.js +312 -0
  21. package/lib/helpers/bomSigner.poku.js +156 -0
  22. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  23. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  24. package/lib/helpers/ciParsers/circleCi.js +286 -0
  25. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  26. package/lib/helpers/ciParsers/common.js +24 -0
  27. package/lib/helpers/ciParsers/githubActions.js +636 -0
  28. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  29. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  30. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  31. package/lib/helpers/ciParsers/jenkins.js +181 -0
  32. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  33. package/lib/helpers/depsUtils.js +219 -0
  34. package/lib/helpers/depsUtils.poku.js +207 -0
  35. package/lib/helpers/display.js +426 -5
  36. package/lib/helpers/envcontext.js +18 -3
  37. package/lib/helpers/formulationParsers.js +351 -0
  38. package/lib/helpers/logger.js +14 -0
  39. package/lib/helpers/protobom.js +9 -9
  40. package/lib/helpers/pythonutils.js +9 -0
  41. package/lib/helpers/remote/dependency-track.js +84 -0
  42. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  43. package/lib/helpers/table.js +384 -0
  44. package/lib/helpers/table.poku.js +186 -0
  45. package/lib/helpers/utils.js +865 -416
  46. package/lib/helpers/utils.poku.js +172 -265
  47. package/lib/helpers/versutils.js +202 -0
  48. package/lib/helpers/versutils.poku.js +315 -0
  49. package/lib/helpers/vsixutils.js +1061 -0
  50. package/lib/helpers/vsixutils.poku.js +2247 -0
  51. package/lib/managers/binary.js +19 -19
  52. package/lib/managers/docker.js +108 -1
  53. package/lib/managers/oci.js +10 -0
  54. package/lib/managers/piptree.js +3 -9
  55. package/lib/parsers/npmrc.js +17 -13
  56. package/lib/parsers/npmrc.poku.js +41 -5
  57. package/lib/server/openapi.yaml +34 -1
  58. package/lib/server/server.js +50 -13
  59. package/lib/server/server.poku.js +332 -144
  60. package/lib/stages/postgen/annotator.js +1 -1
  61. package/lib/stages/postgen/auditBom.js +196 -0
  62. package/lib/stages/postgen/auditBom.poku.js +378 -0
  63. package/lib/stages/postgen/postgen.js +54 -1
  64. package/lib/stages/postgen/postgen.poku.js +90 -1
  65. package/lib/stages/postgen/ruleEngine.js +369 -0
  66. package/lib/stages/pregen/envAudit.js +299 -0
  67. package/lib/stages/pregen/envAudit.poku.js +572 -0
  68. package/lib/stages/pregen/pregen.js +12 -8
  69. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  70. package/lib/validator/complianceEngine.js +241 -0
  71. package/lib/validator/complianceEngine.poku.js +168 -0
  72. package/lib/validator/complianceRules.js +1610 -0
  73. package/lib/validator/complianceRules.poku.js +328 -0
  74. package/lib/validator/index.js +222 -0
  75. package/lib/validator/index.poku.js +144 -0
  76. package/lib/validator/reporters/annotations.js +121 -0
  77. package/lib/validator/reporters/console.js +149 -0
  78. package/lib/validator/reporters/index.js +41 -0
  79. package/lib/validator/reporters/json.js +37 -0
  80. package/lib/validator/reporters/sarif.js +184 -0
  81. package/lib/validator/reporters.poku.js +150 -0
  82. package/package.json +8 -9
  83. package/types/bin/sign.d.ts +3 -0
  84. package/types/bin/sign.d.ts.map +1 -0
  85. package/types/bin/validate.d.ts +3 -0
  86. package/types/bin/validate.d.ts.map +1 -0
  87. package/types/helpers/utils.d.ts +0 -1
  88. package/types/lib/cli/index.d.ts +49 -52
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/evinser/db.d.ts +34 -0
  91. package/types/lib/evinser/db.d.ts.map +1 -0
  92. package/types/lib/evinser/evinser.d.ts +63 -16
  93. package/types/lib/evinser/evinser.d.ts.map +1 -1
  94. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  95. package/types/lib/helpers/bomSigner.d.ts +27 -0
  96. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  102. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  104. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  106. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  108. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  109. package/types/lib/helpers/depsUtils.d.ts +21 -0
  110. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  111. package/types/lib/helpers/display.d.ts +111 -11
  112. package/types/lib/helpers/display.d.ts.map +1 -1
  113. package/types/lib/helpers/envcontext.d.ts +19 -7
  114. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  115. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  116. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  117. package/types/lib/helpers/logger.d.ts +15 -1
  118. package/types/lib/helpers/logger.d.ts.map +1 -1
  119. package/types/lib/helpers/protobom.d.ts +2 -2
  120. package/types/lib/helpers/pythonutils.d.ts +10 -1
  121. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  122. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  123. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  124. package/types/lib/helpers/table.d.ts +6 -0
  125. package/types/lib/helpers/table.d.ts.map +1 -0
  126. package/types/lib/helpers/utils.d.ts +533 -128
  127. package/types/lib/helpers/utils.d.ts.map +1 -1
  128. package/types/lib/helpers/versutils.d.ts +8 -0
  129. package/types/lib/helpers/versutils.d.ts.map +1 -0
  130. package/types/lib/helpers/vsixutils.d.ts +130 -0
  131. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  132. package/types/lib/managers/docker.d.ts +12 -31
  133. package/types/lib/managers/docker.d.ts.map +1 -1
  134. package/types/lib/managers/oci.d.ts +11 -1
  135. package/types/lib/managers/oci.d.ts.map +1 -1
  136. package/types/lib/managers/piptree.d.ts.map +1 -1
  137. package/types/lib/parsers/npmrc.d.ts +4 -1
  138. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  139. package/types/lib/server/server.d.ts +22 -2
  140. package/types/lib/server/server.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  142. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  144. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  145. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  146. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  147. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  148. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  149. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  150. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  151. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  152. package/types/lib/validator/complianceEngine.d.ts +66 -0
  153. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  154. package/types/lib/validator/complianceRules.d.ts +70 -0
  155. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  156. package/types/lib/validator/index.d.ts +70 -0
  157. package/types/lib/validator/index.d.ts.map +1 -0
  158. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  159. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  160. package/types/lib/validator/reporters/console.d.ts +30 -0
  161. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  162. package/types/lib/validator/reporters/index.d.ts +21 -0
  163. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  164. package/types/lib/validator/reporters/json.d.ts +11 -0
  165. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  166. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  167. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  168. package/lib/helpers/db.js +0 -162
  169. package/lib/stages/pregen/env-audit.js +0 -34
  170. package/lib/stages/pregen/env-audit.poku.js +0 -290
  171. package/types/helpers/db.d.ts +0 -35
  172. package/types/helpers/db.d.ts.map +0 -1
  173. package/types/lib/helpers/db.d.ts +0 -35
  174. package/types/lib/helpers/db.d.ts.map +0 -1
  175. package/types/lib/helpers/validator.d.ts.map +0 -1
  176. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  177. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  178. package/types/managers/binary.d.ts +0 -37
  179. package/types/managers/binary.d.ts.map +0 -1
  180. package/types/managers/docker.d.ts +0 -56
  181. package/types/managers/docker.d.ts.map +0 -1
  182. package/types/managers/oci.d.ts +0 -2
  183. package/types/managers/oci.d.ts.map +0 -1
  184. package/types/managers/piptree.d.ts +0 -2
  185. package/types/managers/piptree.d.ts.map +0 -1
  186. package/types/server/server.d.ts +0 -34
  187. package/types/server/server.d.ts.map +0 -1
  188. package/types/stages/postgen/annotator.d.ts +0 -27
  189. package/types/stages/postgen/annotator.d.ts.map +0 -1
  190. package/types/stages/postgen/postgen.d.ts +0 -51
  191. package/types/stages/postgen/postgen.d.ts.map +0 -1
  192. package/types/stages/pregen/pregen.d.ts +0 -59
  193. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -0,0 +1,351 @@
1
+ import process from "node:process";
2
+
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ import { collectOSCryptoLibs } from "./cbomutils.js";
6
+ import { azurePipelinesParser } from "./ciParsers/azurePipelines.js";
7
+ import { circleCiParser } from "./ciParsers/circleCi.js";
8
+ import { githubActionsParser } from "./ciParsers/githubActions.js";
9
+ import { gitlabCiParser } from "./ciParsers/gitlabCi.js";
10
+ import { jenkinsParser } from "./ciParsers/jenkins.js";
11
+ import { trimComponents } from "./depsUtils.js";
12
+ import {
13
+ collectEnvInfo,
14
+ getBranch,
15
+ getOriginUrl,
16
+ gitTreeHashes,
17
+ listFiles,
18
+ } from "./envcontext.js";
19
+ import { getAllFiles } from "./utils.js";
20
+
21
+ /**
22
+ * The parser registry. Pre-populated with the five built-in CI system parsers.
23
+ *
24
+ * External parsers added via {@link registerParser} are appended here.
25
+ *
26
+ * Each entry must satisfy the FormulationParser contract:
27
+ * ```
28
+ * {
29
+ * id: string, // unique stable identifier
30
+ * patterns: string[], // non-empty array of glob patterns for file discovery
31
+ * parse(files: string[], options: Object): // synchronous function
32
+ * { workflows?, components?, services?, properties?, dependencies? }
33
+ * }
34
+ * ```
35
+ */
36
+ const _parsers = [
37
+ githubActionsParser,
38
+ gitlabCiParser,
39
+ jenkinsParser,
40
+ circleCiParser,
41
+ azurePipelinesParser,
42
+ ];
43
+
44
+ /**
45
+ * Register an external formulation parser.
46
+ *
47
+ * The parser is appended to the registry and will be invoked by
48
+ * {@link addFormulationSection} on the next call.
49
+ *
50
+ * @param {{ id: string, patterns: string[], parse: Function }} parser
51
+ */
52
+ export function registerParser(parser) {
53
+ const hasValidPatterns =
54
+ Array.isArray(parser?.patterns) &&
55
+ parser.patterns.length > 0 &&
56
+ parser.patterns.every(
57
+ (pattern) => typeof pattern === "string" && pattern.trim().length > 0,
58
+ );
59
+ if (
60
+ typeof parser?.id !== "string" ||
61
+ parser.id.trim().length === 0 ||
62
+ !hasValidPatterns ||
63
+ typeof parser?.parse !== "function"
64
+ ) {
65
+ throw new TypeError(
66
+ "registerParser: parser must have id (string), patterns (non-empty string[]), and parse (function)",
67
+ );
68
+ }
69
+ _parsers.push(parser);
70
+ }
71
+
72
+ /**
73
+ * Return a shallow copy of the currently registered parsers.
74
+ *
75
+ * @returns {Array<{ id: string, patterns: string[], parse: Function }>}
76
+ */
77
+ export function getParsers() {
78
+ return [..._parsers];
79
+ }
80
+
81
+ /**
82
+ * Environment-variable prefixes whose values are safe to include in the
83
+ * formulation section. All other variables are ignored.
84
+ */
85
+ const ENV_PREFIXES = [
86
+ "GIT_",
87
+ "ANDROID_",
88
+ "DENO_",
89
+ "DOTNET_",
90
+ "JAVA_",
91
+ "SDKMAN_",
92
+ "CARGO_",
93
+ "CONDA_",
94
+ "RUST",
95
+ "GEM_",
96
+ "SCALA_",
97
+ "MAVEN_",
98
+ "GRADLE_",
99
+ ];
100
+
101
+ /**
102
+ * Sub-strings that, when found (case-insensitively) in the variable *name*
103
+ * or *value*, cause the variable to be excluded from the formulation section.
104
+ *
105
+ * This blocklist is intentionally conservative to avoid leaking secrets.
106
+ * Common CI tokens and credentials patterns are enumerated explicitly.
107
+ */
108
+ const ENV_BLOCKLIST = [
109
+ "key",
110
+ "token",
111
+ "pass",
112
+ "secret",
113
+ "user",
114
+ "email",
115
+ "auth",
116
+ "session",
117
+ "proxy",
118
+ "cred",
119
+ "askpass",
120
+ "api_key",
121
+ "apikey",
122
+ "private",
123
+ "signature",
124
+ "webhook",
125
+ ];
126
+
127
+ /**
128
+ * Build the formulation section for a CycloneDX BOM.
129
+ *
130
+ * This function is the top-level aggregator: it collects git metadata,
131
+ * invokes every registered CI parser, and merges the results into a single
132
+ * CycloneDX formulation entry.
133
+ *
134
+ * The function falls back to a minimal stub workflow when no CI config files
135
+ * are detected at the given path.
136
+ *
137
+ * @param {string} filePath File path
138
+ * @param {Object} options CLI options; `options.path` is used as the
139
+ * project root for file discovery.
140
+ * @param {Object} [context={}] Optional context object. If it contains a
141
+ * non-empty `formulationList` array those
142
+ * components are merged into the result.
143
+ *
144
+ * @returns {{ formulation: Object[], dependencies: Object[] }}
145
+ * `formulation` – array to be placed at `bomJson.formulation`
146
+ * `dependencies` – dependency objects to be merged into
147
+ * `bomJson.dependencies` via `mergeDependencies`
148
+ */
149
+ export function addFormulationSection(filePath, options, context = {}) {
150
+ const projectPath = filePath;
151
+ const formulation = [];
152
+ const dependencies = [];
153
+
154
+ // ── Git metadata ─────────────────────────────────────────────────────────
155
+ const gitBranch = getBranch(undefined, projectPath);
156
+ const originUrl = getOriginUrl(projectPath);
157
+ const gitFiles = listFiles(projectPath);
158
+ const treeHashes = gitTreeHashes(projectPath);
159
+
160
+ let components = [];
161
+
162
+ // Reuse any existing formulation components (e.g. from Pixi lock data)
163
+ // See: PR #1172
164
+ if (context?.formulationList?.length) {
165
+ components = components.concat(trimComponents(context.formulationList));
166
+ }
167
+
168
+ // OmniBOR / Artifact Dependency Graph components (spec 1.6+)
169
+ let parentOmniborId;
170
+ let treeOmniborId;
171
+ if (options.specVersion >= 1.6 && Object.keys(treeHashes).length === 2) {
172
+ // treeHashes.parent is the parent commit SHA → gitoid:commit:sha1:
173
+ // treeHashes.tree is the git tree object SHA → gitoid:tree:sha1:
174
+ parentOmniborId = `gitoid:commit:sha1:${treeHashes.parent}`;
175
+ treeOmniborId = `gitoid:tree:sha1:${treeHashes.tree}`;
176
+ components.push({
177
+ type: "file",
178
+ name: "git-parent",
179
+ description: "Git Parent Node.",
180
+ "bom-ref": parentOmniborId,
181
+ omniborId: [parentOmniborId],
182
+ swhid: [`swh:1:rev:${treeHashes.parent}`],
183
+ });
184
+ components.push({
185
+ type: "file",
186
+ name: "git-tree",
187
+ description: "Git Tree Node.",
188
+ "bom-ref": treeOmniborId,
189
+ omniborId: [treeOmniborId],
190
+ swhid: [`swh:1:dir:${treeHashes.tree}`],
191
+ });
192
+ // OmniBOR linkage goes into the top-level dependencies array
193
+ dependencies.push({ ref: parentOmniborId, provides: [treeOmniborId] });
194
+ }
195
+
196
+ // Git file list
197
+ if (gitBranch && gitFiles?.length) {
198
+ const gitFileComponents = gitFiles.map((f) =>
199
+ options.specVersion >= 1.6
200
+ ? {
201
+ type: "file",
202
+ name: f.name,
203
+ version: f.hash,
204
+ "bom-ref": f.omniborId,
205
+ omniborId: [f.omniborId],
206
+ swhid: [f.swhid],
207
+ }
208
+ : {
209
+ type: "file",
210
+ name: f.name,
211
+ version: f.hash,
212
+ },
213
+ );
214
+ components = components.concat(gitFileComponents);
215
+
216
+ // Complete the Artifact Dependency Graph: tree → blob links
217
+ if (options.specVersion >= 1.6 && treeOmniborId) {
218
+ dependencies.push({
219
+ ref: treeOmniborId,
220
+ provides: gitFiles.map((f) => f.omniborId).filter(Boolean),
221
+ });
222
+ }
223
+ }
224
+
225
+ // Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby)
226
+ const infoComponents = collectEnvInfo(projectPath);
227
+ if (infoComponents?.length) {
228
+ components = components.concat(infoComponents);
229
+ }
230
+
231
+ // OS crypto libraries (cbom mode)
232
+ if (options.includeCrypto) {
233
+ const cryptoLibs = collectOSCryptoLibs(options);
234
+ if (cryptoLibs?.length) {
235
+ components = components.concat(cryptoLibs);
236
+ }
237
+ }
238
+
239
+ // ── CI parser dispatch ────────────────────────────────────────────────────
240
+ const ciWorkflows = [];
241
+ const ciComponents = [];
242
+ const ciServices = [];
243
+ const ciProperties = [];
244
+
245
+ const discoveryPath = projectPath || ".";
246
+
247
+ for (const parser of _parsers) {
248
+ const matchedFiles = [];
249
+ for (const pattern of parser.patterns) {
250
+ const found = getAllFiles(discoveryPath, pattern, options);
251
+ if (found?.length) {
252
+ matchedFiles.push(...found);
253
+ }
254
+ }
255
+ const uniqueMatchedFiles = [...new Set(matchedFiles)];
256
+ if (!uniqueMatchedFiles.length) {
257
+ continue;
258
+ }
259
+
260
+ let result;
261
+ try {
262
+ result = parser.parse(uniqueMatchedFiles, options);
263
+ } catch (err) {
264
+ // A broken parser must not kill SBOM generation
265
+ console.warn(
266
+ `[formulationParsers] Parser "${parser.id}" threw an error:`,
267
+ err.message,
268
+ );
269
+ continue;
270
+ }
271
+
272
+ if (result?.workflows?.length) {
273
+ ciWorkflows.push(...result.workflows);
274
+ }
275
+ if (result?.components?.length) {
276
+ ciComponents.push(...result.components);
277
+ }
278
+ if (result?.services?.length) {
279
+ ciServices.push(...result.services);
280
+ }
281
+ if (result?.properties?.length) {
282
+ ciProperties.push(...result.properties);
283
+ }
284
+ if (result?.dependencies?.length) {
285
+ dependencies.push(...result.dependencies);
286
+ }
287
+ }
288
+
289
+ // Merge CI components into the formulation component list
290
+ if (ciComponents.length) {
291
+ components = components.concat(ciComponents);
292
+ }
293
+
294
+ // ── Environment variables ─────────────────────────────────────────────────
295
+ let environmentVars = gitBranch?.length
296
+ ? [{ name: "GIT_BRANCH", value: gitBranch }]
297
+ : [];
298
+
299
+ for (const aevar of Object.keys(process.env)) {
300
+ const lower = aevar.toLowerCase();
301
+ const value = process.env[aevar] ?? "";
302
+ if (
303
+ ENV_PREFIXES.some((p) => aevar.startsWith(p)) &&
304
+ !ENV_BLOCKLIST.some((b) => lower.includes(b)) &&
305
+ !ENV_BLOCKLIST.some((b) => value.toLowerCase().includes(b)) &&
306
+ value.length
307
+ ) {
308
+ environmentVars.push({ name: aevar, value });
309
+ }
310
+ }
311
+
312
+ if (!environmentVars.length) {
313
+ environmentVars = undefined;
314
+ }
315
+
316
+ // ── Assemble formulation object ───────────────────────────────────────────
317
+ const aformulation = {
318
+ "bom-ref": uuidv4(),
319
+ components: trimComponents(components),
320
+ };
321
+
322
+ if (ciServices.length) {
323
+ aformulation.services = ciServices;
324
+ }
325
+
326
+ if (ciProperties.length) {
327
+ aformulation.properties = ciProperties;
328
+ }
329
+
330
+ // Use CI-detected workflows; fall back to a minimal stub when none found
331
+ if (ciWorkflows.length) {
332
+ aformulation.workflows = ciWorkflows;
333
+ } else {
334
+ let sourceInput;
335
+ if (environmentVars) {
336
+ sourceInput = { environmentVars };
337
+ }
338
+ const sourceWorkflow = {
339
+ "bom-ref": uuidv4(),
340
+ uid: uuidv4(),
341
+ taskTypes: originUrl ? ["build", "clone"] : ["build"],
342
+ };
343
+ if (sourceInput) {
344
+ sourceWorkflow.inputs = [sourceInput];
345
+ }
346
+ aformulation.workflows = [sourceWorkflow];
347
+ }
348
+
349
+ formulation.push(aformulation);
350
+ return { formulation, dependencies };
351
+ }
@@ -44,6 +44,14 @@ const traceLogger = new Console({
44
44
  if (THINK_MODE) {
45
45
  thinkLogger.group(colorizeText("<think>"));
46
46
  }
47
+ /**
48
+ * Logs a thought message to the think logger if THINK_MODE is enabled.
49
+ * Automatically appends a period to the message if it lacks terminal punctuation.
50
+ *
51
+ * @param {string} s The thought message to log
52
+ * @param {Object} [args] Optional additional arguments to log alongside the message
53
+ * @returns {void}
54
+ */
47
55
  export function thoughtLog(s, args) {
48
56
  if (!THINK_MODE) {
49
57
  return;
@@ -58,6 +66,12 @@ export function thoughtLog(s, args) {
58
66
  thinkLogger.log(colorizeText(`${s}`));
59
67
  }
60
68
  }
69
+ /**
70
+ * Closes the think log group by emitting the closing `</think>` marker.
71
+ * Has no effect if THINK_MODE is not enabled.
72
+ *
73
+ * @returns {void}
74
+ */
61
75
  export function thoughtEnd() {
62
76
  if (THINK_MODE) {
63
77
  thinkLogger.groupEnd();
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
 
3
- import { cdx_15, cdx_16 } from "@appthreat/cdx-proto";
3
+ import { cdx_16, cdx_17 } from "@appthreat/cdx-proto";
4
4
  import {
5
5
  fromBinary,
6
6
  fromJsonString,
@@ -32,10 +32,10 @@ const stringifyIfNeeded = (bomJson) => {
32
32
  export const writeBinary = (bomJson, binFile) => {
33
33
  if (bomJson && binFile) {
34
34
  let bomSchema;
35
- if (+bomJson.specVersion === 1.6) {
36
- bomSchema = cdx_16.BomSchema;
35
+ if (+bomJson.specVersion === 1.7) {
36
+ bomSchema = cdx_17.BomSchema;
37
37
  } else {
38
- bomSchema = cdx_15.BomSchema;
38
+ bomSchema = cdx_16.BomSchema;
39
39
  }
40
40
  writeFileSync(
41
41
  binFile,
@@ -57,17 +57,17 @@ export const writeBinary = (bomJson, binFile) => {
57
57
  *
58
58
  * @param {string} binFile Binary file name
59
59
  * @param {boolean} asJson Convert to JSON
60
- * @param {number} specVersion Specification version. Defaults to 1.6
60
+ * @param {number} specVersion Specification version. Defaults to 1.7
61
61
  */
62
- export const readBinary = (binFile, asJson = true, specVersion = 1.6) => {
62
+ export const readBinary = (binFile, asJson = true, specVersion = 1.7) => {
63
63
  if (!safeExistsSync(binFile)) {
64
64
  return undefined;
65
65
  }
66
66
  let bomSchema;
67
- if (specVersion === 1.6) {
68
- bomSchema = cdx_16.BomSchema;
67
+ if (specVersion === 1.7) {
68
+ bomSchema = cdx_17.BomSchema;
69
69
  } else {
70
- bomSchema = cdx_15.BomSchema;
70
+ bomSchema = cdx_16.BomSchema;
71
71
  }
72
72
  const bomObject = fromBinary(bomSchema, readFileSync(binFile), {
73
73
  readUnknownFields: true,
@@ -266,6 +266,15 @@ function _findCondaPythonPackage(condaMetaDir) {
266
266
  }
267
267
  }
268
268
 
269
+ /**
270
+ * Determines the appropriate Python executable path from a virtual environment.
271
+ * Inspects the virtual environment metadata to detect the Python type (system,
272
+ * conda, pyenv, etc.) and returns the most specific executable found, falling
273
+ * back to the global `PYTHON_CMD` constant when no executable is detected.
274
+ *
275
+ * @param {string} env Path to the Python virtual environment directory
276
+ * @returns {string} Path to the Python executable or the fallback command name
277
+ */
269
278
  export function get_python_command_from_env(env) {
270
279
  const fallbackCmd = PYTHON_CMD;
271
280
  const meta = getVenvMetadata(env);
@@ -0,0 +1,84 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ /**
4
+ * Returns the Dependency-Track BOM API URL.
5
+ *
6
+ * @param {string} serverUrl Dependency-Track server URL
7
+ * @returns {string} API URL to submit BOM payload
8
+ */
9
+ export function getDependencyTrackBomUrl(serverUrl) {
10
+ return `${serverUrl.replace(/\/$/, "")}/api/v1/bom`;
11
+ }
12
+
13
+ /**
14
+ * Build the payload for Dependency-Track BOM submission.
15
+ *
16
+ * @param {Object} args CLI/server arguments
17
+ * @param {Object} bomContents BOM Json
18
+ * @returns {Object | undefined} payload object if project coordinates are valid
19
+ */
20
+ export function buildDependencyTrackBomPayload(args, bomContents) {
21
+ let encodedBomContents = Buffer.from(JSON.stringify(bomContents)).toString(
22
+ "base64",
23
+ );
24
+ if (encodedBomContents.startsWith("77u/")) {
25
+ encodedBomContents = encodedBomContents.substring(4);
26
+ }
27
+ const autoCreate =
28
+ typeof args.autoCreate === "boolean"
29
+ ? args.autoCreate
30
+ : args.autoCreate !== "false";
31
+ const bomPayload = {
32
+ autoCreate: String(autoCreate),
33
+ bom: encodedBomContents,
34
+ };
35
+ if (
36
+ typeof args.projectId !== "undefined" ||
37
+ typeof args.projectName !== "undefined"
38
+ ) {
39
+ if (typeof args.projectId !== "undefined") {
40
+ bomPayload.project = args.projectId;
41
+ }
42
+ if (typeof args.projectName !== "undefined") {
43
+ bomPayload.projectName = args.projectName;
44
+ }
45
+ // Dependency-Track submissions use "main" as fallback when no version is provided.
46
+ bomPayload.projectVersion = args.projectVersion || "main";
47
+ } else {
48
+ return undefined;
49
+ }
50
+ const parentProjectId = args.parentProjectId || args.parentUUID;
51
+ const hasParentUuidMode = typeof parentProjectId !== "undefined";
52
+ const hasParentName = typeof args.parentProjectName !== "undefined";
53
+ const hasParentVersion = typeof args.parentProjectVersion !== "undefined";
54
+ const hasParentCoordsMode = hasParentName || hasParentVersion;
55
+ if (hasParentUuidMode && hasParentCoordsMode) {
56
+ return undefined;
57
+ }
58
+ if (!hasParentUuidMode && hasParentName !== hasParentVersion) {
59
+ return undefined;
60
+ }
61
+ if (hasParentUuidMode) {
62
+ bomPayload.parentUUID = parentProjectId;
63
+ }
64
+ if (hasParentName && hasParentVersion) {
65
+ bomPayload.parentName = args.parentProjectName;
66
+ bomPayload.parentVersion = args.parentProjectVersion;
67
+ }
68
+ if (
69
+ typeof args.isLatest === "boolean" ||
70
+ args.isLatest === "true" ||
71
+ args.isLatest === "false"
72
+ ) {
73
+ bomPayload.isLatest =
74
+ typeof args.isLatest === "boolean"
75
+ ? args.isLatest
76
+ : args.isLatest === "true";
77
+ }
78
+ if (typeof args.projectTag !== "undefined") {
79
+ bomPayload.projectTags = (
80
+ Array.isArray(args.projectTag) ? args.projectTag : [args.projectTag]
81
+ ).map((tag) => ({ name: tag }));
82
+ }
83
+ return bomPayload;
84
+ }
@@ -0,0 +1,119 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ buildDependencyTrackBomPayload,
5
+ getDependencyTrackBomUrl,
6
+ } from "./dependency-track.js";
7
+
8
+ describe("Dependency-Track helper tests", () => {
9
+ it("returns submission URL without trailing slash duplication", () => {
10
+ assert.strictEqual(
11
+ getDependencyTrackBomUrl("https://dtrack.example.com/"),
12
+ "https://dtrack.example.com/api/v1/bom",
13
+ );
14
+ assert.strictEqual(
15
+ getDependencyTrackBomUrl("https://dtrack.example.com"),
16
+ "https://dtrack.example.com/api/v1/bom",
17
+ );
18
+ });
19
+
20
+ it("builds payload with parentUUID and tags", () => {
21
+ const payload = buildDependencyTrackBomPayload(
22
+ {
23
+ projectName: "child",
24
+ projectVersion: "1.0.0",
25
+ parentProjectId: "d9628844-5f04-4ca7-88a2-64eb6bc64db0",
26
+ projectTag: ["tag1", "tag2"],
27
+ },
28
+ { bom: "test" },
29
+ );
30
+ assert.deepStrictEqual(payload, {
31
+ autoCreate: "true",
32
+ bom: "eyJib20iOiJ0ZXN0In0=",
33
+ parentUUID: "d9628844-5f04-4ca7-88a2-64eb6bc64db0",
34
+ projectName: "child",
35
+ projectTags: [{ name: "tag1" }, { name: "tag2" }],
36
+ projectVersion: "1.0.0",
37
+ });
38
+ });
39
+
40
+ it("builds payload with parentName and parentVersion", () => {
41
+ const payload = buildDependencyTrackBomPayload(
42
+ {
43
+ projectName: "child",
44
+ projectVersion: "1.0.0",
45
+ parentProjectName: "parent",
46
+ parentProjectVersion: "2.0.0",
47
+ },
48
+ { bom: "test2" },
49
+ );
50
+ assert.deepStrictEqual(payload, {
51
+ autoCreate: "true",
52
+ bom: "eyJib20iOiJ0ZXN0MiJ9",
53
+ parentName: "parent",
54
+ parentVersion: "2.0.0",
55
+ projectName: "child",
56
+ projectVersion: "1.0.0",
57
+ });
58
+ });
59
+
60
+ it("returns undefined when project identity is missing", () => {
61
+ const payload = buildDependencyTrackBomPayload({}, { bom: "test3" });
62
+ assert.strictEqual(payload, undefined);
63
+ });
64
+
65
+ it("supports configurable autoCreate and isLatest", () => {
66
+ const payload = buildDependencyTrackBomPayload(
67
+ {
68
+ autoCreate: false,
69
+ isLatest: true,
70
+ projectName: "child",
71
+ },
72
+ { bom: "test4" },
73
+ );
74
+ assert.deepStrictEqual(payload, {
75
+ autoCreate: "false",
76
+ bom: "eyJib20iOiJ0ZXN0NCJ9",
77
+ isLatest: true,
78
+ projectName: "child",
79
+ projectVersion: "main",
80
+ });
81
+ });
82
+
83
+ it("defaults projectVersion to main when only projectName is provided", () => {
84
+ const payload = buildDependencyTrackBomPayload(
85
+ { projectName: "child" },
86
+ { bom: "test5" },
87
+ );
88
+ assert.deepStrictEqual(payload, {
89
+ autoCreate: "true",
90
+ bom: "eyJib20iOiJ0ZXN0NSJ9",
91
+ projectName: "child",
92
+ projectVersion: "main",
93
+ });
94
+ });
95
+
96
+ it("returns undefined when parent UUID and parent name/version are both provided", () => {
97
+ const payload = buildDependencyTrackBomPayload(
98
+ {
99
+ parentProjectId: "d9628844-5f04-4ca7-88a2-64eb6bc64db0",
100
+ parentProjectName: "parent",
101
+ parentProjectVersion: "1.0.0",
102
+ projectName: "child",
103
+ },
104
+ { bom: "test6" },
105
+ );
106
+ assert.strictEqual(payload, undefined);
107
+ });
108
+
109
+ it("returns undefined when parent name/version mode is incomplete", () => {
110
+ const payload = buildDependencyTrackBomPayload(
111
+ {
112
+ parentProjectName: "parent",
113
+ projectName: "child",
114
+ },
115
+ { bom: "test7" },
116
+ );
117
+ assert.strictEqual(payload, undefined);
118
+ });
119
+ });