@cyclonedx/cdxgen 12.1.4 → 12.2.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 (184) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +181 -90
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +3 -3
  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 +484 -440
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +5 -18
  17. package/lib/evinser/swiftsem.js +1 -1
  18. package/lib/helpers/bomSigner.js +312 -0
  19. package/lib/helpers/bomSigner.poku.js +156 -0
  20. package/lib/helpers/caxa.js +1 -1
  21. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  22. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  23. package/lib/helpers/ciParsers/circleCi.js +286 -0
  24. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  25. package/lib/helpers/ciParsers/common.js +24 -0
  26. package/lib/helpers/ciParsers/githubActions.js +636 -0
  27. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  28. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  29. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  30. package/lib/helpers/ciParsers/jenkins.js +181 -0
  31. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  32. package/lib/helpers/depsUtils.js +203 -0
  33. package/lib/helpers/depsUtils.poku.js +150 -0
  34. package/lib/helpers/display.js +429 -14
  35. package/lib/helpers/envcontext.js +23 -8
  36. package/lib/helpers/formulationParsers.js +351 -0
  37. package/lib/helpers/logger.js +14 -0
  38. package/lib/helpers/protobom.js +9 -9
  39. package/lib/helpers/pythonutils.js +305 -0
  40. package/lib/helpers/pythonutils.poku.js +469 -0
  41. package/lib/helpers/utils.js +970 -528
  42. package/lib/helpers/utils.poku.js +139 -256
  43. package/lib/helpers/versutils.js +202 -0
  44. package/lib/helpers/versutils.poku.js +315 -0
  45. package/lib/helpers/vsixutils.js +1061 -0
  46. package/lib/helpers/vsixutils.poku.js +2247 -0
  47. package/lib/managers/binary.js +19 -19
  48. package/lib/managers/docker.js +108 -1
  49. package/lib/managers/oci.js +10 -0
  50. package/lib/managers/piptree.js +4 -10
  51. package/lib/parsers/npmrc.js +92 -0
  52. package/lib/parsers/npmrc.poku.js +528 -0
  53. package/lib/server/openapi.yaml +1 -10
  54. package/lib/server/server.js +58 -16
  55. package/lib/server/server.poku.js +123 -144
  56. package/lib/stages/postgen/annotator.js +1 -1
  57. package/lib/stages/postgen/auditBom.js +197 -0
  58. package/lib/stages/postgen/auditBom.poku.js +378 -0
  59. package/lib/stages/postgen/postgen.js +54 -1
  60. package/lib/stages/postgen/postgen.poku.js +90 -1
  61. package/lib/stages/postgen/ruleEngine.js +369 -0
  62. package/lib/stages/pregen/envAudit.js +299 -0
  63. package/lib/stages/pregen/envAudit.poku.js +572 -0
  64. package/lib/stages/pregen/pregen.js +12 -8
  65. package/lib/third-party/arborist/lib/deepest-nesting-target.js +1 -1
  66. package/lib/third-party/arborist/lib/node.js +3 -3
  67. package/lib/third-party/arborist/lib/shrinkwrap.js +1 -1
  68. package/lib/third-party/arborist/lib/tree-check.js +1 -1
  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 -8
  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/bomSigner.d.ts +27 -0
  95. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  96. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  98. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  100. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  102. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  104. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  106. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  108. package/types/lib/helpers/depsUtils.d.ts +21 -0
  109. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  110. package/types/lib/helpers/display.d.ts +111 -11
  111. package/types/lib/helpers/display.d.ts.map +1 -1
  112. package/types/lib/helpers/envcontext.d.ts +19 -7
  113. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  114. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  115. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  116. package/types/lib/helpers/logger.d.ts +15 -1
  117. package/types/lib/helpers/logger.d.ts.map +1 -1
  118. package/types/lib/helpers/protobom.d.ts +2 -2
  119. package/types/lib/helpers/pythonutils.d.ts +18 -0
  120. package/types/lib/helpers/pythonutils.d.ts.map +1 -0
  121. package/types/lib/helpers/utils.d.ts +532 -128
  122. package/types/lib/helpers/utils.d.ts.map +1 -1
  123. package/types/lib/helpers/versutils.d.ts +8 -0
  124. package/types/lib/helpers/versutils.d.ts.map +1 -0
  125. package/types/lib/helpers/vsixutils.d.ts +130 -0
  126. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  127. package/types/lib/managers/docker.d.ts +12 -31
  128. package/types/lib/managers/docker.d.ts.map +1 -1
  129. package/types/lib/managers/oci.d.ts +11 -1
  130. package/types/lib/managers/oci.d.ts.map +1 -1
  131. package/types/lib/managers/piptree.d.ts.map +1 -1
  132. package/types/lib/parsers/npmrc.d.ts +26 -0
  133. package/types/lib/parsers/npmrc.d.ts.map +1 -0
  134. package/types/lib/server/server.d.ts +21 -2
  135. package/types/lib/server/server.d.ts.map +1 -1
  136. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  137. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  138. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  139. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  140. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  141. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  142. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  143. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  144. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  145. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  146. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  147. package/types/lib/validator/complianceEngine.d.ts +66 -0
  148. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  149. package/types/lib/validator/complianceRules.d.ts +70 -0
  150. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  151. package/types/lib/validator/index.d.ts +70 -0
  152. package/types/lib/validator/index.d.ts.map +1 -0
  153. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  154. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  155. package/types/lib/validator/reporters/console.d.ts +30 -0
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  157. package/types/lib/validator/reporters/index.d.ts +21 -0
  158. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  159. package/types/lib/validator/reporters/json.d.ts +11 -0
  160. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  161. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  162. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  163. package/lib/helpers/db.js +0 -162
  164. package/types/helpers/db.d.ts +0 -35
  165. package/types/helpers/db.d.ts.map +0 -1
  166. package/types/lib/helpers/db.d.ts +0 -35
  167. package/types/lib/helpers/db.d.ts.map +0 -1
  168. package/types/lib/helpers/validator.d.ts.map +0 -1
  169. package/types/managers/binary.d.ts +0 -37
  170. package/types/managers/binary.d.ts.map +0 -1
  171. package/types/managers/docker.d.ts +0 -56
  172. package/types/managers/docker.d.ts.map +0 -1
  173. package/types/managers/oci.d.ts +0 -2
  174. package/types/managers/oci.d.ts.map +0 -1
  175. package/types/managers/piptree.d.ts +0 -2
  176. package/types/managers/piptree.d.ts.map +0 -1
  177. package/types/server/server.d.ts +0 -34
  178. package/types/server/server.d.ts.map +0 -1
  179. package/types/stages/postgen/annotator.d.ts +0 -27
  180. package/types/stages/postgen/annotator.d.ts.map +0 -1
  181. package/types/stages/postgen/postgen.d.ts +0 -51
  182. package/types/stages/postgen/postgen.d.ts.map +0 -1
  183. package/types/stages/pregen/pregen.d.ts +0 -59
  184. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -0,0 +1,636 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { PackageURL } from "packageurl-js";
