@preventive/triage 1.0.0-alpha.44 → 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-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 +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-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 -1
- 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 +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 +51 -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 +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-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/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 +1122 -52
- package/server-managed/index.ts +6 -2
- 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 +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/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 +113 -105
- 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 +19 -3
- package/server-managed/workspace-shares.ts +4 -1
- package/server.ts +11 -0
- 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.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,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,13 +122,21 @@
|
|
|
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",
|
|
130
142
|
"server-managed/http-response.ts",
|
|
@@ -146,13 +158,21 @@
|
|
|
146
158
|
"server-managed/kept.ts",
|
|
147
159
|
"server-managed/npm-kept.ts",
|
|
148
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",
|
|
149
164
|
"server-managed/npm-manifest.ts",
|
|
150
165
|
"server-managed/npm-packages.ts",
|
|
151
166
|
"server-managed/package-entry-points.ts",
|
|
152
167
|
"server-managed/pretty-print.ts",
|
|
168
|
+
"server-managed/pull-reviews.ts",
|
|
153
169
|
"server-managed/rename-diff.ts",
|
|
154
170
|
"server-managed/rename-diff-worker.js",
|
|
155
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",
|
|
156
176
|
"server-managed/queue.ts",
|
|
157
177
|
"server-managed/repo-path.ts",
|
|
158
178
|
"server-managed/report-migration.ts",
|
|
@@ -162,12 +182,22 @@
|
|
|
162
182
|
"server-managed/report-sources.ts",
|
|
163
183
|
"server-managed/revisions.ts",
|
|
164
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",
|
|
165
193
|
"server-managed/repository-aliases.ts",
|
|
166
194
|
"server-managed/repository-discovery.ts",
|
|
167
195
|
"server-managed/repository-policy.ts",
|
|
196
|
+
"server-managed/repository-actions.ts",
|
|
168
197
|
"server-managed/repository-risk.ts",
|
|
169
198
|
"server-managed/repository-risk-assess.ts",
|
|
170
199
|
"server-managed/repository-risk-audit.ts",
|
|
200
|
+
"server-managed/repository-risk-read.ts",
|
|
171
201
|
"server-managed/scan-downloads.ts",
|
|
172
202
|
"server-managed/session.ts",
|
|
173
203
|
"server-managed/slugs.ts",
|
|
@@ -243,7 +273,10 @@
|
|
|
243
273
|
"common/line-diff.d.ts",
|
|
244
274
|
"common/line-diff.js",
|
|
245
275
|
"common/linked-workspace-groups.js",
|
|
276
|
+
"common/managed/agent-tasks.ts",
|
|
246
277
|
"common/managed/comments.ts",
|
|
278
|
+
"common/managed/github-agents.ts",
|
|
279
|
+
"common/managed/github-logins.ts",
|
|
247
280
|
"common/managed/npm-packages.d.ts",
|
|
248
281
|
"common/managed/npm-packages.js",
|
|
249
282
|
"common/managed/permissions.ts",
|
|
@@ -295,6 +328,14 @@
|
|
|
295
328
|
"risk/npm/score.js",
|
|
296
329
|
"risk/npm/specs.js",
|
|
297
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",
|
|
298
339
|
"client/dependency-paths.d.ts",
|
|
299
340
|
"client/dependency-paths.js",
|
|
300
341
|
"client/finding-link.js",
|
|
@@ -305,6 +346,8 @@
|
|
|
305
346
|
"out/client-managed.js",
|
|
306
347
|
"out/client-managed-import.js",
|
|
307
348
|
"out/client-sync.js",
|
|
349
|
+
"out/compare.js",
|
|
350
|
+
"out/dialogs.js",
|
|
308
351
|
"out/graph.js",
|
|
309
352
|
"out/github-issue-authorized.html",
|
|
310
353
|
"out/github-issue-authorized.js",
|
|
@@ -313,6 +356,7 @@
|
|
|
313
356
|
"out/index.html",
|
|
314
357
|
"out/manifest.webmanifest",
|
|
315
358
|
"out/prism.js",
|
|
359
|
+
"out/scan.js",
|
|
316
360
|
"out/stasis.svg",
|
|
317
361
|
"out/terminal.js",
|
|
318
362
|
"out/view.css",
|
|
@@ -356,13 +400,14 @@
|
|
|
356
400
|
"dependencies": {
|
|
357
401
|
"@exodus/stasis": "1.0.0-beta.9",
|
|
358
402
|
"@exodus/stasis-core": "1.0.0-beta.9",
|
|
359
|
-
"@preventive/
|
|
360
|
-
"@preventive/
|
|
361
|
-
"@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",
|
|
362
407
|
"@preventive/publishers": "^1.1.0",
|
|
363
|
-
"@preventive/report": "1.0.0
|
|
408
|
+
"@preventive/report": "^1.0.0",
|
|
364
409
|
"@preventive/sourcemap": "1.0.0-alpha.1",
|
|
365
|
-
"@preventive/upstream": "1.
|
|
410
|
+
"@preventive/upstream": "^1.2.0",
|
|
366
411
|
"css-tree": "^3.2.1",
|
|
367
412
|
"oxc-minify": "^0.152.0",
|
|
368
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
|
+
}
|