@cyclonedx/cdxgen 12.2.0 → 12.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- 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/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.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/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +30 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -35
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- package/types/bin/licenses.d.ts.map +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import { PackageURL } from "packageurl-js";
|
|
4
5
|
import { v4 as uuidv4 } from "uuid";
|
|
5
6
|
import { parse as _load } from "yaml";
|
|
6
7
|
|
|
8
|
+
import { scanTextForHiddenUnicode } from "../unicodeScan.js";
|
|
7
9
|
import { disambiguateSteps } from "./common.js";
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -39,6 +41,119 @@ const HIGH_RISK_TRIGGERS = [
|
|
|
39
41
|
"workflow_run",
|
|
40
42
|
];
|
|
41
43
|
|
|
44
|
+
const LOW_RISK_INTERPOLATION_PATTERNS = [
|
|
45
|
+
/^github\.sha$/,
|
|
46
|
+
/^github\.event\.pull_request\.(?:head|base)\.sha$/,
|
|
47
|
+
/^github\.event\.workflow_run\.head_sha$/,
|
|
48
|
+
/^github\.event\.pull_request\.number$/,
|
|
49
|
+
/^github\.event\.issue\.number$/,
|
|
50
|
+
/^github\.run_attempt$/,
|
|
51
|
+
/^github\.run_id$/,
|
|
52
|
+
/^github\.run_number$/,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const LEGACY_PUBLISH_TOKEN_ENV_NAMES = new Set([
|
|
56
|
+
"NPM_CONFIG_TOKEN",
|
|
57
|
+
"TWINE_PASSWORD",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const SECRET_LIKE_ENV_NAME_PATTERN =
|
|
61
|
+
/token|secret|password|credential|auth|api[_-]?key|access[_-]?key|client[_-]?secret/i;
|
|
62
|
+
|
|
63
|
+
const SENSITIVE_ENV_VALUE_PATTERN =
|
|
64
|
+
/secrets\.[A-Za-z0-9_]+|github\.token|ACTIONS_ID_TOKEN_REQUEST_(?:TOKEN|URL)/i;
|
|
65
|
+
|
|
66
|
+
const SHELL_VARIABLE_REFERENCE_PATTERN =
|
|
67
|
+
/\$[A-Za-z_][A-Za-z0-9_]*\b|\$\{[A-Za-z_][A-Za-z0-9_]*}|%[A-Za-z_][A-Za-z0-9_]*%|\$env:[A-Za-z_][A-Za-z0-9_]*\b/i;
|
|
68
|
+
|
|
69
|
+
const IMPLICIT_SENSITIVE_ENV_NAMES = [
|
|
70
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
|
71
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
72
|
+
"ACTIONS_RUNTIME_TOKEN",
|
|
73
|
+
"GITHUB_TOKEN",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const OUTBOUND_NETWORK_TOOLS = [
|
|
77
|
+
["curl", /\bcurl\b/i],
|
|
78
|
+
["wget", /\bwget\b/i],
|
|
79
|
+
["invoke-webrequest", /\b(?:invoke-webrequest|iwr)\b/i],
|
|
80
|
+
["invoke-restmethod", /\b(?:invoke-restmethod|irm)\b/i],
|
|
81
|
+
["nc", /\b(?:nc|ncat|netcat)\b/i],
|
|
82
|
+
["scp", /\bscp\b/i],
|
|
83
|
+
["rsync", /\brsync\b/i],
|
|
84
|
+
["ftp", /\b(?:ftp|sftp)\b/i],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const KNOWN_DISPATCH_ACTIONS = [
|
|
88
|
+
{
|
|
89
|
+
kind: "repository_dispatch",
|
|
90
|
+
mechanism: "repository-dispatch-action",
|
|
91
|
+
pattern: /^peter-evans\/repository-dispatch(?:@|$)/i,
|
|
92
|
+
repoKeys: ["repository"],
|
|
93
|
+
targetKeys: ["event-type"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
kind: "workflow_dispatch",
|
|
97
|
+
mechanism: "workflow-dispatch-action",
|
|
98
|
+
pattern:
|
|
99
|
+
/^(?:benc-uk\/workflow-dispatch|lasith-kg\/dispatch-workflow|convictional\/trigger-workflow-and-wait-for-workflow)(?:@|$)/i,
|
|
100
|
+
repoKeys: ["repo", "repository"],
|
|
101
|
+
targetKeys: ["workflow", "workflow_id", "event-type", "ref"],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const FORK_CONTEXT_PATTERNS = [
|
|
106
|
+
[
|
|
107
|
+
"github.event.pull_request.head.repo.fork",
|
|
108
|
+
/github\.event\.pull_request\.head\.repo\.fork/i,
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
"github.event.pull_request.head.repo.full_name",
|
|
112
|
+
/github\.event\.pull_request\.head\.repo\.full_name/i,
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"github.event.pull_request.head.repo.clone_url",
|
|
116
|
+
/github\.event\.pull_request\.head\.repo\.clone_url/i,
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
"github.event.workflow_run.head_repository.fork",
|
|
120
|
+
/github\.event\.workflow_run\.head_repository\.fork/i,
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
"github.event.workflow_run.head_repository.full_name",
|
|
124
|
+
/github\.event\.workflow_run\.head_repository\.full_name/i,
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
"github.event.pull_request.head.ref",
|
|
128
|
+
/github\.event\.pull_request\.head\.ref/i,
|
|
129
|
+
],
|
|
130
|
+
["github.head_ref", /github\.head_ref/i],
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const UNTRUSTED_CHECKOUT_CONTEXT_PATTERNS = [
|
|
134
|
+
[
|
|
135
|
+
"github.event.pull_request.head.sha",
|
|
136
|
+
/github\.event\.pull_request\.head\.sha/i,
|
|
137
|
+
],
|
|
138
|
+
[
|
|
139
|
+
"github.event.pull_request.head.ref",
|
|
140
|
+
/github\.event\.pull_request\.head\.ref/i,
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"github.event.pull_request.head.label",
|
|
144
|
+
/github\.event\.pull_request\.head\.label/i,
|
|
145
|
+
],
|
|
146
|
+
["github.head_ref", /github\.head_ref/i],
|
|
147
|
+
[
|
|
148
|
+
"github.event.workflow_run.head_sha",
|
|
149
|
+
/github\.event\.workflow_run\.head_sha/i,
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
"github.event.workflow_run.head_branch",
|
|
153
|
+
/github\.event\.workflow_run\.head_branch/i,
|
|
154
|
+
],
|
|
155
|
+
];
|
|
156
|
+
|
|
42
157
|
/**
|
|
43
158
|
* Analyse a workflow-level or job-level permissions map for any write grants.
|
|
44
159
|
*
|
|
@@ -67,6 +182,137 @@ function analyzePermissions(permissions) {
|
|
|
67
182
|
return false;
|
|
68
183
|
}
|
|
69
184
|
|
|
185
|
+
function extractWriteScopes(permissions) {
|
|
186
|
+
if (!permissions) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
if (typeof permissions === "string") {
|
|
190
|
+
return permissions === "write-all" ? ["all"] : [];
|
|
191
|
+
}
|
|
192
|
+
if (typeof permissions !== "object") {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const scopes = [];
|
|
196
|
+
for (const scope of WRITE_SCOPES) {
|
|
197
|
+
if (permissions[scope] === "write") {
|
|
198
|
+
scopes.push(scope);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return scopes;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hasIdTokenWritePermission(permissions) {
|
|
205
|
+
if (!permissions) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (typeof permissions === "string") {
|
|
209
|
+
return permissions === "write-all";
|
|
210
|
+
}
|
|
211
|
+
if (typeof permissions !== "object") {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return permissions["id-token"] === "write";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getPropertyValueFromProperties(properties, propName) {
|
|
218
|
+
return properties.find((property) => property.name === propName)?.value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function appendSensitiveOperationProperties(properties) {
|
|
222
|
+
const sensitiveOperations = new Set();
|
|
223
|
+
if (
|
|
224
|
+
getPropertyValueFromProperties(
|
|
225
|
+
properties,
|
|
226
|
+
"cdx:github:step:referencesSensitiveContext",
|
|
227
|
+
) === "true"
|
|
228
|
+
) {
|
|
229
|
+
sensitiveOperations.add("references-sensitive-context");
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
getPropertyValueFromProperties(
|
|
233
|
+
properties,
|
|
234
|
+
"cdx:github:step:dispatchesWorkflow",
|
|
235
|
+
) === "true"
|
|
236
|
+
) {
|
|
237
|
+
sensitiveOperations.add("dispatches-workflow");
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
getPropertyValueFromProperties(
|
|
241
|
+
properties,
|
|
242
|
+
"cdx:github:step:mutatesRunnerState",
|
|
243
|
+
) === "true"
|
|
244
|
+
) {
|
|
245
|
+
sensitiveOperations.add("mutates-runner-state");
|
|
246
|
+
}
|
|
247
|
+
if (
|
|
248
|
+
getPropertyValueFromProperties(
|
|
249
|
+
properties,
|
|
250
|
+
"cdx:github:step:usesLegacyPublishToken",
|
|
251
|
+
) === "true"
|
|
252
|
+
) {
|
|
253
|
+
sensitiveOperations.add("legacy-publish-token");
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
getPropertyValueFromProperties(
|
|
257
|
+
properties,
|
|
258
|
+
"cdx:github:step:hasOutboundNetworkCommand",
|
|
259
|
+
) === "true" &&
|
|
260
|
+
getPropertyValueFromProperties(
|
|
261
|
+
properties,
|
|
262
|
+
"cdx:github:step:referencesSensitiveContext",
|
|
263
|
+
) === "true"
|
|
264
|
+
) {
|
|
265
|
+
sensitiveOperations.add("outbound-network-with-sensitive-context");
|
|
266
|
+
}
|
|
267
|
+
const actionUses = getPropertyValueFromProperties(
|
|
268
|
+
properties,
|
|
269
|
+
"cdx:github:action:uses",
|
|
270
|
+
);
|
|
271
|
+
const persistCredentials = getPropertyValueFromProperties(
|
|
272
|
+
properties,
|
|
273
|
+
"cdx:github:checkout:persistCredentials",
|
|
274
|
+
);
|
|
275
|
+
if (
|
|
276
|
+
actionUses?.includes("actions/checkout") &&
|
|
277
|
+
persistCredentials !== "false"
|
|
278
|
+
) {
|
|
279
|
+
sensitiveOperations.add("checkout-persist-credentials");
|
|
280
|
+
}
|
|
281
|
+
if (!sensitiveOperations.size) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
properties.push({
|
|
285
|
+
name: "cdx:github:step:hasSensitiveOperations",
|
|
286
|
+
value: "true",
|
|
287
|
+
});
|
|
288
|
+
properties.push({
|
|
289
|
+
name: "cdx:github:step:sensitiveOperations",
|
|
290
|
+
value: Array.from(sensitiveOperations).join(","),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function normalizeRunnerLabels(runsOn) {
|
|
295
|
+
if (!runsOn) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
if (Array.isArray(runsOn)) {
|
|
299
|
+
return runsOn.map((label) => String(label).trim()).filter(Boolean);
|
|
300
|
+
}
|
|
301
|
+
if (typeof runsOn === "string") {
|
|
302
|
+
return runsOn
|
|
303
|
+
.split(",")
|
|
304
|
+
.map((label) => label.trim())
|
|
305
|
+
.filter(Boolean);
|
|
306
|
+
}
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isSelfHostedRunner(runsOn) {
|
|
311
|
+
return normalizeRunnerLabels(runsOn).some((label) =>
|
|
312
|
+
label.toLowerCase().includes("self-hosted"),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
70
316
|
/**
|
|
71
317
|
* Detect if a step uses `actions/checkout` and extract the
|
|
72
318
|
* `persist-credentials` setting (defaults to `true` when absent).
|
|
@@ -78,14 +324,66 @@ function analyzeCheckoutStep(step) {
|
|
|
78
324
|
const props = [];
|
|
79
325
|
if (step.uses?.includes("actions/checkout")) {
|
|
80
326
|
const persistCreds = step.with?.["persist-credentials"] ?? true;
|
|
327
|
+
const checkoutRef = step.with?.ref;
|
|
328
|
+
const checkoutRepository = step.with?.repository;
|
|
81
329
|
props.push({
|
|
82
330
|
name: "cdx:github:checkout:persistCredentials",
|
|
83
331
|
value: String(persistCreds),
|
|
84
332
|
});
|
|
333
|
+
if (checkoutRef) {
|
|
334
|
+
props.push({ name: "cdx:github:checkout:ref", value: checkoutRef });
|
|
335
|
+
}
|
|
336
|
+
if (checkoutRepository) {
|
|
337
|
+
props.push({
|
|
338
|
+
name: "cdx:github:checkout:repository",
|
|
339
|
+
value: checkoutRepository,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const untrustedCheckoutContexts = [
|
|
343
|
+
...detectCheckoutUntrustedContexts(checkoutRef),
|
|
344
|
+
...detectCheckoutUntrustedContexts(checkoutRepository),
|
|
345
|
+
];
|
|
346
|
+
if (untrustedCheckoutContexts.length) {
|
|
347
|
+
props.push({
|
|
348
|
+
name: "cdx:github:checkout:checksOutUntrustedRef",
|
|
349
|
+
value: "true",
|
|
350
|
+
});
|
|
351
|
+
props.push({
|
|
352
|
+
name: "cdx:github:checkout:untrustedRefContexts",
|
|
353
|
+
value: [...new Set(untrustedCheckoutContexts)].join(","),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const forkContextRefs = [
|
|
357
|
+
...detectForkContextReferences(checkoutRef),
|
|
358
|
+
...detectForkContextReferences(checkoutRepository),
|
|
359
|
+
];
|
|
360
|
+
if (forkContextRefs.length) {
|
|
361
|
+
props.push({
|
|
362
|
+
name: "cdx:github:checkout:referencesForkContext",
|
|
363
|
+
value: "true",
|
|
364
|
+
});
|
|
365
|
+
props.push({
|
|
366
|
+
name: "cdx:github:checkout:forkContextRefs",
|
|
367
|
+
value: [...new Set(forkContextRefs)].join(","),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
85
370
|
}
|
|
86
371
|
return props;
|
|
87
372
|
}
|
|
88
373
|
|
|
374
|
+
function detectCheckoutUntrustedContexts(textValue) {
|
|
375
|
+
if (!textValue || typeof textValue !== "string") {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
const refs = [];
|
|
379
|
+
UNTRUSTED_CHECKOUT_CONTEXT_PATTERNS.forEach(([name, pattern]) => {
|
|
380
|
+
if (pattern.test(textValue)) {
|
|
381
|
+
refs.push(name);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return refs;
|
|
385
|
+
}
|
|
386
|
+
|
|
89
387
|
/**
|
|
90
388
|
* Detect `actions/cache` usage and extract key, path, and restore-keys
|
|
91
389
|
* metadata from the step's `with` block.
|
|
@@ -96,8 +394,15 @@ function analyzeCheckoutStep(step) {
|
|
|
96
394
|
function analyzeCacheStep(step) {
|
|
97
395
|
const props = [];
|
|
98
396
|
if (step.uses?.includes("actions/cache")) {
|
|
397
|
+
const cacheKey = step.with?.key;
|
|
99
398
|
if (step.with?.key) {
|
|
100
|
-
props.push({ name: "cdx:github:cache:key", value:
|
|
399
|
+
props.push({ name: "cdx:github:cache:key", value: cacheKey });
|
|
400
|
+
if (/hashFiles\s*\(/i.test(cacheKey)) {
|
|
401
|
+
props.push({
|
|
402
|
+
name: "cdx:github:cache:keyUsesHashFiles",
|
|
403
|
+
value: "true",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
101
406
|
}
|
|
102
407
|
if (step.with?.path) {
|
|
103
408
|
props.push({ name: "cdx:github:cache:path", value: step.with.path });
|
|
@@ -114,6 +419,7 @@ function analyzeCacheStep(step) {
|
|
|
114
419
|
.join(",");
|
|
115
420
|
}
|
|
116
421
|
props.push({ name: "cdx:github:cache:restoreKeys", value: keys });
|
|
422
|
+
props.push({ name: "cdx:github:cache:hasRestoreKeys", value: "true" });
|
|
117
423
|
}
|
|
118
424
|
}
|
|
119
425
|
return props;
|
|
@@ -140,10 +446,19 @@ function detectUntrustedInterpolation(runValue) {
|
|
|
140
446
|
|
|
141
447
|
for (const match of matches) {
|
|
142
448
|
const expr = match[1].trim();
|
|
449
|
+
if (LOW_RISK_INTERPOLATION_PATTERNS.some((pattern) => pattern.test(expr))) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
143
452
|
if (
|
|
144
|
-
expr.startsWith("github.event.pull_request") ||
|
|
145
|
-
expr.startsWith("github.event.
|
|
146
|
-
expr.startsWith("github.event.
|
|
453
|
+
expr.startsWith("github.event.pull_request.title") ||
|
|
454
|
+
expr.startsWith("github.event.pull_request.body") ||
|
|
455
|
+
expr.startsWith("github.event.pull_request.head.ref") ||
|
|
456
|
+
expr.startsWith("github.event.pull_request.head.label") ||
|
|
457
|
+
expr.startsWith("github.event.issue.title") ||
|
|
458
|
+
expr.startsWith("github.event.issue.body") ||
|
|
459
|
+
expr.startsWith("github.event.comment.body") ||
|
|
460
|
+
expr.startsWith("github.event.review.body") ||
|
|
461
|
+
expr.startsWith("github.event.review_comment.body") ||
|
|
147
462
|
expr.startsWith("github.head_ref") ||
|
|
148
463
|
expr.startsWith("inputs.")
|
|
149
464
|
) {
|
|
@@ -157,6 +472,504 @@ function detectUntrustedInterpolation(runValue) {
|
|
|
157
472
|
};
|
|
158
473
|
}
|
|
159
474
|
|
|
475
|
+
function isLegacyPublishTokenEnvName(envName) {
|
|
476
|
+
if (!envName || typeof envName !== "string") {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
return (
|
|
480
|
+
envName.endsWith("_TOKEN") ||
|
|
481
|
+
envName.startsWith("POETRY_PYPI_TOKEN") ||
|
|
482
|
+
LEGACY_PUBLISH_TOKEN_ENV_NAMES.has(envName)
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function detectPublishEcosystem(runValue) {
|
|
487
|
+
if (!runValue || typeof runValue !== "string") {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
if (/\b(?:npm|pnpm|yarn|bun)\s+publish\b/i.test(runValue)) {
|
|
491
|
+
return "npm";
|
|
492
|
+
}
|
|
493
|
+
if (
|
|
494
|
+
/\btwine\s+upload\b/i.test(runValue) ||
|
|
495
|
+
/\bpoetry\s+publish\b/i.test(runValue) ||
|
|
496
|
+
/\bflit\s+publish\b/i.test(runValue)
|
|
497
|
+
) {
|
|
498
|
+
return "pypi";
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function analyzeLegacyPublishStep(step, effectiveEnv) {
|
|
504
|
+
const props = [];
|
|
505
|
+
const publishEcosystem = detectPublishEcosystem(step?.run);
|
|
506
|
+
if (!publishEcosystem) {
|
|
507
|
+
return props;
|
|
508
|
+
}
|
|
509
|
+
const tokenSources = [];
|
|
510
|
+
if (/\B--token(?:=|\s+\S+)/i.test(step.run)) {
|
|
511
|
+
tokenSources.push("cli-flag");
|
|
512
|
+
}
|
|
513
|
+
const legacyEnvNames = Object.keys(effectiveEnv || {}).filter(
|
|
514
|
+
isLegacyPublishTokenEnvName,
|
|
515
|
+
);
|
|
516
|
+
legacyEnvNames.forEach((envName) => {
|
|
517
|
+
tokenSources.push(`env:${envName}`);
|
|
518
|
+
});
|
|
519
|
+
props.push({
|
|
520
|
+
name: "cdx:github:step:isPublishCommand",
|
|
521
|
+
value: "true",
|
|
522
|
+
});
|
|
523
|
+
props.push({
|
|
524
|
+
name: "cdx:github:step:publishEcosystem",
|
|
525
|
+
value: publishEcosystem,
|
|
526
|
+
});
|
|
527
|
+
if (!tokenSources.length) {
|
|
528
|
+
return props;
|
|
529
|
+
}
|
|
530
|
+
props.push({
|
|
531
|
+
name: "cdx:github:step:usesLegacyPublishToken",
|
|
532
|
+
value: "true",
|
|
533
|
+
});
|
|
534
|
+
props.push({
|
|
535
|
+
name: "cdx:github:step:legacyPublishTokenSources",
|
|
536
|
+
value: tokenSources.join(","),
|
|
537
|
+
});
|
|
538
|
+
return props;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function detectRunnerStateMutation(runValue) {
|
|
542
|
+
if (!runValue || typeof runValue !== "string") {
|
|
543
|
+
return { hasMutation: false, targets: [] };
|
|
544
|
+
}
|
|
545
|
+
const targets = new Set();
|
|
546
|
+
const patterns = [
|
|
547
|
+
[
|
|
548
|
+
"GITHUB_ENV",
|
|
549
|
+
/(?:>>?|1>>?)\s*["']?(?:\$GITHUB_ENV|\$\{GITHUB_ENV}|%GITHUB_ENV%|\$env:GITHUB_ENV)["']?/i,
|
|
550
|
+
],
|
|
551
|
+
[
|
|
552
|
+
"GITHUB_PATH",
|
|
553
|
+
/(?:>>?|1>>?)\s*["']?(?:\$GITHUB_PATH|\$\{GITHUB_PATH}|%GITHUB_PATH%|\$env:GITHUB_PATH)["']?/i,
|
|
554
|
+
],
|
|
555
|
+
[
|
|
556
|
+
"GITHUB_OUTPUT",
|
|
557
|
+
/(?:>>?|1>>?)\s*["']?(?:\$GITHUB_OUTPUT|\$\{GITHUB_OUTPUT}|%GITHUB_OUTPUT%|\$env:GITHUB_OUTPUT)["']?/i,
|
|
558
|
+
],
|
|
559
|
+
];
|
|
560
|
+
patterns.forEach(([target, pattern]) => {
|
|
561
|
+
if (pattern.test(runValue)) {
|
|
562
|
+
targets.add(target);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (/::set-output\b/i.test(runValue)) {
|
|
566
|
+
targets.add("GITHUB_OUTPUT");
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
hasMutation: targets.size > 0,
|
|
570
|
+
targets: Array.from(targets),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function detectOutboundNetworkCommand(runValue) {
|
|
575
|
+
if (!runValue || typeof runValue !== "string") {
|
|
576
|
+
return { hasOutboundCommand: false, tools: [] };
|
|
577
|
+
}
|
|
578
|
+
const tools = [];
|
|
579
|
+
OUTBOUND_NETWORK_TOOLS.forEach(([name, pattern]) => {
|
|
580
|
+
if (pattern.test(runValue)) {
|
|
581
|
+
tools.push(name);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
hasOutboundCommand: tools.length > 0,
|
|
586
|
+
tools,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function collectSensitiveEnvBindings(effectiveEnv) {
|
|
591
|
+
const sensitiveRefs = [];
|
|
592
|
+
Object.entries(effectiveEnv || {}).forEach(([envName, envValue]) => {
|
|
593
|
+
if (isSensitiveEnvBinding(envName, envValue)) {
|
|
594
|
+
sensitiveRefs.push(`env:${envName}`);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
return sensitiveRefs;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isSensitiveEnvBinding(envName, envValue) {
|
|
601
|
+
if (!envName || typeof envName !== "string") {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (IMPLICIT_SENSITIVE_ENV_NAMES.includes(envName)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
if (SECRET_LIKE_ENV_NAME_PATTERN.test(envName)) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
if (typeof envValue !== "string") {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
return SENSITIVE_ENV_VALUE_PATTERN.test(envValue);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function detectSensitiveContextReferences(runValue, effectiveEnv) {
|
|
617
|
+
if (!runValue || typeof runValue !== "string") {
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
const sensitiveRefs = new Set();
|
|
621
|
+
Object.entries(effectiveEnv || {}).forEach(([envName, envValue]) => {
|
|
622
|
+
if (!isSensitiveEnvBinding(envName, envValue)) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const envPattern = new RegExp(
|
|
626
|
+
`(?:\\$${envName}\\b|\\$\\{${envName}\\}|%${envName}%|\\$env:${envName}\\b|process\\.env\\.${envName}\\b|process\\.env\\[['"]${envName}['"]])`,
|
|
627
|
+
"i",
|
|
628
|
+
);
|
|
629
|
+
if (envPattern.test(runValue)) {
|
|
630
|
+
sensitiveRefs.add(`env:${envName}`);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
const contextPatterns = [
|
|
634
|
+
["context:github.token", /github\.token/i],
|
|
635
|
+
["context:secrets", /secrets\.[A-Za-z0-9_]+/i],
|
|
636
|
+
[
|
|
637
|
+
"context:github-token-input",
|
|
638
|
+
/github-token|process\.env\.GITHUB_TOKEN|process\.env\[['"]GITHUB_TOKEN['"]]/i,
|
|
639
|
+
],
|
|
640
|
+
[
|
|
641
|
+
"context:actions-id-token",
|
|
642
|
+
/ACTIONS_ID_TOKEN_REQUEST_(?:TOKEN|URL)|id-token/i,
|
|
643
|
+
],
|
|
644
|
+
];
|
|
645
|
+
contextPatterns.forEach(([name, pattern]) => {
|
|
646
|
+
if (pattern.test(runValue)) {
|
|
647
|
+
sensitiveRefs.add(name);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return Array.from(sensitiveRefs);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function detectOutboundExfiltrationIndicators(runValue, sensitiveContextRefs) {
|
|
654
|
+
if (
|
|
655
|
+
!runValue ||
|
|
656
|
+
typeof runValue !== "string" ||
|
|
657
|
+
!Array.isArray(sensitiveContextRefs) ||
|
|
658
|
+
!sensitiveContextRefs.length
|
|
659
|
+
) {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
const indicators = new Set();
|
|
663
|
+
if (
|
|
664
|
+
/(?:^|\s)(?:--header|-H)\s+[^\n]*(?:authorization|x-(?:api-key|auth-token|github-token)|private-token|token:)/i.test(
|
|
665
|
+
runValue,
|
|
666
|
+
) &&
|
|
667
|
+
SHELL_VARIABLE_REFERENCE_PATTERN.test(runValue)
|
|
668
|
+
) {
|
|
669
|
+
indicators.add("auth-header");
|
|
670
|
+
}
|
|
671
|
+
if (
|
|
672
|
+
/\b(?:--data(?:-raw|-binary|-urlencode)?|--body|--form|--upload-file|-InFile|-Body|-Form)\b|(?:^|\s)-[dFT]\b/i.test(
|
|
673
|
+
runValue,
|
|
674
|
+
)
|
|
675
|
+
) {
|
|
676
|
+
indicators.add("request-payload");
|
|
677
|
+
}
|
|
678
|
+
if (
|
|
679
|
+
/(?:^|\s)(?:-X|--request)\s*(?:POST|PUT|PATCH)\b|\b-Method\s+(?:Post|Put|Patch)\b/i.test(
|
|
680
|
+
runValue,
|
|
681
|
+
)
|
|
682
|
+
) {
|
|
683
|
+
indicators.add("state-changing-method");
|
|
684
|
+
}
|
|
685
|
+
if (
|
|
686
|
+
/\?[^\n"'\s]*(?:token|sig|signature|auth|secret|key)=/i.test(runValue) &&
|
|
687
|
+
SHELL_VARIABLE_REFERENCE_PATTERN.test(runValue)
|
|
688
|
+
) {
|
|
689
|
+
indicators.add("query-parameter");
|
|
690
|
+
}
|
|
691
|
+
if (/\b(?:scp|rsync)\b/i.test(runValue)) {
|
|
692
|
+
indicators.add("file-transfer");
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
/\b(?:nc|ncat|netcat)\b[^\n]*(?:<|<<)/i.test(runValue) ||
|
|
696
|
+
/\|\s*(?:nc|ncat|netcat)\b/i.test(runValue)
|
|
697
|
+
) {
|
|
698
|
+
indicators.add("stream-transfer");
|
|
699
|
+
}
|
|
700
|
+
if (
|
|
701
|
+
/\b(?:base64|openssl\s+enc)\b[^\n|]*\|\s*(?:curl|wget|nc|ncat|netcat)\b/i.test(
|
|
702
|
+
runValue,
|
|
703
|
+
)
|
|
704
|
+
) {
|
|
705
|
+
indicators.add("encoded-payload");
|
|
706
|
+
}
|
|
707
|
+
if (
|
|
708
|
+
sensitiveContextRefs.some(
|
|
709
|
+
(ref) =>
|
|
710
|
+
ref === "context:actions-id-token" ||
|
|
711
|
+
ref === "context:github.token" ||
|
|
712
|
+
ref.startsWith("context:secrets"),
|
|
713
|
+
)
|
|
714
|
+
) {
|
|
715
|
+
indicators.add("platform-credential");
|
|
716
|
+
}
|
|
717
|
+
return Array.from(indicators);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function detectForkContextReferences(textValue) {
|
|
721
|
+
if (!textValue || typeof textValue !== "string") {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
const refs = [];
|
|
725
|
+
FORK_CONTEXT_PATTERNS.forEach(([name, pattern]) => {
|
|
726
|
+
if (pattern.test(textValue)) {
|
|
727
|
+
refs.push(name);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
return refs;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function addDispatchTarget(targets, prefix, value) {
|
|
734
|
+
if (!value || typeof value !== "string") {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const normalizedValue = value.trim();
|
|
738
|
+
if (!normalizedValue) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
targets.add(`${prefix}:${normalizedValue}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeDispatchTargetPrefix(key) {
|
|
745
|
+
if (!key) {
|
|
746
|
+
return "unknown";
|
|
747
|
+
}
|
|
748
|
+
if (["repository", "repo"].includes(key)) {
|
|
749
|
+
return "repo";
|
|
750
|
+
}
|
|
751
|
+
if (["workflow", "workflow_id"].includes(key)) {
|
|
752
|
+
return "workflow";
|
|
753
|
+
}
|
|
754
|
+
if (key === "event-type") {
|
|
755
|
+
return "event";
|
|
756
|
+
}
|
|
757
|
+
return key.replace(/_/g, "-");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function detectWorkflowDispatchInvocations(textValue) {
|
|
761
|
+
const kinds = new Set();
|
|
762
|
+
const mechanisms = new Set();
|
|
763
|
+
const targets = new Set();
|
|
764
|
+
if (!textValue || typeof textValue !== "string") {
|
|
765
|
+
return {
|
|
766
|
+
hasDispatch: false,
|
|
767
|
+
kinds: [],
|
|
768
|
+
mechanisms: [],
|
|
769
|
+
targets: [],
|
|
770
|
+
usesExplicitRepositoryTarget: false,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const ghWorkflowRunMatch = textValue.match(
|
|
774
|
+
/\bgh\s+workflow\s+run\s+([^\s"'`]+)/i,
|
|
775
|
+
);
|
|
776
|
+
if (ghWorkflowRunMatch) {
|
|
777
|
+
kinds.add("workflow_dispatch");
|
|
778
|
+
mechanisms.add("gh-workflow-run");
|
|
779
|
+
addDispatchTarget(targets, "workflow", ghWorkflowRunMatch[1]);
|
|
780
|
+
}
|
|
781
|
+
const ghRepoMatch = textValue.match(
|
|
782
|
+
/\b--repo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)/i,
|
|
783
|
+
);
|
|
784
|
+
if (ghRepoMatch) {
|
|
785
|
+
addDispatchTarget(targets, "repo", ghRepoMatch[1]);
|
|
786
|
+
}
|
|
787
|
+
for (const match of textValue.matchAll(
|
|
788
|
+
/\/repos\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/actions\/workflows\/([^/\s"'`]+)\/dispatches\b/gi,
|
|
789
|
+
)) {
|
|
790
|
+
kinds.add("workflow_dispatch");
|
|
791
|
+
mechanisms.add("github-api-workflow-dispatch");
|
|
792
|
+
addDispatchTarget(targets, "repo", match[1]);
|
|
793
|
+
addDispatchTarget(targets, "workflow", match[2]);
|
|
794
|
+
}
|
|
795
|
+
for (const match of textValue.matchAll(
|
|
796
|
+
/\/repos\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/dispatches\b/gi,
|
|
797
|
+
)) {
|
|
798
|
+
kinds.add("repository_dispatch");
|
|
799
|
+
mechanisms.add("github-api-repository-dispatch");
|
|
800
|
+
addDispatchTarget(targets, "repo", match[1]);
|
|
801
|
+
}
|
|
802
|
+
if (
|
|
803
|
+
/\b(?:github|octokit)\.rest\.actions\.createWorkflowDispatch\b/i.test(
|
|
804
|
+
textValue,
|
|
805
|
+
)
|
|
806
|
+
) {
|
|
807
|
+
kinds.add("workflow_dispatch");
|
|
808
|
+
mechanisms.add("github-script-workflow-dispatch");
|
|
809
|
+
}
|
|
810
|
+
if (
|
|
811
|
+
/\b(?:github|octokit)\.rest\.repos\.createDispatchEvent\b/i.test(textValue)
|
|
812
|
+
) {
|
|
813
|
+
kinds.add("repository_dispatch");
|
|
814
|
+
mechanisms.add("github-script-repository-dispatch");
|
|
815
|
+
}
|
|
816
|
+
if (
|
|
817
|
+
/\b(?:github|octokit)\.request\s*\(\s*["'`](?:POST\s+)?\/repos\/\{owner}\/\{repo}\/actions\/workflows\/\{workflow_id}\/dispatches/i.test(
|
|
818
|
+
textValue,
|
|
819
|
+
)
|
|
820
|
+
) {
|
|
821
|
+
kinds.add("workflow_dispatch");
|
|
822
|
+
mechanisms.add("octokit-request-workflow-dispatch");
|
|
823
|
+
}
|
|
824
|
+
if (
|
|
825
|
+
/\b(?:github|octokit)\.request\s*\(\s*["'`](?:POST\s+)?\/repos\/\{owner}\/\{repo}\/dispatches/i.test(
|
|
826
|
+
textValue,
|
|
827
|
+
)
|
|
828
|
+
) {
|
|
829
|
+
kinds.add("repository_dispatch");
|
|
830
|
+
mechanisms.add("octokit-request-repository-dispatch");
|
|
831
|
+
}
|
|
832
|
+
const ownerMatch = textValue.match(/\bowner\s*:\s*["'`]([^"'`]+)["'`]/i);
|
|
833
|
+
const repoMatch = textValue.match(/\brepo\s*:\s*["'`]([^"'`]+)["'`]/i);
|
|
834
|
+
const workflowMatch = textValue.match(
|
|
835
|
+
/\bworkflow(?:_id)?\s*:\s*["'`]([^"'`]+)["'`]/i,
|
|
836
|
+
);
|
|
837
|
+
const eventTypeMatch = textValue.match(
|
|
838
|
+
/\bevent_type\s*:\s*["'`]([^"'`]+)["'`]/i,
|
|
839
|
+
);
|
|
840
|
+
const refMatch = textValue.match(/\bref\s*:\s*["'`]([^"'`]+)["'`]/i);
|
|
841
|
+
if (ownerMatch && repoMatch) {
|
|
842
|
+
addDispatchTarget(targets, "repo", `${ownerMatch[1]}/${repoMatch[1]}`);
|
|
843
|
+
}
|
|
844
|
+
if (workflowMatch) {
|
|
845
|
+
addDispatchTarget(targets, "workflow", workflowMatch[1]);
|
|
846
|
+
}
|
|
847
|
+
if (eventTypeMatch) {
|
|
848
|
+
addDispatchTarget(targets, "event", eventTypeMatch[1]);
|
|
849
|
+
}
|
|
850
|
+
if (refMatch) {
|
|
851
|
+
addDispatchTarget(targets, "ref", refMatch[1]);
|
|
852
|
+
}
|
|
853
|
+
const targetList = Array.from(targets);
|
|
854
|
+
return {
|
|
855
|
+
hasDispatch: kinds.size > 0,
|
|
856
|
+
kinds: Array.from(kinds),
|
|
857
|
+
mechanisms: Array.from(mechanisms),
|
|
858
|
+
targets: targetList,
|
|
859
|
+
usesExplicitRepositoryTarget: targetList.some((target) =>
|
|
860
|
+
target.startsWith("repo:"),
|
|
861
|
+
),
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function analyzeDispatchActionStep(step) {
|
|
866
|
+
const props = [];
|
|
867
|
+
if (!step?.uses || typeof step.uses !== "string") {
|
|
868
|
+
return props;
|
|
869
|
+
}
|
|
870
|
+
const dispatchAction = KNOWN_DISPATCH_ACTIONS.find((candidate) =>
|
|
871
|
+
candidate.pattern.test(step.uses),
|
|
872
|
+
);
|
|
873
|
+
if (!dispatchAction) {
|
|
874
|
+
return props;
|
|
875
|
+
}
|
|
876
|
+
const targets = new Set();
|
|
877
|
+
dispatchAction.repoKeys.forEach((key) => {
|
|
878
|
+
addDispatchTarget(
|
|
879
|
+
targets,
|
|
880
|
+
normalizeDispatchTargetPrefix(key),
|
|
881
|
+
step.with?.[key],
|
|
882
|
+
);
|
|
883
|
+
});
|
|
884
|
+
dispatchAction.targetKeys.forEach((key) => {
|
|
885
|
+
addDispatchTarget(
|
|
886
|
+
targets,
|
|
887
|
+
normalizeDispatchTargetPrefix(key),
|
|
888
|
+
step.with?.[key],
|
|
889
|
+
);
|
|
890
|
+
});
|
|
891
|
+
props.push({ name: "cdx:github:step:dispatchesWorkflow", value: "true" });
|
|
892
|
+
props.push({
|
|
893
|
+
name: "cdx:github:step:dispatchKinds",
|
|
894
|
+
value: dispatchAction.kind,
|
|
895
|
+
});
|
|
896
|
+
props.push({
|
|
897
|
+
name: "cdx:github:step:dispatchMechanisms",
|
|
898
|
+
value: dispatchAction.mechanism,
|
|
899
|
+
});
|
|
900
|
+
if (targets.size) {
|
|
901
|
+
props.push({
|
|
902
|
+
name: "cdx:github:step:dispatchTargets",
|
|
903
|
+
value: Array.from(targets).join(","),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (Array.from(targets).some((target) => target.startsWith("repo:"))) {
|
|
907
|
+
props.push({
|
|
908
|
+
name: "cdx:github:step:dispatchUsesExplicitRepositoryTarget",
|
|
909
|
+
value: "true",
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return props;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function appendDispatchProperties(properties, dispatchInfo) {
|
|
916
|
+
if (!dispatchInfo?.hasDispatch) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
properties.push({
|
|
920
|
+
name: "cdx:github:step:dispatchesWorkflow",
|
|
921
|
+
value: "true",
|
|
922
|
+
});
|
|
923
|
+
properties.push({
|
|
924
|
+
name: "cdx:github:step:dispatchKinds",
|
|
925
|
+
value: dispatchInfo.kinds.join(","),
|
|
926
|
+
});
|
|
927
|
+
properties.push({
|
|
928
|
+
name: "cdx:github:step:dispatchMechanisms",
|
|
929
|
+
value: dispatchInfo.mechanisms.join(","),
|
|
930
|
+
});
|
|
931
|
+
if (dispatchInfo.targets.length) {
|
|
932
|
+
properties.push({
|
|
933
|
+
name: "cdx:github:step:dispatchTargets",
|
|
934
|
+
value: dispatchInfo.targets.join(","),
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (dispatchInfo.usesExplicitRepositoryTarget) {
|
|
938
|
+
properties.push({
|
|
939
|
+
name: "cdx:github:step:dispatchUsesExplicitRepositoryTarget",
|
|
940
|
+
value: "true",
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function appendHiddenUnicodeProperties(properties, scan, prefix) {
|
|
946
|
+
if (!scan?.hasHiddenUnicode) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
properties.push({
|
|
950
|
+
name: `${prefix}:hasHiddenUnicode`,
|
|
951
|
+
value: "true",
|
|
952
|
+
});
|
|
953
|
+
properties.push({
|
|
954
|
+
name: `${prefix}:hiddenUnicodeCodePoints`,
|
|
955
|
+
value: scan.codePoints.join(","),
|
|
956
|
+
});
|
|
957
|
+
properties.push({
|
|
958
|
+
name: `${prefix}:hiddenUnicodeLineNumbers`,
|
|
959
|
+
value: scan.lineNumbers.join(","),
|
|
960
|
+
});
|
|
961
|
+
if (scan.inComments) {
|
|
962
|
+
properties.push({
|
|
963
|
+
name: `${prefix}:hiddenUnicodeInComments`,
|
|
964
|
+
value: "true",
|
|
965
|
+
});
|
|
966
|
+
properties.push({
|
|
967
|
+
name: `${prefix}:hiddenUnicodeCommentCodePoints`,
|
|
968
|
+
value: scan.commentCodePoints.join(","),
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
160
973
|
/**
|
|
161
974
|
* Classify a GitHub Actions version reference as `"sha"`, `"tag"`, or `"branch"`.
|
|
162
975
|
*
|
|
@@ -167,7 +980,7 @@ function getVersionPinningType(versionRef) {
|
|
|
167
980
|
if (!versionRef) {
|
|
168
981
|
return "unknown";
|
|
169
982
|
}
|
|
170
|
-
if (/^[a-f0-9]{40}$/.test(versionRef)
|
|
983
|
+
if (/^[a-f0-9]{40}$/.test(versionRef)) {
|
|
171
984
|
return "sha";
|
|
172
985
|
}
|
|
173
986
|
if (
|
|
@@ -199,6 +1012,58 @@ function normalizeTriggers(triggers) {
|
|
|
199
1012
|
return Object.keys(triggers).join(",");
|
|
200
1013
|
}
|
|
201
1014
|
|
|
1015
|
+
function extractWorkflowDispatchInputs(triggers) {
|
|
1016
|
+
if (!triggers || typeof triggers !== "object") {
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
1019
|
+
if (!triggers.workflow_dispatch?.inputs) {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
return Object.keys(triggers.workflow_dispatch.inputs);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function extractRepositoryDispatchTypes(triggers) {
|
|
1026
|
+
if (!triggers || typeof triggers !== "object") {
|
|
1027
|
+
return [];
|
|
1028
|
+
}
|
|
1029
|
+
const repositoryDispatch = triggers.repository_dispatch;
|
|
1030
|
+
if (!repositoryDispatch || typeof repositoryDispatch !== "object") {
|
|
1031
|
+
return [];
|
|
1032
|
+
}
|
|
1033
|
+
if (!Array.isArray(repositoryDispatch.types)) {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
1036
|
+
return repositoryDispatch.types
|
|
1037
|
+
.map((eventType) => String(eventType || "").trim())
|
|
1038
|
+
.filter(Boolean);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function normalizeTriggerNames(triggers) {
|
|
1042
|
+
const csv = normalizeTriggers(triggers);
|
|
1043
|
+
if (!csv) {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
return csv
|
|
1047
|
+
.split(",")
|
|
1048
|
+
.map((trigger) => trigger.trim())
|
|
1049
|
+
.filter(Boolean);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function extractWorkflowCallMetadata(triggers) {
|
|
1053
|
+
if (!triggers || typeof triggers !== "object") {
|
|
1054
|
+
return { inputs: [], outputs: [], secrets: [] };
|
|
1055
|
+
}
|
|
1056
|
+
const workflowCall = triggers.workflow_call;
|
|
1057
|
+
if (!workflowCall || typeof workflowCall !== "object") {
|
|
1058
|
+
return { inputs: [], outputs: [], secrets: [] };
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
inputs: Object.keys(workflowCall.inputs || {}),
|
|
1062
|
+
outputs: Object.keys(workflowCall.outputs || {}),
|
|
1063
|
+
secrets: Object.keys(workflowCall.secrets || {}),
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
202
1067
|
/**
|
|
203
1068
|
* Determine whether the given trigger value includes at least one high-risk
|
|
204
1069
|
* trigger (`pull_request_target`, `issue_comment`, or `workflow_run`).
|
|
@@ -227,13 +1092,29 @@ function hasHighRiskTrigger(triggers) {
|
|
|
227
1092
|
* @returns {Array<{name: string, value: string}>}
|
|
228
1093
|
*/
|
|
229
1094
|
function buildWorkflowContextProperties({
|
|
1095
|
+
hasExplicitPermissionsBlock,
|
|
1096
|
+
hasAnyExplicitPermissionsBlock,
|
|
230
1097
|
hasWritePermissions,
|
|
231
1098
|
hasIdTokenWrite,
|
|
232
1099
|
triggers,
|
|
1100
|
+
triggerNames,
|
|
233
1101
|
isHighRisk,
|
|
234
1102
|
concurrencyGroup,
|
|
1103
|
+
writeScopes,
|
|
1104
|
+
dispatchInputs,
|
|
1105
|
+
repositoryDispatchTypes,
|
|
1106
|
+
workflowReceiverAliases,
|
|
1107
|
+
workflowCallMetadata,
|
|
235
1108
|
}) {
|
|
236
1109
|
const props = [];
|
|
1110
|
+
props.push({
|
|
1111
|
+
name: "cdx:github:workflow:hasExplicitPermissionsBlock",
|
|
1112
|
+
value: String(Boolean(hasExplicitPermissionsBlock)),
|
|
1113
|
+
});
|
|
1114
|
+
props.push({
|
|
1115
|
+
name: "cdx:github:workflow:hasAnyExplicitPermissionsBlock",
|
|
1116
|
+
value: String(Boolean(hasAnyExplicitPermissionsBlock)),
|
|
1117
|
+
});
|
|
237
1118
|
if (hasWritePermissions) {
|
|
238
1119
|
props.push({
|
|
239
1120
|
name: "cdx:github:workflow:hasWritePermissions",
|
|
@@ -246,9 +1127,30 @@ function buildWorkflowContextProperties({
|
|
|
246
1127
|
value: "true",
|
|
247
1128
|
});
|
|
248
1129
|
}
|
|
1130
|
+
if (writeScopes?.length) {
|
|
1131
|
+
props.push({
|
|
1132
|
+
name: "cdx:github:workflow:writeScopes",
|
|
1133
|
+
value: [...new Set(writeScopes)].join(","),
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
249
1136
|
if (triggers) {
|
|
250
1137
|
props.push({ name: "cdx:github:workflow:triggers", value: triggers });
|
|
251
1138
|
}
|
|
1139
|
+
const triggerSet = new Set(triggerNames || normalizeTriggerNames(triggers));
|
|
1140
|
+
const triggerFlags = [
|
|
1141
|
+
["pull_request", "cdx:github:workflow:hasPullRequestTrigger"],
|
|
1142
|
+
["pull_request_target", "cdx:github:workflow:hasPullRequestTargetTrigger"],
|
|
1143
|
+
["issue_comment", "cdx:github:workflow:hasIssueCommentTrigger"],
|
|
1144
|
+
["repository_dispatch", "cdx:github:workflow:hasRepositoryDispatchTrigger"],
|
|
1145
|
+
["workflow_run", "cdx:github:workflow:hasWorkflowRunTrigger"],
|
|
1146
|
+
["workflow_dispatch", "cdx:github:workflow:hasWorkflowDispatchTrigger"],
|
|
1147
|
+
["workflow_call", "cdx:github:workflow:hasWorkflowCallTrigger"],
|
|
1148
|
+
];
|
|
1149
|
+
triggerFlags.forEach(([triggerName, propName]) => {
|
|
1150
|
+
if (triggerSet.has(triggerName)) {
|
|
1151
|
+
props.push({ name: propName, value: "true" });
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
252
1154
|
if (isHighRisk) {
|
|
253
1155
|
props.push({
|
|
254
1156
|
name: "cdx:github:workflow:hasHighRiskTrigger",
|
|
@@ -261,21 +1163,477 @@ function buildWorkflowContextProperties({
|
|
|
261
1163
|
value: concurrencyGroup,
|
|
262
1164
|
});
|
|
263
1165
|
}
|
|
1166
|
+
if (dispatchInputs?.length) {
|
|
1167
|
+
props.push({
|
|
1168
|
+
name: "cdx:github:workflow:hasWorkflowDispatchInputs",
|
|
1169
|
+
value: "true",
|
|
1170
|
+
});
|
|
1171
|
+
props.push({
|
|
1172
|
+
name: "cdx:github:workflow:workflowDispatchInputs",
|
|
1173
|
+
value: dispatchInputs.join(","),
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (repositoryDispatchTypes?.length) {
|
|
1177
|
+
props.push({
|
|
1178
|
+
name: "cdx:github:workflow:repositoryDispatchTypes",
|
|
1179
|
+
value: repositoryDispatchTypes.join(","),
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
if (workflowReceiverAliases?.length) {
|
|
1183
|
+
props.push({
|
|
1184
|
+
name: "cdx:github:workflow:workflowDispatchReceiverAliases",
|
|
1185
|
+
value: workflowReceiverAliases.join(","),
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
if (workflowCallMetadata?.inputs?.length) {
|
|
1189
|
+
props.push({
|
|
1190
|
+
name: "cdx:github:workflow:workflowCallInputs",
|
|
1191
|
+
value: workflowCallMetadata.inputs.join(","),
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (workflowCallMetadata?.secrets?.length) {
|
|
1195
|
+
props.push({
|
|
1196
|
+
name: "cdx:github:workflow:workflowCallSecrets",
|
|
1197
|
+
value: workflowCallMetadata.secrets.join(","),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
if (workflowCallMetadata?.outputs?.length) {
|
|
1201
|
+
props.push({
|
|
1202
|
+
name: "cdx:github:workflow:workflowCallOutputs",
|
|
1203
|
+
value: workflowCallMetadata.outputs.join(","),
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
if (
|
|
1207
|
+
workflowCallMetadata?.inputs?.length ||
|
|
1208
|
+
workflowCallMetadata?.secrets?.length ||
|
|
1209
|
+
workflowCallMetadata?.outputs?.length
|
|
1210
|
+
) {
|
|
1211
|
+
props.push({
|
|
1212
|
+
name: "cdx:github:workflow:isWorkflowCallProducer",
|
|
1213
|
+
value: "true",
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
return props;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function buildJobContextProperties({
|
|
1220
|
+
hasExplicitPermissionsBlock,
|
|
1221
|
+
hasWritePermissions,
|
|
1222
|
+
hasIdTokenWrite,
|
|
1223
|
+
isSelfHosted,
|
|
1224
|
+
writeScopes,
|
|
1225
|
+
condition,
|
|
1226
|
+
}) {
|
|
1227
|
+
const props = [];
|
|
1228
|
+
props.push({
|
|
1229
|
+
name: "cdx:github:job:hasExplicitPermissionsBlock",
|
|
1230
|
+
value: String(Boolean(hasExplicitPermissionsBlock)),
|
|
1231
|
+
});
|
|
1232
|
+
if (hasWritePermissions) {
|
|
1233
|
+
props.push({
|
|
1234
|
+
name: "cdx:github:job:hasWritePermissions",
|
|
1235
|
+
value: "true",
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
if (hasIdTokenWrite) {
|
|
1239
|
+
props.push({
|
|
1240
|
+
name: "cdx:github:job:hasIdTokenWrite",
|
|
1241
|
+
value: "true",
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
if (isSelfHosted) {
|
|
1245
|
+
props.push({
|
|
1246
|
+
name: "cdx:github:job:isSelfHosted",
|
|
1247
|
+
value: "true",
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
if (writeScopes?.length) {
|
|
1251
|
+
props.push({
|
|
1252
|
+
name: "cdx:github:job:writeScopes",
|
|
1253
|
+
value: [...new Set(writeScopes)].join(","),
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
if (condition) {
|
|
1257
|
+
props.push({ name: "cdx:github:job:if", value: condition });
|
|
1258
|
+
}
|
|
264
1259
|
return props;
|
|
265
1260
|
}
|
|
266
1261
|
|
|
267
1262
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1263
|
+
* @param {string} filePath workflow file path
|
|
1264
|
+
* @returns {string} workflow name derived from the file stem
|
|
1265
|
+
*/
|
|
1266
|
+
function deriveWorkflowNameFromPath(filePath) {
|
|
1267
|
+
const pathImpl = filePath.includes("\\") ? path.win32 : path.posix;
|
|
1268
|
+
return pathImpl.parse(pathImpl.basename(filePath)).name;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function deriveWorkflowReceiverAliases(filePath, workflowName) {
|
|
1272
|
+
const aliases = new Set();
|
|
1273
|
+
if (workflowName) {
|
|
1274
|
+
aliases.add(String(workflowName).trim());
|
|
1275
|
+
}
|
|
1276
|
+
if (filePath) {
|
|
1277
|
+
const normalizedPath = String(filePath).replace(/\\/g, "/");
|
|
1278
|
+
const fileName = normalizedPath.split("/").pop() || normalizedPath;
|
|
1279
|
+
const fileStem = fileName.replace(/\.ya?ml$/i, "");
|
|
1280
|
+
aliases.add(fileName);
|
|
1281
|
+
aliases.add(fileStem);
|
|
1282
|
+
aliases.add(normalizedPath);
|
|
1283
|
+
}
|
|
1284
|
+
return Array.from(aliases)
|
|
1285
|
+
.map((alias) => alias.trim())
|
|
1286
|
+
.filter(Boolean);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function getPropertyValue(obj, propName) {
|
|
1290
|
+
return obj?.properties?.find((property) => property.name === propName)?.value;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function upsertCsvProperty(properties, name, values) {
|
|
1294
|
+
const normalizedValues = [...new Set((values || []).filter(Boolean))];
|
|
1295
|
+
if (!normalizedValues.length) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const existingProperty = properties.find(
|
|
1299
|
+
(property) => property.name === name,
|
|
1300
|
+
);
|
|
1301
|
+
if (!existingProperty) {
|
|
1302
|
+
properties.push({ name, value: normalizedValues.join(",") });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
existingProperty.value = [
|
|
1306
|
+
...new Set([
|
|
1307
|
+
...String(existingProperty.value || "")
|
|
1308
|
+
.split(",")
|
|
1309
|
+
.map((value) => value.trim())
|
|
1310
|
+
.filter(Boolean),
|
|
1311
|
+
...normalizedValues,
|
|
1312
|
+
]),
|
|
1313
|
+
].join(",");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function upsertBooleanProperty(properties, name) {
|
|
1317
|
+
const existingProperty = properties.find(
|
|
1318
|
+
(property) => property.name === name,
|
|
1319
|
+
);
|
|
1320
|
+
if (existingProperty) {
|
|
1321
|
+
existingProperty.value = "true";
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
properties.push({ name, value: "true" });
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function parseDispatchTargets(value) {
|
|
1328
|
+
return String(value || "")
|
|
1329
|
+
.split(",")
|
|
1330
|
+
.map((target) => target.trim())
|
|
1331
|
+
.filter(Boolean)
|
|
1332
|
+
.map((target) => {
|
|
1333
|
+
const separatorIndex = target.indexOf(":");
|
|
1334
|
+
if (separatorIndex === -1) {
|
|
1335
|
+
return { type: "unknown", value: target };
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
type: target.slice(0, separatorIndex),
|
|
1339
|
+
value: target.slice(separatorIndex + 1),
|
|
1340
|
+
};
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function normalizeDispatchTargetValue(value) {
|
|
1345
|
+
return String(value || "")
|
|
1346
|
+
.trim()
|
|
1347
|
+
.toLowerCase();
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function buildLocalDispatchReceiverIndexes(workflows) {
|
|
1351
|
+
const workflowDispatchAliasIndex = new Map();
|
|
1352
|
+
const repositoryDispatchTypeIndex = new Map();
|
|
1353
|
+
(workflows || []).forEach((workflow) => {
|
|
1354
|
+
if (
|
|
1355
|
+
getPropertyValue(
|
|
1356
|
+
workflow,
|
|
1357
|
+
"cdx:github:workflow:hasWorkflowDispatchTrigger",
|
|
1358
|
+
) === "true"
|
|
1359
|
+
) {
|
|
1360
|
+
const aliases = String(
|
|
1361
|
+
getPropertyValue(
|
|
1362
|
+
workflow,
|
|
1363
|
+
"cdx:github:workflow:workflowDispatchReceiverAliases",
|
|
1364
|
+
) || "",
|
|
1365
|
+
)
|
|
1366
|
+
.split(",")
|
|
1367
|
+
.map((alias) => alias.trim())
|
|
1368
|
+
.filter(Boolean);
|
|
1369
|
+
aliases.forEach((alias) => {
|
|
1370
|
+
const normalizedAlias = normalizeDispatchTargetValue(alias);
|
|
1371
|
+
if (!workflowDispatchAliasIndex.has(normalizedAlias)) {
|
|
1372
|
+
workflowDispatchAliasIndex.set(normalizedAlias, []);
|
|
1373
|
+
}
|
|
1374
|
+
workflowDispatchAliasIndex.get(normalizedAlias).push(workflow);
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
if (
|
|
1378
|
+
getPropertyValue(
|
|
1379
|
+
workflow,
|
|
1380
|
+
"cdx:github:workflow:hasRepositoryDispatchTrigger",
|
|
1381
|
+
) === "true"
|
|
1382
|
+
) {
|
|
1383
|
+
const eventTypes = String(
|
|
1384
|
+
getPropertyValue(
|
|
1385
|
+
workflow,
|
|
1386
|
+
"cdx:github:workflow:repositoryDispatchTypes",
|
|
1387
|
+
) || "",
|
|
1388
|
+
)
|
|
1389
|
+
.split(",")
|
|
1390
|
+
.map((eventType) => eventType.trim())
|
|
1391
|
+
.filter(Boolean);
|
|
1392
|
+
eventTypes.forEach((eventType) => {
|
|
1393
|
+
const normalizedEventType = normalizeDispatchTargetValue(eventType);
|
|
1394
|
+
if (!repositoryDispatchTypeIndex.has(normalizedEventType)) {
|
|
1395
|
+
repositoryDispatchTypeIndex.set(normalizedEventType, []);
|
|
1396
|
+
}
|
|
1397
|
+
repositoryDispatchTypeIndex.get(normalizedEventType).push(workflow);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
return {
|
|
1402
|
+
repositoryDispatchTypeIndex,
|
|
1403
|
+
workflowDispatchAliasIndex,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function enrichLocalDispatchRelationships(workflows, components) {
|
|
1408
|
+
const { repositoryDispatchTypeIndex, workflowDispatchAliasIndex } =
|
|
1409
|
+
buildLocalDispatchReceiverIndexes(workflows);
|
|
1410
|
+
(components || []).forEach((component) => {
|
|
1411
|
+
if (
|
|
1412
|
+
getPropertyValue(component, "cdx:github:step:dispatchesWorkflow") !==
|
|
1413
|
+
"true"
|
|
1414
|
+
) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const dispatchTargets = parseDispatchTargets(
|
|
1418
|
+
getPropertyValue(component, "cdx:github:step:dispatchTargets"),
|
|
1419
|
+
);
|
|
1420
|
+
if (dispatchTargets.some((target) => target.type === "repo")) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const matchedWorkflows = [];
|
|
1424
|
+
const matchBases = [];
|
|
1425
|
+
dispatchTargets.forEach((target) => {
|
|
1426
|
+
if (target.type === "workflow") {
|
|
1427
|
+
const candidates =
|
|
1428
|
+
workflowDispatchAliasIndex.get(
|
|
1429
|
+
normalizeDispatchTargetValue(target.value),
|
|
1430
|
+
) || [];
|
|
1431
|
+
if (candidates.length === 1) {
|
|
1432
|
+
matchedWorkflows.push(candidates[0]);
|
|
1433
|
+
matchBases.push(`workflow:${target.value}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (target.type === "event") {
|
|
1437
|
+
const candidates =
|
|
1438
|
+
repositoryDispatchTypeIndex.get(
|
|
1439
|
+
normalizeDispatchTargetValue(target.value),
|
|
1440
|
+
) || [];
|
|
1441
|
+
if (candidates.length === 1) {
|
|
1442
|
+
matchedWorkflows.push(candidates[0]);
|
|
1443
|
+
matchBases.push(`repository_dispatch:${target.value}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
const uniqueMatchedWorkflows = [...new Set(matchedWorkflows)];
|
|
1448
|
+
if (!uniqueMatchedWorkflows.length) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const receiverWorkflowFiles = uniqueMatchedWorkflows
|
|
1452
|
+
.map((workflow) => getPropertyValue(workflow, "cdx:github:workflow:file"))
|
|
1453
|
+
.filter(Boolean);
|
|
1454
|
+
const receiverWorkflowNames = uniqueMatchedWorkflows
|
|
1455
|
+
.map((workflow) => getPropertyValue(workflow, "cdx:github:workflow:name"))
|
|
1456
|
+
.filter(Boolean);
|
|
1457
|
+
upsertBooleanProperty(
|
|
1458
|
+
component.properties,
|
|
1459
|
+
"cdx:github:step:hasLocalDispatchReceiver",
|
|
1460
|
+
);
|
|
1461
|
+
upsertCsvProperty(
|
|
1462
|
+
component.properties,
|
|
1463
|
+
"cdx:github:step:dispatchReceiverWorkflowFiles",
|
|
1464
|
+
receiverWorkflowFiles,
|
|
1465
|
+
);
|
|
1466
|
+
upsertCsvProperty(
|
|
1467
|
+
component.properties,
|
|
1468
|
+
"cdx:github:step:dispatchReceiverWorkflowNames",
|
|
1469
|
+
receiverWorkflowNames,
|
|
1470
|
+
);
|
|
1471
|
+
upsertCsvProperty(
|
|
1472
|
+
component.properties,
|
|
1473
|
+
"cdx:github:step:dispatchReceiverMatchBasis",
|
|
1474
|
+
matchBases,
|
|
1475
|
+
);
|
|
1476
|
+
upsertCsvProperty(
|
|
1477
|
+
component.properties,
|
|
1478
|
+
"cdx:github:step:dispatchReceiverConfidence",
|
|
1479
|
+
["high"],
|
|
1480
|
+
);
|
|
1481
|
+
uniqueMatchedWorkflows.forEach((workflow) => {
|
|
1482
|
+
const senderWorkflowFile = getPropertyValue(
|
|
1483
|
+
component,
|
|
1484
|
+
"cdx:github:workflow:file",
|
|
1485
|
+
);
|
|
1486
|
+
const senderWorkflowName = getPropertyValue(
|
|
1487
|
+
component,
|
|
1488
|
+
"cdx:github:workflow:name",
|
|
1489
|
+
);
|
|
1490
|
+
upsertBooleanProperty(
|
|
1491
|
+
workflow.properties,
|
|
1492
|
+
"cdx:github:workflow:hasLocalDispatchSender",
|
|
1493
|
+
);
|
|
1494
|
+
upsertCsvProperty(
|
|
1495
|
+
workflow.properties,
|
|
1496
|
+
"cdx:github:workflow:dispatchSenderWorkflowFiles",
|
|
1497
|
+
[senderWorkflowFile],
|
|
1498
|
+
);
|
|
1499
|
+
upsertCsvProperty(
|
|
1500
|
+
workflow.properties,
|
|
1501
|
+
"cdx:github:workflow:dispatchSenderWorkflowNames",
|
|
1502
|
+
[senderWorkflowName],
|
|
1503
|
+
);
|
|
1504
|
+
upsertCsvProperty(
|
|
1505
|
+
workflow.properties,
|
|
1506
|
+
"cdx:github:workflow:dispatchSenderMatchBasis",
|
|
1507
|
+
matchBases,
|
|
1508
|
+
);
|
|
1509
|
+
});
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function buildReusableWorkflowComponent(
|
|
1514
|
+
job,
|
|
1515
|
+
jobName,
|
|
1516
|
+
filePath,
|
|
1517
|
+
workflowName,
|
|
1518
|
+
jobRunner,
|
|
1519
|
+
jobContextProperties,
|
|
1520
|
+
workflowContextProperties,
|
|
1521
|
+
options,
|
|
1522
|
+
) {
|
|
1523
|
+
const uses = job?.uses;
|
|
1524
|
+
if (!uses || typeof uses !== "string") {
|
|
1525
|
+
return undefined;
|
|
1526
|
+
}
|
|
1527
|
+
let group;
|
|
1528
|
+
let name = uses;
|
|
1529
|
+
let purl;
|
|
1530
|
+
let versionRef;
|
|
1531
|
+
let versionPinningType = "unknown";
|
|
1532
|
+
let isShaPinned = false;
|
|
1533
|
+
const isExternal = !uses.startsWith("./");
|
|
1534
|
+
|
|
1535
|
+
if (isExternal) {
|
|
1536
|
+
const tmpA = uses.split("@");
|
|
1537
|
+
const workflowRef = tmpA[0];
|
|
1538
|
+
versionRef = tmpA[1];
|
|
1539
|
+
versionPinningType = getVersionPinningType(versionRef);
|
|
1540
|
+
isShaPinned = versionPinningType === "sha";
|
|
1541
|
+
if (workflowRef.includes("/.github/workflows/")) {
|
|
1542
|
+
const [repoPath, workflowPath] = workflowRef.split("/.github/workflows/");
|
|
1543
|
+
group = repoPath;
|
|
1544
|
+
name = workflowPath;
|
|
1545
|
+
} else {
|
|
1546
|
+
const refParts = workflowRef.split("/");
|
|
1547
|
+
name = refParts.pop() || workflowRef;
|
|
1548
|
+
group = refParts.join("/");
|
|
1549
|
+
}
|
|
1550
|
+
if (versionRef) {
|
|
1551
|
+
purl = new PackageURL(
|
|
1552
|
+
"github",
|
|
1553
|
+
group || undefined,
|
|
1554
|
+
name,
|
|
1555
|
+
versionRef,
|
|
1556
|
+
null,
|
|
1557
|
+
null,
|
|
1558
|
+
).toString();
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
const pathImpl = uses.includes("\\") ? path.win32 : path.posix;
|
|
1562
|
+
name = pathImpl.basename(uses);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const componentRef = purl || `github-workflow:${uses}`;
|
|
1566
|
+
const properties = [
|
|
1567
|
+
{ name: "SrcFile", value: filePath },
|
|
1568
|
+
{ name: "cdx:github:workflow:name", value: workflowName },
|
|
1569
|
+
{ name: "cdx:github:workflow:file", value: filePath },
|
|
1570
|
+
{ name: "cdx:github:job:name", value: jobName },
|
|
1571
|
+
{
|
|
1572
|
+
name: "cdx:github:job:runner",
|
|
1573
|
+
value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
|
|
1574
|
+
},
|
|
1575
|
+
{ name: "cdx:github:reusableWorkflow:uses", value: uses },
|
|
1576
|
+
{
|
|
1577
|
+
name: "cdx:github:reusableWorkflow:isExternal",
|
|
1578
|
+
value: String(isExternal),
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
name: "cdx:github:reusableWorkflow:versionPinningType",
|
|
1582
|
+
value: versionPinningType,
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
name: "cdx:github:reusableWorkflow:isShaPinned",
|
|
1586
|
+
value: String(isShaPinned),
|
|
1587
|
+
},
|
|
1588
|
+
];
|
|
1589
|
+
if (versionRef) {
|
|
1590
|
+
properties.push({
|
|
1591
|
+
name: "cdx:github:reusableWorkflow:ref",
|
|
1592
|
+
value: versionRef,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (job.secrets === "inherit") {
|
|
1596
|
+
properties.push({
|
|
1597
|
+
name: "cdx:github:reusableWorkflow:secretsInherit",
|
|
1598
|
+
value: "true",
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
if (job.with && typeof job.with === "object") {
|
|
1602
|
+
const withKeys = Object.keys(job.with);
|
|
1603
|
+
if (withKeys.length) {
|
|
1604
|
+
properties.push({
|
|
1605
|
+
name: "cdx:github:reusableWorkflow:withKeys",
|
|
1606
|
+
value: withKeys.join(","),
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
properties.push(...jobContextProperties);
|
|
1611
|
+
properties.push(...workflowContextProperties);
|
|
1612
|
+
const component = {
|
|
1613
|
+
"bom-ref": componentRef,
|
|
1614
|
+
type: "application",
|
|
1615
|
+
group,
|
|
1616
|
+
name,
|
|
1617
|
+
version: versionRef,
|
|
1618
|
+
purl,
|
|
1619
|
+
properties,
|
|
1620
|
+
scope: isExternal ? "required" : "excluded",
|
|
1621
|
+
tags: ["reusable-workflow"],
|
|
1622
|
+
};
|
|
1623
|
+
if (options?.specVersion >= 1.7 && isExternal) {
|
|
1624
|
+
component.isExternal = true;
|
|
1625
|
+
}
|
|
1626
|
+
return component;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Parse a single GitHub Actions workflow file into workflow, component, and dependency data.
|
|
274
1631
|
*
|
|
275
|
-
* @param {string} f
|
|
276
|
-
* @param {Object} options
|
|
1632
|
+
* @param {string} f Absolute path to a workflow YAML file
|
|
1633
|
+
* @param {Object} options CLI options
|
|
277
1634
|
* @returns {{ workflows: Object[], components: Object[], dependencies: Object[] }}
|
|
278
1635
|
*/
|
|
1636
|
+
|
|
279
1637
|
export function parseWorkflowFile(f, options) {
|
|
280
1638
|
const workflows = [];
|
|
281
1639
|
const components = [];
|
|
@@ -289,6 +1647,7 @@ export function parseWorkflowFile(f, options) {
|
|
|
289
1647
|
}
|
|
290
1648
|
|
|
291
1649
|
let yamlObj;
|
|
1650
|
+
const hiddenUnicodeScan = scanTextForHiddenUnicode(raw, { syntax: "yaml" });
|
|
292
1651
|
try {
|
|
293
1652
|
yamlObj = _load(raw);
|
|
294
1653
|
} catch (_e) {
|
|
@@ -298,23 +1657,40 @@ export function parseWorkflowFile(f, options) {
|
|
|
298
1657
|
if (!yamlObj?.jobs) {
|
|
299
1658
|
return { workflows, components, dependencies };
|
|
300
1659
|
}
|
|
301
|
-
const workflowName =
|
|
302
|
-
yamlObj.name ||
|
|
303
|
-
f
|
|
304
|
-
.split("/")
|
|
305
|
-
.pop()
|
|
306
|
-
.replace(/\.[^.]+$/, "");
|
|
1660
|
+
const workflowName = yamlObj.name || deriveWorkflowNameFromPath(f);
|
|
307
1661
|
const workflowTriggers = yamlObj.on || yamlObj.true;
|
|
308
|
-
const
|
|
1662
|
+
const workflowHasExplicitPermissionsBlock = Object.hasOwn(
|
|
1663
|
+
yamlObj,
|
|
1664
|
+
"permissions",
|
|
1665
|
+
);
|
|
1666
|
+
const workflowPermissions = workflowHasExplicitPermissionsBlock
|
|
1667
|
+
? yamlObj.permissions || {}
|
|
1668
|
+
: {};
|
|
1669
|
+
const workflowEnv = yamlObj.env || {};
|
|
309
1670
|
const workflowHasWritePermissions = analyzePermissions(workflowPermissions);
|
|
1671
|
+
const workflowWriteScopes = new Set(extractWriteScopes(workflowPermissions));
|
|
310
1672
|
const workflowConcurrency = yamlObj.concurrency || {};
|
|
311
|
-
const
|
|
1673
|
+
const workflowHasIdTokenWrite =
|
|
1674
|
+
hasIdTokenWritePermission(workflowPermissions);
|
|
312
1675
|
const triggers = normalizeTriggers(workflowTriggers);
|
|
1676
|
+
const triggerNames = normalizeTriggerNames(workflowTriggers);
|
|
313
1677
|
const isHighRisk = hasHighRiskTrigger(workflowTriggers);
|
|
1678
|
+
const workflowDispatchInputs =
|
|
1679
|
+
extractWorkflowDispatchInputs(workflowTriggers);
|
|
1680
|
+
const repositoryDispatchTypes =
|
|
1681
|
+
extractRepositoryDispatchTypes(workflowTriggers);
|
|
1682
|
+
const workflowCallMetadata = extractWorkflowCallMetadata(workflowTriggers);
|
|
1683
|
+
const workflowReceiverAliases = deriveWorkflowReceiverAliases(
|
|
1684
|
+
f,
|
|
1685
|
+
workflowName,
|
|
1686
|
+
);
|
|
314
1687
|
|
|
315
1688
|
const workflowRef = uuidv4();
|
|
316
1689
|
const tasks = [];
|
|
317
1690
|
const workflowDependsOn = [];
|
|
1691
|
+
let anyJobHasExplicitPermissionsBlock = false;
|
|
1692
|
+
let anyJobHasWritePermissions = false;
|
|
1693
|
+
let anyJobHasIdTokenWrite = false;
|
|
318
1694
|
|
|
319
1695
|
for (const jobName of Object.keys(yamlObj.jobs)) {
|
|
320
1696
|
const job = yamlObj.jobs[jobName];
|
|
@@ -330,19 +1706,54 @@ export function parseWorkflowFile(f, options) {
|
|
|
330
1706
|
|
|
331
1707
|
const jobRunner = job["runs-on"] || "unknown";
|
|
332
1708
|
const jobEnvironment = job.environment?.name || job.environment || "";
|
|
1709
|
+
const jobEnv = job.env || {};
|
|
333
1710
|
const jobTimeout = job["timeout-minutes"] || null;
|
|
334
|
-
const
|
|
1711
|
+
const jobHasExplicitPermissionsBlock = Object.hasOwn(job, "permissions");
|
|
1712
|
+
const jobPermissions = jobHasExplicitPermissionsBlock
|
|
1713
|
+
? job.permissions || {}
|
|
1714
|
+
: {};
|
|
335
1715
|
const jobHasWritePermissions = analyzePermissions(jobPermissions);
|
|
1716
|
+
const jobWriteScopes = extractWriteScopes(jobPermissions);
|
|
1717
|
+
const jobHasIdTokenWrite = hasIdTokenWritePermission(jobPermissions);
|
|
336
1718
|
const jobServices = job.services ? Object.keys(job.services) : [];
|
|
1719
|
+
const jobIsSelfHosted = isSelfHostedRunner(jobRunner);
|
|
337
1720
|
const effectiveWritePerms =
|
|
338
1721
|
workflowHasWritePermissions || jobHasWritePermissions;
|
|
1722
|
+
const effectiveIdTokenWrite = workflowHasIdTokenWrite || jobHasIdTokenWrite;
|
|
1723
|
+
const effectiveWriteScopes = [
|
|
1724
|
+
...workflowWriteScopes,
|
|
1725
|
+
...jobWriteScopes,
|
|
1726
|
+
].filter(Boolean);
|
|
1727
|
+
anyJobHasExplicitPermissionsBlock ||= jobHasExplicitPermissionsBlock;
|
|
1728
|
+
anyJobHasWritePermissions ||= jobHasWritePermissions;
|
|
1729
|
+
anyJobHasIdTokenWrite ||= jobHasIdTokenWrite;
|
|
1730
|
+
jobWriteScopes.forEach((scope) => {
|
|
1731
|
+
workflowWriteScopes.add(scope);
|
|
1732
|
+
});
|
|
339
1733
|
|
|
340
1734
|
// Shared workflow-context properties for this job's components
|
|
341
1735
|
const sharedCtxProps = buildWorkflowContextProperties({
|
|
1736
|
+
hasExplicitPermissionsBlock: workflowHasExplicitPermissionsBlock,
|
|
1737
|
+
hasAnyExplicitPermissionsBlock:
|
|
1738
|
+
workflowHasExplicitPermissionsBlock || jobHasExplicitPermissionsBlock,
|
|
342
1739
|
hasWritePermissions: effectiveWritePerms,
|
|
343
|
-
hasIdTokenWrite,
|
|
1740
|
+
hasIdTokenWrite: effectiveIdTokenWrite,
|
|
344
1741
|
triggers,
|
|
1742
|
+
triggerNames,
|
|
345
1743
|
isHighRisk,
|
|
1744
|
+
writeScopes: effectiveWriteScopes,
|
|
1745
|
+
dispatchInputs: workflowDispatchInputs,
|
|
1746
|
+
repositoryDispatchTypes,
|
|
1747
|
+
workflowReceiverAliases,
|
|
1748
|
+
workflowCallMetadata,
|
|
1749
|
+
});
|
|
1750
|
+
const sharedJobCtxProps = buildJobContextProperties({
|
|
1751
|
+
hasExplicitPermissionsBlock: jobHasExplicitPermissionsBlock,
|
|
1752
|
+
hasWritePermissions: jobHasWritePermissions,
|
|
1753
|
+
hasIdTokenWrite: jobHasIdTokenWrite,
|
|
1754
|
+
isSelfHosted: jobIsSelfHosted,
|
|
1755
|
+
writeScopes: jobWriteScopes,
|
|
1756
|
+
condition: job.if,
|
|
346
1757
|
});
|
|
347
1758
|
|
|
348
1759
|
const jobProperties = [
|
|
@@ -364,12 +1775,6 @@ export function parseWorkflowFile(f, options) {
|
|
|
364
1775
|
value: jobTimeout.toString(),
|
|
365
1776
|
});
|
|
366
1777
|
}
|
|
367
|
-
if (jobHasWritePermissions) {
|
|
368
|
-
jobProperties.push({
|
|
369
|
-
name: "cdx:github:job:hasWritePermissions",
|
|
370
|
-
value: "true",
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
1778
|
if (jobServices.length) {
|
|
374
1779
|
jobProperties.push({
|
|
375
1780
|
name: "cdx:github:job:services",
|
|
@@ -382,12 +1787,40 @@ export function parseWorkflowFile(f, options) {
|
|
|
382
1787
|
value: jobNeeds.join(","),
|
|
383
1788
|
});
|
|
384
1789
|
}
|
|
1790
|
+
if (job.uses) {
|
|
1791
|
+
jobProperties.push({ name: "cdx:github:job:uses", value: job.uses });
|
|
1792
|
+
jobProperties.push({
|
|
1793
|
+
name: "cdx:github:job:isReusableWorkflowCall",
|
|
1794
|
+
value: "true",
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
jobProperties.push(...sharedJobCtxProps);
|
|
385
1798
|
jobProperties.push(...sharedCtxProps);
|
|
386
1799
|
|
|
1800
|
+
const reusableWorkflowComponent = buildReusableWorkflowComponent(
|
|
1801
|
+
job,
|
|
1802
|
+
jobName,
|
|
1803
|
+
f,
|
|
1804
|
+
workflowName,
|
|
1805
|
+
jobRunner,
|
|
1806
|
+
sharedJobCtxProps,
|
|
1807
|
+
sharedCtxProps,
|
|
1808
|
+
options,
|
|
1809
|
+
);
|
|
1810
|
+
if (reusableWorkflowComponent) {
|
|
1811
|
+
components.push(reusableWorkflowComponent);
|
|
1812
|
+
jobDependsOn.push(reusableWorkflowComponent["bom-ref"]);
|
|
1813
|
+
steps.push({
|
|
1814
|
+
name: job.uses,
|
|
1815
|
+
commands: [{ executed: job.uses }],
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
|
|
387
1819
|
for (const step of job.steps || []) {
|
|
388
1820
|
const stepName = step.name || step.uses || "unnamed step";
|
|
389
1821
|
const commands = [];
|
|
390
1822
|
let actionProperties = [];
|
|
1823
|
+
const effectiveEnv = { ...workflowEnv, ...jobEnv, ...(step.env || {}) };
|
|
391
1824
|
if (step.uses) {
|
|
392
1825
|
commands.push({ executed: step.uses });
|
|
393
1826
|
// Collect action references as components
|
|
@@ -441,6 +1874,10 @@ export function parseWorkflowFile(f, options) {
|
|
|
441
1874
|
name: "cdx:github:step:condition",
|
|
442
1875
|
value: step.if,
|
|
443
1876
|
});
|
|
1877
|
+
actionProperties.push({
|
|
1878
|
+
name: "cdx:github:step:if",
|
|
1879
|
+
value: step.if,
|
|
1880
|
+
});
|
|
444
1881
|
}
|
|
445
1882
|
if (step["continue-on-error"]) {
|
|
446
1883
|
actionProperties.push({
|
|
@@ -454,20 +1891,64 @@ export function parseWorkflowFile(f, options) {
|
|
|
454
1891
|
value: step.timeout.toString(),
|
|
455
1892
|
});
|
|
456
1893
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
1894
|
+
const isOfficial =
|
|
1895
|
+
group?.startsWith("github/") || group === "actions";
|
|
1896
|
+
const isVerified = group?.startsWith("github/");
|
|
1897
|
+
actionProperties.push({
|
|
1898
|
+
name: "cdx:actions:isOfficial",
|
|
1899
|
+
value: String(isOfficial),
|
|
1900
|
+
});
|
|
1901
|
+
actionProperties.push({
|
|
1902
|
+
name: "cdx:actions:isVerified",
|
|
1903
|
+
value: String(isVerified),
|
|
1904
|
+
});
|
|
469
1905
|
actionProperties.push(...analyzeCheckoutStep(step));
|
|
470
1906
|
actionProperties.push(...analyzeCacheStep(step));
|
|
1907
|
+
actionProperties.push(...analyzeDispatchActionStep(step));
|
|
1908
|
+
if (
|
|
1909
|
+
step.uses?.includes("actions/github-script") &&
|
|
1910
|
+
typeof step.with?.script === "string"
|
|
1911
|
+
) {
|
|
1912
|
+
const scriptDispatchInfo = detectWorkflowDispatchInvocations(
|
|
1913
|
+
step.with.script,
|
|
1914
|
+
);
|
|
1915
|
+
appendDispatchProperties(actionProperties, scriptDispatchInfo);
|
|
1916
|
+
const githubScriptSensitiveRefs = [
|
|
1917
|
+
...detectSensitiveContextReferences(
|
|
1918
|
+
step.with.script,
|
|
1919
|
+
effectiveEnv,
|
|
1920
|
+
),
|
|
1921
|
+
...collectSensitiveEnvBindings(effectiveEnv),
|
|
1922
|
+
];
|
|
1923
|
+
if (step.with["github-token"]) {
|
|
1924
|
+
githubScriptSensitiveRefs.push("input:github-token");
|
|
1925
|
+
}
|
|
1926
|
+
if (githubScriptSensitiveRefs.length) {
|
|
1927
|
+
actionProperties.push({
|
|
1928
|
+
name: "cdx:github:step:referencesSensitiveContext",
|
|
1929
|
+
value: "true",
|
|
1930
|
+
});
|
|
1931
|
+
actionProperties.push({
|
|
1932
|
+
name: "cdx:github:step:sensitiveContextRefs",
|
|
1933
|
+
value: [...new Set(githubScriptSensitiveRefs)].join(","),
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
const forkContextRefs = detectForkContextReferences(
|
|
1937
|
+
step.with.script,
|
|
1938
|
+
);
|
|
1939
|
+
if (forkContextRefs.length) {
|
|
1940
|
+
actionProperties.push({
|
|
1941
|
+
name: "cdx:github:step:referencesForkContext",
|
|
1942
|
+
value: "true",
|
|
1943
|
+
});
|
|
1944
|
+
actionProperties.push({
|
|
1945
|
+
name: "cdx:github:step:forkContextRefs",
|
|
1946
|
+
value: [...new Set(forkContextRefs)].join(","),
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
appendSensitiveOperationProperties(actionProperties);
|
|
1951
|
+
actionProperties.push(...sharedJobCtxProps);
|
|
471
1952
|
actionProperties.push(...sharedCtxProps);
|
|
472
1953
|
const evidence = {
|
|
473
1954
|
identity: [
|
|
@@ -502,7 +1983,7 @@ export function parseWorkflowFile(f, options) {
|
|
|
502
1983
|
jobDependsOn.push(purl);
|
|
503
1984
|
}
|
|
504
1985
|
} else if (step.run) {
|
|
505
|
-
commands.push({ executed: step
|
|
1986
|
+
commands.push({ executed: step?.run?.trim().split("\n")[0] });
|
|
506
1987
|
const stepRef = `${jobRef}-step-${steps.length + 1}`;
|
|
507
1988
|
const runProperties = [
|
|
508
1989
|
{ name: "SrcFile", value: f },
|
|
@@ -512,9 +1993,26 @@ export function parseWorkflowFile(f, options) {
|
|
|
512
1993
|
{ name: "cdx:github:step:type", value: "run" },
|
|
513
1994
|
{
|
|
514
1995
|
name: "cdx:github:step:command",
|
|
515
|
-
value: step
|
|
1996
|
+
value: step?.run?.trim().split("\n")[0],
|
|
516
1997
|
},
|
|
517
1998
|
];
|
|
1999
|
+
if (step.if) {
|
|
2000
|
+
runProperties.push({
|
|
2001
|
+
name: "cdx:github:step:condition",
|
|
2002
|
+
value: step.if,
|
|
2003
|
+
});
|
|
2004
|
+
runProperties.push({
|
|
2005
|
+
name: "cdx:github:step:if",
|
|
2006
|
+
value: step.if,
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
if (step["continue-on-error"]) {
|
|
2010
|
+
runProperties.push({
|
|
2011
|
+
name: "cdx:github:step:continueOnError",
|
|
2012
|
+
value: "true",
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
runProperties.push(...sharedJobCtxProps);
|
|
518
2016
|
runProperties.push(...sharedCtxProps);
|
|
519
2017
|
|
|
520
2018
|
const { hasInterpolation, vars } = detectUntrustedInterpolation(
|
|
@@ -530,6 +2028,77 @@ export function parseWorkflowFile(f, options) {
|
|
|
530
2028
|
value: vars.join(","),
|
|
531
2029
|
});
|
|
532
2030
|
}
|
|
2031
|
+
const { hasMutation, targets } = detectRunnerStateMutation(step.run);
|
|
2032
|
+
if (hasMutation) {
|
|
2033
|
+
runProperties.push({
|
|
2034
|
+
name: "cdx:github:step:mutatesRunnerState",
|
|
2035
|
+
value: "true",
|
|
2036
|
+
});
|
|
2037
|
+
runProperties.push({
|
|
2038
|
+
name: "cdx:github:step:runnerStateTargets",
|
|
2039
|
+
value: targets.join(","),
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
const { hasOutboundCommand, tools } = detectOutboundNetworkCommand(
|
|
2043
|
+
step.run,
|
|
2044
|
+
);
|
|
2045
|
+
if (hasOutboundCommand) {
|
|
2046
|
+
runProperties.push({
|
|
2047
|
+
name: "cdx:github:step:hasOutboundNetworkCommand",
|
|
2048
|
+
value: "true",
|
|
2049
|
+
});
|
|
2050
|
+
runProperties.push({
|
|
2051
|
+
name: "cdx:github:step:outboundNetworkTools",
|
|
2052
|
+
value: tools.join(","),
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
const sensitiveContextRefs = detectSensitiveContextReferences(
|
|
2056
|
+
step.run,
|
|
2057
|
+
effectiveEnv,
|
|
2058
|
+
);
|
|
2059
|
+
const dispatchInfo = detectWorkflowDispatchInvocations(step.run);
|
|
2060
|
+
if (dispatchInfo.hasDispatch) {
|
|
2061
|
+
collectSensitiveEnvBindings(effectiveEnv).forEach((ref) => {
|
|
2062
|
+
sensitiveContextRefs.push(ref);
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
if (sensitiveContextRefs.length) {
|
|
2066
|
+
runProperties.push({
|
|
2067
|
+
name: "cdx:github:step:referencesSensitiveContext",
|
|
2068
|
+
value: "true",
|
|
2069
|
+
});
|
|
2070
|
+
runProperties.push({
|
|
2071
|
+
name: "cdx:github:step:sensitiveContextRefs",
|
|
2072
|
+
value: sensitiveContextRefs.join(","),
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
appendDispatchProperties(runProperties, dispatchInfo);
|
|
2076
|
+
const forkContextRefs = detectForkContextReferences(step.run);
|
|
2077
|
+
if (forkContextRefs.length) {
|
|
2078
|
+
runProperties.push({
|
|
2079
|
+
name: "cdx:github:step:referencesForkContext",
|
|
2080
|
+
value: "true",
|
|
2081
|
+
});
|
|
2082
|
+
runProperties.push({
|
|
2083
|
+
name: "cdx:github:step:forkContextRefs",
|
|
2084
|
+
value: [...new Set(forkContextRefs)].join(","),
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
const exfiltrationIndicators = hasOutboundCommand
|
|
2088
|
+
? detectOutboundExfiltrationIndicators(step.run, sensitiveContextRefs)
|
|
2089
|
+
: [];
|
|
2090
|
+
if (exfiltrationIndicators.length) {
|
|
2091
|
+
runProperties.push({
|
|
2092
|
+
name: "cdx:github:step:likelyExfiltration",
|
|
2093
|
+
value: "true",
|
|
2094
|
+
});
|
|
2095
|
+
runProperties.push({
|
|
2096
|
+
name: "cdx:github:step:exfiltrationIndicators",
|
|
2097
|
+
value: exfiltrationIndicators.join(","),
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
runProperties.push(...analyzeLegacyPublishStep(step, effectiveEnv));
|
|
2101
|
+
appendSensitiveOperationProperties(runProperties);
|
|
533
2102
|
components.push({
|
|
534
2103
|
"bom-ref": stepRef,
|
|
535
2104
|
purl: undefined,
|
|
@@ -570,14 +2139,31 @@ export function parseWorkflowFile(f, options) {
|
|
|
570
2139
|
// Build workflow-level properties using the same helpers
|
|
571
2140
|
const workflowProperties = [
|
|
572
2141
|
{ name: "cdx:github:workflow:file", value: f },
|
|
2142
|
+
{ name: "cdx:github:workflow:name", value: workflowName },
|
|
573
2143
|
...buildWorkflowContextProperties({
|
|
574
|
-
|
|
575
|
-
|
|
2144
|
+
hasExplicitPermissionsBlock: workflowHasExplicitPermissionsBlock,
|
|
2145
|
+
hasAnyExplicitPermissionsBlock:
|
|
2146
|
+
workflowHasExplicitPermissionsBlock ||
|
|
2147
|
+
anyJobHasExplicitPermissionsBlock,
|
|
2148
|
+
hasWritePermissions:
|
|
2149
|
+
workflowHasWritePermissions || anyJobHasWritePermissions,
|
|
2150
|
+
hasIdTokenWrite: workflowHasIdTokenWrite || anyJobHasIdTokenWrite,
|
|
576
2151
|
triggers,
|
|
2152
|
+
triggerNames,
|
|
577
2153
|
isHighRisk,
|
|
578
2154
|
concurrencyGroup: workflowConcurrency?.group,
|
|
2155
|
+
writeScopes: Array.from(workflowWriteScopes),
|
|
2156
|
+
dispatchInputs: workflowDispatchInputs,
|
|
2157
|
+
repositoryDispatchTypes,
|
|
2158
|
+
workflowReceiverAliases,
|
|
2159
|
+
workflowCallMetadata,
|
|
579
2160
|
}),
|
|
580
2161
|
];
|
|
2162
|
+
appendHiddenUnicodeProperties(
|
|
2163
|
+
workflowProperties,
|
|
2164
|
+
hiddenUnicodeScan,
|
|
2165
|
+
"cdx:github:workflow",
|
|
2166
|
+
);
|
|
581
2167
|
const workflow = {
|
|
582
2168
|
"bom-ref": workflowRef,
|
|
583
2169
|
uid: workflowRef,
|
|
@@ -625,6 +2211,7 @@ export const githubActionsParser = {
|
|
|
625
2211
|
components.push(...result.components);
|
|
626
2212
|
dependencies.push(...result.dependencies);
|
|
627
2213
|
}
|
|
2214
|
+
enrichLocalDispatchRelationships(workflows, components);
|
|
628
2215
|
return {
|
|
629
2216
|
workflows,
|
|
630
2217
|
components,
|