@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- 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-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +3 -0
- package/common/managed/finding-detail.js +66 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +7 -0
- package/common/managed/npm-packages.js +51 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +359 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +6 -0
- package/common/managed/team-app.js +47 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2K6UN3PC.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7JBSSRD7.js +3 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FMXWXDR4.js +36 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -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-KOGNQDSJ.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- 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 +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-VDRXHIHN.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WRAJVD35.js +1 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +364 -59
- 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/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +23 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +76 -0
- package/server-managed/comparison.ts +52 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +468 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +143 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +98 -0
- package/server-managed/dependency-reviews.ts +364 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +376 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +136 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5503 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +152 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +225 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +351 -0
- package/server-managed/pull-dependency-reviews.ts +334 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +448 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +166 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +212 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +319 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// A repository as a repository risk scan reads it of GitHub
|
|
2
|
+
// (repository-risk.ts): opened at its default branch's head, its lockfiles
|
|
3
|
+
// of npm's ecosystem listed (findRepoLockfiles), those of its `paths` alone,
|
|
4
|
+
// each read for its packages (@preventive/lockfile), and access to it checked
|
|
5
|
+
// again before they are told (`recheck`); and its Dependabot alerts, open or dismissed, of
|
|
6
|
+
// npm's, where the source asks them (dependabot-alerts.ts), asked meanwhile
|
|
7
|
+
// and never kept, a failure to have them its own, and its GitHub Actions
|
|
8
|
+
// (repository-actions.ts). A listing is kept by the commit it was listed at,
|
|
9
|
+
// its Actions' files too, and a lockfile's packages by its blob: in the
|
|
10
|
+
// instance's memory, and the source's `listings`, `actionFiles` and
|
|
11
|
+
// `lockfiles` for SCAN_READ_KEPT_MS, so that only opening a repository and
|
|
12
|
+
// checking access to it again ask GitHub where all else is kept.
|
|
13
|
+
import type { LockfileFormat, RepoLockfile } from '@preventive/upstream/github.js'
|
|
14
|
+
import type { AlertsRead } from './dependabot-access.ts'
|
|
15
|
+
import type { DependabotAlert } from './dependabot-alerts.ts'
|
|
16
|
+
import { type LockedPackage, isTestFixture, lockedPackages } from './locked-packages.ts'
|
|
17
|
+
import type { Lru } from './lru.ts'
|
|
18
|
+
import type { KeptStore } from './npm-kept.ts'
|
|
19
|
+
import { inTurns } from './queue.ts'
|
|
20
|
+
import { type WorkflowsRead, lastingRead, readActions } from './repository-actions.ts'
|
|
21
|
+
import { type RiskFailure, failureOf } from './repository-risk-assess.ts'
|
|
22
|
+
import type { RepositoryRiskLine, RepositoryRiskRepo } from './repository-risk.ts'
|
|
23
|
+
|
|
24
|
+
// The formats of npm's ecosystem, as findRepoLockfiles names them, and those
|
|
25
|
+
// of them read here.
|
|
26
|
+
const NPM_LOCKFILE_FORMATS: ReadonlySet<LockfileFormat> = new Set(['npm', 'pnpm', 'yarn', 'bun', 'deno'])
|
|
27
|
+
const READ_FORMATS: ReadonlySet<LockfileFormat> = new Set(['npm', 'pnpm', 'yarn'])
|
|
28
|
+
|
|
29
|
+
// How long the source's `listings` and `lockfiles` are read for: what
|
|
30
|
+
// GitHub has at a commit or a blob doesn't change, how it is read here may.
|
|
31
|
+
export const SCAN_READ_KEPT_MS = 24 * 60 * 60_000
|
|
32
|
+
// How many of a repository's lockfiles are read at once: each one GitHub is
|
|
33
|
+
// asked for where it isn't kept, then parsed.
|
|
34
|
+
const LOCKFILES_AT_ONCE = 4
|
|
35
|
+
|
|
36
|
+
export type ListedLockfile = Pick<RepoLockfile, 'path' | 'format' | 'sha'>
|
|
37
|
+
export type LockfileRead = { packages: LockedPackage[] } | RiskFailure
|
|
38
|
+
// A repository's Dependabot alerts, those of its `paths` alone, with access to
|
|
39
|
+
// them to check again before they are told (`recheck`), or why they couldn't
|
|
40
|
+
// be had.
|
|
41
|
+
export type DependabotRead = { alerts: DependabotAlert[]; recheck?(): Promise<void> } | RiskFailure
|
|
42
|
+
type LockfileLine = Extract<RepositoryRiskLine, { type: 'lockfile' }>
|
|
43
|
+
export type RepositoryFailure = Extract<RepositoryRiskLine, { type: 'repo'; error: string }>
|
|
44
|
+
|
|
45
|
+
// A repository opened at its default branch's head (`commit`): its lockfiles,
|
|
46
|
+
// as findRepoLockfiles lists them, each with its blob's id, and a file's
|
|
47
|
+
// text; `recheck` checks access to it again, throwing where it is gone; and
|
|
48
|
+
// `dependabot`, where asked, its Dependabot alerts of npm's, open or dismissed, as GitHub
|
|
49
|
+
// has them now, with access to them to check again before they are told.
|
|
50
|
+
export interface OpenedRepository {
|
|
51
|
+
commit: string
|
|
52
|
+
lockfiles(): Promise<ListedLockfile[]>
|
|
53
|
+
file(path: string): Promise<string>
|
|
54
|
+
recheck(): Promise<void>
|
|
55
|
+
dependabot?(): Promise<AlertsRead>
|
|
56
|
+
// Its workflows' paths, none where it has none.
|
|
57
|
+
workflows?(): Promise<string[]>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// What a repository is read with: the scan's source and caches, and its
|
|
61
|
+
// signal.
|
|
62
|
+
export interface RepositoryReading<R extends RepositoryRiskRepo> {
|
|
63
|
+
source: { open(repo: R): Promise<OpenedRepository>; listings?: KeptStore<ListedLockfile[]>; lockfiles?: KeptStore<LockfileRead>; actionFiles?: KeptStore<WorkflowsRead> }
|
|
64
|
+
caches: { listings: Lru<ListedLockfile[]>; lockfiles: Lru<LockfileRead>; actionFiles: Lru<WorkflowsRead> }
|
|
65
|
+
signal: AbortSignal
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A repository read: its commit, its lockfiles' lines, each with its blob's
|
|
69
|
+
// id, its Dependabot alerts where asked, and access to it to check again just
|
|
70
|
+
// before they are told (OpenedRepository `recheck`), as GitHub may have
|
|
71
|
+
// withdrawn it meanwhile.
|
|
72
|
+
export interface RepositoryRead {
|
|
73
|
+
commit: string
|
|
74
|
+
lockfiles: { sha: string; line: LockfileLine }[]
|
|
75
|
+
dependabot: DependabotRead | null
|
|
76
|
+
// Its Actions' files, where it lists its workflows.
|
|
77
|
+
actions: WorkflowsRead | null
|
|
78
|
+
recheck(): Promise<void>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Whether a lockfile at `path` is of `paths` (RepositoryRiskRepo).
|
|
82
|
+
function lockfileOf(path: string, paths: readonly string[] | undefined) {
|
|
83
|
+
const dir = path.slice(0, Math.max(path.lastIndexOf('/'), 0))
|
|
84
|
+
return !paths || paths.some(scope => dir === '' || dir === scope || dir.startsWith(`${scope}/`) || scope.startsWith(`${dir}/`))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// What is kept by `key`: in this instance's memory, else in `store` within
|
|
88
|
+
// SCAN_READ_KEPT_MS, else `read` and kept in both, where it is to be.
|
|
89
|
+
async function keptRead<T>(memory: Lru<T>, store: KeptStore<T> | undefined, key: string, read: () => Promise<T>, keep = (_value: T) => true) {
|
|
90
|
+
const known = memory.get(key)
|
|
91
|
+
if (known !== undefined) return known
|
|
92
|
+
const kept = await store?.read(key)
|
|
93
|
+
if (kept && Date.now() - kept.at < SCAN_READ_KEPT_MS) { memory.set(key, kept.value); return kept.value }
|
|
94
|
+
const value = await read()
|
|
95
|
+
if (!keep(value)) return value
|
|
96
|
+
memory.set(key, value)
|
|
97
|
+
await store?.write(key, value, Date.now())
|
|
98
|
+
return value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A lockfile's line, its packages or why they couldn't be had: as kept by its
|
|
102
|
+
// blob, else read from GitHub and kept.
|
|
103
|
+
async function readLockfile({ source, caches }: RepositoryReading<RepositoryRiskRepo>, repo: number, opened: OpenedRepository,
|
|
104
|
+
{ path, format, sha }: ListedLockfile): Promise<LockfileLine> {
|
|
105
|
+
const at = { type: 'lockfile', repo, path, format } as const
|
|
106
|
+
if (!READ_FORMATS.has(format)) return { ...at, error: 'unsupported-format' }
|
|
107
|
+
try {
|
|
108
|
+
return { ...at, ...await keptRead(caches.lockfiles, source.lockfiles, `${format}\0${sha}`, async (): Promise<LockfileRead> => {
|
|
109
|
+
const text = await opened.file(path)
|
|
110
|
+
try { return { packages: lockedPackages(format, text) } } catch (error) {
|
|
111
|
+
return { error: 'lockfile-unreadable', ...error instanceof Error ? { detail: error.message.slice(0, 500) } : {} }
|
|
112
|
+
}
|
|
113
|
+
}) }
|
|
114
|
+
} catch (error) { return { ...at, ...failureOf(error) } }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A repository opened and its lockfiles of npm's ecosystem listed, as kept
|
|
118
|
+
// for its commit or else from GitHub, those of its `paths` alone; its line
|
|
119
|
+
// telling why where either failed; null where the scan stopped.
|
|
120
|
+
async function listLockfiles<R extends RepositoryRiskRepo>(reading: RepositoryReading<R>, repo: R) {
|
|
121
|
+
let doing = 'opening it'
|
|
122
|
+
try {
|
|
123
|
+
const opened = await reading.source.open(repo)
|
|
124
|
+
if (reading.signal.aborted) return null
|
|
125
|
+
doing = 'listing its lockfiles'
|
|
126
|
+
const listed = await keptRead(reading.caches.listings, reading.source.listings, `${repo.repoId}@${opened.commit}`, () => opened.lockfiles())
|
|
127
|
+
const lockfiles = listed.filter(({ path, format }) => NPM_LOCKFILE_FORMATS.has(format) && lockfileOf(path, repo.paths))
|
|
128
|
+
return { opened, lockfiles }
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return reading.signal.aborted ? null : { type: 'repo', id: repo.repoId, ...failureOf(error, doing) } as const
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A repository's Dependabot alerts of its `paths` (RepositoryRiskRepo), as
|
|
135
|
+
// its lockfiles are, or why they couldn't be had; null where they aren't
|
|
136
|
+
// asked.
|
|
137
|
+
async function readDependabot(opened: OpenedRepository, paths: readonly string[] | undefined): Promise<DependabotRead | null> {
|
|
138
|
+
if (!opened.dependabot) return null
|
|
139
|
+
try {
|
|
140
|
+
const { alerts, recheck } = await opened.dependabot()
|
|
141
|
+
return { alerts: alerts.filter(alert => lockfileOf(alert.manifest, paths)), recheck }
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return failureOf(error, 'asking its Dependabot alerts')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A repository read: its lockfiles listed and read, a few at once, its
|
|
148
|
+
// Dependabot alerts asked and its Actions read meanwhile; its line telling why where listing
|
|
149
|
+
// failed; null where the scan stopped. A test's
|
|
150
|
+
// fixture or example its reader refuses (isTestFixture) is left out, as a
|
|
151
|
+
// test may keep one so on purpose.
|
|
152
|
+
export async function readRepository<R extends RepositoryRiskRepo>(reading: RepositoryReading<R>, repo: R): Promise<RepositoryRead | RepositoryFailure | null> {
|
|
153
|
+
const listed = await listLockfiles(reading, repo)
|
|
154
|
+
if (!listed || 'type' in listed) return listed
|
|
155
|
+
const alerting = readDependabot(listed.opened, repo.paths)
|
|
156
|
+
const { opened } = listed, { workflows } = opened
|
|
157
|
+
const acting = workflows ? keptRead(reading.caches.actionFiles, reading.source.actionFiles, `${repo.repoId}@${opened.commit}`,
|
|
158
|
+
() => readActions({ workflows, file: path => opened.file(path) }), lastingRead) : Promise.resolve(null)
|
|
159
|
+
acting.catch(() => {})
|
|
160
|
+
const inTurn = inTurns(LOCKFILES_AT_ONCE)
|
|
161
|
+
const lines = await Promise.all(listed.lockfiles.map(lockfile => inTurn(() => (reading.signal.aborted ? Promise.resolve(null) : readLockfile(reading, repo.repoId, opened, lockfile)))))
|
|
162
|
+
const lockfiles: RepositoryRead['lockfiles'] = []
|
|
163
|
+
for (const [at, line] of lines.entries()) {
|
|
164
|
+
if (!line) return null
|
|
165
|
+
if (!('error' in line && line.error === 'lockfile-unreadable' && isTestFixture(line.path))) lockfiles.push({ sha: listed.lockfiles[at]!.sha, line })
|
|
166
|
+
}
|
|
167
|
+
const dependabot = await alerting
|
|
168
|
+
const actions = await acting
|
|
169
|
+
return reading.signal.aborted ? null : { commit: listed.opened.commit, lockfiles, dependabot, actions, recheck: () => listed.opened.recheck() }
|
|
170
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// Every connected repository's npm packages, as its lockfiles lock them, each
|
|
2
|
+
// assessed by risk (npm-risk.ts), for admins: GET /api/admin/repositories/risk
|
|
3
|
+
// answers a line at a time as each is found and assessed, and stops once its
|
|
4
|
+
// reader leaves (`signal`).
|
|
5
|
+
//
|
|
6
|
+
// A repository's lockfiles are listed at its default branch's head by
|
|
7
|
+
// @preventive/upstream's findRepoLockfiles; of them, those of npm's ecosystem
|
|
8
|
+
// that @preventive/lockfile reads (package-lock.json and npm-shrinkwrap.json
|
|
9
|
+
// of `lockfileVersion: 3`, pnpm-lock.yaml of pnpm 9 to 12, and Yarn 1's
|
|
10
|
+
// yarn.lock) are read for their packages. A package from npm's registry is
|
|
11
|
+
// assessed once a scan, however many lockfiles lock it; one from another
|
|
12
|
+
// host with an integrity, such as a company's mirror of npm's, as npm's
|
|
13
|
+
// package of its name and version where npm's integrity is among it; one
|
|
14
|
+
// from elsewhere (git, a tarball with no integrity, a file) is listed, not
|
|
15
|
+
// assessed. Repositories are read a few at a time (READ_AHEAD), each told in
|
|
16
|
+
// turn, as GitHub's limits are the caller's; packages are assessed meanwhile, as many at a time as npm-risk.ts
|
|
17
|
+
// makes spare assessments, those only devDependencies need in every lockfile
|
|
18
|
+
// that locks them (LockedPackage `dev`) once all the rest are, as an install
|
|
19
|
+
// of no devDependencies leaves them out. A lockfile's packages of npm's
|
|
20
|
+
// registry or a mirror are asked of npm's advisories all at once, as `npm
|
|
21
|
+
// audit` asks, each advisory told with whether it is of malware (risk's
|
|
22
|
+
// npmMalwareAdvisory), which alone is a supply chain's risk; the rest are
|
|
23
|
+
// vulnerabilities, told beside. A repository's Dependabot alerts of npm's, open or dismissed,
|
|
24
|
+
// are told once its lockfiles' advisories are, each with the versions its
|
|
25
|
+
// lockfile locks that it covers, for the page to blend them into npm's; one
|
|
26
|
+
// whose alerts couldn't be had tells why, and the scan goes on. Its GitHub
|
|
27
|
+
// Actions are told once its lockfiles are, the references they make
|
|
28
|
+
// resolved as the scan goes on (repository-actions.ts).
|
|
29
|
+
import type { LockfileFormat } from '@preventive/upstream/github.js'
|
|
30
|
+
import { type Severity, npmMalwareAdvisory } from '../risk/index.js'
|
|
31
|
+
import { isNpmPackageName, isNpmPackageSpec } from '../common/managed/npm-packages.js'
|
|
32
|
+
import type { NpmVersionDocument } from './npm-packages.ts'
|
|
33
|
+
import { type Lru, lru } from './lru.ts'
|
|
34
|
+
import type { KeptStore } from './npm-kept.ts'
|
|
35
|
+
import { MAX_SPARE_ASSESSMENTS } from './npm-risk.ts'
|
|
36
|
+
import type { LockedPackage } from './locked-packages.ts'
|
|
37
|
+
import { type Queue, inTurns, queue } from './queue.ts'
|
|
38
|
+
import { type DownloadsSource, readyingDownloads } from './scan-downloads.ts'
|
|
39
|
+
import { type AssessedPackage, type Assessment, BUSY_RETRIES, BUSY_WAIT_MS, RISK_KEPT_MS, type RiskFailure, assessPackage, failureOf, isBusy } from './repository-risk-assess.ts'
|
|
40
|
+
import { type AdvisoryRow, type Audit, type AuditSource, auditLockfiles } from './repository-risk-audit.ts'
|
|
41
|
+
import { type DependabotRead, type ListedLockfile, type LockfileRead, type OpenedRepository, type RepositoryFailure, type RepositoryRead, SCAN_READ_KEPT_MS,
|
|
42
|
+
readRepository } from './repository-risk-read.ts'
|
|
43
|
+
import { type DependabotAlert, alertVersions } from './dependabot-alerts.ts'
|
|
44
|
+
import type { ActionsSource } from '../risk/index.js'
|
|
45
|
+
import { type ActionAnswers, type ActionLine, type ActionRef, type WorkflowLine, type WorkflowsLine, type WorkflowsRead, actionsLines, answerWeight,
|
|
46
|
+
lockedNames, resolvingActions } from './repository-actions.ts'
|
|
47
|
+
|
|
48
|
+
export { RISK_KEPT_MS, highestSeverity } from './repository-risk-assess.ts'
|
|
49
|
+
export { type OpenedRepository, SCAN_READ_KEPT_MS } from './repository-risk-read.ts'
|
|
50
|
+
|
|
51
|
+
// A repository to read: where `paths` are given, a team's directories in it,
|
|
52
|
+
// only the lockfiles in them or above them, as a workspace's root lockfile
|
|
53
|
+
// locks them too.
|
|
54
|
+
export interface RepositoryRiskRepo { repoId: number; fullName: string; paths?: readonly string[] }
|
|
55
|
+
|
|
56
|
+
export interface RepositoryRiskSource<R extends RepositoryRiskRepo> extends AuditSource {
|
|
57
|
+
repos(): Promise<R[]>
|
|
58
|
+
open(repo: R): Promise<OpenedRepository>
|
|
59
|
+
// Whether a team lists a package's scope, its own packages.
|
|
60
|
+
teamScoped(name: string): Promise<boolean>
|
|
61
|
+
// npm's document of a version; an NpmPackageError where npm has none.
|
|
62
|
+
version(name: string, version: string, signal: AbortSignal): Promise<NpmVersionDocument>
|
|
63
|
+
assess(doc: NpmVersionDocument, teamScoped: boolean, signal: AbortSignal): Promise<Assessment>
|
|
64
|
+
// A version npm has no document of, as risk tells one npm removed: its
|
|
65
|
+
// malware alone, its package taken down by npm; an NpmRiskError where
|
|
66
|
+
// nothing tells of it (npm-risk.ts npmRiskRemoved).
|
|
67
|
+
removed(name: string, version: string, teamScoped: boolean, signal: AbortSignal): Promise<Assessment>
|
|
68
|
+
// Where risk lines are kept beyond this instance, as RiskCaches keeps them
|
|
69
|
+
// in it (npm-kept.ts riskLineStore, the database's).
|
|
70
|
+
risks?: KeptStore<RiskLine>
|
|
71
|
+
// Where a repository's lockfiles as listed at a commit, and a lockfile's
|
|
72
|
+
// packages by its blob, are kept beyond this instance, as RiskCaches keeps
|
|
73
|
+
// them in it (npm-kept.ts scanReadStore, the database's), each read for
|
|
74
|
+
// SCAN_READ_KEPT_MS: a scan answered in parts (`partMs`) goes on from
|
|
75
|
+
// where the last stopped on whichever instance answers the next.
|
|
76
|
+
listings?: KeptStore<ListedLockfile[]>
|
|
77
|
+
lockfiles?: KeptStore<LockfileRead>
|
|
78
|
+
// Where a scan answered in parts keeps, from one part to the next, the
|
|
79
|
+
// repositories it told whole (`partMs`): the next tells none of them again,
|
|
80
|
+
// nor asks GitHub of them, but looks up their packages' risks as their
|
|
81
|
+
// lockfiles are kept (`lockfiles`), as the reader has the rest already.
|
|
82
|
+
progress?: { read(): Promise<{ at: number; value: ScanProgress } | null>; write(progress: ScanProgress): Promise<void> } | undefined
|
|
83
|
+
// Readies npm's downloads of packages before they are assessed
|
|
84
|
+
// (scan-downloads.ts); without it, each assessment asks its own.
|
|
85
|
+
downloads?: DownloadsSource
|
|
86
|
+
// GitHub as references are resolved with; none without it.
|
|
87
|
+
actions?: ActionsSource
|
|
88
|
+
// Where Actions' files read and references resolved are kept.
|
|
89
|
+
actionFiles?: KeptStore<WorkflowsRead>
|
|
90
|
+
actionLines?: KeptStore<ActionLine>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
// A Dependabot alert as a scan tells it: with the versions of its package
|
|
96
|
+
// the lockfile it is of locks that it covers, none where it is of none read
|
|
97
|
+
// (a package.json, bun's lockfile), and whether it is of malware, as an
|
|
98
|
+
// advisory's is told.
|
|
99
|
+
export type DependabotAlertRow = DependabotAlert & { versions: string[]; malware: boolean }
|
|
100
|
+
|
|
101
|
+
// A risk line is of npm's package of its name and version, or, with an
|
|
102
|
+
// `integrity`, of a mirror's that its lockfiles hold to it; its `score` risk's
|
|
103
|
+
// of it, out of 100 (assessNpm `score`, risk/npm/score.js).
|
|
104
|
+
export type RepositoryRiskLine =
|
|
105
|
+
| { type: 'repos'; repos: { id: number; fullName: string }[] }
|
|
106
|
+
| ({ type: 'repo'; id: number } & ({ commit: string; lockfiles: { path: string; format: LockfileFormat }[] } | RiskFailure))
|
|
107
|
+
| ({ type: 'lockfile'; repo: number; path: string; format: LockfileFormat } & ({ packages: LockedPackage[] } | RiskFailure))
|
|
108
|
+
| ({ type: 'risk'; name: string; version: string; integrity?: string; stale?: { assessedAt: number } }
|
|
109
|
+
& ({ published: string | null; severity: Severity | null; score: number; findings: { id: string; severity: Severity; takenDown?: true }[] } | RiskFailure))
|
|
110
|
+
| ({ type: 'advisories'; repo: number; path: string } & ({ advisories: AdvisoryRow[] } | RiskFailure))
|
|
111
|
+
| ({ type: 'dependabot'; repo: number } & ({ alerts: DependabotAlertRow[] } | RiskFailure))
|
|
112
|
+
| WorkflowsLine | WorkflowLine | ActionLine
|
|
113
|
+
| { type: 'done' }
|
|
114
|
+
// The scan stopped for its part's time (repositoryRiskLines `partMs`), what
|
|
115
|
+
// it told kept: to be asked again, from where it stopped.
|
|
116
|
+
| { type: 'continue' }
|
|
117
|
+
|
|
118
|
+
// The repositories a scan answered in parts told whole by a part's end: each
|
|
119
|
+
// by its id, its name and a team's directories in it then, and its
|
|
120
|
+
// lockfiles' keys (`${format}\0${sha}`) of those read, and the references
|
|
121
|
+
// its Actions make, to resolve.
|
|
122
|
+
export interface ScanProgress { repos: ToldRepository[] }
|
|
123
|
+
interface ToldRepository { id: number; name: string; paths: readonly string[] | null; lockfiles: string[]; actions?: ActionRef[] }
|
|
124
|
+
|
|
125
|
+
type RiskLine = Extract<RepositoryRiskLine, { type: 'risk' }>
|
|
126
|
+
|
|
127
|
+
// What scans keep between them, in this instance's memory: a repository's
|
|
128
|
+
// lockfiles as listed at a commit, and a lockfile's packages by its blob,
|
|
129
|
+
// neither of which changes; a lockfile's advisories, asked again after
|
|
130
|
+
// RISK_KEPT_MS, as npm publishes advisories; and a package's risk line,
|
|
131
|
+
// told stale after RISK_KEPT_MS and assessed again, as risk tells again what
|
|
132
|
+
// of a package changes in time (a deprecation, its age, its downloads). A
|
|
133
|
+
// lockfile's, and its packages' lines, are told only once access to its
|
|
134
|
+
// repository is checked as when it was read.
|
|
135
|
+
export interface RiskCaches {
|
|
136
|
+
listings: Lru<ListedLockfile[]>
|
|
137
|
+
lockfiles: Lru<LockfileRead>
|
|
138
|
+
risks: Lru<{ at: number; line: RiskLine }>
|
|
139
|
+
advisories: Lru<{ at: number; audit: Audit }>
|
|
140
|
+
actionFiles: Lru<WorkflowsRead>
|
|
141
|
+
actions: Lru<{ at: number; line: ActionLine }>
|
|
142
|
+
actionAnswers: ActionAnswers
|
|
143
|
+
}
|
|
144
|
+
export const riskCaches = (): RiskCaches => ({
|
|
145
|
+
listings: lru(1000),
|
|
146
|
+
// By the packages they hold, a few hundred bytes each.
|
|
147
|
+
lockfiles: lru(200_000, read => 'packages' in read ? read.packages.length + 1 : 1),
|
|
148
|
+
risks: lru(50_000),
|
|
149
|
+
advisories: lru(50_000, kept => Array.isArray(kept.audit) ? kept.audit.length + 1 : 1),
|
|
150
|
+
// By their text, a kilobyte at a time.
|
|
151
|
+
actionFiles: lru(64 * 1024, read => 'files' in read ? read.files.reduce((sum, file) => sum + ('text' in file ? Math.ceil(file.text.length / 1024) : 1), 1) : 1),
|
|
152
|
+
actions: lru(10_000),
|
|
153
|
+
// What GitHub answered of public repositories' actions, by answerWeight.
|
|
154
|
+
actionAnswers: lru(200_000, answerWeight),
|
|
155
|
+
})
|
|
156
|
+
// How many repositories are read at once (repository-risk-read.ts), ahead of
|
|
157
|
+
// the one told: each asks GitHub one thing after another (opening it, and
|
|
158
|
+
// its head) even where all else is kept.
|
|
159
|
+
const READ_AHEAD = 6
|
|
160
|
+
// How many lockfiles' advisories are asked of npm at once, those of the
|
|
161
|
+
// repositories read ahead as they are read, each told in its turn.
|
|
162
|
+
const AUDITS_AT_ONCE = 4
|
|
163
|
+
// How long a part's progress, written as it ends (`progress`), is waited on.
|
|
164
|
+
const PROGRESS_WRITE_MS = 5000
|
|
165
|
+
// How many kept risk lines are read of the source's `risks` at once.
|
|
166
|
+
const KEPT_READ = 500
|
|
167
|
+
const shared = riskCaches()
|
|
168
|
+
|
|
169
|
+
interface Scan {
|
|
170
|
+
source: RepositoryRiskSource<RepositoryRiskRepo>
|
|
171
|
+
caches: RiskCaches
|
|
172
|
+
signal: AbortSignal
|
|
173
|
+
lines: Queue<RepositoryRiskLine>
|
|
174
|
+
// Each lockfile's packages not yet seen, to look up as kept (lookUp); those
|
|
175
|
+
// already seen, by seenKey, each with whether only devDependencies need it
|
|
176
|
+
// so far; those of them with none kept, put off as long as that holds, by
|
|
177
|
+
// seenKey; and those told stale, to assess again once those with none kept
|
|
178
|
+
// are.
|
|
179
|
+
lookups: Queue<AssessedPackage[]>
|
|
180
|
+
seen: Map<string, boolean>
|
|
181
|
+
dev: Map<string, AssessedPackage>
|
|
182
|
+
stale: AssessedPackage[]
|
|
183
|
+
// The repositories told whole, this part or one before (ScanProgress).
|
|
184
|
+
told: Map<number, ToldRepository>
|
|
185
|
+
actions: ReturnType<typeof resolvingActions> | null
|
|
186
|
+
audits: ReturnType<typeof inTurns>
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A package as a scan meets it: by its name, version and a mirror's integrity.
|
|
190
|
+
const seenKey = (pkg: AssessedPackage) => `${pkg.name}@${pkg.version}\0${pkg.integrity ?? ''}`
|
|
191
|
+
|
|
192
|
+
// A package's key, as its risk line is kept: its name, version and a
|
|
193
|
+
// mirror's integrity, and whether a team lists its scope.
|
|
194
|
+
const riskKey = (pkg: AssessedPackage, teamScoped: boolean) => `${pkg.name}@${pkg.version}\0${pkg.integrity ?? ''}\0${teamScoped}`
|
|
195
|
+
// Whether a line kept `at` is told as it is: within RISK_KEPT_MS, and kept
|
|
196
|
+
// since risk told a score (assessNpm `score`).
|
|
197
|
+
const isFresh = ({ at, line }: { at: number; line: RiskLine }) => Date.now() - at < RISK_KEPT_MS && ('error' in line || typeof line.score === 'number')
|
|
198
|
+
|
|
199
|
+
// A package assessed (assess), its line kept, in RiskCaches and the source's
|
|
200
|
+
// `risks`, where it tells what risk found, or that a mirror's copy isn't npm's.
|
|
201
|
+
async function assessed(scan: Scan, pkg: AssessedPackage): Promise<RiskLine | null> {
|
|
202
|
+
let teamScoped
|
|
203
|
+
try { teamScoped = await scan.source.teamScoped(pkg.name) } catch (error) { return { type: 'risk', ...pkg, ...failureOf(error) } }
|
|
204
|
+
const key = riskKey(pkg, teamScoped)
|
|
205
|
+
const line = await assessPackage(scan.source, scan.signal, pkg, teamScoped)
|
|
206
|
+
if (line && (!('error' in line) || line.error === 'integrity-mismatch')) {
|
|
207
|
+
const at = Date.now()
|
|
208
|
+
scan.caches.risks.set(key, { at, line })
|
|
209
|
+
await scan.source.risks?.write(key, line, at)
|
|
210
|
+
}
|
|
211
|
+
return line
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Risk lines kept of `keys`, by key: in this instance's memory, else the
|
|
215
|
+
// source's `risks`, KEPT_READ at a time.
|
|
216
|
+
async function keptLines({ source, caches }: Scan, keys: string[]) {
|
|
217
|
+
const kept = new Map<string, { at: number; line: RiskLine }>()
|
|
218
|
+
const left = keys.filter(key => { const hit = caches.risks.get(key); if (hit) kept.set(key, hit); return !hit })
|
|
219
|
+
for (let from = 0; source.risks && from < left.length; from += KEPT_READ) {
|
|
220
|
+
for (const [key, { at, value }] of await source.risks.readMany(left.slice(from, from + KEPT_READ))) {
|
|
221
|
+
caches.risks.set(key, { at, line: value })
|
|
222
|
+
kept.set(key, { at, line: value })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return kept
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Each lockfile's packages, as assessLater gives them: their risk lines as
|
|
229
|
+
// kept, however old, told at once, a fresh one as it is and an older one as
|
|
230
|
+
// stale (`stale`, when it was assessed), its package assessed again once
|
|
231
|
+
// those with none kept are (`scan.stale`); those with none given to `assess`,
|
|
232
|
+
// but those only devDependencies need so far, put off (`scan.dev`).
|
|
233
|
+
async function lookUp(scan: Scan, assess: (packages: AssessedPackage[]) => void) {
|
|
234
|
+
for (let packages = await scan.lookups.take(); packages !== undefined; packages = await scan.lookups.take()) {
|
|
235
|
+
const keyed: { pkg: AssessedPackage; key: string }[] = []
|
|
236
|
+
for (const pkg of packages) {
|
|
237
|
+
try { keyed.push({ pkg, key: riskKey(pkg, await scan.source.teamScoped(pkg.name)) }) } catch (error) { scan.lines.add({ type: 'risk', ...pkg, ...failureOf(error) }) }
|
|
238
|
+
}
|
|
239
|
+
const kept = await keptLines(scan, keyed.map(({ key }) => key))
|
|
240
|
+
const missing: AssessedPackage[] = []
|
|
241
|
+
for (const { pkg, key } of keyed) {
|
|
242
|
+
const hit = kept.get(key)
|
|
243
|
+
if (hit && isFresh(hit)) scan.lines.add(hit.line)
|
|
244
|
+
else if (hit) { scan.lines.add({ ...hit.line, stale: { assessedAt: hit.at } }); scan.stale.push(pkg) }
|
|
245
|
+
else if (scan.seen.get(seenKey(pkg)) === true) scan.dev.set(seenKey(pkg), pkg)
|
|
246
|
+
else missing.push(pkg)
|
|
247
|
+
}
|
|
248
|
+
if (missing.length > 0) assess(missing)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function assessor(scan: Scan, pending: Queue<AssessedPackage>) {
|
|
253
|
+
for (let next = await pending.take(); next !== undefined; next = await pending.take()) {
|
|
254
|
+
const line = await assessed(scan, next)
|
|
255
|
+
if (line) scan.lines.add(line)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The packages `feed` gives assessed, each given to the assessors once its
|
|
260
|
+
// downloads are readied, where the source readies them (scan-downloads.ts):
|
|
261
|
+
// once all it gives are told, or the scan stops.
|
|
262
|
+
async function assessing(scan: Scan, feed: (assess: (packages: AssessedPackage[]) => void) => Promise<void>) {
|
|
263
|
+
const pending = queue<AssessedPackage>(scan.signal)
|
|
264
|
+
const readying = scan.source.downloads
|
|
265
|
+
? readyingDownloads<AssessedPackage>(scan.source.downloads, scan.signal, pkg => { pending.add(pkg) }, { busy: isBusy, retries: BUSY_RETRIES, waitMs: BUSY_WAIT_MS }) : null
|
|
266
|
+
const assessors = Array.from({ length: MAX_SPARE_ASSESSMENTS }, () => assessor(scan, pending))
|
|
267
|
+
try { await feed(packages => { if (readying) readying.add(packages); else for (const pkg of packages) pending.add(pkg) }) } finally {
|
|
268
|
+
await readying?.close()
|
|
269
|
+
pending.close()
|
|
270
|
+
}
|
|
271
|
+
await Promise.all(assessors)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// A lockfile's packages of npm's registry or a mirror not yet seen this scan,
|
|
275
|
+
// to look up as kept (lookUp), and assess where none is; and one seen as only
|
|
276
|
+
// devDependencies need it, and put off, that this lockfile needs otherwise,
|
|
277
|
+
// to look up again and assess now.
|
|
278
|
+
function assessLater(scan: Scan, packages: LockedPackage[]) {
|
|
279
|
+
const fresh: AssessedPackage[] = []
|
|
280
|
+
for (const { name, version, source, integrity, dev = false } of packages) {
|
|
281
|
+
if ((source !== 'registry' && source !== 'mirror') || version === null || !isNpmPackageName(name) || !isNpmPackageSpec(version)) continue
|
|
282
|
+
const pkg = { name, version, ...integrity === undefined ? {} : { integrity } }
|
|
283
|
+
const key = seenKey(pkg)
|
|
284
|
+
const was = scan.seen.get(key)
|
|
285
|
+
if (was === false || (was && dev)) continue
|
|
286
|
+
scan.seen.set(key, dev)
|
|
287
|
+
if (was === undefined || scan.dev.delete(key)) fresh.push(pkg)
|
|
288
|
+
}
|
|
289
|
+
if (fresh.length > 0) scan.lookups.add(fresh)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type LockfileLine = Extract<RepositoryRiskLine, { type: 'lockfile' }>
|
|
293
|
+
|
|
294
|
+
// A lockfile's advisories line, of its audit (repository-risk-audit.ts auditLockfile).
|
|
295
|
+
const advisoriesLine = ({ repo, path }: LockfileLine, audit: Audit) => ({ type: 'advisories', repo, path, ...Array.isArray(audit) ? { advisories: audit } : audit } as const)
|
|
296
|
+
|
|
297
|
+
// A repository read, its lockfiles' audits asked meanwhile, AUDITS_AT_ONCE
|
|
298
|
+
// across the scan (auditLockfiles), to tell in turn once access is checked again.
|
|
299
|
+
type Audited = (RepositoryRead & { audits: Promise<Audit | null>[] }) | RepositoryFailure
|
|
300
|
+
const audited = (scan: Scan, read: RepositoryRead | RepositoryFailure | null): Audited | null => (read === null || 'type' in read ? read : { ...read, audits: auditLockfiles(scan.source, scan.caches.advisories,
|
|
301
|
+
scan.signal, read.lockfiles.map(({ sha, line }) => ({ key: `${line.format}\0${sha}`, packages: 'packages' in line ? line.packages : null })), scan.audits) })
|
|
302
|
+
|
|
303
|
+
type DependabotLine = Extract<RepositoryRiskLine, { type: 'dependabot' }>
|
|
304
|
+
|
|
305
|
+
// A repository's Dependabot alerts' line, each alert with the versions the
|
|
306
|
+
// lockfile it is of locks that it covers (`lockfiles`, a repository read's),
|
|
307
|
+
// or why they couldn't be had.
|
|
308
|
+
function dependabotLine(repo: number, read: DependabotRead, lockfiles: RepositoryRead['lockfiles']): DependabotLine {
|
|
309
|
+
if ('error' in read) return { type: 'dependabot', repo, ...read }
|
|
310
|
+
const locked = new Map(lockfiles.flatMap(({ line }) => 'packages' in line ? [[line.path, line.packages] as const] : []))
|
|
311
|
+
return { type: 'dependabot', repo, alerts: read.alerts.map(alert => ({ ...alert, versions: alertVersions(alert, locked.get(alert.manifest)),
|
|
312
|
+
malware: npmMalwareAdvisory({ id: alert.ghsa, aliases: alert.aliases, cwe: alert.cwe, title: alert.title }) })) }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// A repository's alerts checked again just before they are told, as access to
|
|
316
|
+
// the repository is (DependabotRead `recheck`): why not, where the user's
|
|
317
|
+
// access to them is withdrawn since they were read.
|
|
318
|
+
async function rechecked(read: DependabotRead): Promise<DependabotRead> {
|
|
319
|
+
if (!('alerts' in read) || !read.recheck) return read
|
|
320
|
+
try { await read.recheck() } catch (error) { return failureOf(error, 'checking access to its Dependabot alerts again') }
|
|
321
|
+
return read
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// A repository read (repository-risk-read.ts readRepository) told: its
|
|
325
|
+
// line, or why it couldn't be read, then its lockfiles', and their
|
|
326
|
+
// advisories, asked as it was read, its packages assessed meanwhile, then
|
|
327
|
+
// its Dependabot alerts.
|
|
328
|
+
// Access to it is checked again
|
|
329
|
+
// just before, however long ago it was read, as GitHub may have withdrawn it.
|
|
330
|
+
async function tellRepository(scan: Scan, repo: RepositoryRiskRepo, read: Audited | null) {
|
|
331
|
+
if (!read || scan.signal.aborted) return
|
|
332
|
+
if ('type' in read) { scan.lines.add(read); return }
|
|
333
|
+
try { await read.recheck() } catch (error) {
|
|
334
|
+
if (!scan.signal.aborted) scan.lines.add({ type: 'repo', id: repo.repoId, ...failureOf(error, 'checking access to it again') })
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
if (scan.signal.aborted) return
|
|
338
|
+
const { commit, lockfiles } = read
|
|
339
|
+
scan.lines.add({ type: 'repo', id: repo.repoId, commit, lockfiles: lockfiles.map(({ line: { path, format } }) => ({ path, format })) })
|
|
340
|
+
for (const { line } of lockfiles) {
|
|
341
|
+
scan.lines.add(line)
|
|
342
|
+
if ('packages' in line) assessLater(scan, line.packages)
|
|
343
|
+
}
|
|
344
|
+
const refs = read.actions ? tellActions(scan, repo, read.actions, lockfiles) : []
|
|
345
|
+
for (const [at, { line }] of lockfiles.entries()) {
|
|
346
|
+
if (scan.signal.aborted) return
|
|
347
|
+
const audit = await read.audits[at]
|
|
348
|
+
if (audit) scan.lines.add(advisoriesLine(line, audit))
|
|
349
|
+
}
|
|
350
|
+
if (scan.signal.aborted) return
|
|
351
|
+
const dependabot = read.dependabot && await rechecked(read.dependabot)
|
|
352
|
+
if (scan.signal.aborted) return
|
|
353
|
+
if (dependabot) scan.lines.add(dependabotLine(repo.repoId, dependabot, lockfiles))
|
|
354
|
+
scan.told.set(repo.repoId, { id: repo.repoId, name: repo.fullName, paths: repo.paths ?? null,
|
|
355
|
+
lockfiles: lockfiles.flatMap(({ sha, line }) => 'packages' in line ? [`${line.format}\0${sha}`] : []), ...refs.length > 0 ? { actions: refs } : {} })
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// A repository's Actions told, and the references they make to resolve.
|
|
359
|
+
function tellActions(scan: Scan, repo: RepositoryRiskRepo, read: WorkflowsRead, lockfiles: RepositoryRead['lockfiles']) {
|
|
360
|
+
const locked = lockfiles.flatMap(({ line }) => 'packages' in line ? [lockedNames(line.path, line.packages)] : [])
|
|
361
|
+
const { lines, refs } = actionsLines(repo.repoId, repo.fullName.split('/')[0]!, read, locked)
|
|
362
|
+
for (const line of lines) scan.lines.add(line)
|
|
363
|
+
scan.actions?.add(refs)
|
|
364
|
+
return refs
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Whether an earlier part told `repo` whole, by its name and a team's
|
|
368
|
+
// directories in it as they are now, its packages then looked up as its
|
|
369
|
+
// lockfiles are kept, GitHub asked nothing of it: not where one isn't. It is
|
|
370
|
+
// told as read then, its default branch's head not asked again.
|
|
371
|
+
async function toldBefore(scan: Scan, repo: RepositoryRiskRepo) {
|
|
372
|
+
const told = scan.told.get(repo.repoId)
|
|
373
|
+
if (!told || told.name !== repo.fullName || JSON.stringify(told.paths) !== JSON.stringify(repo.paths ?? null)) return false
|
|
374
|
+
const reads: LockedPackage[][] = [], unknown = told.lockfiles.filter(key => !scan.caches.lockfiles.get(key))
|
|
375
|
+
const stored = unknown.length > 0 && scan.source.lockfiles ? await scan.source.lockfiles.readMany(unknown) : null
|
|
376
|
+
for (const key of told.lockfiles) {
|
|
377
|
+
const read = scan.caches.lockfiles.get(key) ?? stored?.get(key)?.value
|
|
378
|
+
if (!read || !('packages' in read)) return false
|
|
379
|
+
reads.push(read.packages)
|
|
380
|
+
}
|
|
381
|
+
for (const packages of reads) assessLater(scan, packages)
|
|
382
|
+
scan.actions?.add(told.actions ?? [])
|
|
383
|
+
return true
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Each repository, READ_AHEAD read at a time, each told in turn, but those an
|
|
387
|
+
// earlier part told whole (ScanProgress), read of none.
|
|
388
|
+
async function readRepositories(scan: Scan) {
|
|
389
|
+
const repos = await scan.source.repos()
|
|
390
|
+
scan.lines.add({ type: 'repos', repos: repos.map(repo => ({ id: repo.repoId, fullName: repo.fullName })) })
|
|
391
|
+
// Those still listed: one no longer is read again should it be once more.
|
|
392
|
+
const listed = new Set(repos.map(repo => repo.repoId))
|
|
393
|
+
// Those within SCAN_READ_KEPT_MS, as their lockfiles are kept that long.
|
|
394
|
+
const kept = await scan.source.progress?.read()
|
|
395
|
+
const progress = kept && Date.now() - kept.at < SCAN_READ_KEPT_MS ? kept.value.repos : []
|
|
396
|
+
for (const told of progress) if (listed.has(told.id)) scan.told.set(told.id, told)
|
|
397
|
+
const reads: Promise<Audited | null>[] = []
|
|
398
|
+
const read = (at: number) => {
|
|
399
|
+
if (reads[at]) return reads[at]
|
|
400
|
+
const repo = repos[at]!
|
|
401
|
+
const started = reads[at] = toldBefore(scan, repo).then(async told => (told ? null : audited(scan, await readRepository(scan, repo))))
|
|
402
|
+
// One read ahead and no longer waited on, as the scan stopped, fails alone.
|
|
403
|
+
started.catch(() => {})
|
|
404
|
+
return started
|
|
405
|
+
}
|
|
406
|
+
for (const [at, repo] of repos.entries()) {
|
|
407
|
+
if (scan.signal.aborted) return
|
|
408
|
+
for (let ahead = at; ahead < Math.min(at + READ_AHEAD, repos.length); ahead++) void read(ahead)
|
|
409
|
+
await tellRepository(scan, repo, await read(at))
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// What is left to assess once the repositories are read, a turn at a time:
|
|
414
|
+
// the stale ones that more than devDependencies need, then those only
|
|
415
|
+
// devDependencies need, those with none kept before the stale ones.
|
|
416
|
+
function leftOver(scan: Scan): AssessedPackage[][] {
|
|
417
|
+
const dev = (pkg: AssessedPackage) => scan.seen.get(seenKey(pkg)) === true
|
|
418
|
+
return [scan.stale.filter(pkg => !dev(pkg)), [...scan.dev.values(), ...scan.stale.filter(dev)]]
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// The scan's lines, in the order they are found: the repositories first,
|
|
422
|
+
// then each repository's lockfiles and their packages once read and access
|
|
423
|
+
// is checked again, and each package's risk: as kept, however old, at once
|
|
424
|
+
// (lookUp), then those with none kept once assessed, while the repositories
|
|
425
|
+
// are still being read, then those told stale once assessed again, and those
|
|
426
|
+
// only devDependencies need after all the rest (leftOver); `done`
|
|
427
|
+
// last. Ends at once, the work
|
|
428
|
+
// under way given up on, where `signal` aborts or the lines are no longer read. A failure of
|
|
429
|
+
// a repository, a lockfile or a package is told in its line; one of the scan
|
|
430
|
+
// itself is thrown, the lines before it told. What it reads is kept in
|
|
431
|
+
// `caches` (RiskCaches), the instance's own unless given. Given `partMs`, it
|
|
432
|
+
// stops once that long, as a function time-limited must (Vercel's), the work
|
|
433
|
+
// under way given up on, its last line `continue`, for its reader to ask
|
|
434
|
+
// again: those it assessed are kept, and told at once then, and the
|
|
435
|
+
// repositories it told whole kept as its source's `progress`.
|
|
436
|
+
export async function* repositoryRiskLines<R extends RepositoryRiskRepo>(source: RepositoryRiskSource<R>, signal: AbortSignal, caches = shared,
|
|
437
|
+
{ partMs = null }: { partMs?: number | null } = {}): AsyncGenerator<RepositoryRiskLine> {
|
|
438
|
+
const part = new AbortController(), stopped = new AbortController()
|
|
439
|
+
const parting = partMs === null ? undefined : setTimeout(() => { part.abort() }, partMs)
|
|
440
|
+
const running = AbortSignal.any([signal, stopped.signal, part.signal])
|
|
441
|
+
// The lines told until the part's time is up, those found by then told, and
|
|
442
|
+
// none waited for: the work under way, such as a request to GitHub, which
|
|
443
|
+
// isn't given up on at once, outlasting it.
|
|
444
|
+
const lines = queue<RepositoryRiskLine>(AbortSignal.any([signal, part.signal]), { drain: true })
|
|
445
|
+
const actions = source.actions ? resolvingActions({ source: source.actions, signal: running, tell: line => { lines.add(line) }, memory: caches.actions,
|
|
446
|
+
answers: caches.actionAnswers, store: source.actionLines }) : null
|
|
447
|
+
const scan: Scan = { source, caches, signal: running, lines, lookups: queue(running), seen: new Map(), dev: new Map(), stale: [], told: new Map(), actions,
|
|
448
|
+
audits: inTurns(AUDITS_AT_ONCE) }
|
|
449
|
+
const failed: unknown[] = []
|
|
450
|
+
void (async () => {
|
|
451
|
+
try {
|
|
452
|
+
// Those with no risk line kept assessed first, as their lockfiles are read,
|
|
453
|
+
await assessing(scan, async assess => {
|
|
454
|
+
const looking = lookUp(scan, assess)
|
|
455
|
+
// stopped at once where reading fails;
|
|
456
|
+
try { await readRepositories(scan) } catch (error) { failed.push(error); stopped.abort() } finally { scan.lookups.close() }
|
|
457
|
+
await looking
|
|
458
|
+
})
|
|
459
|
+
// then those told stale, and those only devDependencies need, in turn.
|
|
460
|
+
for (const left of leftOver(scan)) {
|
|
461
|
+
if (!running.aborted && left.length > 0) await assessing(scan, assess => { assess(left); return Promise.resolve() })
|
|
462
|
+
}
|
|
463
|
+
await actions?.close()
|
|
464
|
+
if (!running.aborted) scan.lines.add({ type: 'done' })
|
|
465
|
+
} catch (error) {
|
|
466
|
+
failed.push(error)
|
|
467
|
+
stopped.abort()
|
|
468
|
+
} finally { scan.lines.close() }
|
|
469
|
+
})()
|
|
470
|
+
try {
|
|
471
|
+
let done = false
|
|
472
|
+
for (let line = await scan.lines.take(); line !== undefined; line = await scan.lines.take()) {
|
|
473
|
+
done ||= line.type === 'done'
|
|
474
|
+
yield line
|
|
475
|
+
}
|
|
476
|
+
if (done || signal.aborted) return
|
|
477
|
+
if (part.signal.aborted) {
|
|
478
|
+
// Not waited on past PROGRESS_WRITE_MS, within what Vercel's limit
|
|
479
|
+
// leaves past the part's: the next part then goes on from less.
|
|
480
|
+
let timer
|
|
481
|
+
await Promise.race([source.progress?.write({ repos: [...scan.told.values()] }), new Promise(resolve => { timer = setTimeout(resolve, PROGRESS_WRITE_MS) })])
|
|
482
|
+
clearTimeout(timer)
|
|
483
|
+
yield { type: 'continue' }
|
|
484
|
+
}
|
|
485
|
+
else if (failed.length > 0) throw failed[0]
|
|
486
|
+
} finally {
|
|
487
|
+
// A GitHub request under way isn't waited on: it settles on its own.
|
|
488
|
+
clearTimeout(parting)
|
|
489
|
+
stopped.abort()
|
|
490
|
+
}
|
|
491
|
+
}
|