@preventive/triage 1.0.0-alpha.44 → 1.0.0-alpha.46
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/client/linked-findings.js +3 -0
- package/common/bundle-metadata.d.ts +1 -0
- package/common/bundle-metadata.js +1 -1
- package/common/bundle-sources.js +25 -2
- package/common/default-scan-models.ts +1 -1
- package/common/dependency-reviews.d.ts +0 -5
- package/common/dependency-reviews.js +0 -17
- package/common/line-diff.js +5 -4
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/github-agents.ts +92 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/roles.ts +7 -0
- package/common/managed/routes.js +88 -7
- package/common/managed/team-app.d.ts +5 -2
- package/common/managed/team-app.js +2 -2
- package/common/server-info.ts +3 -0
- package/common/workspace-app.js +6 -3
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2XUEZHIP.js +18 -18
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-3PS4KWD2.js +10 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4UFAMJAX.js +1 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7XRR4OFU.js +1 -0
- package/out/chunk-BC2UCLFJ.js +1 -0
- package/out/chunk-BOIXMUU5.js +3 -0
- package/out/chunk-C4Z6ZBXV.js +6 -6
- package/out/chunk-CC4OM6TZ.js +3 -0
- package/out/chunk-G24A42V6.js +2 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-I2MDGSQX.js +3 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -1
- package/out/chunk-JKGQ3O7O.js +6 -0
- package/out/chunk-LE5ZWLG4.js +235 -0
- package/out/chunk-MSION3G4.js +36 -0
- package/out/chunk-NRPC74AM.js +1 -1
- package/out/chunk-OKRLLE5I.js +17 -0
- package/out/chunk-OOJM4CTU.js +1 -1
- package/out/chunk-PNHBZGDD.js +1 -1
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +1 -1
- package/out/chunk-S6GIXWQW.js +52 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TUII34K2.js +1 -1
- package/out/chunk-V3KWNNMY.js +1 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WQ74ZBZF.js +7 -0
- package/out/client-managed-import.js +3 -3
- package/out/client-managed.js +31 -35
- package/out/client-sync.js +11 -11
- package/out/compare.js +35 -0
- package/out/dialogs.js +51 -0
- package/out/github-issue-authorized.html +1 -1
- package/out/github-issue-authorized.js +1 -1
- package/out/graph.js +17 -17
- package/out/index.html +30 -34
- package/out/prism.js +14 -14
- package/out/scan.js +2 -0
- package/out/terminal.js +294 -293
- package/out/view.css +1 -1
- package/out/view.js +16 -107
- package/package.json +53 -6
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/hosts.js +6 -6
- package/risk/files/readability.js +23 -17
- package/risk/files/runs-code.js +18 -6
- package/risk/files/script.js +7 -3
- package/risk/files/stamps.js +8 -7
- package/risk/index.d.ts +71 -1
- package/risk/index.js +6 -2
- package/risk/npm/assess.js +1 -1
- package/risk/npm/compare.js +34 -10
- package/risk/npm/files.js +413 -2
- package/risk/npm/tarball.js +56 -73
- package/server-e2e/app.ts +5 -4
- package/server-e2e/http.ts +5 -2
- package/server-managed/agent-merges.ts +198 -0
- package/server-managed/app-agents.ts +30 -0
- package/server-managed/avatar-store.ts +1 -0
- package/server-managed/bundle-cache.ts +7 -1
- package/server-managed/combined.ts +3 -2
- package/server-managed/commit-coauthors.ts +83 -0
- package/server-managed/commit-lockfiles.ts +72 -0
- package/server-managed/config.ts +6 -0
- package/server-managed/db-methods.ts +66 -19
- package/server-managed/db-neon.ts +89 -21
- package/server-managed/db-schema.ts +13 -2
- package/server-managed/db.ts +9 -1
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +97 -0
- package/server-managed/dependency-reviews.ts +78 -18
- package/server-managed/github-accounts.ts +33 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +33 -13
- package/server-managed/github-errors.ts +2 -0
- package/server-managed/github-issue-links.ts +11 -5
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-oauth.ts +5 -2
- package/server-managed/github-pulls.ts +2 -2
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +10 -2
- package/server-managed/http.ts +1251 -97
- package/server-managed/index.ts +13 -8
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/locked-packages.ts +37 -16
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +178 -0
- package/server-managed/npm-insights.ts +20 -11
- package/server-managed/npm-kept.ts +83 -6
- package/server-managed/npm-loads.ts +114 -37
- package/server-managed/npm-packages.ts +110 -64
- package/server-managed/npm-risk.ts +4 -1
- package/server-managed/pretty-print.ts +67 -23
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +300 -0
- package/server-managed/pull-dependency-reviews.ts +318 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +16 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff.ts +22 -13
- package/server-managed/report-sources.ts +1 -1
- package/server-managed/report-store.ts +42 -9
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +220 -0
- package/server-managed/repository-browser.ts +166 -37
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +52 -0
- package/server-managed/repository-dependency-reviews.ts +157 -0
- package/server-managed/repository-history.ts +461 -0
- package/server-managed/repository-issues.ts +181 -0
- package/server-managed/repository-pulse.ts +439 -0
- package/server-managed/repository-risk-audit.ts +13 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +133 -118
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/session.ts +27 -9
- package/server-managed/sql.ts +6 -3
- package/server-managed/storage-stores.ts +5 -0
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-app.ts +19 -3
- package/server-managed/team-reports.ts +24 -12
- package/server-managed/upstream-cache.ts +12 -0
- package/server-managed/workspace-shares.ts +4 -1
- package/server.ts +25 -5
- package/out/chunk-2ABLKCOY.js +0 -46
- package/out/chunk-2KBF5TZ7.js +0 -2
- package/out/chunk-35VE6CDH.js +0 -2
- package/out/chunk-3BZ7JERB.js +0 -1
- package/out/chunk-4XCDFGDP.js +0 -10
- package/out/chunk-5LSXOLUS.js +0 -1
- package/out/chunk-5RCM2J63.js +0 -1
- package/out/chunk-7B6HB4D7.js +0 -1
- package/out/chunk-AJ4GTATZ.js +0 -3
- package/out/chunk-CH4UN6WK.js +0 -225
- package/out/chunk-CMQA4DM6.js +0 -1
- package/out/chunk-CVVJAXVY.js +0 -2
- package/out/chunk-DIJJNTRH.js +0 -10
- package/out/chunk-EDKLBQQN.js +0 -17
- package/out/chunk-F47DXCEV.js +0 -33
- package/out/chunk-FK3DZEKL.js +0 -1
- package/out/chunk-G6AJWENO.js +0 -1
- package/out/chunk-GJ4IUCNS.js +0 -1
- package/out/chunk-GNXOKIGP.js +0 -80
- package/out/chunk-GZ4MS3TE.js +0 -13
- package/out/chunk-GZWM7UQM.js +0 -2
- package/out/chunk-IU3CCINR.js +0 -3
- package/out/chunk-K3X5EJBQ.js +0 -2
- package/out/chunk-KEX2ATEW.js +0 -2
- package/out/chunk-KYCNJVB7.js +0 -1
- package/out/chunk-LPCTJYNA.js +0 -7
- package/out/chunk-O4RJY5TP.js +0 -2
- package/out/chunk-OD4LPN3H.js +0 -1
- package/out/chunk-QWDWTBM7.js +0 -1
- package/out/chunk-QYWKC557.js +0 -1
- package/out/chunk-R75XOKUP.js +0 -1
- package/out/chunk-SWTLADW5.js +0 -1
- package/out/chunk-T7OKH4VW.js +0 -1
- package/out/chunk-TZ5KB2Y5.js +0 -10
- package/out/chunk-UNTRGE26.js +0 -2
- package/out/chunk-UTR2LVME.js +0 -2
- package/out/chunk-UYAXVRVL.js +0 -1
- package/out/chunk-V3ME6FI5.js +0 -2
- package/out/chunk-V5O7M2YT.js +0 -1
- package/out/chunk-VIHGO77Y.js +0 -1
- package/out/chunk-W4J3MGLB.js +0 -1
- package/out/chunk-WGG7KQUH.js +0 -1
- package/out/chunk-XYFXXB2G.js +0 -10
- package/out/chunk-ZESLQCTF.js +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/triage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.46",
|
|
4
4
|
"description": "Client & relay server for triaging of automated reports",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -89,6 +89,9 @@
|
|
|
89
89
|
"server-e2e/ws-server.ts",
|
|
90
90
|
"server-e2e/config.example.json",
|
|
91
91
|
"server-managed/activity.ts",
|
|
92
|
+
"server-managed/agent-merges.ts",
|
|
93
|
+
"server-managed/release-commits.ts",
|
|
94
|
+
"server-managed/app-agents.ts",
|
|
92
95
|
"server-managed/avatar-store.ts",
|
|
93
96
|
"server-managed/blob-store.ts",
|
|
94
97
|
"server-managed/brotli.ts",
|
|
@@ -108,6 +111,7 @@
|
|
|
108
111
|
"server-managed/cache-storage.ts",
|
|
109
112
|
"server-managed/cli.js",
|
|
110
113
|
"server-managed/combined.ts",
|
|
114
|
+
"server-managed/commit-coauthors.ts",
|
|
111
115
|
"server-managed/comparison.ts",
|
|
112
116
|
"server-managed/comments.ts",
|
|
113
117
|
"server-managed/config.ts",
|
|
@@ -118,15 +122,24 @@
|
|
|
118
122
|
"server-managed/db-schema.ts",
|
|
119
123
|
"server-managed/db-table-names.ts",
|
|
120
124
|
"server-managed/db.ts",
|
|
125
|
+
"server-managed/dependabot-access.ts",
|
|
126
|
+
"server-managed/dependabot-alerts.ts",
|
|
127
|
+
"server-managed/dependency-review-comments.ts",
|
|
121
128
|
"server-managed/dependency-reviews.ts",
|
|
129
|
+
"server-managed/github-accounts.ts",
|
|
130
|
+
"server-managed/github-actions.ts",
|
|
122
131
|
"server-managed/github-app.ts",
|
|
123
132
|
"server-managed/github-errors.ts",
|
|
124
133
|
"server-managed/github-metadata.ts",
|
|
125
134
|
"server-managed/github-issue-oauth.ts",
|
|
126
135
|
"server-managed/github-issues.ts",
|
|
127
136
|
"server-managed/github-issue-links.ts",
|
|
137
|
+
"server-managed/github-link-oauth.ts",
|
|
138
|
+
"server-managed/github-links.ts",
|
|
139
|
+
"server-managed/github-users.ts",
|
|
128
140
|
"server-managed/github-oauth.ts",
|
|
129
141
|
"server-managed/github-pulls.ts",
|
|
142
|
+
"server-managed/github-requests.ts",
|
|
130
143
|
"server-managed/http-response.ts",
|
|
131
144
|
"server-managed/http.ts",
|
|
132
145
|
"server-managed/link-reports.ts",
|
|
@@ -144,15 +157,24 @@
|
|
|
144
157
|
"server-managed/npm-risk.ts",
|
|
145
158
|
"server-managed/npm-provenance.ts",
|
|
146
159
|
"server-managed/kept.ts",
|
|
160
|
+
"server-managed/kept-briefly.ts",
|
|
147
161
|
"server-managed/npm-kept.ts",
|
|
148
162
|
"server-managed/npm-loads.ts",
|
|
163
|
+
"server-managed/npm-delta.ts",
|
|
164
|
+
"server-managed/npm-delta-worker.js",
|
|
165
|
+
"server-managed/lockfile-scope.ts",
|
|
149
166
|
"server-managed/npm-manifest.ts",
|
|
150
167
|
"server-managed/npm-packages.ts",
|
|
151
168
|
"server-managed/package-entry-points.ts",
|
|
152
169
|
"server-managed/pretty-print.ts",
|
|
170
|
+
"server-managed/pull-reviews.ts",
|
|
153
171
|
"server-managed/rename-diff.ts",
|
|
154
172
|
"server-managed/rename-diff-worker.js",
|
|
155
173
|
"server-managed/public-workspace.ts",
|
|
174
|
+
"server-managed/pull-dependencies.ts",
|
|
175
|
+
"server-managed/pull-dependency-reviews.ts",
|
|
176
|
+
"server-managed/pull-comment-reviews.ts",
|
|
177
|
+
"server-managed/commit-lockfiles.ts",
|
|
156
178
|
"server-managed/queue.ts",
|
|
157
179
|
"server-managed/repo-path.ts",
|
|
158
180
|
"server-managed/report-migration.ts",
|
|
@@ -162,12 +184,22 @@
|
|
|
162
184
|
"server-managed/report-sources.ts",
|
|
163
185
|
"server-managed/revisions.ts",
|
|
164
186
|
"server-managed/repository-browser.ts",
|
|
187
|
+
"server-managed/repository-changes.ts",
|
|
188
|
+
"server-managed/repository-code.ts",
|
|
189
|
+
"server-managed/repository-dependency-reviews.ts",
|
|
190
|
+
"server-managed/reviewed-dependencies.ts",
|
|
191
|
+
"server-managed/repository-agents.ts",
|
|
192
|
+
"server-managed/repository-history.ts",
|
|
193
|
+
"server-managed/repository-issues.ts",
|
|
194
|
+
"server-managed/repository-pulse.ts",
|
|
165
195
|
"server-managed/repository-aliases.ts",
|
|
166
196
|
"server-managed/repository-discovery.ts",
|
|
167
197
|
"server-managed/repository-policy.ts",
|
|
198
|
+
"server-managed/repository-actions.ts",
|
|
168
199
|
"server-managed/repository-risk.ts",
|
|
169
200
|
"server-managed/repository-risk-assess.ts",
|
|
170
201
|
"server-managed/repository-risk-audit.ts",
|
|
202
|
+
"server-managed/repository-risk-read.ts",
|
|
171
203
|
"server-managed/scan-downloads.ts",
|
|
172
204
|
"server-managed/session.ts",
|
|
173
205
|
"server-managed/slugs.ts",
|
|
@@ -243,7 +275,10 @@
|
|
|
243
275
|
"common/line-diff.d.ts",
|
|
244
276
|
"common/line-diff.js",
|
|
245
277
|
"common/linked-workspace-groups.js",
|
|
278
|
+
"common/managed/agent-tasks.ts",
|
|
246
279
|
"common/managed/comments.ts",
|
|
280
|
+
"common/managed/github-agents.ts",
|
|
281
|
+
"common/managed/github-logins.ts",
|
|
247
282
|
"common/managed/npm-packages.d.ts",
|
|
248
283
|
"common/managed/npm-packages.js",
|
|
249
284
|
"common/managed/permissions.ts",
|
|
@@ -295,6 +330,14 @@
|
|
|
295
330
|
"risk/npm/score.js",
|
|
296
331
|
"risk/npm/specs.js",
|
|
297
332
|
"risk/npm/tarball.js",
|
|
333
|
+
"risk/actions/findings.js",
|
|
334
|
+
"risk/actions/resolve.js",
|
|
335
|
+
"risk/actions/uses.js",
|
|
336
|
+
"risk/actions/workflow.js",
|
|
337
|
+
"risk/actions/scripts/commands.js",
|
|
338
|
+
"risk/actions/scripts/grade.js",
|
|
339
|
+
"risk/actions/scripts/packages.js",
|
|
340
|
+
"risk/actions/scripts/shell.js",
|
|
298
341
|
"client/dependency-paths.d.ts",
|
|
299
342
|
"client/dependency-paths.js",
|
|
300
343
|
"client/finding-link.js",
|
|
@@ -305,6 +348,8 @@
|
|
|
305
348
|
"out/client-managed.js",
|
|
306
349
|
"out/client-managed-import.js",
|
|
307
350
|
"out/client-sync.js",
|
|
351
|
+
"out/compare.js",
|
|
352
|
+
"out/dialogs.js",
|
|
308
353
|
"out/graph.js",
|
|
309
354
|
"out/github-issue-authorized.html",
|
|
310
355
|
"out/github-issue-authorized.js",
|
|
@@ -313,6 +358,7 @@
|
|
|
313
358
|
"out/index.html",
|
|
314
359
|
"out/manifest.webmanifest",
|
|
315
360
|
"out/prism.js",
|
|
361
|
+
"out/scan.js",
|
|
316
362
|
"out/stasis.svg",
|
|
317
363
|
"out/terminal.js",
|
|
318
364
|
"out/view.css",
|
|
@@ -356,13 +402,14 @@
|
|
|
356
402
|
"dependencies": {
|
|
357
403
|
"@exodus/stasis": "1.0.0-beta.9",
|
|
358
404
|
"@exodus/stasis-core": "1.0.0-beta.9",
|
|
359
|
-
"@preventive/
|
|
360
|
-
"@preventive/
|
|
361
|
-
"@preventive/
|
|
405
|
+
"@preventive/archive": "^1.1.0",
|
|
406
|
+
"@preventive/compare": "^1.2.11",
|
|
407
|
+
"@preventive/diff": "^1.1.0",
|
|
408
|
+
"@preventive/lockfile": "^1.2.0",
|
|
362
409
|
"@preventive/publishers": "^1.1.0",
|
|
363
|
-
"@preventive/report": "1.0.0
|
|
410
|
+
"@preventive/report": "^1.0.0",
|
|
364
411
|
"@preventive/sourcemap": "1.0.0-alpha.1",
|
|
365
|
-
"@preventive/upstream": "1.
|
|
412
|
+
"@preventive/upstream": "^1.2.0",
|
|
366
413
|
"css-tree": "^3.2.1",
|
|
367
414
|
"oxc-minify": "^0.152.0",
|
|
368
415
|
"oxc-parser": "^0.152.0",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// What a workflow, or an action's metadata, calls for by its own text (README.md).
|
|
2
|
+
import { YamlError } from '@preventive/lockfile/yaml.js'
|
|
3
|
+
import { readWorkflow } from './workflow.js'
|
|
4
|
+
import { installsOf } from './scripts/commands.js'
|
|
5
|
+
import { dotted, imageFinding, lockedName, namesPullHead, scriptFindings, scriptInjections, within } from './scripts/grade.js'
|
|
6
|
+
import { ownerKind, usesKey } from './uses.js'
|
|
7
|
+
import { resolved } from '../npm/files.js'
|
|
8
|
+
|
|
9
|
+
// What a `uses:` calls for by its pin, a reference's with its `key`.
|
|
10
|
+
function usesFinding(uses, owner) {
|
|
11
|
+
if (uses.kind === 'invalid') return { id: 'invalid-uses', severity: 'low', uses: uses.uses }
|
|
12
|
+
if (uses.kind === 'docker') return uses.digest ? null : imageFinding(uses.uses, uses)
|
|
13
|
+
if ((uses.kind !== 'action' && uses.kind !== 'workflow') || uses.pin === 'sha') return null
|
|
14
|
+
const at = { uses: uses.uses, key: usesKey(uses) }
|
|
15
|
+
return uses.pin === 'short-sha' ? { id: 'short-sha', severity: 'high', ...at } : { id: 'unpinned', severity: ownerKind(uses.repo, owner) === 'third-party' ? 'high' : 'moderate', ...at, ref: uses.ref }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// An action that installs a project's npm packages, by its name: npm's,
|
|
19
|
+
// Yarn's, pnpm's or Bun's install of dependencies (`bahmutov/npm-install`),
|
|
20
|
+
// not another tool's (`cachix/install-nix-action`).
|
|
21
|
+
const INSTALLER = /(?:npm|yarn|pnpm|bun|node[-_]?modules)[-_]?(?:ci|install)|install[-_]?(?:npm|yarn|pnpm|bun|node[-_]?modules|dep(?:endencie)?s|packages)/iu
|
|
22
|
+
|
|
23
|
+
// Where a step installs a project's packages, each `{ at, prod }`: a
|
|
24
|
+
// script's install's directory or the one it names, and whether it leaves
|
|
25
|
+
// devDependencies out; an action of its own's, by its metadata (ScanOptions
|
|
26
|
+
// `actions`), anywhere (null) where none is told; and anywhere for an action
|
|
27
|
+
// that installs them (cypress-io/github-action's unless `install: false`,
|
|
28
|
+
// pnpm/action-setup's with `run_install`).
|
|
29
|
+
const ANYWHERE = [{ at: null, prod: false }]
|
|
30
|
+
function installs(step, actions, seen = new Set()) {
|
|
31
|
+
if (step.uses?.kind === 'local') return actions ? ownInstalls(actions, step.uses.path, seen) : ANYWHERE
|
|
32
|
+
const repo = step.uses?.repo?.toLowerCase()
|
|
33
|
+
if (repo && (INSTALLER.test(repo) || (repo === 'cypress-io/github-action' && String(step.with?.install) !== 'false'))) return ANYWHERE
|
|
34
|
+
if (repo === 'pnpm/action-setup' && /^(?:true|\[|\{|-)/u.test(String(step.with?.run_install ?? '').trim())) return ANYWHERE
|
|
35
|
+
return step.run ? installsOf(step.run.script, step.run.production).map(({ into, prod }) => ({ at: directoryOf(step.run.workingDirectory, into), prod })) : []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Where the repository's own action at `dir` installs, by its steps, each
|
|
39
|
+
// action of its own once.
|
|
40
|
+
function ownInstalls(actions, dir, seen) {
|
|
41
|
+
const read = actions.has(dir) && !seen.has(dir) && seen.add(dir) ? readWorkflow('action', actions.get(dir)) : null
|
|
42
|
+
return read && !(read instanceof YamlError) ? read.jobs.flatMap(job => job.steps.flatMap(step => installs(step, actions, seen))) : []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A step's directory, and a path from it (`into`), from the repository's
|
|
46
|
+
// root, '' for it; as written where an expression or a variable names it, or
|
|
47
|
+
// it is out of the repository, so that it is within the same alone.
|
|
48
|
+
function directoryOf(workingDirectory, into = null) {
|
|
49
|
+
const path = into?.startsWith('/') ? into : [workingDirectory, into].filter(Boolean).join('/')
|
|
50
|
+
return /\$|^\//u.test(path) ? path : resolved([], path)?.join('/') ?? path
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Whether the nearest lockfiles at or above `directory`, its project's, lock
|
|
54
|
+
// what npx runs by `name`: `dev` where only devDependencies need it in each
|
|
55
|
+
// that does.
|
|
56
|
+
const lockDir = path => path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : ''
|
|
57
|
+
function lockedIn(lockfiles, directory, name) {
|
|
58
|
+
const above = lockfiles.filter(({ path }) => within(lockDir(path), directory))
|
|
59
|
+
const nearest = Math.max(...above.map(({ path }) => lockDir(path).length))
|
|
60
|
+
const locking = above.filter(({ path, names }) => lockDir(path).length === nearest && lockedName(names, name))
|
|
61
|
+
return locking.length > 0 && (locking.every(({ dev }) => dev && lockedName(dev, name)) ? 'dev' : true)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The inputs a text names (`${{ inputs.title }}`).
|
|
65
|
+
const inputsIn = text => typeof text === 'string' ? [...dotted(text).matchAll(/\$\{\{[^}]*?\binputs\s*\.\s*([\w-]+)/gu)].map(match => match[1]) : []
|
|
66
|
+
|
|
67
|
+
// The inputs a step's action, or a job's reusable workflow, writes into a
|
|
68
|
+
// script: github-script's `script`; those one of the repository's own
|
|
69
|
+
// (ScanOptions `actions`) names in its `run:`s or its jobs' images, or hands
|
|
70
|
+
// to such an input of a step's or a job's; and each of another reusable
|
|
71
|
+
// workflow's, or one whose text isn't told, as it may.
|
|
72
|
+
function scriptInputs(step, actions, seen = new Set()) {
|
|
73
|
+
if (step.uses?.repo?.toLowerCase() === 'actions/github-script') return ['script']
|
|
74
|
+
if (step.uses?.kind === 'workflow' || (step.uses?.job && !actions?.has(step.uses.path))) return Object.keys(step.with ?? {})
|
|
75
|
+
const path = step.uses?.kind === 'local' && actions?.has(step.uses.path) && !seen.has(step.uses.path) ? step.uses.path : null
|
|
76
|
+
const read = path === null ? null : readWorkflow(step.uses.job ? 'workflow' : 'action', actions.get(path))
|
|
77
|
+
if (!read || read instanceof YamlError) return []
|
|
78
|
+
const inner = new Set(seen).add(path)
|
|
79
|
+
const handed = each => scriptInputs(each, actions, inner).flatMap(input => inputsIn(each.with?.[input]))
|
|
80
|
+
return [...new Set(read.jobs.flatMap(job => [...job.uses ? handed(job) : [], ...job.images.flatMap(image => inputsIn(image.text)),
|
|
81
|
+
...job.steps.flatMap(each => [...inputsIn(each.run?.script), ...handed(each)])]))]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Injections through the inputs an action writes into its scripts, and a
|
|
85
|
+
// fork's code or artifacts taken where the workflow runs with its secrets.
|
|
86
|
+
function stepFindings(step, triggers, actions) {
|
|
87
|
+
const repo = step.uses?.repo?.toLowerCase()
|
|
88
|
+
const found = scriptInputs(step, actions).flatMap(input => scriptInjections(step.with?.[input], triggers).map(({ index: _index, ...finding }) => ({ ...finding, line: step.withLines[input] })))
|
|
89
|
+
const forks = ['pull_request_target', 'workflow_run', 'issue_comment'].some(event => triggers.includes(event))
|
|
90
|
+
if (forks && repo === 'actions/checkout' && (namesPullHead(step.with?.ref) || namesPullHead(step.with?.repository))) {
|
|
91
|
+
found.push({ id: 'pwn-request', severity: 'critical', ref: String(step.with.ref ?? step.with.repository), line: step.withLines.ref ?? step.withLines.repository ?? step.line })
|
|
92
|
+
}
|
|
93
|
+
if (forks && step.run && /\bgh\s+pr\s+checkout\b/u.test(step.run.script)) found.push({ id: 'pwn-request', severity: 'critical', ref: 'gh pr checkout', line: step.run.line })
|
|
94
|
+
if (triggers.includes('workflow_run') && ((repo === 'actions/download-artifact' && (step.with?.['run-id'] !== undefined || step.with?.['github-token'] !== undefined))
|
|
95
|
+
|| repo === 'dawidd6/action-download-artifact' || /\bgh\s+run\s+download\b/u.test(step.run?.script ?? ''))) found.push({ id: 'untrusted-artifact', severity: 'high', line: step.uses?.line ?? step.run.line })
|
|
96
|
+
return found
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A job's findings, each script graded by the installs before it and its lockfiles.
|
|
100
|
+
function jobFindings(job, triggers, { owner, lockfiles, external, actions }) {
|
|
101
|
+
const found = []
|
|
102
|
+
if (job.uses) {
|
|
103
|
+
const finding = usesFinding(job.uses, owner)
|
|
104
|
+
if (finding) found.push({ ...finding, line: job.uses.line })
|
|
105
|
+
if (job.secrets === 'inherit' && job.uses.repo && ownerKind(job.uses.repo, owner) === 'third-party') found.push({ id: 'secrets-inherited', severity: 'high', uses: job.uses.uses, key: usesKey(job.uses), line: job.uses.line })
|
|
106
|
+
// An input a reusable workflow writes into its scripts.
|
|
107
|
+
for (const input of scriptInputs(job, actions)) for (const { index: _index, ...injected } of scriptInjections(job.with?.[input], triggers)) found.push({ ...injected, line: job.withLines[input] })
|
|
108
|
+
}
|
|
109
|
+
for (const image of job.images) {
|
|
110
|
+
if (!image.read?.digest && !image.text.includes('${{')) found.push({ ...imageFinding(image.text, image.read), line: image.line })
|
|
111
|
+
for (const { index: _index, ...injected } of scriptInjections(image.text, triggers)) found.push({ ...injected, line: image.line })
|
|
112
|
+
}
|
|
113
|
+
const installed = []
|
|
114
|
+
for (const step of job.steps) {
|
|
115
|
+
const finding = step.uses && usesFinding(step.uses, owner)
|
|
116
|
+
if (finding) found.push({ ...finding, line: step.uses.line })
|
|
117
|
+
if (step.run) {
|
|
118
|
+
const directory = dir => directoryOf(step.run.workingDirectory, dir)
|
|
119
|
+
const locked = (name, dir) => !external && lockedIn(lockfiles, directory(dir), name)
|
|
120
|
+
const before = (dev, dir) => installed.some(({ at, prod }) => within(at, directory(dir)) && !(prod && dev))
|
|
121
|
+
found.push(...scriptFindings(step.run, { locked, installed: before, external, triggers }))
|
|
122
|
+
}
|
|
123
|
+
found.push(...stepFindings(step, triggers, actions))
|
|
124
|
+
installed.push(...installs(step, actions))
|
|
125
|
+
}
|
|
126
|
+
return found
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Every finding of a workflow read (readWorkflow), in its lines' order, by
|
|
130
|
+
// index.d.ts ScanOptions.
|
|
131
|
+
export function workflowFindings(read, { owner = null, lockfiles = [], external = false, actions = null } = {}) {
|
|
132
|
+
const found = read.jobs.flatMap(job => jobFindings(job, read.triggers, { owner, lockfiles, external, actions }))
|
|
133
|
+
if (/^node(?:12|16)$/u.test(read.using ?? '')) found.push({ id: 'deprecated-runtime', severity: 'low', using: read.using, line: read.usingLine })
|
|
134
|
+
return found.toSorted((a, b) => (a.line ?? 0) - (b.line ?? 0))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The references a workflow read makes: of GitHub's, each with its `key`, and
|
|
138
|
+
// of its own (`local`).
|
|
139
|
+
export function workflowUses(read) {
|
|
140
|
+
return read.jobs.flatMap(job => [job.uses, ...job.steps.map(step => step.uses)]).flatMap(uses => {
|
|
141
|
+
if (uses?.kind === 'local') return [uses]
|
|
142
|
+
return uses?.kind === 'action' || uses?.kind === 'workflow' ? [{ ...uses, key: usesKey(uses) }] : []
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The actions of the repository's own a workflow or an action uses, by
|
|
147
|
+
// their directories, and a Docker action's Dockerfile; none for one GitHub
|
|
148
|
+
// can't run.
|
|
149
|
+
export function ownFilesOf(kind, text) {
|
|
150
|
+
const read = readWorkflow(kind, text)
|
|
151
|
+
if (read instanceof YamlError) return { actions: [], dockerfile: null }
|
|
152
|
+
return { actions: workflowUses(read).flatMap(uses => uses.kind === 'local' && !uses.job ? [uses.path] : []), dockerfile: read.dockerfile?.path ?? null }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A workflow's or an action's metadata's (`kind`) references and findings;
|
|
156
|
+
// why not (`workflow-unreadable`) for one GitHub can't run.
|
|
157
|
+
export function scanWorkflow(kind, text, options = {}) {
|
|
158
|
+
const read = readWorkflow(kind, text)
|
|
159
|
+
if (read instanceof YamlError) return { error: 'workflow-unreadable', detail: read.message.slice(0, 500) }
|
|
160
|
+
return { uses: workflowUses(read), findings: workflowFindings(read, options) }
|
|
161
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// A reference resolved on GitHub (`source`, index.d.ts ActionsSource): its
|
|
2
|
+
// repository, its ref's commit and its own code's findings (README.md).
|
|
3
|
+
import { YamlError } from '@preventive/lockfile/yaml.js'
|
|
4
|
+
import { readWorkflow } from './workflow.js'
|
|
5
|
+
import { workflowFindings, workflowUses } from './findings.js'
|
|
6
|
+
import { imageFinding, scriptFindings } from './scripts/grade.js'
|
|
7
|
+
import { isWorkflowPath, ownerKind, readImage, readUses, usesKey } from './uses.js'
|
|
8
|
+
|
|
9
|
+
// An action's tags as versions: a release's (`v4.2.1`), and a major (`v4`) or
|
|
10
|
+
// minor one (`v4.2`) moved to its line's latest. `[major, minor, patch]`,
|
|
11
|
+
// null for a part a tag hasn't; null for no version.
|
|
12
|
+
export function tagVersion(tag) {
|
|
13
|
+
const m = typeof tag === 'string' ? /^v?(\d{1,9})(?:\.(\d{1,9}))?(?:\.(\d{1,9}))?$/u.exec(tag) : null
|
|
14
|
+
return m && [Number(m[1]), m[2] === undefined ? null : Number(m[2]), m[3] === undefined ? null : Number(m[3])]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isRelease = version => version !== null && version[2] !== null
|
|
18
|
+
|
|
19
|
+
const byVersion = (a, b) => (b[0] - a[0]) || ((b[1] ?? -1) - (a[1] ?? -1)) || ((b[2] ?? -1) - (a[2] ?? -1))
|
|
20
|
+
|
|
21
|
+
// Whether a release is of a major or minor tag's line: `v4`'s `4.x.y`.
|
|
22
|
+
const ofLine = (version, other) => isRelease(other) && other[0] === version[0] && (version[1] === null || other[1] === version[1])
|
|
23
|
+
|
|
24
|
+
// The versions of `tags` (`{ tag, oid }` each), the highest first.
|
|
25
|
+
const versionsOf = tags => tags.flatMap(({ tag, oid }) => { const version = tagVersion(tag); return version ? [{ tag, oid, version }] : [] })
|
|
26
|
+
.toSorted((a, b) => byVersion(a.version, b.version))
|
|
27
|
+
|
|
28
|
+
// The latest release's tag of `tags`, else the highest version's; null where
|
|
29
|
+
// none is one.
|
|
30
|
+
export function latestOf(tags) {
|
|
31
|
+
const versions = versionsOf(tags)
|
|
32
|
+
return (versions.find(({ version }) => isRelease(version)) ?? versions[0])?.tag ?? null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The highest release of each major line, the latest line first: where a
|
|
36
|
+
// commit is most likely in a release's history.
|
|
37
|
+
export function newestOfLines(tags) {
|
|
38
|
+
const seen = new Set()
|
|
39
|
+
return versionsOf(tags).filter(({ version }) => isRelease(version) && !seen.has(version[0]) && seen.add(version[0]))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// An instruction's lines, its continuations (`\` at a line's end) joined,
|
|
43
|
+
// and each heredoc's (`<<EOF`) through its delimiter, as BuildKit reads one.
|
|
44
|
+
function instructions(text) {
|
|
45
|
+
const lines = text.split('\n').map(line => line.replace(/\r$/u, ''))
|
|
46
|
+
const found = []
|
|
47
|
+
for (let at = 0; at < lines.length; at++) {
|
|
48
|
+
const start = at
|
|
49
|
+
while (lines[at].endsWith('\\') && at + 1 < lines.length) at++
|
|
50
|
+
for (const { 2: quoted, 3: bare } of lines.slice(start, at + 1).join('\n').matchAll(/(?<!<)<<-?\s*(?:(["'])(.+?)\1|([^\s"'<>|&;()]+))/gu)) {
|
|
51
|
+
do at++
|
|
52
|
+
while (at < lines.length - 1 && lines[at].trim() !== (quoted ?? bare))
|
|
53
|
+
}
|
|
54
|
+
found.push({ start, lines: lines.slice(start, Math.min(at, lines.length - 1) + 1) })
|
|
55
|
+
at = Math.min(at, lines.length - 1)
|
|
56
|
+
}
|
|
57
|
+
return found
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A `RUN`'s command, its exec form (`RUN ["sh", "-c", "…"]`) as words.
|
|
61
|
+
function runCommand(text) {
|
|
62
|
+
if (!text.trimStart().startsWith('[')) return text
|
|
63
|
+
try {
|
|
64
|
+
const words = JSON.parse(text)
|
|
65
|
+
return Array.isArray(words) ? words.map(word => /[\s'"]/u.test(String(word)) ? `'${String(word).replaceAll("'", "'\\''")}'` : String(word)).join(' ') : text
|
|
66
|
+
} catch { return text }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A Docker action's Dockerfile: each `FROM` by a tag, a build argument's
|
|
70
|
+
// yellow, and each `RUN` read as a script's (scripts/grade.js).
|
|
71
|
+
export function dockerfileFindings(text) {
|
|
72
|
+
const findings = [], script = [], stages = new Set()
|
|
73
|
+
for (const { start, lines } of instructions(text)) {
|
|
74
|
+
const first = /^\s*(FROM|RUN)\s+(.*)$/iu.exec(lines[0])
|
|
75
|
+
// A RUN's options (`--mount=…`, `--network=…`) aren't its command.
|
|
76
|
+
if (first?.[1].toUpperCase() === 'RUN') { script.push(runCommand(first[2].replace(/^(?:--[a-z-]+(?:=\S*)?\s+)*/iu, '')), ...lines.slice(1)); continue }
|
|
77
|
+
script.push(...lines.map(() => ''))
|
|
78
|
+
if (!first) continue
|
|
79
|
+
const words = lines.join(' ').replaceAll('\\', ' ').trim().split(/\s+/u).slice(1).filter(word => !word.startsWith('--'))
|
|
80
|
+
const [image, as, stage] = words
|
|
81
|
+
const built = image && stages.has(image.toLowerCase())
|
|
82
|
+
if (as?.toLowerCase() === 'as' && stage) stages.add(stage.toLowerCase())
|
|
83
|
+
if (!image || image === 'scratch' || built) continue
|
|
84
|
+
const read = readImage(image)
|
|
85
|
+
if (image.includes('$')) findings.push({ id: 'unpinned-image', severity: 'moderate', image, line: start + 1 })
|
|
86
|
+
else if (!read?.digest) findings.push({ ...imageFinding(image, read), line: start + 1 })
|
|
87
|
+
}
|
|
88
|
+
const run = { script: script.join('\n'), line: 1, start: 1 }
|
|
89
|
+
findings.push(...scriptFindings(run, { locked: () => false, installed: () => false, external: true, triggers: [] }))
|
|
90
|
+
return findings.toSorted((a, b) => a.line - b.line)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// How many major lines' releases a commit is looked for in.
|
|
94
|
+
const MAX_LINES = 10
|
|
95
|
+
|
|
96
|
+
async function repositoryOf(uses, source, owner) {
|
|
97
|
+
const info = await source.repo(uses.repo)
|
|
98
|
+
if (!info) return { info: null, findings: [{ id: 'repo-missing', severity: ownerKind(uses.repo, owner) === 'own' ? 'moderate' : 'critical' }] }
|
|
99
|
+
const findings = []
|
|
100
|
+
if (info.fullName.toLowerCase() !== uses.repo.toLowerCase()) findings.push({ id: 'repo-moved', severity: 'high', to: info.fullName })
|
|
101
|
+
if (info.fork) findings.push({ id: 'repo-fork', severity: 'low', parent: info.fork })
|
|
102
|
+
if (info.archived) findings.push({ id: 'repo-archived', severity: 'low' })
|
|
103
|
+
return { info, findings }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Where `sha` is in the history (`default`, `release`, `branch`), `none`,
|
|
107
|
+
// `unknown` where not all could be looked in, or `missing`: each branch and
|
|
108
|
+
// release looked in once, from its commit.
|
|
109
|
+
async function reachable(source, info, sha, tags) {
|
|
110
|
+
const name = info.fullName, releases = newestOfLines(tags ?? []).slice(0, MAX_LINES)
|
|
111
|
+
if (releases.some(({ oid }) => oid === sha)) return 'release'
|
|
112
|
+
const head = await source.branch(name, info.defaultBranch)
|
|
113
|
+
const onDefault = await source.contains(name, head ?? info.defaultBranch, sha)
|
|
114
|
+
if (onDefault === null) return 'missing'
|
|
115
|
+
if (onDefault) return 'default'
|
|
116
|
+
const looked = new Set([head])
|
|
117
|
+
const contains = oid => !looked.has(oid) && looked.add(oid) && source.contains(name, oid, sha)
|
|
118
|
+
for (const { oid } of releases) if (await contains(oid)) return 'release'
|
|
119
|
+
const { branches, complete } = await source.branches(name)
|
|
120
|
+
for (const { oid } of branches) if (oid === sha || await contains(oid)) return 'branch'
|
|
121
|
+
return complete && tags !== null ? 'none' : 'unknown'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tagsOn = (tags, commit) => tags?.filter(({ oid }) => oid === commit).map(({ tag }) => tag) ?? []
|
|
125
|
+
|
|
126
|
+
function tagFindings(ref, commit, tags) {
|
|
127
|
+
const version = tagVersion(ref)
|
|
128
|
+
if (!version) return [{ id: 'not-version', severity: 'moderate' }]
|
|
129
|
+
if (!tags) return []
|
|
130
|
+
const findings = []
|
|
131
|
+
const releases = versionsOf(tags).filter(other => ofLine(version, other.version))
|
|
132
|
+
if (version[2] === null && releases.length > 0 && !releases.some(({ oid }) => oid === commit)) findings.push({ id: 'tag-off-release', severity: 'high', release: releases[0].tag })
|
|
133
|
+
const latest = latestOf(tags)
|
|
134
|
+
if (latest && tagVersion(latest)[0] > version[0]) findings.push({ id: 'outdated', severity: 'low', latest })
|
|
135
|
+
return findings
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A ref not a commit, resolved as GitHub resolves it, a tag before a branch.
|
|
139
|
+
async function refOf(uses, info, source, tags) {
|
|
140
|
+
const name = info.fullName
|
|
141
|
+
const [tagged, branched] = await Promise.all([tags ? tags.find(({ tag }) => tag === uses.ref)?.oid ?? null : source.tag(name, uses.ref), source.branch(name, uses.ref)])
|
|
142
|
+
if (!tagged && !branched) return { commit: null, at: null, findings: [{ id: 'ref-missing', severity: 'moderate' }] }
|
|
143
|
+
if (!tagged) return { commit: branched, at: 'branch', findings: [{ id: 'branch-ref', severity: 'high' }] }
|
|
144
|
+
const findings = branched ? [{ id: 'ambiguous-ref', severity: 'high', branch: branched }] : []
|
|
145
|
+
findings.push(...tagFindings(uses.ref, tagged, tags))
|
|
146
|
+
const where = await reachable(source, info, tagged, tags)
|
|
147
|
+
if (where === 'none') findings.push({ id: 'tag-off-branch', severity: 'low' })
|
|
148
|
+
return { commit: tagged, at: 'tag', findings }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Whether a version a comment names is a tag on the commit, or one's line.
|
|
152
|
+
function commentHolds(comment, named) {
|
|
153
|
+
const version = tagVersion(comment)
|
|
154
|
+
return named.some(tag => tag === comment || tag.replace(/^v/u, '') === comment.replace(/^v/u, '') || (version && version[2] === null && ofLine(version, tagVersion(tag))))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A commit, looked for in the history where no tag names it, held to its comment.
|
|
158
|
+
async function pinnedOf(uses, info, source, tags) {
|
|
159
|
+
const commit = uses.ref
|
|
160
|
+
let named = tagsOn(tags, commit)
|
|
161
|
+
if (!tags && uses.comment && await source.tag(info.fullName, uses.comment) === commit) named = [uses.comment]
|
|
162
|
+
const findings = []
|
|
163
|
+
if (named.length === 0) {
|
|
164
|
+
const where = await reachable(source, info, commit, tags)
|
|
165
|
+
if (where === 'missing') findings.push({ id: 'commit-missing', severity: 'high' })
|
|
166
|
+
else if (where === 'none') findings.push({ id: 'imposter-commit', severity: 'critical' })
|
|
167
|
+
else if (where === 'unknown') findings.push({ id: 'unverified-commit', severity: 'high' })
|
|
168
|
+
else if (tags) findings.push({ id: 'untagged-commit', severity: 'moderate', on: where })
|
|
169
|
+
}
|
|
170
|
+
if (uses.comment && !commentHolds(uses.comment, named)) findings.push({ id: 'comment-mismatch', severity: 'high', comment: uses.comment, tags: named })
|
|
171
|
+
return { commit, at: 'sha', findings }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A `$/path` (the repository's own at the commit running) as a reference.
|
|
175
|
+
const selfRef = (uses, info, commit) => readUses(`${info.fullName}/${uses.path}@${commit}`, { job: isWorkflowPath(uses.path) })
|
|
176
|
+
|
|
177
|
+
// What a reference runs at `commit` (action.yml, a reusable workflow, a
|
|
178
|
+
// Dockerfile): its findings, each with its file, and the references it makes.
|
|
179
|
+
async function codeOf(uses, info, commit, source) {
|
|
180
|
+
const dir = uses.path ? `${uses.path}/` : '', name = info.fullName
|
|
181
|
+
const candidates = uses.kind === 'workflow' ? [uses.path] : [`${dir}action.yml`, `${dir}action.yaml`]
|
|
182
|
+
let file = null, text = null
|
|
183
|
+
for (const path of candidates) {
|
|
184
|
+
text = await source.file(name, path, commit)
|
|
185
|
+
if (text !== null) { file = path; break }
|
|
186
|
+
}
|
|
187
|
+
if (text === null) return { findings: [{ id: 'action-missing', severity: 'moderate' }], uses: [] }
|
|
188
|
+
const read = readWorkflow(uses.kind === 'workflow' ? 'workflow' : 'action', text)
|
|
189
|
+
if (read instanceof YamlError) return { findings: [{ id: 'unreadable', severity: 'moderate', file, detail: read.message }], uses: [] }
|
|
190
|
+
const findings = workflowFindings(read, { owner: name.split('/')[0], external: true }).map(finding => ({ ...finding, file }))
|
|
191
|
+
const dockerfile = read.dockerfile && `${dir}${read.dockerfile.path}`
|
|
192
|
+
const docker = dockerfile && !dockerfile.split('/').includes('..') ? await source.file(name, dockerfile, commit) : null
|
|
193
|
+
if (docker !== null) findings.push(...dockerfileFindings(docker).map(finding => ({ ...finding, file: dockerfile })))
|
|
194
|
+
const nested = workflowUses(read).map(each => each.kind === 'local' ? (each.self || each.job) && selfRef(each, info, commit) : each)
|
|
195
|
+
return { findings, uses: nested.filter(each => each?.kind === 'action' || each?.kind === 'workflow').map(usesKey) }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A reference resolved (index.d.ts ResolvedAction); `owner`, whose workflow names it.
|
|
199
|
+
export async function resolveAction(uses, source, { owner = null } = {}) {
|
|
200
|
+
const at = { key: usesKey(uses), kind: uses.kind, repo: uses.repo, path: uses.path, ref: uses.ref }
|
|
201
|
+
const { info, findings } = await repositoryOf(uses, source, owner)
|
|
202
|
+
if (!info) return { ...at, repository: null, commit: null, at: null, tags: [], findings, uses: [] }
|
|
203
|
+
const tags = await source.tags(info.fullName)
|
|
204
|
+
const ref = uses.pin === 'sha' ? await pinnedOf(uses, info, source, tags) : await refOf(uses, info, source, tags)
|
|
205
|
+
const code = ref.commit && !ref.findings.some(({ id }) => id === 'commit-missing') ? await codeOf(uses, info, ref.commit, source) : null
|
|
206
|
+
return { ...at, repository: { fullName: info.fullName, private: Boolean(info.private) }, commit: ref.commit, at: ref.at, tags: tagsOn(tags, ref.commit),
|
|
207
|
+
findings: [...findings, ...ref.findings, ...code?.findings ?? []], uses: code?.uses ?? [] }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// `source` with its answers kept, by what was asked, while held: a scan's.
|
|
211
|
+
export function keptSource(source) {
|
|
212
|
+
const kept = new Map()
|
|
213
|
+
const keep = method => (...args) => {
|
|
214
|
+
const key = `${method}\0${JSON.stringify(args)}`
|
|
215
|
+
if (!kept.has(key)) kept.set(key, source[method](...args).catch(error => { kept.delete(key); throw error }))
|
|
216
|
+
return kept.get(key)
|
|
217
|
+
}
|
|
218
|
+
return Object.fromEntries(['repo', 'tags', 'tag', 'branch', 'branches', 'contains', 'file'].map(method => [method, keep(method)]))
|
|
219
|
+
}
|