@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,445 @@
|
|
|
1
|
+
// What risk reads of each file of a version, once a version (stamps.js
|
|
2
|
+
// npmFileRisk), and every host its scripts name: for compare.js's findings
|
|
3
|
+
// and held.js's, and for the viewer, which reads its files' tags from it.
|
|
4
|
+
import { bundleFileSizes, bundleFilesAsMap } from '../../common/bundle-sources.js'
|
|
5
|
+
import { NPM_PROSE } from '../files/readability.js'
|
|
6
|
+
import { npmIsScript } from '../files/script.js'
|
|
7
|
+
import { npmFileRisk } from '../files/stamps.js'
|
|
8
|
+
|
|
9
|
+
// What a version's files are kept by: its bundle where it is one, else its
|
|
10
|
+
// files' list, else itself.
|
|
11
|
+
const keyOf = version => version.bundle ?? version.json ?? version
|
|
12
|
+
|
|
13
|
+
// A string's escapes as JavaScript reads them (`\x2e`, `\u002e`, `\u{2e}`, a
|
|
14
|
+
// control's letter, and an octal's, `\056`, as code outside strict mode
|
|
15
|
+
// reads it), a line continuation for nothing, any other for itself: `\X2E`
|
|
16
|
+
// and `\U002E` too, their letters in lowercase alone introducing a code.
|
|
17
|
+
const ESCAPE = /\\(?:x([\dA-Fa-f]{2})|u([\dA-Fa-f]{4})|u\{([\dA-Fa-f]{1,6})\}|([0-3][0-7]{0,2}|[4-7][0-7]?)|(\r\n|[\s\S]))/gu
|
|
18
|
+
const CONTROLS = { b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' }
|
|
19
|
+
const unescaped = body => body.replaceAll(ESCAPE, (escape, x, u, braced, octal, other) => {
|
|
20
|
+
if (octal !== undefined) return String.fromCodePoint(Number.parseInt(octal, 8))
|
|
21
|
+
if (other !== undefined) return /^(?:\r\n|[\n\r\u2028\u2029])$/u.test(other) ? '' : CONTROLS[other] ?? other
|
|
22
|
+
const code = Number.parseInt(x ?? u ?? braced, 16)
|
|
23
|
+
return code <= 0x10FFFF ? String.fromCodePoint(code) : escape
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// The characters a filesystem telling neither case apart (folded) takes for
|
|
27
|
+
// one of a package's name, which npm keeps to ASCII: its cases, and those
|
|
28
|
+
// Unicode cases into it (`ı`, `ſ`, the Kelvin sign).
|
|
29
|
+
const FOLDING_INTO = { i: '\u0131', k: '\u212A', s: '\u017F' }
|
|
30
|
+
const foldsInto = char => new Set([char, char.toLowerCase(), char.toUpperCase(), ...FOLDING_INTO[char.toLowerCase()] ?? ''])
|
|
31
|
+
|
|
32
|
+
// Each string a text writes out that may name a file of its package, its
|
|
33
|
+
// value starting `.`, with an escape, or with one of `firsts` (its name's
|
|
34
|
+
// first character, foldsInto, and `#` where its package.json files map
|
|
35
|
+
// specifiers of its own): from any quote no backslash escapes (an even run of them
|
|
36
|
+
// before it, or none) to the next of its kind none escapes, a template's
|
|
37
|
+
// across lines, the others' within their line, a line continuation aside.
|
|
38
|
+
// Tried from every such quote, not only where code's tokens say a string
|
|
39
|
+
// starts, which code can be written to have read wrong (a regular
|
|
40
|
+
// expression, a comment or a template's text taken for another): so one in
|
|
41
|
+
// a comment, or in another string, is read too. Linear on any text, each
|
|
42
|
+
// quote's string read to the next quote of its kind.
|
|
43
|
+
function quoted(firsts) {
|
|
44
|
+
const starts = String.raw`(?=[.\\${[...firsts].map(char => String.raw`\u{${char.codePointAt(0).toString(16)}}`).join('')}])`
|
|
45
|
+
const line = quote => String.raw`${quote}(?=${starts}((?:[^${quote}\\\n]|\\(?:\r\n|[\s\S]))*)${quote})`
|
|
46
|
+
return new RegExp(String.raw`(?<=(?:^|[^\\])(?:\\\\)*)(?:${line('"')}|${line("'")}|\x60(?=${starts}((?:[^\x60\\]|\\[\s\S])*)\x60))`, 'gu')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A string's values, from its quote and what it holds: as JavaScript reads
|
|
50
|
+
// its escapes (unescaped), and a template's as written too, as a tag reads it
|
|
51
|
+
// (`String.raw`); none for a template with something put in it, a `${` no
|
|
52
|
+
// backslash escapes (`\${` its text).
|
|
53
|
+
function valuesOf(quote, body) {
|
|
54
|
+
if (!body.includes('\\')) return quote === '`' && body.includes('${') ? [] : [body]
|
|
55
|
+
if (quote !== '`') return [unescaped(body)]
|
|
56
|
+
return body.replaceAll(/\\[\s\S]/gu, '').includes('${') ? [] : [unescaped(body), body]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Where a string is what `import` takes, rather than what `require` may: a
|
|
60
|
+
// declaration's (`import './x'`, `from './x'`), on the keyword's line with
|
|
61
|
+
// blanks alone between, as a line break could begin another statement
|
|
62
|
+
// there (`from\n'./x'.split(',').forEach(require)`), and not a template,
|
|
63
|
+
// which `from` would be called with as a tag; or the whole first argument of
|
|
64
|
+
// `import(…)`, on its line too.
|
|
65
|
+
const DECLARED = /(?<=(?<![\p{ID_Continue}$.#])(?:import|from)[ \t]*)/uy
|
|
66
|
+
const CALLED = /(?<=(?<![\p{ID_Continue}$.#])import[ \t]*\([ \t]*)/uy
|
|
67
|
+
const ARGUMENT_END = /[ \t]*[,)]/uy
|
|
68
|
+
const matchesAt = (regex, text, index) => {
|
|
69
|
+
regex.lastIndex = index
|
|
70
|
+
return regex.test(text)
|
|
71
|
+
}
|
|
72
|
+
const imported = (text, quote, start, end) => (quote !== '`' && matchesAt(DECLARED, text, start)) || (matchesAt(CALLED, text, start) && matchesAt(ARGUMENT_END, text, end))
|
|
73
|
+
|
|
74
|
+
// A relative path: from `./` or `../`, or `.` or `..` alone.
|
|
75
|
+
const RELATIVE = /^\.\.?(?:\/|$)/u
|
|
76
|
+
// What `require` doesn't run as JavaScript: JSON, and a native addon, by
|
|
77
|
+
// their names as it knows them, in lowercase: `.JSON` it runs.
|
|
78
|
+
const NOT_RUN = /\.(?:json|node)$/u
|
|
79
|
+
// The extensions `require` tries a path with that names no file.
|
|
80
|
+
const TRIED = ['.js', '.json', '.node']
|
|
81
|
+
// A path `require` takes as a directory's alone, never a file's: one ending
|
|
82
|
+
// in `/`, or in `.` or `..` after one, or either alone, as node tells it, by
|
|
83
|
+
// `/` alone, on Windows too (`./payload/`, not `.\payload\`).
|
|
84
|
+
const DIRECTORY = /(?:^|\/)\.{0,2}$/u
|
|
85
|
+
// A name with an extension, which `import` runs only where it knows it: not
|
|
86
|
+
// `.npmignore`, nor `payload`.
|
|
87
|
+
const EXTENSION = /[^/.][^/]*\.[^/]*$/u
|
|
88
|
+
|
|
89
|
+
// A path's parts, `spec`'s from `parts`, each `..` a directory up; null for
|
|
90
|
+
// one leading past the top.
|
|
91
|
+
export function resolved(parts, spec) {
|
|
92
|
+
const all = [...parts]
|
|
93
|
+
for (const part of spec.split('/')) {
|
|
94
|
+
if (part === '..' && all.pop() === undefined) return null
|
|
95
|
+
if (part !== '..' && part !== '.' && part !== '') all.push(part)
|
|
96
|
+
}
|
|
97
|
+
return all
|
|
98
|
+
}
|
|
99
|
+
// Those parts as a path in the package where it is installed (`root`, its
|
|
100
|
+
// directory's parts, `node_modules/<name>`), told as a filesystem telling
|
|
101
|
+
// neither case nor Unicode's normalization apart tells them (folded,
|
|
102
|
+
// `../LOADS/x` for `loads`): null for one outside it.
|
|
103
|
+
const within = (root, parts) => parts !== null && root.every((part, index) => index < parts.length && (parts[index] === part || folded(parts[index]) === part)) ? parts.slice(root.length).join('/') : null
|
|
104
|
+
// `spec` from the directory `dir` ('' for the package's top) as a path in the
|
|
105
|
+
// package, one leading out of it and back in by its name too
|
|
106
|
+
// (`../<name>/x`); null for one leading elsewhere.
|
|
107
|
+
const joined = (root, dir, spec) => within(root, resolved([...root, ...dir === '' ? [] : dir.split('/')], spec))
|
|
108
|
+
// A path as `require` takes it: its `\` a separator on Windows, a name's
|
|
109
|
+
// character elsewhere, so read both ways.
|
|
110
|
+
const separated = spec => new Set([spec, spec.replaceAll('\\', '/')])
|
|
111
|
+
// Each relative one of those (RELATIVE), from `dir`, as a path in the package
|
|
112
|
+
// (joined).
|
|
113
|
+
const requiredPaths = (root, dir, spec) => [...separated(spec)].filter(path => RELATIVE.test(path)).map(path => joined(root, dir, path))
|
|
114
|
+
// What a URL reads otherwise than a path: an escape, a query, a fragment, a
|
|
115
|
+
// `\`, a tab or a line break, or a blank or a control at its end, which it
|
|
116
|
+
// drops.
|
|
117
|
+
const URL_READ = /[%?#\\\t\n\r]|[\0- ]$/u
|
|
118
|
+
// A relative path as `import` takes it, and `exports` and `imports` targets,
|
|
119
|
+
// a URL's: its query and fragment dropped, tabs and line breaks too, `\` a
|
|
120
|
+
// separator, its escapes read (`%2E`); null for one escaping a separator
|
|
121
|
+
// (`%2F`), as node refuses, or leading out of the package (joined).
|
|
122
|
+
function urlJoined(root, dir, spec) {
|
|
123
|
+
if (!RELATIVE.test(spec)) return null
|
|
124
|
+
if (!URL_READ.test(spec)) return joined(root, dir, spec)
|
|
125
|
+
try {
|
|
126
|
+
const base = [...root, ...dir === '' ? [] : dir.split('/')].map(part => `${encodeURIComponent(part)}/`).join('')
|
|
127
|
+
const { pathname } = new URL(spec, `file:///${base}`)
|
|
128
|
+
return /%(?:2f|5c)/iu.test(pathname) ? null : within(root, resolved([], decodeURIComponent(pathname)))
|
|
129
|
+
} catch {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Where a package is installed (`node_modules/<name>`), as parts, folded
|
|
135
|
+
// (within); none for one with no name, or one no directory could be.
|
|
136
|
+
const NAME = /^(?:@[^/]+\/)?[^/@.][^/]*$/u
|
|
137
|
+
const rootOf = name => typeof name === 'string' && NAME.test(name) && name.split('/').every(part => part !== '.' && part !== '..') ? ['node_modules', ...name.split('/')].map(folded) : []
|
|
138
|
+
|
|
139
|
+
// A path as a filesystem telling neither case nor Unicode's normalization
|
|
140
|
+
// apart takes it, as Windows' and macOS's do by default: `Payload.txt` for
|
|
141
|
+
// `payload.txt`; a lone surrogate the U+FFFD node writes a name with for it.
|
|
142
|
+
const folded = path => path.toWellFormed().normalize('NFC').toUpperCase().toLowerCase()
|
|
143
|
+
// The files a path names once installed, as a filesystem tells them
|
|
144
|
+
// (folded): its own, and for a .npmignore, a .gitignore npm installs as that;
|
|
145
|
+
// and the filesystems node may find them on, each telling the files a path
|
|
146
|
+
// names (`named`) by the path as it takes it (`fold`): one telling case
|
|
147
|
+
// apart (`cased`), as Linux's does, and one telling neither case nor
|
|
148
|
+
// Unicode's normalization apart (`folding`, folded), a path folded naming
|
|
149
|
+
// those with what follows it in ASCII (`.json`, `/index`), which folds as
|
|
150
|
+
// it is, every file the other names among them.
|
|
151
|
+
function installedOf(files) {
|
|
152
|
+
const exact = new Map(), index = new Map()
|
|
153
|
+
const put = (map, key, path) => map.has(key) ? map.get(key).push(path) : map.set(key, [path])
|
|
154
|
+
for (const path of files.keys()) {
|
|
155
|
+
const aliases = /(?:^|\/)\.gitignore$/u.test(path) ? [path, path.replace(/\.gitignore$/u, '.npmignore')] : [path]
|
|
156
|
+
for (const alias of aliases) {
|
|
157
|
+
put(index, folded(alias), path)
|
|
158
|
+
put(exact, alias, path)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const keyed = key => index.get(key) ?? []
|
|
162
|
+
return { installed: path => path === null ? [] : keyed(folded(path)), cased: { named: key => exact.get(key) ?? [], fold: path => path }, folding: { named: keyed, fold: folded } }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Every string a JSON value holds, however deep it nests them, read
|
|
166
|
+
// without recursing, which nesting deeper than a call stack would end.
|
|
167
|
+
function leaves(value) {
|
|
168
|
+
const found = [], pending = [value]
|
|
169
|
+
while (pending.length > 0) {
|
|
170
|
+
const item = pending.pop()
|
|
171
|
+
if (typeof item === 'string') found.push(item)
|
|
172
|
+
else if (item && typeof item === 'object') for (const each of Object.values(item)) pending.push(each)
|
|
173
|
+
}
|
|
174
|
+
return found
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The targets an `exports` or `imports` map holds (leaves), by its keys
|
|
178
|
+
// (those `subpaths` tells, `./x`, `#x`), each a pattern's where its key is
|
|
179
|
+
// one (`./lib/*`), as node puts what a consumer asks for in a target's `*`
|
|
180
|
+
// only then; a map with none of those keys its `.`'s whole (`"exports":
|
|
181
|
+
// "./x"`, or conditions alone).
|
|
182
|
+
function mapped(map, subpaths) {
|
|
183
|
+
const keyed = map && typeof map === 'object' && !Array.isArray(map) && Object.keys(map).some(subpaths) ? Object.entries(map) : [['.', map]]
|
|
184
|
+
return keyed.flatMap(([key, value]) => leaves(value).map(target => ({ key, pattern: key.includes('*'), target })))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// A package.json's `name`, `main`, and the paths its `exports` and `imports`
|
|
188
|
+
// name (`./x`, mapped); and its maps by their keys (`./x`, `#x`), its
|
|
189
|
+
// `imports` to the package's own name too (`<name>/x`), read past a byte
|
|
190
|
+
// order mark as node reads it; none where it is no JSON object.
|
|
191
|
+
function entriesOf(text) {
|
|
192
|
+
const none = { name: null, main: null, targets: [], exports: [], hasExports: false, imports: [] }
|
|
193
|
+
if (typeof text !== 'string') return none
|
|
194
|
+
try {
|
|
195
|
+
const { name, main, exports, imports } = JSON.parse(text.replace(/^\uFEFF/u, ''))
|
|
196
|
+
const subpaths = mapped(exports, key => key.startsWith('.')).filter(({ key, target }) => key.startsWith('.') && target.startsWith('./'))
|
|
197
|
+
const own = mapped(imports, key => key.startsWith('#')).filter(({ key }) => key.startsWith('#'))
|
|
198
|
+
const targets = [...subpaths, ...own.filter(({ target }) => target.startsWith('./'))]
|
|
199
|
+
return { name, main: typeof main === 'string' && main !== '' ? main : null, targets, exports: subpaths, hasExports: exports !== undefined && exports !== null, imports: own }
|
|
200
|
+
} catch {
|
|
201
|
+
return none
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// A path as a filesystem reads it, from the package's top: each run of `/`
|
|
206
|
+
// one, none leading.
|
|
207
|
+
const collapsed = path => path.replaceAll(/\/+/gu, '/').replace(/^\//u, '')
|
|
208
|
+
|
|
209
|
+
// The paths an `exports` or `imports` target names, from the directory of the
|
|
210
|
+
// package.json naming it, read as a URL (urlJoined): its own, `*` and all,
|
|
211
|
+
// or where its key is a pattern (`pattern`, mapped) each file it matches, as a filesystem tells them (folded, collapsed), each of
|
|
212
|
+
// its `*` the same text, as node puts what a consumer asks for (`pkg/x`) in
|
|
213
|
+
// every one, where the pattern names what follows the last (`./lib/*.txt`);
|
|
214
|
+
// where it names nothing (`./*`), only one with no extension that isn't prose
|
|
215
|
+
// by its name (not `LICENSE`), the rest a consumer reaches only asking for
|
|
216
|
+
// them by their names. What a `*` stands for may be text with a `/` before
|
|
217
|
+
// it, after it, both or neither, or a `/` alone, the filesystem folding it
|
|
218
|
+
// into a `/` beside it (`pkg//` reaching `lib/.txt` through `./lib/*.txt`):
|
|
219
|
+
// each such shape's length told by one `x`'s, a file of the pattern's is the
|
|
220
|
+
// pattern with its text in each; a `/` before or after it counted apart only
|
|
221
|
+
// where it folds into a `/` of the pattern's, or the top, beside one `*` and
|
|
222
|
+
// not another, else its text holds it, or it folds alike at every one.
|
|
223
|
+
function exported(names, root, dir, target, pattern) {
|
|
224
|
+
const path = urlJoined(root, dir, target)
|
|
225
|
+
if (path === null || !pattern || !path.includes('*')) return [path]
|
|
226
|
+
const parts = folded(path).split('*'), stars = parts.length - 1
|
|
227
|
+
const head = parts[0].replace(/\/+$/u, ''), slashes = collapsed(parts.join('/')), tail = parts.at(-1).replace(/^\/+/u, '')
|
|
228
|
+
const both = sides => sides.includes(true) && sides.includes(false) ? ['', '/'] : ['']
|
|
229
|
+
const afters = both(parts.slice(1).map(part => part.startsWith('/'))), befores = both(parts.slice(0, -1).map((part, index) => part.endsWith('/') || (index === 0 && part === '')))
|
|
230
|
+
const shapes = befores.flatMap(before => afters.map(after => ({ after, at: collapsed(`${parts[0]}${before}x`).length - 1, before, length: collapsed(parts.join(`${before}x${after}`)).length })))
|
|
231
|
+
return names.filter(([name, file]) => {
|
|
232
|
+
if (!file.startsWith(head) || !file.endsWith(tail) || (parts.at(-1) === '' && (EXTENSION.test(name) || NPM_PROSE.test(name)))) return false
|
|
233
|
+
return file === slashes || shapes.some(({ after, at, before, length }) => {
|
|
234
|
+
const core = (file.length - length) / stars + 1
|
|
235
|
+
if (!Number.isInteger(core) || core <= 0) return false
|
|
236
|
+
const built = parts.join(`${before}${file.slice(at, at + core)}${after}`)
|
|
237
|
+
return built === file || ((built.includes('//') || built.startsWith('/')) && collapsed(built) === file)
|
|
238
|
+
})
|
|
239
|
+
}).map(([name]) => name)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// What a map's entries (`{ dir, key, target }` each) take `value` to, as
|
|
243
|
+
// node maps a specifier through `exports` or `imports`: the target of a key
|
|
244
|
+
// that is it, or of a pattern's (`#lib/*`, `./lib/*`, one `*`) it matches,
|
|
245
|
+
// what it matches put in each of the target's `*`; EVERY for one that would
|
|
246
|
+
// put in more than MAX_EXPANSION characters, which only a package written to
|
|
247
|
+
// hold up its reading has (`*` thousands of times over a long specifier), and
|
|
248
|
+
// whose path a URL's reading could shorten again (tabs, a query, `//`).
|
|
249
|
+
const MAX_EXPANSION = 64 * 1024
|
|
250
|
+
const EVERY = Symbol('every file')
|
|
251
|
+
const mappedTo = (entries, value) => entries.flatMap(({ dir, key, target }) => {
|
|
252
|
+
const star = key.indexOf('*')
|
|
253
|
+
if (star === -1 ? key !== value : star !== key.lastIndexOf('*') || value.length < key.length || !value.startsWith(key.slice(0, star)) || !value.endsWith(key.slice(star + 1))) return []
|
|
254
|
+
if (star === -1) return [{ dir, target }]
|
|
255
|
+
const sub = value.slice(star, value.length - (key.length - star - 1))
|
|
256
|
+
return [{ dir, target: (target.split('*').length - 1) * sub.length > MAX_EXPANSION ? EVERY : target.replaceAll('*', sub) }]
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// A package's own subpath (`./x`, `.`) as its `exports` map it (mappedTo), as
|
|
260
|
+
// node resolves a specifier by its own name (`<name>/x`), read as URLs; with
|
|
261
|
+
// no `exports` at all, the path itself, as node finds the package where it
|
|
262
|
+
// is installed (`root`).
|
|
263
|
+
const selfResolved = (root, { exports, hasExports }, spec) => hasExports
|
|
264
|
+
? mappedTo(exports.map(entry => ({ ...entry, dir: '' })), spec).map(({ target }) => target === EVERY ? EVERY : urlJoined(root, '', target))
|
|
265
|
+
: [urlJoined(root, '', spec)]
|
|
266
|
+
|
|
267
|
+
// The path of the first of `extensions` a path takes that names a file of a
|
|
268
|
+
// version's on a filesystem (`system`, installedOf's), each it tells for it
|
|
269
|
+
// its own: none at the package's top, which is no file.
|
|
270
|
+
function firstNamed({ named, fold }, path, extensions) {
|
|
271
|
+
const key = path === '' ? null : fold(path)
|
|
272
|
+
const extension = key === null ? undefined : extensions.find(each => named(`${key}${each}`).length > 0)
|
|
273
|
+
return extension === undefined ? null : `${path}${extension}`
|
|
274
|
+
}
|
|
275
|
+
const indexFile = (system, path) => firstNamed(system, path === '' ? 'index' : `${path}/index`, TRIED)
|
|
276
|
+
|
|
277
|
+
// The files a path `require` takes finds, the first it tries that names a
|
|
278
|
+
// file: its own, one by its name and an extension (`.js`, `.json`, `.node`,
|
|
279
|
+
// `payload.JSON` as a filesystem folding case finds it), but where it takes
|
|
280
|
+
// it as a directory's (`directory`, DIRECTORY); else a directory's
|
|
281
|
+
// package.json `main`, found so or by its `index` and an extension; else the
|
|
282
|
+
// directory's `index` so, as node falls back to it; on a filesystem
|
|
283
|
+
// (`system`, installedOf's), as each finds its own first.
|
|
284
|
+
function requiredFiles(files, system, root, path, directory) {
|
|
285
|
+
if (path === null) return []
|
|
286
|
+
const file = directory ? null : firstNamed(system, path, ['', ...TRIED])
|
|
287
|
+
if (file !== null) return [file]
|
|
288
|
+
const mains = system.named(path === '' ? 'package.json' : `${system.fold(path)}/package.json`).map(manifest => entriesOf(files.get(manifest)).main).filter(main => main !== null)
|
|
289
|
+
.flatMap(main => [...separated(main)].map(spec => joined(root, path, spec))).filter(at => at !== null)
|
|
290
|
+
.map(at => firstNamed(system, at, ['', ...TRIED]) ?? indexFile(system, at)).filter(found => found !== null)
|
|
291
|
+
return mains.length > 0 ? mains : [indexFile(system, path)].filter(found => found !== null)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// The `#` specifiers each package.json of a version's files maps
|
|
295
|
+
// (`imports`), each from its directory.
|
|
296
|
+
const specifiersOf = files => [...files].filter(([path]) => /(?:^|\/)package\.json$/iu.test(path))
|
|
297
|
+
.flatMap(([path, text]) => entriesOf(text).imports.map(({ key, target }) => ({ dir: path.slice(0, -'package.json'.length - 1), key, target })))
|
|
298
|
+
|
|
299
|
+
// How many `exports` and `imports` patterns a package's package.json files
|
|
300
|
+
// have each matched against its files (exported): more than a package names
|
|
301
|
+
// (4 at most of popular ones). One naming more has every file read as one it
|
|
302
|
+
// may load.
|
|
303
|
+
const MAX_PATTERNS = 64
|
|
304
|
+
|
|
305
|
+
// The files of a version node runs as JavaScript though their names say
|
|
306
|
+
// otherwise (script.js npmIsScript), read as scripts all the same
|
|
307
|
+
// (stamps.js npmFileRisk's `script`): any a script of it, or a file read so,
|
|
308
|
+
// names by a relative path written out whole as a string (quoted), anywhere
|
|
309
|
+
// in its text, whatever takes it, or by the package's own name
|
|
310
|
+
// (`'<name>/x.txt'`); as `require` runs any but JSON and a native addon
|
|
311
|
+
// (`require('./data.txt')`, `require('./.npmignore')`, the .gitignore npm
|
|
312
|
+
// installs as that), and the `main` of a directory's package.json; and as
|
|
313
|
+
// `import` runs one whose name has no extension (`import('./payload')`), as a
|
|
314
|
+
// URL (urlJoined), a string an import declaration or `import(…)` takes whole
|
|
315
|
+
// read as it alone (imported). And the package's own `main`, and what the
|
|
316
|
+
// `exports` and `imports` of its package.json files name, their patterns'
|
|
317
|
+
// too, which a `require` reaching them runs whatever their names. A path a
|
|
318
|
+
// script builds is not followed (`'./x' + '.txt'`, a template with something
|
|
319
|
+
// put in it), nor one written in a string run as code, which runs-code.js
|
|
320
|
+
// tells. Each path names the files a filesystem telling neither case nor
|
|
321
|
+
// Unicode's normalization apart finds by it (folded), as Windows' and
|
|
322
|
+
// macOS's do, a loader running one where its name or theirs says it does;
|
|
323
|
+
// one that isn't text among them too, which node runs decoded as UTF-8 and
|
|
324
|
+
// readability.js npmBinaryCode tells, though nothing it loads is read, but
|
|
325
|
+
// where it can't parse (tarball.js npmRunnable's `runnable`: an image).
|
|
326
|
+
const loadedByVersion = new WeakMap()
|
|
327
|
+
export function npmLoadedScripts(version) {
|
|
328
|
+
let loaded = loadedByVersion.get(keyOf(version))
|
|
329
|
+
if (loaded) return loaded
|
|
330
|
+
loaded = new Set()
|
|
331
|
+
const files = bundleFilesAsMap(version), { installed, cased, folding } = installedOf(files), queue = []
|
|
332
|
+
const top = entriesOf(files.get(files.has('package.json') ? 'package.json' : installed('package.json')[0]))
|
|
333
|
+
const root = rootOf(top.name)
|
|
334
|
+
// What a loader takes (`by`) runs, as the name it is asked by tells, or
|
|
335
|
+
// the name of the file it finds.
|
|
336
|
+
const runs = (path, by) => !(by === 'require' ? NOT_RUN : EXTENSION).test(path)
|
|
337
|
+
// Each path once a loader and whether its name runs: the files it names
|
|
338
|
+
// are the same however else it is cased.
|
|
339
|
+
const tried = new Set()
|
|
340
|
+
const add = (path, by) => {
|
|
341
|
+
const key = path === null ? null : `${by}\0${runs(path, by)}\0${folded(path)}`
|
|
342
|
+
if (key === null || tried.has(key)) return
|
|
343
|
+
tried.add(key)
|
|
344
|
+
for (const file of installed(path)) {
|
|
345
|
+
const text = files.get(file)
|
|
346
|
+
if (loaded.has(file) || npmIsScript(file, text) || text?.runnable === false || (!runs(path, by) && !runs(file, by))) continue
|
|
347
|
+
loaded.add(file)
|
|
348
|
+
if (typeof text === 'string') queue.push(file)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// On either filesystem: where one folding case finds none, neither does
|
|
352
|
+
// one telling case apart, which finds fewer.
|
|
353
|
+
const requires = (path, directory) => {
|
|
354
|
+
const found = requiredFiles(files, folding, root, path, directory)
|
|
355
|
+
if (found.length > 0) for (const file of [...found, ...requiredFiles(files, cased, root, path, directory)]) add(file, 'require')
|
|
356
|
+
}
|
|
357
|
+
requires('', true)
|
|
358
|
+
const targets = [...files].filter(([path]) => /(?:^|\/)package\.json$/iu.test(path))
|
|
359
|
+
.flatMap(([path, text]) => entriesOf(text).targets.map(({ pattern, target }) => [path.slice(0, -'package.json'.length - 1), target, pattern && target.includes('*')]))
|
|
360
|
+
if (targets.filter(([, , pattern]) => pattern).length > MAX_PATTERNS) for (const path of files.keys()) add(path, 'require')
|
|
361
|
+
else {
|
|
362
|
+
// Each file's path and its path as a filesystem tells it (folded), for
|
|
363
|
+
// the patterns to match.
|
|
364
|
+
const names = targets.some(([, , pattern]) => pattern) ? [...files.keys()].map(name => [name, folded(name)]) : []
|
|
365
|
+
for (const [dir, target, pattern] of targets) for (const file of exported(names, root, dir, target, pattern)) add(file, 'require')
|
|
366
|
+
}
|
|
367
|
+
for (const [path, text] of files) if (npmIsScript(path, text)) queue.push(path)
|
|
368
|
+
const specifiers = specifiersOf(files)
|
|
369
|
+
// The package's own name, as a filesystem tells it (rootOf), a path from
|
|
370
|
+
// it after a `/`, or a `\\` as on Windows.
|
|
371
|
+
const named = value => root.length > 0 && value.length > top.name.length && '/\\'.includes(value[top.name.length])
|
|
372
|
+
&& folded(value.slice(0, top.name.length)) === root.slice(1).join('/')
|
|
373
|
+
const seen = new Set(), strings = quoted([...root.length > 0 ? foldsInto(top.name[0]) : [], ...specifiers.length > 0 ? ['#'] : []])
|
|
374
|
+
const subpathsOf = spec => selfResolved(root, top, spec)
|
|
375
|
+
// A path loaded, or for EVERY, every file, once (mappedTo).
|
|
376
|
+
let every = false
|
|
377
|
+
const reach = (path, by) => {
|
|
378
|
+
if (path !== EVERY) return add(path, by)
|
|
379
|
+
if (!every) for (const file of files.keys()) add(file, 'require')
|
|
380
|
+
every = true
|
|
381
|
+
}
|
|
382
|
+
// A `#` specifier as the package's `imports` map it (mappedTo), to a path
|
|
383
|
+
// of its own, read as a URL (urlJoined), or to its own name, as its
|
|
384
|
+
// `exports` map that (subpathsOf), loaded as code takes it (`by`), once
|
|
385
|
+
// each.
|
|
386
|
+
const own = (value, by) => {
|
|
387
|
+
if (seen.has(`${by}\0#\0${value}`)) return
|
|
388
|
+
seen.add(`${by}\0#\0${value}`)
|
|
389
|
+
for (const { dir, target } of mappedTo(specifiers, value)) {
|
|
390
|
+
const paths = target === EVERY ? [EVERY] : target.startsWith('./') ? [urlJoined(root, dir, target)] : named(target) ? subpathsOf(`./${target.slice(top.name.length + 1)}`)
|
|
391
|
+
: root.length > 0 && target === top.name ? subpathsOf('.') : []
|
|
392
|
+
for (const path of paths) reach(path, by)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// A string's value from the directory `dir`, a load `import` takes where
|
|
396
|
+
// its place says so (`importing`), else one `require` may take, once each.
|
|
397
|
+
const follow = (value, dir, importing) => {
|
|
398
|
+
if (value.startsWith('#')) return own(value, importing() ? 'import' : 'require')
|
|
399
|
+
const [from, spec] = named(value) ? ['', `./${value.slice(top.name.length + 1)}`] : [dir, value]
|
|
400
|
+
if (!RELATIVE.test(spec.replaceAll('\\', '/'))) return
|
|
401
|
+
const by = importing() ? 'import' : 'require', key = `${by}\0${from}\0${spec}`
|
|
402
|
+
if (seen.has(key)) return
|
|
403
|
+
seen.add(key)
|
|
404
|
+
if (named(value)) for (const path of subpathsOf(spec)) reach(path, by)
|
|
405
|
+
if (named(value) && top.hasExports) return
|
|
406
|
+
if (by === 'require') for (const required of requiredPaths(root, from, spec)) requires(required, DIRECTORY.test(spec))
|
|
407
|
+
add(urlJoined(root, from, spec), 'import')
|
|
408
|
+
}
|
|
409
|
+
for (let index = 0; index < queue.length; index++) {
|
|
410
|
+
const path = queue[index], text = files.get(path)
|
|
411
|
+
const dir = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : ''
|
|
412
|
+
for (const { 1: double, 2: single, 3: template, index: start } of text.matchAll(strings)) {
|
|
413
|
+
const body = double ?? single ?? template, quote = text[start]
|
|
414
|
+
for (const value of valuesOf(quote, body)) follow(value, dir, () => imported(text, quote, start, start + body.length + 2))
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
loadedByVersion.set(keyOf(version), loaded)
|
|
418
|
+
return loaded
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// What risk reads of each file of a version (stamps.js npmFileRisk), once a
|
|
422
|
+
// version and path: as a script too where node runs it as one though its name
|
|
423
|
+
// says otherwise (npmLoadedScripts).
|
|
424
|
+
const reads = new WeakMap()
|
|
425
|
+
export function npmVersionFileRisk(version, path) {
|
|
426
|
+
let files = reads.get(keyOf(version))
|
|
427
|
+
if (!files) reads.set(keyOf(version), files = new Map())
|
|
428
|
+
if (!files.has(path)) {
|
|
429
|
+
const text = bundleFilesAsMap(version).get(path)
|
|
430
|
+
files.set(path, npmFileRisk(path, typeof text === 'string' ? text : null, bundleFileSizes(version).get(path), npmLoadedScripts(version).has(path)))
|
|
431
|
+
}
|
|
432
|
+
return files.get(path)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Every host a version's scripts name (hosts.js npmHosts), once a version.
|
|
436
|
+
const versionHosts = new WeakMap()
|
|
437
|
+
export function npmVersionHosts(version) {
|
|
438
|
+
let hosts = versionHosts.get(keyOf(version))
|
|
439
|
+
if (!hosts) {
|
|
440
|
+
hosts = new Set()
|
|
441
|
+
for (const path of bundleFilesAsMap(version).keys()) for (const host of npmVersionFileRisk(version, path).hosts?.keys() ?? []) hosts.add(host)
|
|
442
|
+
versionHosts.set(keyOf(version), hosts)
|
|
443
|
+
}
|
|
444
|
+
return hosts
|
|
445
|
+
}
|
package/risk/npm/held.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What calls for a look at a version on its own, as the npm Overview shows
|
|
2
|
+
// it: what npm says of it and when it was published (npmVersionFindings),
|
|
3
|
+
// and what it holds that calls for a look whatever came before it
|
|
4
|
+
// (npmHeldFindings), as Compare's findings (compare.js) say what changed.
|
|
5
|
+
import { bundleFilesAsMap } from '../../common/bundle-sources.js'
|
|
6
|
+
import { npmDownloadTier, npmTierLevel, npmTierLow } from './downloads.js'
|
|
7
|
+
import { npmInstallScripts } from './install-scripts.js'
|
|
8
|
+
import { npmTooNew } from './release.js'
|
|
9
|
+
import { HOST_TAGS, npmHostsTagged } from '../files/hosts.js'
|
|
10
|
+
import { CREDENTIAL_TAGS, npmRunsCodeTagged, npmRunsCodeWithout } from '../files/runs-code.js'
|
|
11
|
+
import { npmVersionFileRisk } from './files.js'
|
|
12
|
+
|
|
13
|
+
// What npm says of a version and of its package, told fresh as it changes in
|
|
14
|
+
// time: deprecated, `deprecated` its message (`deprecated`, red); younger
|
|
15
|
+
// than `minimumReleaseAge` at `now`, published at `published` (release.js
|
|
16
|
+
// npmTooNew, `too-new`, orange); and its package past the top 20,000 by
|
|
17
|
+
// `downloads` (downloads.js, `popularity`, red where unpopular, else orange),
|
|
18
|
+
// `tier` its rank (Infinity past the last); yellow at most, whatever its
|
|
19
|
+
// tier, where an account known to publish one of npm's most downloaded
|
|
20
|
+
// packages published the version (`known`, manifest.js `knownAccounts`), as
|
|
21
|
+
// few download a new package such an account starts; or else, as many are
|
|
22
|
+
// published by a CI that names no account, where the version was published
|
|
23
|
+
// with provenance (`provenance`, manifest.js), built where it says, as a
|
|
24
|
+
// token alone, stolen, publishes none, and the package is in a known scope
|
|
25
|
+
// (`scope`, manifest.js npmKnownScope: one of those packages', or one of
|
|
26
|
+
// those accounts' own, its `account` named), whose owner alone publishes in
|
|
27
|
+
// it.
|
|
28
|
+
export function npmVersionFindings({ deprecated, published, minimumReleaseAge, now, downloads, known = false, scope = null, provenance = false }) {
|
|
29
|
+
const tier = downloads ? npmDownloadTier(downloads) : null
|
|
30
|
+
const scoped = provenance ? scope : null
|
|
31
|
+
const severity = known || scoped ? 'moderate' : npmTierLevel(tier) === 'is-unpopular' ? 'critical' : 'high'
|
|
32
|
+
return [
|
|
33
|
+
...deprecated ? [{ id: 'deprecated', severity: 'critical', message: deprecated }] : [],
|
|
34
|
+
...published && npmTooNew(published, { minimumReleaseAge, now }) ? [{ id: 'too-new', severity: 'high', published }] : [],
|
|
35
|
+
...tier !== null && npmTierLow(tier) ? [{ id: 'popularity', severity, tier, ...known ? { known } : scoped ? { known: true, ...scoped } : {} }] : [],
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// What a version holds that calls for a look, whatever came before it, in
|
|
40
|
+
// orange, as its files' tags are: the scripts npm runs installing it
|
|
41
|
+
// (install-scripts.js, `held-install-scripts`, `[script, command]` each); and
|
|
42
|
+
// its files, each with what it holds, that run code beyond themselves
|
|
43
|
+
// (`held-runs-code`), reach for credentials (runs-code.js CREDENTIAL_TAGS,
|
|
44
|
+
// `held-credentials`), name a service under one of the hosts' tags, at the
|
|
45
|
+
// tag's severity (hosts.js HOST_TAGS: `held-exfiltration` red, `held-remote`
|
|
46
|
+
// orange, `held-ip-lookup` yellow), or bear an obfuscator's fingerprint
|
|
47
|
+
// (`held-obfuscated`).
|
|
48
|
+
export function npmHeldFindings(version) {
|
|
49
|
+
const scripts = npmInstallScripts(version.npm.manifest, version.json?.sources)
|
|
50
|
+
const reads = [...bundleFilesAsMap(version).keys()].map(path => ({ path, read: npmVersionFileRisk(version, path) }))
|
|
51
|
+
const holding = of => reads.map(({ path, read }) => ({ path, found: of(read) })).filter(row => row.found)
|
|
52
|
+
return [
|
|
53
|
+
...scripts.length > 0 ? [{ id: 'held-install-scripts', severity: 'high', scripts }] : [],
|
|
54
|
+
{ id: 'held-runs-code', severity: 'high', files: holding(read => npmRunsCodeWithout(read.runsCode, CREDENTIAL_TAGS)) },
|
|
55
|
+
{ id: 'held-credentials', severity: 'high', files: holding(read => npmRunsCodeTagged(read.runsCode, CREDENTIAL_TAGS)) },
|
|
56
|
+
...[...HOST_TAGS].map(([tag, severity]) => ({ id: `held-${npmHostFinding(tag)}`, severity, files: holding(read => npmHostsTagged(read.exfiltration, tag)) })),
|
|
57
|
+
{ id: 'held-obfuscated', severity: 'high', files: holding(read => read.readability.obfuscation) },
|
|
58
|
+
].filter(finding => !finding.files || finding.files.length > 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A hosts' tag's finding, as its id names it: `exfiltration`, `remote`,
|
|
62
|
+
// `ip-lookup`.
|
|
63
|
+
export const npmHostFinding = tag => tag.toLowerCase().replaceAll(' ', '-')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The scripts npm runs as it installs a package: what release.js flags added
|
|
2
|
+
// or changed, and the npm viewer lists (its Overview, and Compare's tags on a
|
|
3
|
+
// package.json).
|
|
4
|
+
|
|
5
|
+
// The scripts npm runs installing a package from the registry, in the order
|
|
6
|
+
// it runs them; not `prepare`, which runs only for a local or git install.
|
|
7
|
+
export const INSTALL_SCRIPTS = ['preinstall', 'install', 'postinstall']
|
|
8
|
+
|
|
9
|
+
// A package's install scripts, `[script, command]`, in the order npm runs
|
|
10
|
+
// them: those `scripts` names, and `node-gyp rebuild` where the package has
|
|
11
|
+
// a `binding.gyp` at its root (`bindingGyp`) and neither install script of
|
|
12
|
+
// its own, as npm runs then.
|
|
13
|
+
export function installScripts(scripts, bindingGyp = false) {
|
|
14
|
+
const named = { ...scripts }
|
|
15
|
+
if (!named.preinstall && !named.install && bindingGyp) named.install = 'node-gyp rebuild'
|
|
16
|
+
return INSTALL_SCRIPTS.filter(script => typeof named[script] === 'string').map(script => [script, named[script]])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// An npm version's install scripts: those its manifest's `installScripts`
|
|
20
|
+
// names, and node-gyp's where `paths`, its files' (a list or a set), have a
|
|
21
|
+
// binding.gyp at its root.
|
|
22
|
+
export function npmInstallScripts(manifest, paths = []) {
|
|
23
|
+
return installScripts(manifest?.installScripts, Boolean(paths && (paths instanceof Set ? paths.has('binding.gyp') : paths.includes('binding.gyp'))))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A package.json's install scripts, from its text, a byte order mark it
|
|
27
|
+
// starts with dropped, as npm drops one reading it; none where it isn't one,
|
|
28
|
+
// or names none.
|
|
29
|
+
export function packageJsonInstallScripts(text, bindingGyp = false) {
|
|
30
|
+
let json = null
|
|
31
|
+
try { json = typeof text === 'string' ? JSON.parse(text.replace(/^\uFEFF/u, '')) : null } catch {}
|
|
32
|
+
const scripts = json && typeof json === 'object' && json.scripts && typeof json.scripts === 'object' ? json.scripts : {}
|
|
33
|
+
return installScripts(scripts, bindingGyp)
|
|
34
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// What tells of malware in a version: npm's security team taking its package
|
|
2
|
+
// down, whatever version is asked, as it does for malicious code; and an
|
|
3
|
+
// advisory of malware naming the version, npm's or OSV's (registry.js
|
|
4
|
+
// npmVersionAdvisories).
|
|
5
|
+
|
|
6
|
+
// Whether an advisory is of malware rather than a vulnerability: OSV's of the
|
|
7
|
+
// malicious packages OpenSSF lists, each `MAL-…`, or one such a record names
|
|
8
|
+
// (its `aliases`, as upstream's `osv` gives npm's advisory it names); GitHub
|
|
9
|
+
// files its malware advisories as CWE-506, embedded malicious code, whatever
|
|
10
|
+
// their title ("Critical severity vulnerability that affects event-stream
|
|
11
|
+
// and flatmap-stream"); and its title may say so where its CWE doesn't
|
|
12
|
+
// (@solana/web3.js's GHSA-jcxm-7wvp-g6p5, CWE-200 alone).
|
|
13
|
+
export const npmMalwareAdvisory = ({ id, aliases, cwe, title }) => [id, ...aliases ?? []].some(name => (name ?? '').startsWith('MAL-'))
|
|
14
|
+
|| (cwe ?? []).includes('CWE-506') || /\bmalware\b/iu.test(title ?? '')
|
|
15
|
+
|
|
16
|
+
// Whether `json`, a version's document, is the placeholder npm publishes in
|
|
17
|
+
// place of a package its security team took down: a "security holding
|
|
18
|
+
// package" from npm/security-holder, published by npm, the account npm's
|
|
19
|
+
// registry names as its publisher, which no package's author can. Not one
|
|
20
|
+
// by another account, such as the `0.0.1-security` versions 2017's hacktask
|
|
21
|
+
// typosquats are listed as theirs, each npm's own `0.0.2-security` after it.
|
|
22
|
+
export function npmPlaceholder(json) {
|
|
23
|
+
const repository = typeof json?.repository === 'string' ? json.repository : json?.repository?.url
|
|
24
|
+
const npms = json?.description === 'security holding package' || /(?:^|[/:])npm\/security-holder(?:\.git)?\/?$/iu.test(typeof repository === 'string' ? repository : '')
|
|
25
|
+
return npms && json?._npmUser?.name === 'npm'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The `malware` finding of a version, critical: its package taken down by npm
|
|
29
|
+
// (`takenDown`, its latest version npm's placeholder), and the advisories of
|
|
30
|
+
// malware among `advisories` (`{ id, url?, title?, cwe }`), each `{ id, url?,
|
|
31
|
+
// title? }` once; null for neither.
|
|
32
|
+
export function npmMalwareFinding({ takenDown, advisories }) {
|
|
33
|
+
const named = new Map(advisories.filter(npmMalwareAdvisory).map(({ id, url, title }) => [id, { id, ...url ? { url } : {}, ...title ? { title } : {} }]))
|
|
34
|
+
if (!takenDown && named.size === 0) return null
|
|
35
|
+
return { id: 'malware', severity: 'critical', ...takenDown ? { takenDown: true } : {}, ...named.size > 0 ? { advisories: [...named.values()] } : {} }
|
|
36
|
+
}
|