4
+ import { v4 as uuidv4 } from "uuid";
5
+ import { parse as _load } from "yaml";
6
+
7
+ import { disambiguateSteps } from "./common.js";
8
+
9
+ /**
10
+ * Known GitHub Actions permission scopes that grant write access.
11
+ * @type {string[]}
12
+ */
13
+ const WRITE_SCOPES = [
14
+ "actions",
15
+ "artifact-metadata",
16
+ "attestations",
17
+ "checks",
18
+ "contents",
19
+ "deployments",
20
+ "discussions",
21
+ "id-token",
22
+ "issues",
23
+ "models",
24
+ "packages",
25
+ "pages",
26
+ "pull-requests",
27
+ "security-events",
28
+ "statuses",
29
+ ];
30
+
31
+ /**
32
+ * Workflow triggers considered high-risk because they can execute code in a
33
+ * privileged context or expose secrets to untrusted input.
34
+ * @type {string[]}
35
+ */
36
+ const HIGH_RISK_TRIGGERS = [
37
+ "pull_request_target",
38
+ "issue_comment",
39
+ "workflow_run",
40
+ ];
41
+
42
+ /**
43
+ * Analyse a workflow-level or job-level permissions map for any write grants.
44
+ *
45
+ * Accepts the raw `permissions` value from a workflow YAML which can be an
46
+ * object mapping scope names to `"read"` / `"write"`, or the shorthand
47
+ * strings `"write-all"` / `"read-all"`.
48
+ *
49
+ * @param {Object|string|undefined} permissions - The permissions map or shorthand string.
50
+ * @returns {boolean} `true` when at least one scope has write access.
51
+ */
52
+ function analyzePermissions(permissions) {
53
+ if (!permissions) {
54
+ return false;
55
+ }
56
+ if (typeof permissions === "string") {
57
+ return permissions === "write-all";
58
+ }
59
+ if (typeof permissions !== "object") {
60
+ return false;
61
+ }
62
+ for (const scope of WRITE_SCOPES) {
63
+ if (permissions[scope] === "write") {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Detect if a step uses `actions/checkout` and extract the
72
+ * `persist-credentials` setting (defaults to `true` when absent).
73
+ *
74
+ * @param {Object} step - A single workflow step object.
75
+ * @returns {Array<{name: string, value: string}>} Property entries to append.
76
+ */
77
+ function analyzeCheckoutStep(step) {
78
+ const props = [];
79
+ if (step.uses?.includes("actions/checkout")) {
80
+ const persistCreds = step.with?.["persist-credentials"] ?? true;
81
+ props.push({
82
+ name: "cdx:github:checkout:persistCredentials",
83
+ value: String(persistCreds),
84
+ });
85
+ }
86
+ return props;
87
+ }
88
+
89
+ /**
90
+ * Detect `actions/cache` usage and extract key, path, and restore-keys
91
+ * metadata from the step's `with` block.
92
+ *
93
+ * @param {Object} step - A single workflow step object.
94
+ * @returns {Array<{name: string, value: string}>} Property entries to append.
95
+ */
96
+ function analyzeCacheStep(step) {
97
+ const props = [];
98
+ if (step.uses?.includes("actions/cache")) {
99
+ if (step.with?.key) {
100
+ props.push({ name: "cdx:github:cache:key", value: step.with.key });
101
+ }
102
+ if (step.with?.path) {
103
+ props.push({ name: "cdx:github:cache:path", value: step.with.path });
104
+ }
105
+ if (step.with?.["restore-keys"]) {
106
+ let keys = step.with["restore-keys"];
107
+ if (Array.isArray(keys)) {
108
+ keys = keys.join(",");
109
+ } else if (typeof keys === "string" && keys.includes("\n")) {
110
+ keys = keys
111
+ .split("\n")
112
+ .map((k) => k.trim())
113
+ .filter((k) => k)
114
+ .join(",");
115
+ }
116
+ props.push({ name: "cdx:github:cache:restoreKeys", value: keys });
117
+ }
118
+ }
119
+ return props;
120
+ }
121
+
122
+ /**
123
+ * Detect untrusted expression interpolation in `run:` blocks.
124
+ *
125
+ * Scans the raw shell string for `${{ … }}` patterns and flags any that
126
+ * reference user-controlled contexts such as `github.event.pull_request.*`,
127
+ * `github.event.issue.*`, `github.event.comment.*`, `github.head_ref`, or
128
+ * `inputs.*`.
129
+ *
130
+ * @param {string|undefined} runValue - The raw `run:` block string.
131
+ * @returns {{ hasInterpolation: boolean, vars: string[] }}
132
+ */
133
+ function detectUntrustedInterpolation(runValue) {
134
+ if (!runValue) return { hasInterpolation: false, vars: [] };
135
+ // Capture expression content inside ${{ … }}, allowing nested single braces
136
+ // (e.g. the || operator in `${{ a || b }}` where } appears inside the expr).
137
+ const pattern = /\$\{\{\s*([^}]+(?:}[^}])*)}}/g;
138
+ const matches = [...runValue.matchAll(pattern)];
139
+ const untrustedVars = new Set();
140
+
141
+ for (const match of matches) {
142
+ const expr = match[1].trim();
143
+ if (
144
+ expr.startsWith("github.event.pull_request") ||
145
+ expr.startsWith("github.event.issue") ||
146
+ expr.startsWith("github.event.comment") ||
147
+ expr.startsWith("github.head_ref") ||
148
+ expr.startsWith("inputs.")
149
+ ) {
150
+ untrustedVars.add(expr);
151
+ }
152
+ }
153
+
154
+ return {
155
+ hasInterpolation: untrustedVars.size > 0,
156
+ vars: Array.from(untrustedVars),
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Classify a GitHub Actions version reference as `"sha"`, `"tag"`, or `"branch"`.
162
+ *
163
+ * @param {string|undefined} versionRef - The part after `@` in `uses: owner/action@ref`.
164
+ * @returns {"sha"|"tag"|"branch"|"unknown"} The pinning category.
165
+ */
166
+ function getVersionPinningType(versionRef) {
167
+ if (!versionRef) {
168
+ return "unknown";
169
+ }
170
+ if (/^[a-f0-9]{40}$/.test(versionRef) || /^[a-f0-9]{7,}$/.test(versionRef)) {
171
+ return "sha";
172
+ }
173
+ if (
174
+ versionRef === "main" ||
175
+ versionRef === "master" ||
176
+ versionRef.includes("/")
177
+ ) {
178
+ return "branch";
179
+ }
180
+ return "tag";
181
+ }
182
+
183
+ /**
184
+ * Normalise the `on:` trigger value from a workflow YAML into a
185
+ * comma-separated string of trigger names.
186
+ *
187
+ * GitHub Actions supports three forms:
188
+ * - string: `on: push`
189
+ * - array: `on: [push, pull_request]`
190
+ * - object: `on: { push: { branches: [main] } }`
191
+ *
192
+ * @param {string|string[]|Object|undefined} triggers - Raw `on` value.
193
+ * @returns {string} Comma-separated trigger names, or empty string.
194
+ */
195
+ function normalizeTriggers(triggers) {
196
+ if (!triggers) return "";
197
+ if (typeof triggers === "string") return triggers;
198
+ if (Array.isArray(triggers)) return triggers.join(",");
199
+ return Object.keys(triggers).join(",");
200
+ }
201
+
202
+ /**
203
+ * Determine whether the given trigger value includes at least one high-risk
204
+ * trigger (`pull_request_target`, `issue_comment`, or `workflow_run`).
205
+ *
206
+ * @param {string|string[]|Object|undefined} triggers - Raw `on` value.
207
+ * @returns {boolean}
208
+ */
209
+ function hasHighRiskTrigger(triggers) {
210
+ const csv = normalizeTriggers(triggers);
211
+ if (!csv) return false;
212
+ return csv.split(",").some((t) => HIGH_RISK_TRIGGERS.includes(t.trim()));
213
+ }
214
+
215
+ /**
216
+ * Build the set of common workflow-context properties that are duplicated
217
+ * onto every component (action or run-step) so that policy rules written
218
+ * against `components[…]` can evaluate workflow-level attributes without
219
+ * traversing the formulation tree.
220
+ *
221
+ * @param {Object} ctx
222
+ * @param {boolean} ctx.hasWritePermissions - Whether workflow OR job has write perms.
223
+ * @param {boolean} ctx.hasIdTokenWrite - Whether `id-token: write` is granted.
224
+ * @param {string} ctx.triggers - Comma-separated trigger names.
225
+ * @param {boolean} ctx.isHighRisk - Whether any trigger is high-risk.
226
+ * @param {string} concurrencyGroup - Workflow concurrency group.
227
+ * @returns {Array<{name: string, value: string}>}
228
+ */
229
+ function buildWorkflowContextProperties({
230
+ hasWritePermissions,
231
+ hasIdTokenWrite,
232
+ triggers,
233
+ isHighRisk,
234
+ concurrencyGroup,
235
+ }) {
236
+ const props = [];
237
+ if (hasWritePermissions) {
238
+ props.push({
239
+ name: "cdx:github:workflow:hasWritePermissions",
240
+ value: "true",
241
+ });
242
+ }
243
+ if (hasIdTokenWrite) {
244
+ props.push({
245
+ name: "cdx:github:workflow:hasIdTokenWrite",
246
+ value: "true",
247
+ });
248
+ }
249
+ if (triggers) {
250
+ props.push({ name: "cdx:github:workflow:triggers", value: triggers });
251
+ }
252
+ if (isHighRisk) {
253
+ props.push({
254
+ name: "cdx:github:workflow:hasHighRiskTrigger",
255
+ value: "true",
256
+ });
257
+ }
258
+ if (concurrencyGroup) {
259
+ props.push({
260
+ name: "cdx:github:workflow:concurrencyGroup",
261
+ value: concurrencyGroup,
262
+ });
263
+ }
264
+ return props;
265
+ }
266
+
267
+ /**
268
+ * Parse a single GitHub Actions workflow file and return formulation-shaped data.
269
+ *
270
+ * Reads and parses the YAML, then walks every job and step to produce:
271
+ * - **workflows** – CycloneDX formulation workflow objects with tasks
272
+ * - **components** – action references (`pkg:github/…`) and run-step processes
273
+ * - **dependencies** – workflow→job and job→action/step edges
274
+ *
275
+ * @param {string} f - Absolute path to a workflow YAML file.
276
+ * @param {Object} options - CLI options
277
+ * @returns {{ workflows: Object[], components: Object[], dependencies: Object[] }}
278
+ */
279
+ export function parseWorkflowFile(f, options) {
280
+ const workflows = [];
281
+ const components = [];
282
+ const dependencies = [];
283
+
284
+ let raw;
285
+ try {
286
+ raw = readFileSync(f, { encoding: "utf-8" });
287
+ } catch (_e) {
288
+ return { workflows, components, dependencies };
289
+ }
290
+
291
+ let yamlObj;
292
+ try {
293
+ yamlObj = _load(raw);
294
+ } catch (_e) {
295
+ return { workflows, components, dependencies };
296
+ }
297
+
298
+ if (!yamlObj?.jobs) {
299
+ return { workflows, components, dependencies };
300
+ }
301
+ const workflowName =
302
+ yamlObj.name ||
303
+ f
304
+ .split("/")
305
+ .pop()
306
+ .replace(/\.[^.]+$/, "");
307
+ const workflowTriggers = yamlObj.on || yamlObj.true;
308
+ const workflowPermissions = yamlObj.permissions || {};
309
+ const workflowHasWritePermissions = analyzePermissions(workflowPermissions);
310
+ const workflowConcurrency = yamlObj.concurrency || {};
311
+ const hasIdTokenWrite = workflowPermissions?.["id-token"] === "write";
312
+ const triggers = normalizeTriggers(workflowTriggers);
313
+ const isHighRisk = hasHighRiskTrigger(workflowTriggers);
314
+
315
+ const workflowRef = uuidv4();
316
+ const tasks = [];
317
+ const workflowDependsOn = [];
318
+
319
+ for (const jobName of Object.keys(yamlObj.jobs)) {
320
+ const job = yamlObj.jobs[jobName];
321
+ const jobRef = uuidv4();
322
+ const steps = [];
323
+ const jobDependsOn = [];
324
+
325
+ // Job needs (dependency links)
326
+ let jobNeeds = job.needs || [];
327
+ if (!Array.isArray(jobNeeds)) {
328
+ jobNeeds = [jobNeeds];
329
+ }
330
+
331
+ const jobRunner = job["runs-on"] || "unknown";
332
+ const jobEnvironment = job.environment?.name || job.environment || "";
333
+ const jobTimeout = job["timeout-minutes"] || null;
334
+ const jobPermissions = job.permissions || {};
335
+ const jobHasWritePermissions = analyzePermissions(jobPermissions);
336
+ const jobServices = job.services ? Object.keys(job.services) : [];
337
+ const effectiveWritePerms =
338
+ workflowHasWritePermissions || jobHasWritePermissions;
339
+
340
+ // Shared workflow-context properties for this job's components
341
+ const sharedCtxProps = buildWorkflowContextProperties({
342
+ hasWritePermissions: effectiveWritePerms,
343
+ hasIdTokenWrite,
344
+ triggers,
345
+ isHighRisk,
346
+ });
347
+
348
+ const jobProperties = [
349
+ { name: "cdx:github:job:name", value: jobName },
350
+ {
351
+ name: "cdx:github:job:runner",
352
+ value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
353
+ },
354
+ ];
355
+ if (jobEnvironment) {
356
+ jobProperties.push({
357
+ name: "cdx:github:job:environment",
358
+ value: jobEnvironment,
359
+ });
360
+ }
361
+ if (jobTimeout) {
362
+ jobProperties.push({
363
+ name: "cdx:github:job:timeoutMinutes",
364
+ value: jobTimeout.toString(),
365
+ });
366
+ }
367
+ if (jobHasWritePermissions) {
368
+ jobProperties.push({
369
+ name: "cdx:github:job:hasWritePermissions",
370
+ value: "true",
371
+ });
372
+ }
373
+ if (jobServices.length) {
374
+ jobProperties.push({
375
+ name: "cdx:github:job:services",
376
+ value: jobServices.join(","),
377
+ });
378
+ }
379
+ if (jobNeeds.length) {
380
+ jobProperties.push({
381
+ name: "cdx:github:job:needs",
382
+ value: jobNeeds.join(","),
383
+ });
384
+ }
385
+ jobProperties.push(...sharedCtxProps);
386
+
387
+ for (const step of job.steps || []) {
388
+ const stepName = step.name || step.uses || "unnamed step";
389
+ const commands = [];
390
+ let actionProperties = [];
391
+ if (step.uses) {
392
+ commands.push({ executed: step.uses });
393
+ // Collect action references as components
394
+ const tmpA = step.uses.split("@");
395
+ if (tmpA.length === 2) {
396
+ const groupName = tmpA[0];
397
+ const tagOrCommit = tmpA[1];
398
+ const versionPinningType = getVersionPinningType(tagOrCommit);
399
+ const isShaPinned = versionPinningType === "sha";
400
+
401
+ const tmpB = groupName.split("/");
402
+ const name = tmpB.length >= 2 ? tmpB.pop() : tmpB[0];
403
+ const group = tmpB.join("/");
404
+ const purl = new PackageURL(
405
+ "github",
406
+ group || undefined,
407
+ name,
408
+ tagOrCommit,
409
+ null,
410
+ null,
411
+ ).toString();
412
+
413
+ actionProperties = [
414
+ ...actionProperties,
415
+ { name: "SrcFile", value: f },
416
+ { name: "cdx:github:workflow:name", value: workflowName },
417
+ { name: "cdx:github:workflow:file", value: f },
418
+ { name: "cdx:github:job:name", value: jobName },
419
+ {
420
+ name: "cdx:github:job:runner",
421
+ value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
422
+ },
423
+ { name: "cdx:github:action:uses", value: step.uses },
424
+ {
425
+ name: "cdx:github:action:versionPinningType",
426
+ value: versionPinningType,
427
+ },
428
+ {
429
+ name: "cdx:github:action:isShaPinned",
430
+ value: isShaPinned.toString(),
431
+ },
432
+ ];
433
+ if (step.name) {
434
+ actionProperties.push({
435
+ name: "cdx:github:step:name",
436
+ value: step.name,
437
+ });
438
+ }
439
+ if (step.if) {
440
+ actionProperties.push({
441
+ name: "cdx:github:step:condition",
442
+ value: step.if,
443
+ });
444
+ }
445
+ if (step["continue-on-error"]) {
446
+ actionProperties.push({
447
+ name: "cdx:github:step:continueOnError",
448
+ value: "true",
449
+ });
450
+ }
451
+ if (step.timeout) {
452
+ actionProperties.push({
453
+ name: "cdx:github:step:timeout",
454
+ value: step.timeout.toString(),
455
+ });
456
+ }
457
+ if (group?.startsWith("github/") || group === "actions") {
458
+ actionProperties.push({
459
+ name: "cdx:actions:isOfficial",
460
+ value: "true",
461
+ });
462
+ }
463
+ if (group?.startsWith("github/")) {
464
+ actionProperties.push({
465
+ name: "cdx:actions:isVerified",
466
+ value: "true",
467
+ });
468
+ }
469
+ actionProperties.push(...analyzeCheckoutStep(step));
470
+ actionProperties.push(...analyzeCacheStep(step));
471
+ actionProperties.push(...sharedCtxProps);
472
+ const evidence = {
473
+ identity: [
474
+ {
475
+ field: "purl",
476
+ confidence: 0.5,
477
+ methods: [
478
+ {
479
+ technique: "source-code-analysis",
480
+ confidence: 0.5,
481
+ value: f,
482
+ },
483
+ ],
484
+ },
485
+ ],
486
+ };
487
+ const acomp = {
488
+ "bom-ref": purl,
489
+ type: "application",
490
+ group,
491
+ name,
492
+ version: tagOrCommit,
493
+ purl,
494
+ properties: actionProperties,
495
+ scope: "required",
496
+ evidence,
497
+ };
498
+ if (options?.specVersion >= 1.7) {
499
+ acomp.isExternal = true;
500
+ }
501
+ components.push(acomp);
502
+ jobDependsOn.push(purl);
503
+ }
504
+ } else if (step.run) {
505
+ commands.push({ executed: step.run.trim().split("\n")[0] });
506
+ const stepRef = `${jobRef}-step-${steps.length + 1}`;
507
+ const runProperties = [
508
+ { name: "SrcFile", value: f },
509
+ { name: "cdx:github:workflow:name", value: workflowName },
510
+ { name: "cdx:github:workflow:file", value: f },
511
+ { name: "cdx:github:job:name", value: jobName },
512
+ { name: "cdx:github:step:type", value: "run" },
513
+ {
514
+ name: "cdx:github:step:command",
515
+ value: step.run.trim().split("\n")[0],
516
+ },
517
+ ];
518
+ runProperties.push(...sharedCtxProps);
519
+
520
+ const { hasInterpolation, vars } = detectUntrustedInterpolation(
521
+ step.run,
522
+ );
523
+ if (hasInterpolation) {
524
+ runProperties.push({
525
+ name: "cdx:github:step:hasUntrustedInterpolation",
526
+ value: "true",
527
+ });
528
+ runProperties.push({
529
+ name: "cdx:github:step:interpolatedVars",
530
+ value: vars.join(","),
531
+ });
532
+ }
533
+ components.push({
534
+ "bom-ref": stepRef,
535
+ purl: undefined,
536
+ scope: "excluded",
537
+ type: "application",
538
+ name: stepName,
539
+ properties: runProperties,
540
+ tags: ["workflow-step"],
541
+ });
542
+
543
+ jobDependsOn.push(stepRef);
544
+ }
545
+
546
+ steps.push({
547
+ name: stepName,
548
+ commands: commands.length ? commands : undefined,
549
+ });
550
+ }
551
+
552
+ const task = {
553
+ "bom-ref": jobRef,
554
+ uid: jobRef,
555
+ name: jobName,
556
+ taskTypes: ["build"],
557
+ steps: disambiguateSteps(steps),
558
+ properties: jobProperties,
559
+ };
560
+
561
+ tasks.push(task);
562
+ workflowDependsOn.push(jobRef);
563
+
564
+ // Wire job→action dependencies
565
+ if (jobDependsOn.length) {
566
+ dependencies.push({ ref: jobRef, dependsOn: jobDependsOn });
567
+ }
568
+ }
569
+
570
+ // Build workflow-level properties using the same helpers
571
+ const workflowProperties = [
572
+ { name: "cdx:github:workflow:file", value: f },
573
+ ...buildWorkflowContextProperties({
574
+ hasWritePermissions: workflowHasWritePermissions,
575
+ hasIdTokenWrite,
576
+ triggers,
577
+ isHighRisk,
578
+ concurrencyGroup: workflowConcurrency?.group,
579
+ }),
580
+ ];
581
+ const workflow = {
582
+ "bom-ref": workflowRef,
583
+ uid: workflowRef,
584
+ name: workflowName,
585
+ taskTypes: ["build"],
586
+ tasks: tasks.length ? tasks : undefined,
587
+ properties: workflowProperties,
588
+ };
589
+
590
+ workflows.push(workflow);
591
+
592
+ if (workflowDependsOn.length) {
593
+ dependencies.push({ ref: workflowRef, dependsOn: workflowDependsOn });
594
+ }
595
+
596
+ return { workflows, components, dependencies };
597
+ }
598
+
599
+ /**
600
+ * GitHub Actions formulation parser.
601
+ *
602
+ * Matches `.github/workflows/*.yml` and `*.yaml` files and converts them into
603
+ * CycloneDX formulation workflow objects, with referenced actions as components.
604
+ *
605
+ * Parser contract: `parse(files, options)` returns
606
+ * `{ workflows, components, services, properties, dependencies }`.
607
+ */
608
+ export const githubActionsParser = {
609
+ id: "github-actions",
610
+ patterns: [".github/workflows/*.{yml,yaml}"],
611
+
612
+ /**
613
+ * @param {string[]} files Matched workflow file paths
614
+ * @param {Object} options CLI options
615
+ * @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
616
+ */
617
+ parse(files, options) {
618
+ const workflows = [];
619
+ const components = [];
620
+ const dependencies = [];
621
+
622
+ for (const f of files) {
623
+ const result = parseWorkflowFile(f, options);
624
+ workflows.push(...result.workflows);
625
+ components.push(...result.components);
626
+ dependencies.push(...result.dependencies);
627
+ }
628
+ return {
629
+ workflows,
630
+ components,
631
+ services: [],
632
+ properties: [],
633
+ dependencies,
634
+ };
635
+ },
636
+ };