@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,65 @@
|
|
|
1
|
+
// What risk reads of a version from npm's version document, as the npm viewer
|
|
2
|
+
// is served it (server-managed/npm-packages.ts npmManifest): the npm account
|
|
3
|
+
// that published it, or the CI that did by npm's trusted publishing, which of
|
|
4
|
+
// its accounts are known to publish (@preventive/publishers), whether npm
|
|
5
|
+
// keeps a provenance statement for it, the scripts npm runs installing it,
|
|
6
|
+
// its dependencies, and its deprecation; and whether a package's scope is one
|
|
7
|
+
// of npm's most downloaded packages'.
|
|
8
|
+
import publishers from '@preventive/publishers/npm.js'
|
|
9
|
+
import scopes from '@preventive/publishers/npm-scopes.js'
|
|
10
|
+
import { INSTALL_SCRIPTS } from './install-scripts.js'
|
|
11
|
+
|
|
12
|
+
const NPM_ACCOUNT = /^[\w.-]{1,214}$/u
|
|
13
|
+
// The npm accounts known to publish: those with standing on one of npm's
|
|
14
|
+
// most downloaded packages.
|
|
15
|
+
const KNOWN_PUBLISHERS = new Set(publishers.map(name => name.toLowerCase()))
|
|
16
|
+
// The scopes of npm's most downloaded packages, which only their owners, an
|
|
17
|
+
// npm organization or account, publish in; and those accounts' own, by
|
|
18
|
+
// scope: npm's accounts and organizations share one namespace, so `@name` is
|
|
19
|
+
// the account `name`'s.
|
|
20
|
+
const KNOWN_SCOPES = new Set(scopes)
|
|
21
|
+
const PUBLISHER_SCOPES = new Map(publishers.map(name => [`@${name.toLowerCase()}`, name]))
|
|
22
|
+
|
|
23
|
+
// A package's scope where it is known: one of npm's most downloaded
|
|
24
|
+
// packages' (`{ scope }`, `@babel` of `@babel/core`), or else a known
|
|
25
|
+
// publisher's own (`{ scope, account }`, `@sindresorhus` of
|
|
26
|
+
// `@sindresorhus/is`); else null.
|
|
27
|
+
export function npmKnownScope(name) {
|
|
28
|
+
const scope = name.startsWith('@') ? name.slice(0, name.indexOf('/')) : null
|
|
29
|
+
if (!scope) return null
|
|
30
|
+
if (KNOWN_SCOPES.has(scope)) return { scope }
|
|
31
|
+
const account = PUBLISHER_SCOPES.get(scope)
|
|
32
|
+
return account ? { scope, account } : null
|
|
33
|
+
}
|
|
34
|
+
const plainObject = value => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
35
|
+
const stringRecord = value => plainObject(value) ? Object.fromEntries(Object.entries(value).filter(([, item]) => typeof item === 'string')) : undefined
|
|
36
|
+
|
|
37
|
+
// One of package.json's people's name: `{ name }`, or `Name <email> (url)` in
|
|
38
|
+
// one string.
|
|
39
|
+
function personName(value) {
|
|
40
|
+
const text = part => typeof part === 'string' && part.trim() !== '' ? part.trim() : null
|
|
41
|
+
if (plainObject(value)) return text(value.name)
|
|
42
|
+
return typeof value === 'string' ? text(/^([^<(]*)/u.exec(value)?.[1]) : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A version document's manifest, as risk reads it: `publisher` (or
|
|
46
|
+
// `trustedPublisher`), `knownAccounts`, `provenance`, `installScripts`,
|
|
47
|
+
// `dependencies`, `peerDependencies` and `optionalDependencies`, and
|
|
48
|
+
// `deprecated`, each where it has one.
|
|
49
|
+
export function npmVersionManifest(json) {
|
|
50
|
+
const pick = {}
|
|
51
|
+
if (typeof json?.deprecated === 'string') pick.deprecated = json.deprecated
|
|
52
|
+
const publisher = personName(json?._npmUser)
|
|
53
|
+
if (publisher && NPM_ACCOUNT.test(publisher)) pick.publisher = publisher
|
|
54
|
+
else if (publisher && publisher.length <= 64 && plainObject(json._npmUser.trustedPublisher)) pick.trustedPublisher = publisher
|
|
55
|
+
if (pick.publisher && KNOWN_PUBLISHERS.has(pick.publisher.toLowerCase())) pick.knownAccounts = [pick.publisher]
|
|
56
|
+
for (const key of ['dependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
57
|
+
const record = stringRecord(json?.[key])
|
|
58
|
+
if (record && Object.keys(record).length > 0) pick[key] = record
|
|
59
|
+
}
|
|
60
|
+
if (plainObject(json?.dist) && plainObject(json.dist.attestations) && plainObject(json.dist.attestations.provenance)) pick.provenance = true
|
|
61
|
+
const scripts = stringRecord(json?.scripts)
|
|
62
|
+
const install = scripts ? INSTALL_SCRIPTS.filter(script => script in scripts) : []
|
|
63
|
+
if (install.length > 0) pick.installScripts = Object.fromEntries(install.map(script => [script, scripts[script]]))
|
|
64
|
+
return pick
|
|
65
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What risk tells of a dependency a version adds, by its package: its tier by
|
|
2
|
+
// its downloads, its latest two weeks' left out (downloads.js
|
|
3
|
+
// npmSettledDownloads), as a package a release just pulled in, as a
|
|
4
|
+
// compromised one depending on it would, could owe those to that release
|
|
5
|
+
// alone; and below the top 20,000, how old the package was where it didn't
|
|
6
|
+
// exist when the version compared with was published, as plain-crypto-js
|
|
7
|
+
// was eighteen hours old when axios 1.14.1 was published depending on it.
|
|
8
|
+
import { npmDownloadTier, npmSettledDownloads, npmTierLow } from './downloads.js'
|
|
9
|
+
|
|
10
|
+
// A dependency added, from its package's facts so far: `downloads`, npm's
|
|
11
|
+
// count of them (null where npm counts none, as it counts none of a package's
|
|
12
|
+
// until the day after it was first published, nor any of a private one's);
|
|
13
|
+
// `listed`, once its version list is read, `{ created, private, installs? }`,
|
|
14
|
+
// when its first version was published, whether only a token reads it, and
|
|
15
|
+
// the version of it its spec installs (registry.js npmPublicFacts); and
|
|
16
|
+
// `published`, when the version compared with and the open one were; and
|
|
17
|
+
// whether nothing of the version imports it (`unused`). Its `tier`
|
|
18
|
+
// (Infinity, unpopular, where npm counts none of a public package's; null
|
|
19
|
+
// where it can't yet be told), whether its version list is `wanted` to tell
|
|
20
|
+
// more (below the top 20,000, or none counted, or unused whatever its tier,
|
|
21
|
+
// as how new its package is grades it, npmUnusedDependencySeverity),
|
|
22
|
+
// `created`, `gap` where its package is new since the version compared
|
|
23
|
+
// with: when its first version was published, and how long before the open
|
|
24
|
+
// version that was (negative for after, as a name someone took once a
|
|
25
|
+
// release named it); and where told, the version its spec `installs`,
|
|
26
|
+
// `new` where that was published since the version compared with.
|
|
27
|
+
export function npmAddedDependency({ downloads, listed = null, published = [], unused = false }, now = Date.now()) {
|
|
28
|
+
const settled = npmSettledDownloads(downloads, now)
|
|
29
|
+
const counted = settled ? npmDownloadTier(settled) : null
|
|
30
|
+
const wanted = !settled || unused || (counted !== null && npmTierLow(counted))
|
|
31
|
+
const tier = settled ? counted : listed && !listed.private ? Infinity : null
|
|
32
|
+
const [was, open] = published
|
|
33
|
+
const created = wanted && tier !== null && listed?.created && was && open && Date.parse(listed.created) > Date.parse(was) ? listed.created : null
|
|
34
|
+
const installs = listed?.installs ? { ...listed.installs, new: Boolean(was && listed.installs.published && Date.parse(listed.installs.published) > Date.parse(was)) } : null
|
|
35
|
+
return { tier, wanted, created, gap: created ? Date.parse(open) - Date.parse(created) : null, ...installs ? { installs } : {} }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// What a dependency nothing of a version imports calls for (compare.js
|
|
39
|
+
// `unused-deps`), by what risk tells of its package (`told`, its row as
|
|
40
|
+
// assess.js addedDependencies tells it, or none): one kept from the version
|
|
41
|
+
// compared with (`kept`) yellow, as that version, risk's pick of the most
|
|
42
|
+
// likely free of an attack, held it; else red where its package is new
|
|
43
|
+
// since that version (`created`), as plain-crypto-js was to axios 1.14.1,
|
|
44
|
+
// or the version its spec installs (`installs`) is, has install scripts and
|
|
45
|
+
// isn't its package's latest, as an old package's old major line given a
|
|
46
|
+
// release to run them would, where a package still releasing its latest
|
|
47
|
+
// wouldn't; orange where that version has install scripts, as installing an unused
|
|
48
|
+
// dependency runs them and nothing else, or npm has none of its package
|
|
49
|
+
// for anyone (`missing`), as once unpublished or taken down, whatever
|
|
50
|
+
// downloads it still counts; yellow among npm's top 20,000, as
|
|
51
|
+
// a dependency named for another's peer or by a tool's config is (react,
|
|
52
|
+
// typescript, eslint), older and more downloaded than the package naming
|
|
53
|
+
// it; orange past them, or where its package isn't told.
|
|
54
|
+
export function npmUnusedDependencySeverity(told, kept) {
|
|
55
|
+
if (kept) return 'moderate'
|
|
56
|
+
const { scripts, new: anew, version, latest } = told?.installs ?? {}
|
|
57
|
+
if (told?.created || (scripts && anew && version !== latest)) return 'critical'
|
|
58
|
+
if (scripts || told?.missing) return 'high'
|
|
59
|
+
return told?.level && !npmTierLow(told.tier ?? Infinity) ? 'moderate' : 'high'
|
|
60
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The version an open version is compared with where the caller leaves it to
|
|
2
|
+
// risk (assess.js `oldVersion: true`): of the package's others, the one most
|
|
3
|
+
// likely free of what an attack injects, as a pair's findings flag only what
|
|
4
|
+
// is new against it, so an attack's own earlier version, or one published
|
|
5
|
+
// with it, would hide what it injected. Told by what npm's document says of
|
|
6
|
+
// each version alone (registry.js npmPackageFacts): when it was published,
|
|
7
|
+
// its publisher and whether that account is known (manifest.js), its
|
|
8
|
+
// deprecation, and those since removed from npm, whose times it keeps; no
|
|
9
|
+
// tarball is read for it.
|
|
10
|
+
import { DAY_MS } from './downloads.js'
|
|
11
|
+
import { npmAccountStanding } from './release.js'
|
|
12
|
+
|
|
13
|
+
// How long a version is on npm before it is taken as looked at: a month, as
|
|
14
|
+
// an attack's versions are mostly found and pulled within days.
|
|
15
|
+
const SETTLED_MS = 30 * DAY_MS
|
|
16
|
+
// How near the open version's publish, either side, a version comes with it:
|
|
17
|
+
// a week, as an attack publishes its versions within days of each other.
|
|
18
|
+
const CLOSE_MS = 7 * DAY_MS
|
|
19
|
+
// How near a version since removed from npm, either side, a version comes
|
|
20
|
+
// with it: a day, as an attack's versions come within hours of each other,
|
|
21
|
+
// and npm removes those it finds.
|
|
22
|
+
const REMOVED_MS = DAY_MS
|
|
23
|
+
|
|
24
|
+
// A version as npm publishes one, `{ core, pre }`: its MAJOR.MINOR.PATCH, each
|
|
25
|
+
// to 16 digits and a safe integer as semver takes them, and its prerelease's
|
|
26
|
+
// identifiers, build metadata aside; null for any other.
|
|
27
|
+
const VERSION = /^(0|[1-9]\d{0,15})\.(0|[1-9]\d{0,15})\.(0|[1-9]\d{0,15})(?:-([\da-z-]+(?:\.[\da-z-]+)*))?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/iu
|
|
28
|
+
function parsed(version) {
|
|
29
|
+
const match = VERSION.exec(version)
|
|
30
|
+
const core = match?.slice(1, 4).map(Number)
|
|
31
|
+
return core?.every(Number.isSafeInteger) ? { core, pre: match[4]?.split('.') ?? [] } : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Which of two versions is higher, by semver's precedence: their cores, a
|
|
35
|
+
// release above its prereleases, and prereleases by their identifiers, a
|
|
36
|
+
// number below a word.
|
|
37
|
+
function compared(a, b) {
|
|
38
|
+
const core = a.core.findIndex((part, at) => part !== b.core[at])
|
|
39
|
+
if (core !== -1) return a.core[core] - b.core[core]
|
|
40
|
+
if (a.pre.length === 0 || b.pre.length === 0) return b.pre.length - a.pre.length
|
|
41
|
+
for (let at = 0; at < Math.max(a.pre.length, b.pre.length); at++) {
|
|
42
|
+
const x = a.pre[at], y = b.pre[at]
|
|
43
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1
|
|
44
|
+
if (x === y) continue
|
|
45
|
+
const xNumber = /^\d+$/u.test(x), yNumber = /^\d+$/u.test(y)
|
|
46
|
+
if (xNumber !== yNumber) return xNumber ? -1 : 1
|
|
47
|
+
if (xNumber && x.length !== y.length) return x.length - y.length
|
|
48
|
+
return x < y ? -1 : 1
|
|
49
|
+
}
|
|
50
|
+
return 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Of a package's versions (`releases`, `{ version, published, publisher,
|
|
54
|
+
// known, deprecated, removed }` each, `removed` one since removed from npm,
|
|
55
|
+
// its time alone kept), the one `version` is compared with at `now`, or null
|
|
56
|
+
// where none is below it. Each account by its standing on the package when
|
|
57
|
+
// the open version was published (release.js npmAccountStanding), the most
|
|
58
|
+
// first: known, one that publishes one of npm's most downloaded packages;
|
|
59
|
+
// one of its maintainers, on it three months or more, or first to publish
|
|
60
|
+
// it; and new to it. Of those below it, a release where it is one, the
|
|
61
|
+
// highest that is preferred, each preference kept only where some version
|
|
62
|
+
// holds it, in turn:
|
|
63
|
+
//
|
|
64
|
+
// - Where an account new to it published it, one another published, as an
|
|
65
|
+
// attack may have published more than one.
|
|
66
|
+
// - One published before it, not after, as one published since (a
|
|
67
|
+
// backport) is no version it came from.
|
|
68
|
+
// - Where the highest of those an account of a higher standing than its own
|
|
69
|
+
// published, as when a package is taken from its maintainers, one an
|
|
70
|
+
// account of that standing or higher published.
|
|
71
|
+
// - One published a day or more apart from any version since removed from
|
|
72
|
+
// npm, as not one of the attack npm removed those of.
|
|
73
|
+
// - One published a week or more apart from it, as not one of an attack's,
|
|
74
|
+
// and on npm a month or more, as looked at; but for one a known account
|
|
75
|
+
// other than its own published, trusted however new, and one a maintainer
|
|
76
|
+
// other than its own published, held to the week alone, as an account taken
|
|
77
|
+
// leaves another's versions alone, and a maintainer few know of is taken
|
|
78
|
+
// with its peers as often. Its own account's are held to both, as it may be
|
|
79
|
+
// the one taken.
|
|
80
|
+
// - One npm doesn't say is deprecated, as one a maintainer took back.
|
|
81
|
+
//
|
|
82
|
+
// The highest is in its branch (`1.x.y`, `0.2.y`, `0.0.y`) where one is
|
|
83
|
+
// there, else in the closest branch below.
|
|
84
|
+
const RANKS = { new: 0, maintainer: 1, known: 2 }
|
|
85
|
+
export function npmOldVersion(version, releases, now) {
|
|
86
|
+
const open = releases.find(row => row.version === version && !row.removed)
|
|
87
|
+
const at = open && parsed(version)
|
|
88
|
+
if (!at) return null
|
|
89
|
+
const prefer = (rows, wanted) => { const kept = rows.filter(wanted); return kept.length > 0 ? kept : rows }
|
|
90
|
+
const time = row => row.published === null ? Number.NaN : Date.parse(row.published)
|
|
91
|
+
const opened = time(open)
|
|
92
|
+
const versions = releases.flatMap(row => { const of = parsed(row.version); return of ? [{ ...row, of }] : [] })
|
|
93
|
+
const removed = versions.filter(row => row.removed).map(time).filter(Number.isFinite)
|
|
94
|
+
const standing = npmAccountStanding(releases, open.published)
|
|
95
|
+
const rank = row => row?.publisher ? RANKS[standing(row.publisher, row.known)] : RANKS.new
|
|
96
|
+
const own = rank(open)
|
|
97
|
+
let rows = prefer(versions.filter(row => !row.removed && compared(row.of, at) < 0).toSorted((a, b) => compared(b.of, a.of)),
|
|
98
|
+
row => row.of.pre.length === 0 || at.pre.length > 0)
|
|
99
|
+
if (open.publisher && own === RANKS.new) rows = prefer(rows, row => row.publisher !== open.publisher)
|
|
100
|
+
rows = prefer(rows, row => time(row) < opened)
|
|
101
|
+
const top = rank(rows[0])
|
|
102
|
+
if (open.publisher && top > own) rows = rows.filter(row => rank(row) >= top)
|
|
103
|
+
rows = prefer(rows, row => removed.every(gone => Math.abs(time(row) - gone) >= REMOVED_MS))
|
|
104
|
+
const vouched = row => row.publisher === open.publisher ? RANKS.new : rank(row)
|
|
105
|
+
rows = prefer(prefer(rows, row => vouched(row) === RANKS.known || Math.abs(time(row) - opened) >= CLOSE_MS),
|
|
106
|
+
row => vouched(row) >= RANKS.maintainer || now - time(row) >= SETTLED_MS)
|
|
107
|
+
return prefer(rows, row => !row.deprecated)[0]?.version ?? null
|
|
108
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// What risk asks npm for, each kept in the caller's cache where it gives one
|
|
2
|
+
// (`cache`, as @preventive/upstream's CacheStore: `read(type, key)` and
|
|
3
|
+
// `write(type, key, value)`, each type its caller's to keep where it will):
|
|
4
|
+
//
|
|
5
|
+
// - `npm/tarballs`, `<name>@<version>`: a version's tarball, its bytes, as
|
|
6
|
+
// upstream's getTarball keeps one, checked against its integrity.
|
|
7
|
+
// - `risk/npm/versions`, `<name>@<version>`: what a version's document says,
|
|
8
|
+
// which npm never changes once published: its manifest (manifest.js, its
|
|
9
|
+
// deprecation aside), when it was published, and its dist.
|
|
10
|
+
// - `risk/npm/packages`, `<name>`: the versions npm says are deprecated, those
|
|
11
|
+
// it removed (each one's when), and whether it took the package down, as
|
|
12
|
+
// `at` a time, asked again after an hour, as npm deprecates and removes a
|
|
13
|
+
// version any time.
|
|
14
|
+
// - `risk/npm/old-versions`, `<name>@<version>`: the version it is compared
|
|
15
|
+
// with where risk picks it (old-version.js npmOldVersion), or none, as `at`
|
|
16
|
+
// a time, picked again after an hour, as npm publishes and deprecates
|
|
17
|
+
// versions any time and those it has grow older.
|
|
18
|
+
// - `risk/npm/public`, `<name>`: when a package anyone can read was first
|
|
19
|
+
// published, or `{ missing: true }`, as `at` a time, asked again after an
|
|
20
|
+
// hour.
|
|
21
|
+
// - `risk/npm/downloads`, `<name>@<from>:<to>`: its downloads a day over those
|
|
22
|
+
// days, `{ start, end, days }`, or `{ none: true }` where npm counts none.
|
|
23
|
+
//
|
|
24
|
+
// The package assessed is asked for as upstream asks, with the npm token the
|
|
25
|
+
// environment has for a scoped one (PREVENTIVE_MEASURES_NPM_TOKEN,
|
|
26
|
+
// STASIS_NPM_TOKEN or NPM_TOKEN), so a private one can be read; its
|
|
27
|
+
// dependencies' public facts and downloads with none. A caller may ask for
|
|
28
|
+
// packages' documents (`packument`) and versions' tarballs (`tarball`)
|
|
29
|
+
// itself, as the managed server does, within the bounds it holds every read
|
|
30
|
+
// of npm to; else a tarball is upstream's getTarball's, bounded as upstream
|
|
31
|
+
// bounds it. What risk asks npm itself is given up on after 30 seconds, and
|
|
32
|
+
// read no further than its bounds, `upstream-too-large` past them, as is a
|
|
33
|
+
// tarball unpacking past tarball.js's, `package-too-large`.
|
|
34
|
+
import { advisories } from '@preventive/upstream/advisories.js'
|
|
35
|
+
import { getTarball } from '@preventive/upstream/npm.js'
|
|
36
|
+
import { compareVersions, satisfies, validRange } from '@preventive/upstream/semver.js'
|
|
37
|
+
import { npmVersionManifest } from './manifest.js'
|
|
38
|
+
import { npmAlias } from './specs.js'
|
|
39
|
+
import { npmPlaceholder } from './malware.js'
|
|
40
|
+
import { npmOldVersion } from './old-version.js'
|
|
41
|
+
import { npmTarballVersion } from './tarball.js'
|
|
42
|
+
import { DAY_MS } from './downloads.js'
|
|
43
|
+
|
|
44
|
+
const REGISTRY = 'https://registry.npmjs.org'
|
|
45
|
+
const DOWNLOADS = 'https://api.npmjs.org/downloads/range'
|
|
46
|
+
const PACKAGE_MS = 60 * 60_000
|
|
47
|
+
// How long a version's advisories are kept, as the managed server keeps a
|
|
48
|
+
// package's and a lockfile's: an hour, as a new one may come at any time.
|
|
49
|
+
const ADVISORIES_MS = 60 * 60_000
|
|
50
|
+
// How much of npm's answers is read: a whole document, as the managed server
|
|
51
|
+
// reads one (server-managed/npm-packages.ts PACKUMENT_BYTES), and a few
|
|
52
|
+
// months' downloads a day.
|
|
53
|
+
const PACKUMENT_BYTES = 64 * 1024 * 1024
|
|
54
|
+
const DOWNLOADS_BYTES = 1024 * 1024
|
|
55
|
+
// How long a package's downloads are kept: a week, as the tier they put it in,
|
|
56
|
+
// by the most of its latest six weeks' (downloads.js npmDownloadTier), hardly
|
|
57
|
+
// moves in one, and npm's downloads API answers only some 40 lookups a minute.
|
|
58
|
+
const DOWNLOADS_MS = 7 * DAY_MS
|
|
59
|
+
// How many packages npm's downloads API answers in one lookup, unscoped ones
|
|
60
|
+
// alone (it refuses a scoped one in a bulk lookup), and how much of it is read.
|
|
61
|
+
const BULK_DOWNLOADS = 128
|
|
62
|
+
const BULK_DOWNLOADS_BYTES = 8 * 1024 * 1024
|
|
63
|
+
// How long npm is waited on for an answer, as the managed server waits on its
|
|
64
|
+
// upstreams (server-managed/npm-packages.ts askUpstream).
|
|
65
|
+
const REQUEST_MS = 30_000
|
|
66
|
+
|
|
67
|
+
export class NpmRiskError extends Error {
|
|
68
|
+
constructor(code, message = code) { super(message); this.code = code }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isTime = value => typeof value === 'string' && value.length <= 40 && !Number.isNaN(Date.parse(value))
|
|
72
|
+
// An object as JSON holds one, not an array (manifest.js plainObject).
|
|
73
|
+
const plainObject = value => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
74
|
+
|
|
75
|
+
// A scoped name in a downloads API path, its slash escaped.
|
|
76
|
+
const encoded = name => name.replace('/', '%2f')
|
|
77
|
+
const token = () => typeof process === 'undefined' ? null
|
|
78
|
+
: ['PREVENTIVE_MEASURES_NPM_TOKEN', 'STASIS_NPM_TOKEN', 'NPM_TOKEN'].map(key => process.env[key]).find(Boolean) ?? null
|
|
79
|
+
|
|
80
|
+
// A value kept under `type` and `key`, else `ask`ed and kept; one `fresh`
|
|
81
|
+
// says is stale asked again.
|
|
82
|
+
async function kept({ cache }, type, key, ask, fresh = () => true) {
|
|
83
|
+
const hit = cache ? await cache.read(type, key) : null
|
|
84
|
+
if (hit != null && fresh(hit)) return hit
|
|
85
|
+
const value = await ask()
|
|
86
|
+
if (cache && value != null) await cache.write(type, key, value)
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// What is kept under each `[type, key]` of `entries`, in their order, null
|
|
91
|
+
// where nothing is: in one read where the cache reads records of any types at
|
|
92
|
+
// once (`readEach`), else one a key.
|
|
93
|
+
const readEach = (cache, entries) => cache.readEach ? cache.readEach(entries) : Promise.all(entries.map(([type, key]) => cache.read(type, key)))
|
|
94
|
+
|
|
95
|
+
// An answer's body as JSON, read no further than `limit` bytes.
|
|
96
|
+
async function jsonOf(res, limit, what) {
|
|
97
|
+
const tooLarge = () => new NpmRiskError('upstream-too-large', `npm's answer for ${what} is over ${limit} bytes`)
|
|
98
|
+
if (Number(res.headers.get('content-length')) > limit) { await res.body?.cancel(); throw tooLarge() }
|
|
99
|
+
const chunks = []
|
|
100
|
+
let size = 0
|
|
101
|
+
for await (const chunk of res.body ?? []) {
|
|
102
|
+
size += chunk.byteLength
|
|
103
|
+
if (size > limit) throw tooLarge()
|
|
104
|
+
chunks.push(chunk)
|
|
105
|
+
}
|
|
106
|
+
try { return JSON.parse(await new Blob(chunks).text()) } catch { throw new NpmRiskError('upstream-invalid', `npm's answer for ${what} isn't JSON`) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// What npm answers `url` as JSON (jsonOf), null where it answers 404; given up
|
|
110
|
+
// on after REQUEST_MS, or once the caller's `signal` aborts, its abort
|
|
111
|
+
// rethrown as it is. `upstream-busy` where npm asks to be asked later (429),
|
|
112
|
+
// as its downloads API does past a burst of requests;
|
|
113
|
+
// `upstream-unavailable` where it can't be asked or answers otherwise.
|
|
114
|
+
async function askNpm(url, headers, limit, what, { signal }) {
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(url, { headers: { accept: 'application/json', ...headers }, redirect: 'error', signal: AbortSignal.any([AbortSignal.timeout(REQUEST_MS), ...signal ? [signal] : []]) })
|
|
117
|
+
if (res.status === 404) { await res.body?.cancel(); return null }
|
|
118
|
+
if (!res.ok) { await res.body?.cancel(); throw new NpmRiskError(res.status === 429 ? 'upstream-busy' : 'upstream-unavailable', `npm answered ${res.status} for ${what}`) }
|
|
119
|
+
return await jsonOf(res, limit, what)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof NpmRiskError || signal?.aborted) throw error
|
|
122
|
+
throw new NpmRiskError('upstream-unavailable', `npm couldn't be asked for ${what}: ${error.message}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A package's whole document, null where npm has none for this asker: the
|
|
127
|
+
// caller's (`packument`) where it gives one, else risk's own.
|
|
128
|
+
function packageDocument(name, context, authorized) {
|
|
129
|
+
if (context.packument) return context.packument(name, { authorized, signal: context.signal })
|
|
130
|
+
const secret = authorized && name.startsWith('@') ? token() : null
|
|
131
|
+
return askNpm(`${REGISTRY}/${name}`, secret ? { authorization: `Bearer ${secret}` } : {}, PACKUMENT_BYTES, name, context)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// When a document says its package's first version was published, one since
|
|
135
|
+
// unpublished among them: the earliest time it lists.
|
|
136
|
+
const firstPublished = doc => Object.entries(doc?.time ?? {}).filter(([key, at]) => key !== 'modified' && isTime(at)).map(([, at]) => at)
|
|
137
|
+
.reduce((first, at) => first === null || Date.parse(at) < Date.parse(first) ? at : first, null)
|
|
138
|
+
|
|
139
|
+
// Each version a package's document lists, as old-version.js npmOldVersion
|
|
140
|
+
// picks among them: `{ version, published, publisher, known, deprecated,
|
|
141
|
+
// removed }`, its publisher's account as manifest.js reads it, and whether
|
|
142
|
+
// that is known; and each it lists a time of and no document, as one since
|
|
143
|
+
// removed from npm, `removed`, its time alone, as npmPackageFacts reads none
|
|
144
|
+
// of it. Its times' other keys (`created`, `modified`) are no versions, as
|
|
145
|
+
// none of a version starts with a letter.
|
|
146
|
+
const versionsOf = doc => [...new Set([...Object.keys(doc.versions ?? {}), ...Object.keys(doc.time ?? {}).filter(key => /^\d/u.test(key))])]
|
|
147
|
+
const releasesOf = (doc, deprecated = {}) => versionsOf(doc).map(version => {
|
|
148
|
+
const json = doc.versions?.[version]
|
|
149
|
+
const { publisher = null, knownAccounts } = json ? npmVersionManifest(json) : {}
|
|
150
|
+
return { version, published: isTime(doc.time?.[version]) ? doc.time[version] : null, publisher, known: Boolean(knownAccounts),
|
|
151
|
+
deprecated: Object.hasOwn(deprecated, version), removed: !json }
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// What risk reads of `version` of a package, of the one it is compared with
|
|
155
|
+
// (`oldVersion`: one, null for none, or true for the one old-version.js
|
|
156
|
+
// npmOldVersion picks of its versions, by its document), and of the package:
|
|
157
|
+
// `versions`, each one's `{ manifest, published, dist }`, those of them npm
|
|
158
|
+
// removed since they were published (`removed`, each one's when), none
|
|
159
|
+
// picked to compare a removed one with, `deprecated`, by version, whether
|
|
160
|
+
// npm's security team took the package down (`takenDown`, its latest version
|
|
161
|
+
// npm's placeholder, malware.js npmPlaceholder), `removals`, how many
|
|
162
|
+
// versions of it npm removed, `oldVersion`, the one compared with or null,
|
|
163
|
+
// and where its document was read and either the pick
|
|
164
|
+
// or another account publishing each wants them, `releases`, each version it
|
|
165
|
+
// lists (releasesOf), so that no more of the document is held. Its document
|
|
166
|
+
// is read once at most, and not at all where all of it is kept and fresh. A
|
|
167
|
+
// version's facts are kept for good, as npm never changes a version, but npm
|
|
168
|
+
// may remove it: the package's, kept an hour, has which it removed, and they
|
|
169
|
+
// go by that.
|
|
170
|
+
export async function npmPackageFacts(name, version, oldVersion, context) {
|
|
171
|
+
const { cache, now } = context
|
|
172
|
+
const fresh = hit => hit != null && now - hit.at < PACKAGE_MS
|
|
173
|
+
const picked = cache && oldVersion === true ? await cache.read('risk/npm/old-versions', `${name}@${version}`) : null
|
|
174
|
+
const given = oldVersion === true ? fresh(picked) ? picked.oldVersion : undefined : oldVersion
|
|
175
|
+
if (cache && given !== undefined) {
|
|
176
|
+
const wanted = given === null ? [version] : [version, given]
|
|
177
|
+
const [pkg, ...facts] = await readEach(cache, [['risk/npm/packages', name], ...wanted.map(at => ['risk/npm/versions', `${name}@${at}`])])
|
|
178
|
+
const gone = at => Object.hasOwn(pkg.removed, at)
|
|
179
|
+
if (fresh(pkg) && 'removed' in pkg && wanted.every((at, i) => facts[i] || gone(at))) {
|
|
180
|
+
return { versions: new Map(wanted.flatMap((at, i) => gone(at) ? [] : [[at, facts[i]]])), removed: new Map(wanted.filter(gone).map(at => [at, pkg.removed[at]])),
|
|
181
|
+
removals: Object.keys(pkg.removed).length, deprecated: pkg.deprecated, takenDown: pkg.takenDown, oldVersion: given }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const doc = await packageDocument(name, context, true)
|
|
185
|
+
if (!doc) throw new NpmRiskError('package-not-found', `npm has no package ${name}`)
|
|
186
|
+
const removed = new Map()
|
|
187
|
+
const factsOf = at => {
|
|
188
|
+
const json = doc.versions?.[at]
|
|
189
|
+
if (!json && isTime(doc.time?.[at])) { removed.set(at, doc.time[at]); return null }
|
|
190
|
+
if (!json) throw new NpmRiskError('version-not-found', `npm has no ${name}@${at}`)
|
|
191
|
+
const { deprecated: _, ...manifest } = npmVersionManifest(json)
|
|
192
|
+
return { manifest, published: isTime(doc.time?.[at]) ? doc.time[at] : null, dist: { tarball: json.dist?.tarball, integrity: json.dist?.integrity } }
|
|
193
|
+
}
|
|
194
|
+
const opened = factsOf(version)
|
|
195
|
+
const deprecated = Object.fromEntries(Object.entries(doc.versions ?? {}).filter(([, json]) => typeof json?.deprecated === 'string' && json.deprecated !== '').map(([at, json]) => [at, json.deprecated]))
|
|
196
|
+
let releases = oldVersion === true && opened ? releasesOf(doc, deprecated) : null
|
|
197
|
+
const old = oldVersion === true ? opened && npmOldVersion(version, releases, now) : oldVersion
|
|
198
|
+
const read = new Map([...opened ? [[version, opened]] : [], ...old === null ? [] : [[old, factsOf(old)]]].filter(([, fact]) => fact))
|
|
199
|
+
const takenDown = npmPlaceholder(doc.versions?.[doc['dist-tags']?.latest])
|
|
200
|
+
const publishers = [version, old].map(at => read.get(at)?.manifest.publisher)
|
|
201
|
+
if (publishers.every(Boolean) && publishers[0] !== publishers[1]) releases ??= releasesOf(doc, deprecated)
|
|
202
|
+
const gone = Object.fromEntries(Object.entries(doc.time ?? {}).filter(([at, when]) => at !== 'created' && at !== 'modified' && isTime(when) && !doc.versions?.[at]))
|
|
203
|
+
if (cache) {
|
|
204
|
+
await Promise.all([...read].map(([at, fact]) => cache.write('risk/npm/versions', `${name}@${at}`, fact)))
|
|
205
|
+
await cache.write('risk/npm/packages', name, { at: now, deprecated, takenDown, removed: gone })
|
|
206
|
+
if (oldVersion === true) await cache.write('risk/npm/old-versions', `${name}@${version}`, { at: now, oldVersion: old })
|
|
207
|
+
}
|
|
208
|
+
return { versions: read, removed, removals: Object.keys(gone).length, deprecated, takenDown, oldVersion: old, ...releases ? { releases } : {} }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// npm's advisories of `version` of a package, as `npm audit` asks them, and
|
|
212
|
+
// where `osv`, OSV's records npm's registry hasn't, as the malicious
|
|
213
|
+
// packages' of nx's versions are (MAL-2025-41443, of GHSA-cxm3-wv7p-598c,
|
|
214
|
+
// which npm's registry hasn't): upstream's `advisories` and its `osv`, an
|
|
215
|
+
// advisory of npm's an OSV record names taking the record's ids among its
|
|
216
|
+
// `aliases`. Each `{ id, url?, title?, cwe, aliases }`, kept ADVISORIES_MS,
|
|
217
|
+
// and given up on once `signal` aborts.
|
|
218
|
+
export async function npmVersionAdvisories(name, version, osv, context) {
|
|
219
|
+
const what = `${name}@${version}`
|
|
220
|
+
const { rows } = await kept(context, 'risk/npm/advisories', what, async () => {
|
|
221
|
+
context.signal?.throwIfAborted()
|
|
222
|
+
const asked = await untilAborted(advisories([{ ecosystem: 'npm', name, versions: [version] }], { cache: false, osv }), context.signal).catch(error => {
|
|
223
|
+
throw context.signal?.aborted ? error : new NpmRiskError(error?.status === 429 ? 'upstream-busy' : 'upstream-unavailable', `advisories of ${what} couldn't be had: ${error.message}`)
|
|
224
|
+
})
|
|
225
|
+
return { at: context.now, osv, rows: asked.map(({ id, url, title, cwe, aliases }) => ({ id, ...url ? { url } : {}, ...title ? { title } : {}, cwe, aliases })) }
|
|
226
|
+
}, hit => context.now - hit.at < ADVISORIES_MS && hit.osv === osv)
|
|
227
|
+
return rows
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Each version a package's document lists (releasesOf), the document read
|
|
231
|
+
// anew, as npmPackageFacts reads it.
|
|
232
|
+
export async function npmPackageReleases(name, context) {
|
|
233
|
+
const doc = await packageDocument(name, context, true)
|
|
234
|
+
if (!doc) throw new NpmRiskError('package-not-found', `npm has no package ${name}`)
|
|
235
|
+
return releasesOf(doc)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// `promise`, given up on once `signal` aborts, its reason thrown: what it
|
|
239
|
+
// waits on, which takes no signal (upstream's getTarball), left to settle
|
|
240
|
+
// unheeded.
|
|
241
|
+
function untilAborted(promise, signal) {
|
|
242
|
+
if (!signal) return promise
|
|
243
|
+
if (signal.aborted) return Promise.reject(signal.reason)
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
const onAbort = () => reject(signal.reason)
|
|
246
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
247
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort))
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// A version's files, as compare.js takes a version, from its tarball
|
|
252
|
+
// (tarball.js): the caller's (`tarball`) where it gives one, its failures
|
|
253
|
+
// thrown as they are, else upstream's, given up on once `signal` aborts; with
|
|
254
|
+
// `npm` its `{ name, version, manifest, published }`.
|
|
255
|
+
export async function npmVersionFiles(name, version, { manifest, published, dist }, { cache, tarball: read, signal }) {
|
|
256
|
+
const what = `${name}@${version}`
|
|
257
|
+
signal?.throwIfAborted()
|
|
258
|
+
const tarball = read ? await read(name, version, { signal }) : await untilAborted(getTarball(name, version, dist, cache === undefined ? {} : { cache }), signal).catch(error => {
|
|
259
|
+
throw signal?.aborted ? error : new NpmRiskError('upstream-unavailable', `npm's tarball of ${what} couldn't be had: ${error.message}`)
|
|
260
|
+
})
|
|
261
|
+
try { return { npm: { name, version, manifest, published }, ...await npmTarballVersion(tarball) } } catch (error) {
|
|
262
|
+
throw new NpmRiskError(error.message === 'package-too-large' ? 'package-too-large' : 'upstream-invalid', `npm's tarball of ${what}: ${error.message}`)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The version of a package's document npm installs for a dependency's spec
|
|
267
|
+
// (`spec`, an alias's by its own range, `npm:name@^1`, as specs.js npmAlias
|
|
268
|
+
// reads one), as npm picks one:
|
|
269
|
+
// the tag it names, else its `latest` where that satisfies it, or any range
|
|
270
|
+
// does (`*`, a prerelease latest too), and isn't deprecated, else the highest
|
|
271
|
+
// that satisfies it, one not deprecated first; null for none, or for a spec
|
|
272
|
+
// that is no range nor tag (a URL, a repository).
|
|
273
|
+
function installedVersion(doc, spec) {
|
|
274
|
+
const alias = npmAlias(spec)
|
|
275
|
+
const range = (alias ? alias.spec : String(spec)).trim()
|
|
276
|
+
const tags = plainObject(doc['dist-tags']) ? doc['dist-tags'] : {}, versions = plainObject(doc.versions) ? doc.versions : {}
|
|
277
|
+
if (Object.hasOwn(tags, range) && Object.hasOwn(versions, tags[range])) return tags[range]
|
|
278
|
+
if (validRange(range || '*') === null) return null
|
|
279
|
+
if (Object.hasOwn(versions, tags.latest) && typeof versions[tags.latest]?.deprecated !== 'string' && ((range || '*') === '*' || satisfies(tags.latest, range))) return tags.latest
|
|
280
|
+
const matching = Object.keys(versions).filter(version => satisfies(version, range || '*'))
|
|
281
|
+
const current = matching.filter(version => typeof versions[version]?.deprecated !== 'string')
|
|
282
|
+
return (current.length > 0 ? current : matching).reduce((best, version) => best === null || compareVersions(version, best) > 0 ? version : best, null)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// When a package anyone can read was first published, `{ created }`, or null
|
|
286
|
+
// for one npm has none of for anyone; and for a dependency's spec (`spec`),
|
|
287
|
+
// the version of it npm installs (installedVersion), `installs`: `{ version,
|
|
288
|
+
// latest, published, scripts }`, its package's `latest` beside it, when it
|
|
289
|
+
// was published and whether it has install scripts: its manifest's, or
|
|
290
|
+
// node-gyp's for a `binding.gyp` (`gypfile`), or as the registry says
|
|
291
|
+
// (`hasInstallScript`); null for none.
|
|
292
|
+
export async function npmPublicFacts(name, context, spec = null) {
|
|
293
|
+
const facts = await kept(context, 'risk/npm/public', spec === null ? name : `${name}@${spec}`, async () => {
|
|
294
|
+
const doc = await packageDocument(name, context, false)
|
|
295
|
+
if (!doc) return { at: context.now, missing: true }
|
|
296
|
+
const version = spec === null ? null : installedVersion(doc, spec)
|
|
297
|
+
return { at: context.now, created: firstPublished(doc), ...spec === null ? {} : { installs: version === null ? null
|
|
298
|
+
: { version, latest: typeof doc['dist-tags']?.latest === 'string' ? doc['dist-tags'].latest : null, published: isTime(doc.time?.[version]) ? doc.time[version] : null,
|
|
299
|
+
scripts: Boolean(npmVersionManifest(doc.versions[version]).installScripts) || doc.versions[version]?.gypfile === true || doc.versions[version]?.hasInstallScript === true } } }
|
|
300
|
+
}, hit => context.now - hit.at < PACKAGE_MS)
|
|
301
|
+
return facts.missing ? null : { created: facts.created, ...spec === null ? {} : { installs: facts.installs } }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The days a package's downloads are asked over at `now`: from the first of
|
|
305
|
+
// the month before last to yesterday, as the npm viewer asks them.
|
|
306
|
+
const day = time => new Date(time).toISOString().slice(0, 10)
|
|
307
|
+
function rangeAt(now) {
|
|
308
|
+
const today = new Date(now)
|
|
309
|
+
return `${day(Date.UTC(today.getUTCFullYear(), today.getUTCMonth() - 2, 1))}:${day(now - DAY_MS)}`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A package's downloads as kept, from npm's answer of them over a range of
|
|
313
|
+
// days `asked` then: `{ at, start, end, days }`, a count a day from
|
|
314
|
+
// `start`, or `{ at, none }` where npm counts none (null).
|
|
315
|
+
function downloadsOf(json, asked) {
|
|
316
|
+
if (json === null) return { at: asked, none: true }
|
|
317
|
+
const start = Date.parse(json.start)
|
|
318
|
+
const length = Math.round((Date.parse(json.end) - start) / DAY_MS) + 1
|
|
319
|
+
const days = Array.from({ length: Number.isSafeInteger(length) && length > 0 && length <= 400 ? length : 0 }, () => 0)
|
|
320
|
+
for (const row of json.downloads ?? []) {
|
|
321
|
+
const at = Math.round((Date.parse(row.day) - start) / DAY_MS)
|
|
322
|
+
if (at >= 0 && at < days.length && Number.isSafeInteger(row.downloads)) days[at] = row.downloads
|
|
323
|
+
}
|
|
324
|
+
return { at: asked, start: json.start, end: json.end, days }
|
|
325
|
+
}
|
|
326
|
+
// Whether downloads kept are told as they are: kept within DOWNLOADS_MS.
|
|
327
|
+
const freshDownloads = (held, now) => held != null && now - held.at < DOWNLOADS_MS
|
|
328
|
+
|
|
329
|
+
// A package's downloads a day over rangeAt's days when asked, `{ start, end,
|
|
330
|
+
// days }`, kept a week (DOWNLOADS_MS), null where npm counts none.
|
|
331
|
+
export async function npmDownloads(name, context) {
|
|
332
|
+
const answer = await kept(context, 'risk/npm/downloads', name,
|
|
333
|
+
async () => downloadsOf(await askNpm(`${DOWNLOADS}/${rangeAt(context.now)}/${encoded(name)}`, {}, DOWNLOADS_BYTES, `${name}'s downloads`, context), context.now),
|
|
334
|
+
hit => freshDownloads(hit, context.now))
|
|
335
|
+
return answer.none ? null : answer
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Readies, at once, the downloads assessNpm asks of each of these packages
|
|
339
|
+
// (`names`) that can be, kept in `cache` as npmDownloads keeps them, so that
|
|
340
|
+
// their assessments ask npm none: those kept within a week (DOWNLOADS_MS), as
|
|
341
|
+
// they are, and unscoped ones, asked of npm BULK_DOWNLOADS at a time, as its downloads API
|
|
342
|
+
// answers them; `ready` is told each once its downloads are kept. The rest,
|
|
343
|
+
// scoped ones (it answers those only one by one) and any a bulk lookup
|
|
344
|
+
// didn't answer, are left for npmReadyDownload, one at a time. npm asking to
|
|
345
|
+
// be asked later is thrown (`upstream-busy`), as is any other failure, those
|
|
346
|
+
// readied by then told.
|
|
347
|
+
export async function npmReadyDownloads(names, ready, { cache, signal, now = Date.now() }) {
|
|
348
|
+
const context = { cache, signal, now }
|
|
349
|
+
const range = rangeAt(now)
|
|
350
|
+
const unique = [...new Set(names)]
|
|
351
|
+
const unscoped = []
|
|
352
|
+
const ask = async part => {
|
|
353
|
+
const json = await askNpm(`${DOWNLOADS}/${range}/${part.join(',')}`, {}, BULK_DOWNLOADS_BYTES, `${part.length} packages' downloads`, context)
|
|
354
|
+
const answered = part.filter(name => json && Object.hasOwn(json, name))
|
|
355
|
+
await keepMany(cache, answered.map(name => [name, downloadsOf(json[name], now)]))
|
|
356
|
+
for (const name of answered) ready(name)
|
|
357
|
+
}
|
|
358
|
+
// Those kept read BULK_DOWNLOADS at a time, the rest asked as they fill a
|
|
359
|
+
// lookup.
|
|
360
|
+
for (let at = 0; at < unique.length; at += BULK_DOWNLOADS) {
|
|
361
|
+
const part = unique.slice(at, at + BULK_DOWNLOADS)
|
|
362
|
+
const held = await keptMany(cache, part)
|
|
363
|
+
for (const name of part) {
|
|
364
|
+
if (freshDownloads(held.get(name), now)) ready(name)
|
|
365
|
+
else if (!name.startsWith('@')) unscoped.push(name)
|
|
366
|
+
}
|
|
367
|
+
while (unscoped.length >= BULK_DOWNLOADS) await ask(unscoped.splice(0, BULK_DOWNLOADS))
|
|
368
|
+
}
|
|
369
|
+
// A lookup of one is answered as that package's own, not by its name.
|
|
370
|
+
if (unscoped.length > 1) await ask(unscoped)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Downloads kept under each of `keys`, by key, and many kept at once: in one
|
|
374
|
+
// read and one write where the cache reads and writes many at once
|
|
375
|
+
// (`readMany`, `writeMany`), else a key at a time.
|
|
376
|
+
const keptMany = async (cache, keys) => cache.readMany ? await cache.readMany('risk/npm/downloads', keys)
|
|
377
|
+
: new Map(await Promise.all(keys.map(async key => [key, await cache.read('risk/npm/downloads', key)])))
|
|
378
|
+
const keepMany = async (cache, entries) => {
|
|
379
|
+
if (cache.writeMany) await cache.writeMany('risk/npm/downloads', entries)
|
|
380
|
+
else for (const [key, value] of entries) await cache.write('risk/npm/downloads', key, value)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Readies the downloads assessNpm asks of a package, asked of npm on their
|
|
384
|
+
// own where they aren't kept (npmDownloads), as npmReadyDownloads leaves a
|
|
385
|
+
// scoped one's: npm asking to be asked later thrown (`upstream-busy`).
|
|
386
|
+
export async function npmReadyDownload(name, { cache, signal, now = Date.now() }) {
|
|
387
|
+
await npmDownloads(name, { cache, signal, now })
|
|
388
|
+
}
|