@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
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 +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -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 +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -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-2WDZPXVL.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-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -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-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-UK7RGJT6.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-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 +362 -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 +21 -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 +73 -0
- package/server-managed/comparison.ts +41 -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 +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -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 +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -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/locked-packages.ts +129 -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 +215 -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-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 +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -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 +355 -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 +165 -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 +156 -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 +150 -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 +307 -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,105 @@
|
|
|
1
|
+
// A package's risk line, as a repository risk scan tells it
|
|
2
|
+
// (repository-risk.ts): its version's document read and assessed, as risk
|
|
3
|
+
// tells one npm removed where npm has none; and how a failure is told.
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
5
|
+
import { NpmRiskError, SEVERITIES, type Severity } from '../risk/index.js'
|
|
6
|
+
import { GithubApiError } from './github-errors.ts'
|
|
7
|
+
import { NpmPackageError, type NpmVersionDocument } from './npm-packages.ts'
|
|
8
|
+
import type { RepositoryRiskLine } from './repository-risk.ts'
|
|
9
|
+
|
|
10
|
+
// An assessment refused as busy (`npm-busy`, by npm-risk.ts or the npm reads
|
|
11
|
+
// beneath it), or that npm asked to ask later (risk's `upstream-busy`, as its
|
|
12
|
+
// downloads API answers past a burst of requests), is asked again after a
|
|
13
|
+
// wait, up to this many times.
|
|
14
|
+
export const BUSY_RETRIES = 20
|
|
15
|
+
export const BUSY_WAIT_MS = 1000
|
|
16
|
+
|
|
17
|
+
// How long a package's risk line, or a lockfile's advisories, is told as it
|
|
18
|
+
// is: after it, a risk line is told stale and assessed again, as risk tells
|
|
19
|
+
// again what of a package changes in time, and advisories asked again, as npm
|
|
20
|
+
// publishes them (repository-risk.ts).
|
|
21
|
+
export const RISK_KEPT_MS = 60 * 60_000
|
|
22
|
+
|
|
23
|
+
// The most severe of `findings`, or null for none.
|
|
24
|
+
export function highestSeverity(findings: readonly { severity: Severity }[]): Severity | null {
|
|
25
|
+
return SEVERITIES.find(severity => findings.some(finding => finding.severity === severity)) ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Why a step failed, as a line says it: an error's code, and for a lockfile
|
|
29
|
+
// refused, what of it its reader refused.
|
|
30
|
+
export type RiskFailure = { error: string; detail?: string }
|
|
31
|
+
type RiskLine = Extract<RepositoryRiskLine, { type: 'risk' }>
|
|
32
|
+
|
|
33
|
+
// What an error is known by: a GitHub or npm failure's code, or `failed`;
|
|
34
|
+
// GitHub not reached with what failed (githubUnreachable), while `doing` what
|
|
35
|
+
// of a repository, and risk's with what it asked of npm and how that failed,
|
|
36
|
+
// where they say.
|
|
37
|
+
export function failureOf(error: unknown, doing?: string): RiskFailure {
|
|
38
|
+
if (error instanceof GithubApiError && error.detail) return { error: error.message, detail: doing ? `${doing}: ${error.detail}` : error.detail }
|
|
39
|
+
if (error instanceof GithubApiError || error instanceof NpmPackageError) return { error: error.message }
|
|
40
|
+
if (!(error instanceof NpmRiskError)) return { error: 'failed' }
|
|
41
|
+
return error.message === error.code ? { error: error.code } : { error: error.code, detail: error.message }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const isBusy = (error: unknown) => (error instanceof NpmPackageError && error.status === 429) || (error instanceof NpmRiskError && error.code === 'upstream-busy')
|
|
45
|
+
|
|
46
|
+
// A package to assess: npm's of its name and version, or, with an
|
|
47
|
+
// `integrity`, a mirror's held to it.
|
|
48
|
+
export interface AssessedPackage { name: string; version: string; integrity?: string }
|
|
49
|
+
|
|
50
|
+
export type Assessment = { published: string | null; findings: { id: string; severity: Severity; takenDown?: unknown }[]; score: { score: number } }
|
|
51
|
+
|
|
52
|
+
// What of a scan's source assessing a package asks (RepositoryRiskSource).
|
|
53
|
+
export interface AssessingSource {
|
|
54
|
+
version(name: string, version: string, signal: AbortSignal): Promise<NpmVersionDocument>
|
|
55
|
+
assess(doc: NpmVersionDocument, teamScoped: boolean, signal: AbortSignal): Promise<Assessment>
|
|
56
|
+
removed(name: string, version: string, teamScoped: boolean, signal: AbortSignal): Promise<Assessment>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A package's risk line, its version's document kept and the assessment asked
|
|
60
|
+
// again after a wait while the server is busy; null once the scan stops. A
|
|
61
|
+
// version npm has no document of is told as risk tells one npm removed
|
|
62
|
+
// (removedOf), its malware keeping whether its package was taken down.
|
|
63
|
+
export async function assessPackage(source: AssessingSource, signal: AbortSignal, pkg: AssessedPackage, teamScoped: boolean): Promise<RiskLine | null> {
|
|
64
|
+
const at = { type: 'risk', ...pkg } as const
|
|
65
|
+
let doc: NpmVersionDocument | NpmPackageError | undefined
|
|
66
|
+
for (let attempt = 0; ; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
const read = doc ??= await documentOf(source, pkg, signal)
|
|
69
|
+
if (!(read instanceof NpmPackageError) && pkg.integrity !== undefined && !pkg.integrity.split(/\s+/u).includes(read.dist.integrity)) return { ...at, error: 'integrity-mismatch' }
|
|
70
|
+
const { published, findings, score } = read instanceof NpmPackageError ? await removedOf(source, pkg, teamScoped, signal, read) : await assessedOf(source, pkg, teamScoped, signal, read)
|
|
71
|
+
const told = findings.map(({ id, severity, takenDown }) => ({ id, severity, ...takenDown === true ? { takenDown: true as const } : {} }))
|
|
72
|
+
return { ...at, published, severity: highestSeverity(told), score: score.score, findings: told }
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (signal.aborted) return null
|
|
75
|
+
if (!isBusy(error) || attempt >= BUSY_RETRIES) return { ...at, ...failureOf(error) }
|
|
76
|
+
try { await delay(BUSY_WAIT_MS * Math.min(attempt + 1, 5), undefined, { signal }) } catch { return null }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// npm's document of a version, or its answer that it has none (a 404).
|
|
82
|
+
async function documentOf(source: AssessingSource, pkg: AssessedPackage, signal: AbortSignal): Promise<NpmVersionDocument | NpmPackageError> {
|
|
83
|
+
try { return await source.version(pkg.name, pkg.version, signal) } catch (error) {
|
|
84
|
+
if (error instanceof NpmPackageError && error.status === 404) return error
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A version's assessment; where risk finds npm no longer has it, as a document
|
|
90
|
+
// read from where versions' are kept doesn't tell, as one npm removed.
|
|
91
|
+
async function assessedOf(source: AssessingSource, pkg: AssessedPackage, teamScoped: boolean, signal: AbortSignal, doc: NpmVersionDocument): Promise<Assessment> {
|
|
92
|
+
try { return await source.assess(doc, teamScoped, signal) } catch (error) {
|
|
93
|
+
if (!(error instanceof NpmRiskError && error.code === 'version-not-found')) throw error
|
|
94
|
+
return removedOf(source, pkg, teamScoped, signal, new NpmPackageError(404, 'package-not-found'))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A version npm has no document of, as risk tells one npm removed, as it
|
|
99
|
+
// removes every one it finds malware in (`removed`): `gone`, npm's answer of
|
|
100
|
+
// none, thrown where nothing tells of it.
|
|
101
|
+
async function removedOf(source: AssessingSource, pkg: AssessedPackage, teamScoped: boolean, signal: AbortSignal, gone: NpmPackageError): Promise<Assessment> {
|
|
102
|
+
try { return await source.removed(pkg.name, pkg.version, teamScoped, signal) } catch (error) {
|
|
103
|
+
throw error instanceof NpmRiskError && (error.code === 'version-not-found' || error.code === 'package-not-found') ? gone : error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// A lockfile's advisories, as a repository risk scan asks them
|
|
2
|
+
// (repository-risk.ts): npm's registry's of its packages of the registry or
|
|
3
|
+
// a mirror, asked at once, as `npm audit` asks, each told with whether it is
|
|
4
|
+
// of malware (risk's npmMalwareAdvisory); kept by the lockfile's blob, in the
|
|
5
|
+
// instance's memory and the source's `audits`, for RISK_KEPT_MS, as npm
|
|
6
|
+
// publishes advisories, or why they couldn't be had for AUDIT_FAILED_KEPT_MS.
|
|
7
|
+
import type { Advisory } from '@preventive/upstream/advisories.js'
|
|
8
|
+
import { isExactVersion } from '@preventive/upstream/semver.js'
|
|
9
|
+
import { npmMalwareAdvisory } from '../risk/index.js'
|
|
10
|
+
import { isNpmPackageName } from '../common/managed/npm-packages.js'
|
|
11
|
+
import type { LockedPackage } from './locked-packages.ts'
|
|
12
|
+
import type { Lru } from './lru.ts'
|
|
13
|
+
import type { KeptStore } from './npm-kept.ts'
|
|
14
|
+
import { RISK_KEPT_MS, type RiskFailure, failureOf } from './repository-risk-assess.ts'
|
|
15
|
+
|
|
16
|
+
export type AdvisoryRow = Advisory & { malware: boolean }
|
|
17
|
+
// A lockfile's advisories, or why they couldn't be had.
|
|
18
|
+
export type Audit = AdvisoryRow[] | RiskFailure
|
|
19
|
+
|
|
20
|
+
export interface AuditSource {
|
|
21
|
+
// npm's registry's advisories of these packages' versions, asked at once.
|
|
22
|
+
advisories(packages: { name: string; versions: string[] }[], signal: AbortSignal): Promise<Advisory[]>
|
|
23
|
+
// Where audits are kept beyond this instance, by the lockfile's blob.
|
|
24
|
+
audits?: KeptStore<Audit>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// How long a lockfile's advisories that couldn't be had are told so, rather
|
|
28
|
+
// than asked again: a scan's next parts don't wait on an audit that failed,
|
|
29
|
+
// as by timing out, again and again.
|
|
30
|
+
const AUDIT_FAILED_KEPT_MS = 10 * 60_000
|
|
31
|
+
|
|
32
|
+
// Whether an audit kept `at` is told as it is: its advisories within
|
|
33
|
+
// RISK_KEPT_MS, why they couldn't be had within AUDIT_FAILED_KEPT_MS.
|
|
34
|
+
const isFresh = ({ at, audit }: { at: number; audit: Audit }) => Date.now() - at < (Array.isArray(audit) ? RISK_KEPT_MS : AUDIT_FAILED_KEPT_MS)
|
|
35
|
+
|
|
36
|
+
// The audits of lockfiles, each of its `packages` by its `key` (auditLockfile),
|
|
37
|
+
// `inTurn` with others, asked at once: each null where it has none to ask
|
|
38
|
+
// of, or once `signal` aborts. Each is the caller's to tell once it may.
|
|
39
|
+
export function auditLockfiles(source: AuditSource, memory: Lru<{ at: number; audit: Audit }>, signal: AbortSignal,
|
|
40
|
+
lockfiles: { key: string; packages: readonly LockedPackage[] | null }[], inTurn: <T>(work: () => Promise<T>) => Promise<T>): Promise<Audit | null>[] {
|
|
41
|
+
return lockfiles.map(({ key, packages }) => {
|
|
42
|
+
const audit = packages ? inTurn(() => (signal.aborted ? Promise.resolve(null) : auditLockfile(source, memory, signal, key, packages))) : Promise.resolve(null)
|
|
43
|
+
// One no longer waited on, as the scan stopped, fails alone.
|
|
44
|
+
audit.catch(() => {})
|
|
45
|
+
return audit
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The audit of a lockfile's `packages`, by its `key` as kept (`memory`, then
|
|
50
|
+
// the source's), else asked and kept; null where it locks none to ask of, or
|
|
51
|
+
// once `signal` aborts.
|
|
52
|
+
export async function auditLockfile(source: AuditSource, memory: Lru<{ at: number; audit: Audit }>, signal: AbortSignal, key: string,
|
|
53
|
+
packages: readonly LockedPackage[]): Promise<Audit | null> {
|
|
54
|
+
let kept = memory.get(key)
|
|
55
|
+
if (!kept || !isFresh(kept)) {
|
|
56
|
+
const stored = await source.audits?.read(key)
|
|
57
|
+
kept = stored ? { at: stored.at, audit: stored.value } : undefined
|
|
58
|
+
}
|
|
59
|
+
if (kept && isFresh(kept)) { memory.set(key, kept); return kept.audit }
|
|
60
|
+
const asked = new Map<string, Set<string>>()
|
|
61
|
+
for (const { name, version, source: from } of packages) {
|
|
62
|
+
if ((from !== 'registry' && from !== 'mirror') || !isNpmPackageName(name) || !isExactVersion(version)) continue
|
|
63
|
+
if (!asked.has(name)) asked.set(name, new Set())
|
|
64
|
+
asked.get(name)!.add(version)
|
|
65
|
+
}
|
|
66
|
+
if (asked.size === 0) return null
|
|
67
|
+
let audit: Audit
|
|
68
|
+
try {
|
|
69
|
+
audit = (await source.advisories([...asked].map(([name, versions]) => ({ name, versions: [...versions] })), signal))
|
|
70
|
+
.map(advisory => ({ ...advisory, malware: npmMalwareAdvisory(advisory) }))
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (signal.aborted) return null
|
|
73
|
+
audit = failureOf(error)
|
|
74
|
+
}
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
memory.set(key, { at: now, audit })
|
|
77
|
+
await source.audits?.write(key, audit, now)
|
|
78
|
+
return audit
|
|
79
|
+
}
|
|
@@ -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
|
+
}
|