@preventive/triage 1.0.0-alpha.43 → 1.0.0-alpha.45
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/common/bundle-compare-diff.js +2 -167
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-sources.js +20 -1
- 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/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 +91 -10
- package/common/managed/team-app.d.ts +5 -2
- package/common/managed/team-app.js +2 -2
- package/common/server-info.ts +11 -0
- package/common/workspace-app.js +19 -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-5DHL5NG3.js +2 -0
- package/out/chunk-6HNXNIQY.js +52 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-AFLZH7WG.js +1 -0
- package/out/chunk-BC2UCLFJ.js +1 -0
- package/out/chunk-C4Z6ZBXV.js +6 -6
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HMAJDVXN.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 -0
- package/out/chunk-NPW52BM2.js +17 -0
- package/out/chunk-NRPC74AM.js +1 -1
- package/out/chunk-NV7ES4EW.js +1 -0
- package/out/chunk-OKRLLE5I.js +17 -0
- package/out/chunk-OOJM4CTU.js +1 -1
- package/out/chunk-PNHBZGDD.js +1 -1
- package/out/chunk-PTGDYLNS.js +36 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-Q6MVYGYV.js +3 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-QRD6GIG5.js +229 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +1 -1
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TUII34K2.js +1 -1
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WI7Z5A2T.js +6 -0
- package/out/chunk-WQ74ZBZF.js +7 -0
- package/out/chunk-XVBEOGOS.js +3 -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 +31 -31
- 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 +55 -6
- package/risk/actions/findings.js +156 -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 +165 -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 +68 -1
- package/risk/index.js +6 -2
- package/risk/npm/assess.js +13 -8
- package/risk/npm/compare.js +34 -10
- package/risk/npm/dependencies.js +11 -0
- package/risk/npm/files.js +413 -2
- package/risk/npm/tarball.js +56 -73
- 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/commit-coauthors.ts +83 -0
- package/server-managed/commit-lockfiles.ts +62 -0
- package/server-managed/comparison.ts +41 -0
- package/server-managed/db-methods.ts +29 -10
- package/server-managed/db-neon.ts +30 -2
- package/server-managed/db-schema.ts +9 -2
- package/server-managed/db.ts +7 -0
- 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 +65 -15
- package/server-managed/github-accounts.ts +33 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +23 -9
- package/server-managed/github-errors.ts +2 -0
- 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-users.ts +30 -0
- package/server-managed/http.ts +1219 -81
- package/server-managed/index.ts +6 -2
- package/server-managed/locked-packages.ts +48 -16
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +173 -0
- package/server-managed/npm-insights.ts +20 -11
- package/server-managed/npm-kept.ts +51 -4
- package/server-managed/npm-loads.ts +20 -13
- package/server-managed/npm-packages.ts +24 -64
- package/server-managed/pretty-print.ts +14 -9
- package/server-managed/public-workspace.ts +26 -8
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +280 -0
- package/server-managed/pull-dependency-reviews.ts +318 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/repository-actions.ts +209 -0
- package/server-managed/repository-agents.ts +220 -0
- package/server-managed/repository-browser.ts +54 -13
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +52 -0
- package/server-managed/repository-dependency-reviews.ts +135 -0
- package/server-managed/repository-history.ts +445 -0
- package/server-managed/repository-issues.ts +181 -0
- package/server-managed/repository-pulse.ts +432 -0
- package/server-managed/repository-risk-read.ts +165 -0
- package/server-managed/repository-risk.ts +116 -106
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/session.ts +9 -5
- package/server-managed/sql.ts +1 -1
- package/server-managed/storage-stores.ts +5 -0
- package/server-managed/team-app.ts +54 -14
- package/server-managed/workspace-shares.ts +4 -1
- package/server.ts +11 -0
- package/out/chunk-2KBF5TZ7.js +0 -2
- package/out/chunk-35VE6CDH.js +0 -2
- package/out/chunk-3BZ7JERB.js +0 -1
- package/out/chunk-4F6JSF57.js +0 -3
- package/out/chunk-4XCDFGDP.js +0 -10
- package/out/chunk-7RU5EDRO.js +0 -226
- package/out/chunk-A7M4GV7M.js +0 -1
- package/out/chunk-CLPKNNC7.js +0 -51
- package/out/chunk-CMQA4DM6.js +0 -1
- package/out/chunk-CVVJAXVY.js +0 -2
- package/out/chunk-DIJJNTRH.js +0 -10
- package/out/chunk-GJ4IUCNS.js +0 -1
- package/out/chunk-GNXOKIGP.js +0 -80
- package/out/chunk-GZ4MS3TE.js +0 -13
- 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-LAAB5SZT.js +0 -2
- package/out/chunk-NWZ4PVPN.js +0 -1
- package/out/chunk-OSIMGFUF.js +0 -1
- package/out/chunk-RFZQQXSG.js +0 -17
- package/out/chunk-RSHMVYNS.js +0 -1
- package/out/chunk-TZ5KB2Y5.js +0 -10
- package/out/chunk-UNTRGE26.js +0 -2
- package/out/chunk-UXNUAIIC.js +0 -33
- 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-VQJCXGIY.js +0 -7
- package/out/chunk-W4J3MGLB.js +0 -1
- package/out/chunk-WGG7KQUH.js +0 -1
- package/out/chunk-WZDTZE64.js +0 -1
- package/out/chunk-XOCAPDCM.js +0 -1
- package/out/chunk-XYFXXB2G.js +0 -10
- package/out/chunk-YVZH5SHP.js +0 -1
- package/out/chunk-ZBBOBQ43.js +0 -1
- package/out/chunk-ZMQQCGF6.js +0 -2
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.45",
|
|
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,8 @@
|
|
|
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",
|
|
115
|
+
"server-managed/comparison.ts",
|
|
111
116
|
"server-managed/comments.ts",
|
|
112
117
|
"server-managed/config.ts",
|
|
113
118
|
"server-managed/content-access.ts",
|
|
@@ -117,13 +122,21 @@
|
|
|
117
122
|
"server-managed/db-schema.ts",
|
|
118
123
|
"server-managed/db-table-names.ts",
|
|
119
124
|
"server-managed/db.ts",
|
|
125
|
+
"server-managed/dependabot-access.ts",
|
|
126
|
+
"server-managed/dependabot-alerts.ts",
|
|
127
|
+
"server-managed/dependency-review-comments.ts",
|
|
120
128
|
"server-managed/dependency-reviews.ts",
|
|
129
|
+
"server-managed/github-accounts.ts",
|
|
130
|
+
"server-managed/github-actions.ts",
|
|
121
131
|
"server-managed/github-app.ts",
|
|
122
132
|
"server-managed/github-errors.ts",
|
|
123
133
|
"server-managed/github-metadata.ts",
|
|
124
134
|
"server-managed/github-issue-oauth.ts",
|
|
125
135
|
"server-managed/github-issues.ts",
|
|
126
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",
|
|
127
140
|
"server-managed/github-oauth.ts",
|
|
128
141
|
"server-managed/github-pulls.ts",
|
|
129
142
|
"server-managed/http-response.ts",
|
|
@@ -145,13 +158,21 @@
|
|
|
145
158
|
"server-managed/kept.ts",
|
|
146
159
|
"server-managed/npm-kept.ts",
|
|
147
160
|
"server-managed/npm-loads.ts",
|
|
161
|
+
"server-managed/npm-delta.ts",
|
|
162
|
+
"server-managed/npm-delta-worker.js",
|
|
163
|
+
"server-managed/lockfile-scope.ts",
|
|
148
164
|
"server-managed/npm-manifest.ts",
|
|
149
165
|
"server-managed/npm-packages.ts",
|
|
150
166
|
"server-managed/package-entry-points.ts",
|
|
151
167
|
"server-managed/pretty-print.ts",
|
|
168
|
+
"server-managed/pull-reviews.ts",
|
|
152
169
|
"server-managed/rename-diff.ts",
|
|
153
170
|
"server-managed/rename-diff-worker.js",
|
|
154
171
|
"server-managed/public-workspace.ts",
|
|
172
|
+
"server-managed/pull-dependencies.ts",
|
|
173
|
+
"server-managed/pull-dependency-reviews.ts",
|
|
174
|
+
"server-managed/pull-comment-reviews.ts",
|
|
175
|
+
"server-managed/commit-lockfiles.ts",
|
|
155
176
|
"server-managed/queue.ts",
|
|
156
177
|
"server-managed/repo-path.ts",
|
|
157
178
|
"server-managed/report-migration.ts",
|
|
@@ -161,12 +182,22 @@
|
|
|
161
182
|
"server-managed/report-sources.ts",
|
|
162
183
|
"server-managed/revisions.ts",
|
|
163
184
|
"server-managed/repository-browser.ts",
|
|
185
|
+
"server-managed/repository-changes.ts",
|
|
186
|
+
"server-managed/repository-code.ts",
|
|
187
|
+
"server-managed/repository-dependency-reviews.ts",
|
|
188
|
+
"server-managed/reviewed-dependencies.ts",
|
|
189
|
+
"server-managed/repository-agents.ts",
|
|
190
|
+
"server-managed/repository-history.ts",
|
|
191
|
+
"server-managed/repository-issues.ts",
|
|
192
|
+
"server-managed/repository-pulse.ts",
|
|
164
193
|
"server-managed/repository-aliases.ts",
|
|
165
194
|
"server-managed/repository-discovery.ts",
|
|
166
195
|
"server-managed/repository-policy.ts",
|
|
196
|
+
"server-managed/repository-actions.ts",
|
|
167
197
|
"server-managed/repository-risk.ts",
|
|
168
198
|
"server-managed/repository-risk-assess.ts",
|
|
169
199
|
"server-managed/repository-risk-audit.ts",
|
|
200
|
+
"server-managed/repository-risk-read.ts",
|
|
170
201
|
"server-managed/scan-downloads.ts",
|
|
171
202
|
"server-managed/session.ts",
|
|
172
203
|
"server-managed/slugs.ts",
|
|
@@ -206,6 +237,9 @@
|
|
|
206
237
|
"common/bundle-commit.d.ts",
|
|
207
238
|
"common/bundle-commit.js",
|
|
208
239
|
"common/bundle-compare-diff.js",
|
|
240
|
+
"common/bundle-compare-versions.js",
|
|
241
|
+
"common/bundle-comparison.d.ts",
|
|
242
|
+
"common/bundle-comparison.js",
|
|
209
243
|
"common/bundle-compare-diff.d.ts",
|
|
210
244
|
"common/bundle-compare-inputs.d.ts",
|
|
211
245
|
"common/bundle-compare-inputs.js",
|
|
@@ -239,7 +273,10 @@
|
|
|
239
273
|
"common/line-diff.d.ts",
|
|
240
274
|
"common/line-diff.js",
|
|
241
275
|
"common/linked-workspace-groups.js",
|
|
276
|
+
"common/managed/agent-tasks.ts",
|
|
242
277
|
"common/managed/comments.ts",
|
|
278
|
+
"common/managed/github-agents.ts",
|
|
279
|
+
"common/managed/github-logins.ts",
|
|
243
280
|
"common/managed/npm-packages.d.ts",
|
|
244
281
|
"common/managed/npm-packages.js",
|
|
245
282
|
"common/managed/permissions.ts",
|
|
@@ -291,6 +328,14 @@
|
|
|
291
328
|
"risk/npm/score.js",
|
|
292
329
|
"risk/npm/specs.js",
|
|
293
330
|
"risk/npm/tarball.js",
|
|
331
|
+
"risk/actions/findings.js",
|
|
332
|
+
"risk/actions/resolve.js",
|
|
333
|
+
"risk/actions/uses.js",
|
|
334
|
+
"risk/actions/workflow.js",
|
|
335
|
+
"risk/actions/scripts/commands.js",
|
|
336
|
+
"risk/actions/scripts/grade.js",
|
|
337
|
+
"risk/actions/scripts/packages.js",
|
|
338
|
+
"risk/actions/scripts/shell.js",
|
|
294
339
|
"client/dependency-paths.d.ts",
|
|
295
340
|
"client/dependency-paths.js",
|
|
296
341
|
"client/finding-link.js",
|
|
@@ -301,6 +346,8 @@
|
|
|
301
346
|
"out/client-managed.js",
|
|
302
347
|
"out/client-managed-import.js",
|
|
303
348
|
"out/client-sync.js",
|
|
349
|
+
"out/compare.js",
|
|
350
|
+
"out/dialogs.js",
|
|
304
351
|
"out/graph.js",
|
|
305
352
|
"out/github-issue-authorized.html",
|
|
306
353
|
"out/github-issue-authorized.js",
|
|
@@ -309,6 +356,7 @@
|
|
|
309
356
|
"out/index.html",
|
|
310
357
|
"out/manifest.webmanifest",
|
|
311
358
|
"out/prism.js",
|
|
359
|
+
"out/scan.js",
|
|
312
360
|
"out/stasis.svg",
|
|
313
361
|
"out/terminal.js",
|
|
314
362
|
"out/view.css",
|
|
@@ -352,13 +400,14 @@
|
|
|
352
400
|
"dependencies": {
|
|
353
401
|
"@exodus/stasis": "1.0.0-beta.9",
|
|
354
402
|
"@exodus/stasis-core": "1.0.0-beta.9",
|
|
355
|
-
"@preventive/
|
|
356
|
-
"@preventive/
|
|
357
|
-
"@preventive/
|
|
403
|
+
"@preventive/archive": "^1.1.0",
|
|
404
|
+
"@preventive/compare": "^1.2.11",
|
|
405
|
+
"@preventive/diff": "^1.1.0",
|
|
406
|
+
"@preventive/lockfile": "^1.2.0",
|
|
358
407
|
"@preventive/publishers": "^1.1.0",
|
|
359
|
-
"@preventive/report": "1.0.0
|
|
408
|
+
"@preventive/report": "^1.0.0",
|
|
360
409
|
"@preventive/sourcemap": "1.0.0-alpha.1",
|
|
361
|
-
"@preventive/upstream": "1.
|
|
410
|
+
"@preventive/upstream": "^1.2.0",
|
|
362
411
|
"css-tree": "^3.2.1",
|
|
363
412
|
"oxc-minify": "^0.152.0",
|
|
364
413
|
"oxc-parser": "^0.152.0",
|
|
@@ -0,0 +1,156 @@
|
|
|
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 writes into a script: github-script's
|
|
68
|
+
// `script`, and those an action of the repository's own (ScanOptions
|
|
69
|
+
// `actions`) names in its `run:`s or hands to such an input of a step's.
|
|
70
|
+
function scriptInputs(step, actions, seen = new Set()) {
|
|
71
|
+
if (step.uses?.repo?.toLowerCase() === 'actions/github-script') return ['script']
|
|
72
|
+
const dir = step.uses?.kind === 'local' && actions?.has(step.uses.path) && !seen.has(step.uses.path) ? step.uses.path : null
|
|
73
|
+
const read = dir === null ? null : readWorkflow('action', actions.get(dir))
|
|
74
|
+
if (!read || read instanceof YamlError) return []
|
|
75
|
+
return [...new Set(read.jobs.flatMap(job => job.steps.flatMap(each => [...inputsIn(each.run?.script),
|
|
76
|
+
...scriptInputs(each, actions, new Set(seen).add(dir)).flatMap(input => inputsIn(each.with?.[input]))])))]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Injections through the inputs an action writes into its scripts, and a
|
|
80
|
+
// fork's code or artifacts taken where the workflow runs with its secrets.
|
|
81
|
+
function stepFindings(step, triggers, actions) {
|
|
82
|
+
const repo = step.uses?.repo?.toLowerCase()
|
|
83
|
+
const found = scriptInputs(step, actions).flatMap(input => scriptInjections(step.with?.[input], triggers).map(({ index: _index, ...finding }) => ({ ...finding, line: step.withLines[input] })))
|
|
84
|
+
const forks = ['pull_request_target', 'workflow_run', 'issue_comment'].some(event => triggers.includes(event))
|
|
85
|
+
if (forks && repo === 'actions/checkout' && (namesPullHead(step.with?.ref) || namesPullHead(step.with?.repository))) {
|
|
86
|
+
found.push({ id: 'pwn-request', severity: 'critical', ref: String(step.with.ref ?? step.with.repository), line: step.withLines.ref ?? step.withLines.repository ?? step.line })
|
|
87
|
+
}
|
|
88
|
+
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 })
|
|
89
|
+
if (triggers.includes('workflow_run') && ((repo === 'actions/download-artifact' && (step.with?.['run-id'] !== undefined || step.with?.['github-token'] !== undefined))
|
|
90
|
+
|| 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 })
|
|
91
|
+
return found
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A job's findings, each script graded by the installs before it and its lockfiles.
|
|
95
|
+
function jobFindings(job, triggers, { owner, lockfiles, external, actions }) {
|
|
96
|
+
const found = []
|
|
97
|
+
if (job.uses) {
|
|
98
|
+
const finding = usesFinding(job.uses, owner)
|
|
99
|
+
if (finding) found.push({ ...finding, line: job.uses.line })
|
|
100
|
+
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 })
|
|
101
|
+
// An input of a reusable workflow, which one writes into its scripts.
|
|
102
|
+
for (const [input, value] of Object.entries(job.with ?? {})) for (const { index: _index, ...injected } of scriptInjections(value, triggers)) found.push({ ...injected, line: job.withLines[input] })
|
|
103
|
+
}
|
|
104
|
+
for (const image of job.images) {
|
|
105
|
+
if (!image.read?.digest && !image.text.includes('${{')) found.push({ ...imageFinding(image.text, image.read), line: image.line })
|
|
106
|
+
for (const { index: _index, ...injected } of scriptInjections(image.text, triggers)) found.push({ ...injected, line: image.line })
|
|
107
|
+
}
|
|
108
|
+
const installed = []
|
|
109
|
+
for (const step of job.steps) {
|
|
110
|
+
const finding = step.uses && usesFinding(step.uses, owner)
|
|
111
|
+
if (finding) found.push({ ...finding, line: step.uses.line })
|
|
112
|
+
if (step.run) {
|
|
113
|
+
const directory = dir => directoryOf(step.run.workingDirectory, dir)
|
|
114
|
+
const locked = (name, dir) => !external && lockedIn(lockfiles, directory(dir), name)
|
|
115
|
+
const before = (dev, dir) => installed.some(({ at, prod }) => within(at, directory(dir)) && !(prod && dev))
|
|
116
|
+
found.push(...scriptFindings(step.run, { locked, installed: before, external, triggers }))
|
|
117
|
+
}
|
|
118
|
+
found.push(...stepFindings(step, triggers, actions))
|
|
119
|
+
installed.push(...installs(step, actions))
|
|
120
|
+
}
|
|
121
|
+
return found
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Every finding of a workflow read (readWorkflow), in its lines' order, by
|
|
125
|
+
// index.d.ts ScanOptions.
|
|
126
|
+
export function workflowFindings(read, { owner = null, lockfiles = [], external = false, actions = null } = {}) {
|
|
127
|
+
const found = read.jobs.flatMap(job => jobFindings(job, read.triggers, { owner, lockfiles, external, actions }))
|
|
128
|
+
if (/^node(?:12|16)$/u.test(read.using ?? '')) found.push({ id: 'deprecated-runtime', severity: 'low', using: read.using, line: read.usingLine })
|
|
129
|
+
return found.toSorted((a, b) => (a.line ?? 0) - (b.line ?? 0))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The references a workflow read makes: of GitHub's, each with its `key`, and
|
|
133
|
+
// of its own (`local`).
|
|
134
|
+
export function workflowUses(read) {
|
|
135
|
+
return read.jobs.flatMap(job => [job.uses, ...job.steps.map(step => step.uses)]).flatMap(uses => {
|
|
136
|
+
if (uses?.kind === 'local') return [uses]
|
|
137
|
+
return uses?.kind === 'action' || uses?.kind === 'workflow' ? [{ ...uses, key: usesKey(uses) }] : []
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The actions of the repository's own a workflow or an action uses, by
|
|
142
|
+
// their directories, and a Docker action's Dockerfile; none for one GitHub
|
|
143
|
+
// can't run.
|
|
144
|
+
export function ownFilesOf(kind, text) {
|
|
145
|
+
const read = readWorkflow(kind, text)
|
|
146
|
+
if (read instanceof YamlError) return { actions: [], dockerfile: null }
|
|
147
|
+
return { actions: workflowUses(read).flatMap(uses => uses.kind === 'local' && !uses.job ? [uses.path] : []), dockerfile: read.dockerfile?.path ?? null }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A workflow's or an action's metadata's (`kind`) references and findings;
|
|
151
|
+
// why not (`workflow-unreadable`) for one GitHub can't run.
|
|
152
|
+
export function scanWorkflow(kind, text, options = {}) {
|
|
153
|
+
const read = readWorkflow(kind, text)
|
|
154
|
+
if (read instanceof YamlError) return { error: 'workflow-unreadable', detail: read.message.slice(0, 500) }
|
|
155
|
+
return { uses: workflowUses(read), findings: workflowFindings(read, options) }
|
|
156
|
+
}
|
|
@@ -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
|
+
}
|