@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.
- package/README.md +51 -40
- package/bin/cdxgen.js +194 -97
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +449 -429
- package/lib/cli/index.poku.js +117 -0
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/analyzer.js +606 -3
- package/lib/helpers/analyzer.poku.js +230 -0
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +219 -0
- package/lib/helpers/depsUtils.poku.js +207 -0
- package/lib/helpers/display.js +426 -5
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/utils.js +865 -416
- package/lib/helpers/utils.poku.js +172 -265
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +34 -1
- package/lib/server/server.js +50 -13
- package/lib/server/server.poku.js +332 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +196 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -9
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +533 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +22 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- 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
|
+
};
|