@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41
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 +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -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 +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +14 -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 +310 -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 +23 -0
- package/common/dependency-reviews.js +81 -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 +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2AFQKRAM.js +51 -0
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KVEUXIEU.js +1 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QZ3A376W.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TY4RZD3Z.js +226 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-U6OPEXIA.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-UZAZM2SX.js +2 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -0
- package/out/chunk-XL35ZV6R.js +1 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- 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 +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +298 -59
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +595 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +156 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +223 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -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 +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -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 +113 -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 +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- 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 +116 -18
- 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 +35 -18
- 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 +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- 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 +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -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 +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -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 +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -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 +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -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 +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +420 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +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}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// How a script's text is read, for risk's readers of a file
|
|
2
|
+
// (readability.js, runs-code.js, hosts.js): JavaScript's and CSS's strings,
|
|
3
|
+
// regular expressions and comments, set apart from their code, what of their
|
|
4
|
+
// blanks a minifier drops, which files are scripts node runs, and each
|
|
5
|
+
// offset's line.
|
|
6
|
+
|
|
7
|
+
// Code minified into lines shorter than NPM_LONG_LINE: outside its strings and
|
|
8
|
+
// comments (`/* @__PURE__ */`), its lines average more than anyone writes and
|
|
9
|
+
// next to none of its spaces are ones a minifier drops. Told only in what
|
|
10
|
+
// minifiers write, as GitHub Linguist tells minified files only in JavaScript
|
|
11
|
+
// and CSS, each by its strings and comments (matched in one pass, so that
|
|
12
|
+
// neither starts inside the other, a comment with the spaces around it) and
|
|
13
|
+
// the spaces it can do without.
|
|
14
|
+
export const MINIFIED_AVERAGE = 110
|
|
15
|
+
const MINIFIED_SPACES = .01
|
|
16
|
+
// What a regular expression set aside holds.
|
|
17
|
+
const REGEX = '\uE000'
|
|
18
|
+
const JS = {
|
|
19
|
+
// A hashbang, the file's first line (`#!/usr/bin/env node`), a comment,
|
|
20
|
+
// after a byte order mark too, which node's ES module loader drops first.
|
|
21
|
+
// A string's or template's escape may be a line continuation (`\` before a
|
|
22
|
+
// line break, CRLF's whole).
|
|
23
|
+
// Linear on any text: a regular expression's context looked back on only
|
|
24
|
+
// before a `/`, a comment's leading blanks taken only from where they
|
|
25
|
+
// start, and what is left open read to where it would end, a string or a
|
|
26
|
+
// regular expression its line's end, a template or a comment the text's,
|
|
27
|
+
// rather than tried again from each later quote or slash.
|
|
28
|
+
// A regular expression first where a value starts (after `=>`, `...`, an operator or a keyword, not a property: `x.default/2`, `this.#in/2`), so a quote in it (`/["']/`) starts no string.
|
|
29
|
+
stringOrComment: /((?=\/)(?<=(?:^|\.\.\.|[(,=:[!&|?{};>+\-*/%^<~]|(?<![\p{ID_Continue}$.#])(?:await|case|default|delete|do|else|extends|in|instanceof|new|of|return|throw|typeof|void|yield))[ \t]*)\/(?![/*])(?:[^/\\[\n]|\\.|\[(?:[^\]\\\n]|\\.)*(?:\]|(?=\\?$)))+(?:\/[a-z]*|(?=\\?$))|"(?:[^"\\\n]|\\(?:\r\n|[\s\S]))*(?:"|$)|'(?:[^'\\\n]|\\(?:\r\n|[\s\S]))*(?:'|$)|`(?:[^`\\]|\\(?:\r\n|[\s\S]))*(?:`|(?![\s\S])))|(?<![ \t])[ \t]*(?:\/\*[\s\S]*?(?:\*\/|(?![\s\S]))|\/\/.*|(?<![\s\S])\uFEFF?#!.*)[ \t]*/gmu,
|
|
30
|
+
// Beside punctuation (`a, b`, `x = 1`), not between two words, as
|
|
31
|
+
// JavaScript tells a word's characters (`return a`, `var π`, `var \u03c0`),
|
|
32
|
+
droppable: /(?<![\p{ID_Continue}$\\\u200C\u200D])[ \t]+|[ \t]+(?![\p{ID_Continue}$\\\u200C\u200D])/gu,
|
|
33
|
+
// nor between two `+`, two `-` or two `/` (`a+ +b` is no `a++b`, `a/ /b/`
|
|
34
|
+
// no comment), nor after a regular expression before a word (`/a/ in b`).
|
|
35
|
+
needed: (code, index, run) => ('+-/'.includes(code[index - 1]) && code[index + run.length] === code[index - 1])
|
|
36
|
+
|| (code[index - 2] === REGEX && /[\p{ID_Continue}$\\\u200C\u200D]/u.test(code[index + run.length])),
|
|
37
|
+
}
|
|
38
|
+
const CSS = {
|
|
39
|
+
// No `//` comments, so `url(https://…)` is code.
|
|
40
|
+
stringOrComment: /("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')|[ \t]*\/\*[\s\S]*?\*\/[ \t]*/gu,
|
|
41
|
+
// Beside braces, `;`, `,`, `>`, parentheses, after `:` (`a { color: red }`)
|
|
42
|
+
// and beside a selector's `+` or `~` (`.a + .b`), not those a selector or a
|
|
43
|
+
// value needs (`.a .b`, `1px solid #fff`, `a :hover`),
|
|
44
|
+
droppable: /(?<=[{};,:>(])[ \t]+|[ \t]+(?=[{};,>)!])|(?<=[+~])[ \t]+|[ \t]+(?=[+~])/gu,
|
|
45
|
+
// nor beside `+` or `~` in parentheses (`calc(1px + var(--x))`) or in a
|
|
46
|
+
// custom property's value, kept for a `calc()` to come (`--gap:1px + 2px`).
|
|
47
|
+
needed: (code, index, run, depth) => /[+~]/u.test(code[index - 1] + code[index + run.length])
|
|
48
|
+
&& (depth > 0 || /^\s*--[^:]*:/u.test(code.slice(Math.max(...['{', ';', '}'].map(end => code.lastIndexOf(end, index))) + 1, index))),
|
|
49
|
+
}
|
|
50
|
+
// JavaScript's strings and regular expressions (the first group) and its
|
|
51
|
+
// comments, for a reader that sets them aside (runs-code.js, hosts.js).
|
|
52
|
+
export const JS_STRING_OR_COMMENT = JS.stringOrComment
|
|
53
|
+
export const minifiable = path => /\.[cm]?js$/iu.test(path) ? JS : /\.css$/iu.test(path) ? CSS : null
|
|
54
|
+
// How much of a text is read for how its lines are minified: their average
|
|
55
|
+
// is told by as much of them, and a text of millions of lines is read no
|
|
56
|
+
// further.
|
|
57
|
+
const MINIFIED_READ = 4 * 1024 * 1024
|
|
58
|
+
// Its code's lines' average length where they are minified into short ones,
|
|
59
|
+
// else 0, told by its first MINIFIED_READ characters.
|
|
60
|
+
export function minifiedAverage(whole, { stringOrComment, droppable, needed }) {
|
|
61
|
+
const text = whole.length > MINIFIED_READ ? whole.slice(0, whole.lastIndexOf('\n', MINIFIED_READ) + 1 || MINIFIED_READ) : whole
|
|
62
|
+
// Its lines of code alone, as long as what is on them: a comment set aside
|
|
63
|
+
// leaves nothing but its line breaks, a string or template `""` and its, a
|
|
64
|
+
// regular expression `/REGEX/`, so the code either side stays apart.
|
|
65
|
+
const lines = text.replaceAll(stringOrComment, (match, string) => (string === undefined ? '' : string[0] === '/' ? `/${REGEX}/` : '""') + match.replaceAll(/[^\n]/gu, ''))
|
|
66
|
+
.replaceAll(/^[ \t]+/gmu, '')
|
|
67
|
+
.split('\n').filter(line => line.trim() !== '')
|
|
68
|
+
const code = lines.join('\n'), length = code.length - (lines.length - 1)
|
|
69
|
+
// Counted by character: a run aligning `=` is as many spaces as it is wide.
|
|
70
|
+
let depth = 0, dropped = 0, scanned = 0
|
|
71
|
+
for (const { 0: run, index } of code.matchAll(droppable)) {
|
|
72
|
+
for (; scanned < index; scanned++) depth = Math.max(0, depth + (code[scanned] === '(' ? 1 : code[scanned] === ')' ? -1 : 0))
|
|
73
|
+
if (!needed(code, index, run, depth)) dropped += run.length
|
|
74
|
+
}
|
|
75
|
+
return lines.length > 0 && length > MINIFIED_AVERAGE * lines.length && dropped < MINIFIED_SPACES * length ? Math.round(length / lines.length) : 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// JavaScript or TypeScript (a declaration file too, which node runs as any
|
|
79
|
+
// other), or a script run by node by its hashbang (`bin/cli`, with no
|
|
80
|
+
// extension), a byte order mark before it or not, as node runs one after it
|
|
81
|
+
// as an ES module (in a `"type": "module"` package).
|
|
82
|
+
const SCRIPT = /\.[cm]?[jt]sx?$/iu
|
|
83
|
+
const NODE_HASHBANG = /^\uFEFF?#!.*\bnode\b/u
|
|
84
|
+
// Whether a file (its path and text) is a script, as SCRIPT and NODE_HASHBANG tell.
|
|
85
|
+
export const npmIsScript = (path, text) => typeof text === 'string' && (SCRIPT.test(path) || NODE_HASHBANG.test(text))
|
|
86
|
+
// How many places a file is told doing a thing, the first ones, at most
|
|
87
|
+
// (runs-code.js, hosts.js): a file doing it on millions of lines is told by
|
|
88
|
+
// its first, and what it holds of them kept to a bound however many there
|
|
89
|
+
// are.
|
|
90
|
+
export const NPM_MAX_FOUND = 100
|
|
91
|
+
// Every how many line breaks one is kept where lineOf counts from.
|
|
92
|
+
const LINE_MARK = 1024
|
|
93
|
+
// Each offset in a text as its line: one more than the line breaks before it,
|
|
94
|
+
// counted on from the nearest of every LINE_MARKth, which alone are kept.
|
|
95
|
+
export function lineOf(text) {
|
|
96
|
+
let marks = null
|
|
97
|
+
return offset => {
|
|
98
|
+
if (!marks) {
|
|
99
|
+
marks = []
|
|
100
|
+
for (let at = text.indexOf('\n'), count = 0; at !== -1; at = text.indexOf('\n', at + 1), count++) if (count % LINE_MARK === 0) marks.push(at)
|
|
101
|
+
}
|
|
102
|
+
let high = marks.length, low = 0
|
|
103
|
+
while (low < high) {
|
|
104
|
+
const middle = (low + high) >> 1
|
|
105
|
+
if (marks[middle] < offset) low = middle + 1
|
|
106
|
+
else high = middle
|
|
107
|
+
}
|
|
108
|
+
if (low === 0) return 1
|
|
109
|
+
let breaks = (low - 1) * LINE_MARK + 1
|
|
110
|
+
for (let at = text.indexOf('\n', marks[low - 1] + 1); at !== -1 && at < offset; at = text.indexOf('\n', at + 1)) breaks++
|
|
111
|
+
return breaks + 1
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// A file's stamps: what risk reads of one file (the readers beside this), and
|
|
2
|
+
// what of it calls for a look, as a list of files tags it and orders it by:
|
|
3
|
+
// each `{ name, severity }`, its tag's text and how severe it is (SEVERITIES).
|
|
4
|
+
import { READABILITY, npmFileReadability, npmLabelMark, npmLargeCode, npmReadabilityLabels, npmTaggedCategory } from './readability.js'
|
|
5
|
+
import { npmExfiltration, npmHostSeverity, npmHostTags, npmHosts } from './hosts.js'
|
|
6
|
+
import { npmRunsCode, npmRunsCodeFlags } from './runs-code.js'
|
|
7
|
+
|
|
8
|
+
// How severe a stamp or a finding is, the most first: `critical` red,
|
|
9
|
+
// `high` a warning's orange, `moderate` yellow, `low`, and a `notice`, under
|
|
10
|
+
// a low one.
|
|
11
|
+
export const SEVERITIES = ['critical', 'high', 'moderate', 'low', 'notice']
|
|
12
|
+
|
|
13
|
+
// A label's mark (readability.js READABILITY) as a severity: a warning's
|
|
14
|
+
// orange, or a notice.
|
|
15
|
+
const severityOf = mark => mark === 'warn' ? 'high' : 'notice'
|
|
16
|
+
|
|
17
|
+
// A file's stamps, from what was read of it: what its code does beyond
|
|
18
|
+
// itself (runs-code.js npmRunsCodeFlags), a warning's or, for the whole
|
|
19
|
+
// environment read out and publishing, critical; the services it names
|
|
20
|
+
// (hosts.js), a stamp a tag, at the tag's severity (Exfiltration critical,
|
|
21
|
+
// Remote high, IP lookup moderate); a large code file, a notice; an
|
|
22
|
+
// obfuscator's fingerprint, a warning's; and its control characters, long
|
|
23
|
+
// lines and category, by their marks, but for what reads as most files do
|
|
24
|
+
// (UTF-8, ASCII) and a source map.
|
|
25
|
+
function npmStampsOf({ path, bytes, readability, runsCode, exfiltration }) {
|
|
26
|
+
const category = npmTaggedCategory(readability)
|
|
27
|
+
const listed = category !== 'ascii' && category !== 'map' && READABILITY.get(category).mark
|
|
28
|
+
return [...npmRunsCodeFlags(runsCode),
|
|
29
|
+
...npmHostTags(exfiltration).map(([tag, services]) => ({ name: tag, severity: npmHostSeverity(tag, services) })),
|
|
30
|
+
...npmLargeCode(path, bytes) ? [{ name: 'Large', severity: 'notice' }] : [],
|
|
31
|
+
...readability.obfuscation ? [{ name: 'Obfuscated', severity: 'high' }] : [],
|
|
32
|
+
...npmReadabilityLabels(readability).filter(label => label === 'controls' || label === 'long')
|
|
33
|
+
.map(label => ({ name: READABILITY.get(label).tag, severity: severityOf(npmLabelMark(readability, label)) })),
|
|
34
|
+
...listed ? [{ name: READABILITY.get(category).tag, severity: severityOf(listed) }] : []]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Everything risk reads of a file (`path`, its `text`, null for one that
|
|
38
|
+
// isn't, and its size in `bytes`): how it reads (npmFileReadability), what
|
|
39
|
+
// its code does beyond itself (npmRunsCode), the services it names under the
|
|
40
|
+
// hosts' tags (npmExfiltration) and the hosts its code names (npmHosts), and its
|
|
41
|
+
// `stamps` (npmStampsOf).
|
|
42
|
+
export function npmFileRisk(path, text, bytes) {
|
|
43
|
+
const readability = npmFileReadability(path, text)
|
|
44
|
+
const exfiltration = npmExfiltration(path, text), runsCode = npmRunsCode(path, text)
|
|
45
|
+
return { readability, runsCode, exfiltration, hosts: npmHosts(path, text), stamps: npmStampsOf({ path, bytes, readability, runsCode, exfiltration }) }
|
|
46
|
+
}
|
package/risk/index.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Hand-written against index.js; a change to either belongs with the other.
|
|
2
|
+
import type { CacheStore } from '@preventive/upstream/npm.js'
|
|
3
|
+
|
|
4
|
+
// How severe a stamp or a finding is, the most first: `critical` red, `high`
|
|
5
|
+
// a warning's orange, `moderate` yellow, `low`, and a `notice`.
|
|
6
|
+
export type Severity = 'critical' | 'high' | 'moderate' | 'low' | 'notice'
|
|
7
|
+
|
|
8
|
+
// The severities, the most severe first.
|
|
9
|
+
export const SEVERITIES: readonly Severity[]
|
|
10
|
+
|
|
11
|
+
// How old a version must be to be trusted unless said: two weeks, in ms.
|
|
12
|
+
export const DEFAULT_MINIMUM_RELEASE_AGE: number
|
|
13
|
+
|
|
14
|
+
// npm has no such package or version (`package-not-found`,
|
|
15
|
+
// `version-not-found`), a tarball unpacks past risk's bounds
|
|
16
|
+
// (`package-too-large`), npm asked to be asked later (`upstream-busy`, a 429,
|
|
17
|
+
// for a caller to wait on and ask again), or npm couldn't be asked
|
|
18
|
+
// (`upstream-unavailable`), answered past them (`upstream-too-large`) or
|
|
19
|
+
// answered what can't be read (`upstream-invalid`). `message` tells which
|
|
20
|
+
// request and why.
|
|
21
|
+
export class NpmRiskError extends Error {
|
|
22
|
+
code: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AssessNpmOptions {
|
|
26
|
+
// The version to compare with; true for the one of the package's versions
|
|
27
|
+
// most likely free of what an attack injects, as risk picks it by npm's
|
|
28
|
+
// document (risk/npm/old-version.js), none where none is below it; without
|
|
29
|
+
// one, everything of the version is new.
|
|
30
|
+
oldVersion?: string | true | null
|
|
31
|
+
// Compare all files with an empty package, for an initial dependency review.
|
|
32
|
+
fromEmpty?: boolean
|
|
33
|
+
// Younger than this, in ms, a version is `too-new`; 0 for none, as a
|
|
34
|
+
// caller's own scopes take. Two weeks unless given.
|
|
35
|
+
minimumReleaseAge?: number
|
|
36
|
+
// Whether the package is flagged as few download it (`popularity`); false
|
|
37
|
+
// for none, as a caller's own scopes take, its downloads then not asked.
|
|
38
|
+
// True unless given.
|
|
39
|
+
popularity?: boolean
|
|
40
|
+
// Where what is read is kept, by type (risk/npm/registry.js and assess.js
|
|
41
|
+
// name each): `npm/tarballs`, `risk/npm/versions`, `risk/npm/packages`,
|
|
42
|
+
// `risk/npm/old-versions`, `risk/npm/public`, `risk/npm/downloads`,
|
|
43
|
+
// `risk/npm/advisories` and `risk/npm/assessments`.
|
|
44
|
+
cache?: CacheStore | false
|
|
45
|
+
// Asks for a package's whole document in risk's stead, as the managed
|
|
46
|
+
// server does within its bounds: null where npm has none for this asker.
|
|
47
|
+
// `authorized` whether it may be asked with the caller's npm token: the
|
|
48
|
+
// package assessed, not its dependencies.
|
|
49
|
+
packument?: (name: string, options: { authorized: boolean; signal: AbortSignal | undefined }) => Promise<Record<string, unknown> | null>
|
|
50
|
+
// Reads a version's tarball, gzipped, in place of upstream's getTarball, as
|
|
51
|
+
// the managed server does within its bounds: `version` the one assessed or
|
|
52
|
+
// `oldVersion`, that one only where its files could change what is found.
|
|
53
|
+
// What it throws is thrown as it is.
|
|
54
|
+
tarball?: (name: string, version: string, options: { signal: AbortSignal | undefined }) => Promise<Uint8Array>
|
|
55
|
+
now?: number
|
|
56
|
+
signal?: AbortSignal
|
|
57
|
+
// Whether npm's downloads are asked for: the package's popularity, and its
|
|
58
|
+
// new dependencies' tiers and ages. True unless given.
|
|
59
|
+
downloads?: boolean
|
|
60
|
+
// Whether its new dependencies' tiers and ages are asked for, a request or
|
|
61
|
+
// two of npm each, where `downloads` is. True unless given.
|
|
62
|
+
dependencies?: boolean
|
|
63
|
+
// Whether npm's advisories of the version are asked for, an advisory of
|
|
64
|
+
// malware naming it a `malware` finding, as its package taken down by npm
|
|
65
|
+
// is. True unless given.
|
|
66
|
+
advisories?: boolean
|
|
67
|
+
// Whether OSV's records of malware in the version are asked for too, where
|
|
68
|
+
// `advisories` are: false for a private package, whose name OSV is never
|
|
69
|
+
// to be sent. True unless given.
|
|
70
|
+
osv?: boolean
|
|
71
|
+
// Whether a package is the caller's own, as its own scopes take: a new
|
|
72
|
+
// dependency on one is told no tier nor age, nor is npm asked of it. None
|
|
73
|
+
// unless given.
|
|
74
|
+
owned?: (name: string) => boolean | Promise<boolean>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// What calls for a look, by `id`, with what it found beside.
|
|
78
|
+
export interface Finding {
|
|
79
|
+
id: string
|
|
80
|
+
severity: Severity
|
|
81
|
+
[found: string]: unknown
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A stamp on a file, as a list of files tags it.
|
|
85
|
+
export interface Stamp {
|
|
86
|
+
name: string
|
|
87
|
+
severity: Severity
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface FileRisk {
|
|
91
|
+
stamps: Stamp[]
|
|
92
|
+
readability: Record<string, unknown>
|
|
93
|
+
runsCode: Record<string, number[]> | null
|
|
94
|
+
exfiltration: Record<string, number[]> | null
|
|
95
|
+
hosts: Record<string, number> | null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DependencyRisk {
|
|
99
|
+
key: string
|
|
100
|
+
name: string
|
|
101
|
+
kind: 'peer' | 'optional' | null
|
|
102
|
+
range?: string
|
|
103
|
+
from?: string
|
|
104
|
+
to?: string
|
|
105
|
+
opens: string | null
|
|
106
|
+
flags: { check: string; label: string; tooltip: string }[]
|
|
107
|
+
tier?: number | null
|
|
108
|
+
level?: string
|
|
109
|
+
// Its last whole month's downloads, by what its tier goes by: none where
|
|
110
|
+
// npm counts none of them.
|
|
111
|
+
month?: { total: number; from: string }
|
|
112
|
+
// Whether npm counts none of its downloads, as of a package published
|
|
113
|
+
// the day before; unpopular all the same.
|
|
114
|
+
uncounted?: true
|
|
115
|
+
created?: string
|
|
116
|
+
gap?: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A version's risk as one number (risk/npm/score.js): what its findings call
|
|
120
|
+
// for (`raised`, each it counts), each by its severity, malware all of it,
|
|
121
|
+
// lowered by its exposure (`lowered`): how long it has been out (`days`)
|
|
122
|
+
// among how many download its package (`tier`, its rank, null past the last
|
|
123
|
+
// or where unknown), `by` the share taken off. Its `level` is named as the
|
|
124
|
+
// findings' severities are: from 75 critical, 50 high, 25 moderate, else low.
|
|
125
|
+
export interface RiskScore {
|
|
126
|
+
score: number
|
|
127
|
+
level: 'critical' | 'high' | 'moderate' | 'low'
|
|
128
|
+
raised: { id: string; severity: Severity }[]
|
|
129
|
+
lowered?: { days: number; tier: number | null; by: number }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface NpmAssessment {
|
|
133
|
+
name: string
|
|
134
|
+
version: string
|
|
135
|
+
// The version compared with, as given or as risk picked it; null for none.
|
|
136
|
+
oldVersion: string | null
|
|
137
|
+
published: string | null
|
|
138
|
+
// A version npm removed, as it removes every one it finds malware in, told
|
|
139
|
+
// of by its `malware` finding alone, with no files nor dependencies.
|
|
140
|
+
removed?: true
|
|
141
|
+
findings: Finding[]
|
|
142
|
+
files: Record<string, FileRisk>
|
|
143
|
+
// Against `oldVersion`, each it holds too, flagged against none (`held`).
|
|
144
|
+
dependencies: { added: DependencyRisk[]; changed: DependencyRisk[]; held?: DependencyRisk[] }
|
|
145
|
+
score: RiskScore
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function assessNpm(name: string, version: string, options?: AssessNpmOptions): Promise<NpmAssessment>
|
|
149
|
+
|
|
150
|
+
// Whether an advisory is of malware rather than a vulnerability: OSV's of a
|
|
151
|
+
// malicious package (`MAL-…`), or one such a record names (its `aliases`),
|
|
152
|
+
// CWE-506, embedded malicious code, whatever its title, or a title naming
|
|
153
|
+
// malware.
|
|
154
|
+
export function npmMalwareAdvisory(advisory: { id?: string | undefined; aliases?: readonly string[] | undefined; cwe?: readonly string[] | undefined; title?: string | undefined }): boolean
|
|
155
|
+
|
|
156
|
+
export function npmFileRisk(path: string, text: string | null, bytes?: number): unknown
|
package/risk/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// What calls for a look at a package version, for a reviewer: risk's
|
|
2
|
+
// heuristics, apart from any view of them. See README.md.
|
|
3
|
+
export { assessNpm } from './npm/assess.js'
|
|
4
|
+
export { npmMalwareAdvisory } from './npm/malware.js'
|
|
5
|
+
export { DEFAULT_MINIMUM_RELEASE_AGE } from './npm/release.js'
|
|
6
|
+
export { NpmRiskError } from './npm/registry.js'
|
|
7
|
+
export { SEVERITIES, npmFileRisk } from './files/stamps.js'
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// A package version's risk, as the managed server asks it: what calls for a
|
|
2
|
+
// look at it, against the version before it where one is given or risk picks
|
|
3
|
+
// one (old-version.js), each file's stamps, and its dependencies', from npm's
|
|
4
|
+
// registry, kept in the caller's cache (registry.js) as it goes.
|
|
5
|
+
import { npmBaseFilesTell, npmCompareFindings } from './compare.js'
|
|
6
|
+
import { npmCombinedFindings, npmHeldDependencyFindings } from './combined.js'
|
|
7
|
+
import { npmHeldFindings, npmVersionFindings } from './held.js'
|
|
8
|
+
import { npmVersionFileRisk } from './files.js'
|
|
9
|
+
import { npmDependencyChanges, npmFlagDependencyChanges } from './dependencies.js'
|
|
10
|
+
import { npmDownloadMonths, npmDownloadTier, npmSettledDownloads, npmTierLevel } from './downloads.js'
|
|
11
|
+
import { NpmRiskError, npmDownloads, npmPackageFacts, npmPackageReleases, npmPublicFacts, npmVersionAdvisories, npmVersionFiles } from './registry.js'
|
|
12
|
+
import { npmMalwareFinding } from './malware.js'
|
|
13
|
+
import { npmKnownScope } from './manifest.js'
|
|
14
|
+
import { DEFAULT_MINIMUM_RELEASE_AGE, npmAccountStanding } from './release.js'
|
|
15
|
+
import { npmAddedDependency } from './new-dependency.js'
|
|
16
|
+
import { npmRiskScore } from './score.js'
|
|
17
|
+
|
|
18
|
+
// Which reading of a version an assessment kept (`risk/npm/assessments`) is:
|
|
19
|
+
// one more whenever a heuristic changes what it finds, so one kept by an
|
|
20
|
+
// older reading is read anew.
|
|
21
|
+
const RISK_VERSION = 14
|
|
22
|
+
|
|
23
|
+
// How long an assessment kept may be as JSON, and what risk reads of either
|
|
24
|
+
// version's files: a package past it, whose files name thousands of hosts or
|
|
25
|
+
// do what they run on thousands of lines each, is refused as too large as its
|
|
26
|
+
// files are read, before more of them is held (filesRisk), and the assessment
|
|
27
|
+
// once told, rather than kept and answered. A package's files tell a few
|
|
28
|
+
// hundred characters each, a few MiB for the most files npm's viewer reads.
|
|
29
|
+
const MAX_ASSESSMENT_LENGTH = 16 * 1024 * 1024
|
|
30
|
+
const tooLarge = what => new NpmRiskError('package-too-large', `what risk tells of ${what} is past ${MAX_ASSESSMENT_LENGTH} characters`)
|
|
31
|
+
|
|
32
|
+
// A value as JSON holds it: a Map as an object, its keys as strings.
|
|
33
|
+
const plain = value => value instanceof Map ? Object.fromEntries([...value].map(([key, item]) => [key, plain(item)]))
|
|
34
|
+
: Array.isArray(value) ? value.map(plain)
|
|
35
|
+
: value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)])) : value
|
|
36
|
+
|
|
37
|
+
// A tier among npm's packages as an assessment says it: `{ tier, level }`, the
|
|
38
|
+
// rank a package is among (null past the last, unpopular) and its tint
|
|
39
|
+
// (downloads.js npmTierLevel).
|
|
40
|
+
const tierOf = tier => ({ tier: Number.isFinite(tier) ? tier : null, level: npmTierLevel(tier) })
|
|
41
|
+
|
|
42
|
+
// Each of a version's files' stamps and what risk read of it (stamps.js
|
|
43
|
+
// npmFileRisk), by path, as JSON holds them: read a file at a time and given
|
|
44
|
+
// up on, as too large, once they are past MAX_ASSESSMENT_LENGTH as JSON
|
|
45
|
+
// altogether, so that no more of them is held than that.
|
|
46
|
+
function filesRisk(version, what) {
|
|
47
|
+
let length = 0
|
|
48
|
+
return Object.fromEntries(version.json.sources.map(path => {
|
|
49
|
+
const { readability, runsCode, exfiltration, hosts, stamps } = npmVersionFileRisk(version, path)
|
|
50
|
+
const risk = plain({ stamps, readability, runsCode, exfiltration, hosts })
|
|
51
|
+
length += path.length + JSON.stringify(risk).length
|
|
52
|
+
if (length > MAX_ASSESSMENT_LENGTH) throw tooLarge(what)
|
|
53
|
+
return [path, risk]
|
|
54
|
+
}))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// What of an assessment holds whatever the time, of the open version
|
|
58
|
+
// (`other`) against the one compared with (`base`), or alone without one,
|
|
59
|
+
// each as compare.js takes a version, as JSON holds it: what it holds,
|
|
60
|
+
// always (held.js npmHeldFindings, and its dependencies against none,
|
|
61
|
+
// combined.js npmHeldDependencyFindings), and against `base` what its files,
|
|
62
|
+
// dependencies, install scripts, publisher and provenance call for anew
|
|
63
|
+
// (compare.js npmCompareFindings), folded into what it holds, never lowering
|
|
64
|
+
// it (combined.js npmCombinedFindings); each file's stamps and what risk read
|
|
65
|
+
// of it (filesRisk); and its dependencies added and changed, each with its
|
|
66
|
+
// flags (dependencies.js), against `base` each it holds too (`held`). Its own findings are told fresh
|
|
67
|
+
// (npmAssessmentOf). The managed server's assessments keep it (`lasting`),
|
|
68
|
+
// and a test can tell what one says of versions it holds. Each publisher's
|
|
69
|
+
// standing on the package is told where `standing` is given (release.js
|
|
70
|
+
// npmAccountStanding, of the open version).
|
|
71
|
+
export const npmLastingRisk = (other, base = null, standing = null) => lastingOf(other, base, filesRisk(other, `${other.npm.name}@${other.npm.version}`), standing)
|
|
72
|
+
|
|
73
|
+
// npmLastingRisk's, the open version's files read within bounds (`files`,
|
|
74
|
+
// filesRisk's), as the one compared with's are where `base` has them, before
|
|
75
|
+
// the findings read them all.
|
|
76
|
+
function lastingOf(other, base, files, standing) {
|
|
77
|
+
if (base?.json) filesRisk(base, `${base.npm.name}@${base.npm.version}`)
|
|
78
|
+
const all = npmFlagDependencyChanges(npmDependencyChanges(undefined, other.npm.manifest), null, other)
|
|
79
|
+
const held = [...npmHeldFindings(other), ...npmHeldDependencyFindings(all)]
|
|
80
|
+
const { added, changed } = base ? npmFlagDependencyChanges(npmDependencyChanges(base.npm.manifest, other.npm.manifest), base, other) : all
|
|
81
|
+
const findings = base ? npmCombinedFindings(held, npmCompareFindings(base, other, { added, changed }, standing)) : held
|
|
82
|
+
const kept = { findings: plain(findings), files, dependencies: plain({ added, changed, ...base ? { held: all.added } : {} }) }
|
|
83
|
+
if (JSON.stringify(kept).length > MAX_ASSESSMENT_LENGTH) throw tooLarge(`${other.npm.name}@${other.npm.version}`)
|
|
84
|
+
return kept
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// What of an assessment holds whatever the time (npmLastingRisk), kept once a
|
|
88
|
+
// reading and pair, its versions' files read from npm only where it isn't:
|
|
89
|
+
// the open version's first, then the one compared with's only where they
|
|
90
|
+
// could change what is found (compare.js npmBaseFilesTell), that one else
|
|
91
|
+
// told by what npm's document of it says alone; and where another account
|
|
92
|
+
// published it, each one's standing on the package (release.js
|
|
93
|
+
// npmAccountStanding), by its versions as `facts` read them, or as its
|
|
94
|
+
// document is read again where they were kept. A standing changes as npm
|
|
95
|
+
// removes a version, its publisher then unknown, and never else, so such a
|
|
96
|
+
// pair is kept by how many npm has removed (`~<removals>`), read anew as it
|
|
97
|
+
// removes another.
|
|
98
|
+
async function lasting(name, version, oldVersion, facts, context, fromEmpty) {
|
|
99
|
+
const old = oldVersion === null ? null : facts.versions.get(oldVersion)
|
|
100
|
+
const publishers = [old, facts.versions.get(version)].map(fact => fact?.manifest.publisher)
|
|
101
|
+
const changed = publishers.every(Boolean) && publishers[0] !== publishers[1]
|
|
102
|
+
const key = `${RISK_VERSION}/${name}@${version}${fromEmpty ? '..(empty)' : oldVersion === null ? '' : `..${oldVersion}`}${changed ? `~${facts.removals}` : ''}`
|
|
103
|
+
const hit = context.cache ? await context.cache.read('risk/npm/assessments', key) : null
|
|
104
|
+
if (hit) return hit
|
|
105
|
+
const other = await npmVersionFiles(name, version, facts.versions.get(version), context)
|
|
106
|
+
const files = filesRisk(other, `${name}@${version}`)
|
|
107
|
+
const base = fromEmpty ? { npm: { name, version: '', manifest: {} }, json: { sources: [], sourcesContent: [] } } : old === null ? null : npmBaseFilesTell(other, old.manifest) ? await npmVersionFiles(name, oldVersion, old, context)
|
|
108
|
+
: { npm: { name, version: oldVersion, manifest: old.manifest, published: old.published }, json: null }
|
|
109
|
+
const standing = changed ? npmAccountStanding(facts.releases ?? await npmPackageReleases(name, context), other.npm.published) : null
|
|
110
|
+
const kept = lastingOf(other, base, files, standing)
|
|
111
|
+
if (context.cache) await context.cache.write('risk/npm/assessments', key, kept)
|
|
112
|
+
return kept
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// How many dependencies added are told of at most, however many a version
|
|
116
|
+
// adds, as each costs npm a request or two: the rest are left as they are,
|
|
117
|
+
// their packages unknown.
|
|
118
|
+
const MAX_TOLD_DEPENDENCIES = 100
|
|
119
|
+
|
|
120
|
+
// Each dependency `added` that npm installs from its registry, up to
|
|
121
|
+
// MAX_TOLD_DEPENDENCIES of them, but for the caller's own packages (`owned`,
|
|
122
|
+
// as its own scopes take, few downloading them, as new as its releases),
|
|
123
|
+
// with what risk tells of its package
|
|
124
|
+
// (new-dependency.js npmAddedDependency): its tier, its last whole month's
|
|
125
|
+
// downloads by what its tier goes by where it has one (`month`, `{ total,
|
|
126
|
+
// from }`), `uncounted` where npm counts none of them, and `created` and `gap`
|
|
127
|
+
// where it is new since the version compared with (`published`, when that
|
|
128
|
+
// and the open version were); four asked at a time, whether each is the
|
|
129
|
+
// caller's own among them, none more once one fails or MAX_TOLD_DEPENDENCIES
|
|
130
|
+
// not the caller's own were taken, however many a version adds.
|
|
131
|
+
async function addedDependencies(added, published, context, owned) {
|
|
132
|
+
const told = [...added]
|
|
133
|
+
const asked = told.flatMap((row, at) => row.opens ? [at] : [])
|
|
134
|
+
let next = 0, taken = 0
|
|
135
|
+
const tell = async row => {
|
|
136
|
+
const downloads = await npmDownloads(row.opens, context)
|
|
137
|
+
let facts = npmAddedDependency({ downloads, published }, context.now)
|
|
138
|
+
if (facts.wanted) {
|
|
139
|
+
const listed = await npmPublicFacts(row.opens, context)
|
|
140
|
+
facts = npmAddedDependency({ downloads, listed: listed && { ...listed, private: false }, published }, context.now)
|
|
141
|
+
}
|
|
142
|
+
const settled = npmSettledDownloads(downloads, context.now)
|
|
143
|
+
const month = npmDownloadMonths(settled).at(-1)
|
|
144
|
+
return { ...row, ...facts.tier === null ? {} : { ...tierOf(facts.tier), ...settled ? month && { month: { total: month.total, from: month.from.toISOString().slice(0, 10) } } : { uncounted: true } },
|
|
145
|
+
...facts.created ? { created: facts.created, gap: facts.gap } : {} }
|
|
146
|
+
}
|
|
147
|
+
await Promise.all(Array.from({ length: 4 }, async () => {
|
|
148
|
+
while (next < asked.length && taken < MAX_TOLD_DEPENDENCIES) {
|
|
149
|
+
const at = asked[next++]
|
|
150
|
+
try {
|
|
151
|
+
if (await owned(told[at].opens)) continue
|
|
152
|
+
if (taken >= MAX_TOLD_DEPENDENCIES) break
|
|
153
|
+
taken++
|
|
154
|
+
told[at] = await tell(told[at])
|
|
155
|
+
} catch (error) { next = asked.length; throw error }
|
|
156
|
+
}
|
|
157
|
+
}))
|
|
158
|
+
return told
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// What calls for a look at `version` of package `name`, as JSON holds it:
|
|
162
|
+
// malware first (malware.js npmMalwareFinding: its package taken down by
|
|
163
|
+
// npm, and npm's advisories of malware naming it where `advisories`, true
|
|
164
|
+
// unless given false, has them asked, OSV's too where `osv` is, true unless
|
|
165
|
+
// given false, as a private package's name is never sent it); against `oldVersion` where given,
|
|
166
|
+
// true for the one of its versions most likely free of what an attack
|
|
167
|
+
// injects, as risk picks it by npm's document (old-version.js npmOldVersion;
|
|
168
|
+
// none where none is below it), the one it picked said as `oldVersion`
|
|
169
|
+
// (compare.js npmCompareFindings's order), else what it holds (held.js
|
|
170
|
+
// npmHeldFindings), a deprecation first, and last
|
|
171
|
+
// one younger than `minimumReleaseAge` (two weeks unless given; 0 for none, as
|
|
172
|
+
// a caller's own scopes take), and the package's tier past the top 20,000
|
|
173
|
+
// (`popularity`, held.js npmVersionFindings, unless `popularity` is given
|
|
174
|
+
// false, as a caller's own scopes take); `files`, each file's stamps and what risk read of it; and
|
|
175
|
+
// `dependencies`, those added and changed against it, each with its flags,
|
|
176
|
+
// an added one with its package's tier and age where `downloads` (true unless
|
|
177
|
+
// given false) has them asked, and `dependencies` (true unless given false)
|
|
178
|
+
// too, but where `owned` says its package is the caller's own, as its own scopes
|
|
179
|
+
// take (none unless given). `cache` keeps what is read (registry.js),
|
|
180
|
+
// `packument` asks for a package's whole document and `tarball` for a
|
|
181
|
+
// version's tarball in risk's stead (registry.js), `now` is when it is asked, `signal` gives up on what is
|
|
182
|
+
// still asked of npm. A version npm removed, as it removes every one it finds
|
|
183
|
+
// malware in, is told of by its malware alone (`removed`), nothing of it left
|
|
184
|
+
// to read. Throws an NpmRiskError (registry.js), its `code` saying why, where
|
|
185
|
+
// npm has no such package or version (nor malware explaining one removed),
|
|
186
|
+
// answers past risk's bounds, or can't be asked.
|
|
187
|
+
export async function assessNpm(name, version, { oldVersion = null, fromEmpty = false, minimumReleaseAge = DEFAULT_MINIMUM_RELEASE_AGE, popularity = true, cache, packument, tarball, now = Date.now(), signal, downloads = true, dependencies = true, advisories = true, osv = true, owned = () => false } = {}) {
|
|
188
|
+
const context = { cache, packument, tarball, now, signal }
|
|
189
|
+
const facts = await npmPackageFacts(name, version, oldVersion, context)
|
|
190
|
+
const old = facts.oldVersion
|
|
191
|
+
if (old !== null && !facts.versions.has(old)) throw new NpmRiskError('version-not-found', `npm has no ${name}@${old}`)
|
|
192
|
+
const malware = npmMalwareFinding({ takenDown: facts.takenDown, advisories: advisories ? await npmVersionAdvisories(name, version, osv, context) : [] })
|
|
193
|
+
if (!facts.versions.has(version)) {
|
|
194
|
+
if (!malware) throw new NpmRiskError('version-not-found', `npm has no ${name}@${version}`)
|
|
195
|
+
return { name, version, oldVersion: null, published: facts.removed.get(version), removed: true, findings: [malware], files: {}, dependencies: { added: [], changed: [] },
|
|
196
|
+
score: npmRiskScore([malware]) }
|
|
197
|
+
}
|
|
198
|
+
const { published, manifest } = facts.versions.get(version)
|
|
199
|
+
const kept = await lasting(name, version, old, facts, context, fromEmpty)
|
|
200
|
+
const counted = downloads && popularity ? await npmDownloads(name, context) : null
|
|
201
|
+
const own = npmVersionFindings({ deprecated: facts.deprecated[version], published, minimumReleaseAge, now, known: Boolean(manifest.knownAccounts), scope: npmKnownScope(name),
|
|
202
|
+
provenance: Boolean(manifest.provenance), downloads: counted })
|
|
203
|
+
const was = old === null ? null : facts.versions.get(old).published
|
|
204
|
+
const added = downloads && dependencies ? await addedDependencies(kept.dependencies.added, [was, published], context, owned) : kept.dependencies.added
|
|
205
|
+
const assessment = npmAssessmentOf(kept, malware ? [malware, ...own] : own, added)
|
|
206
|
+
return { name, version, oldVersion: old, published, ...assessment, score: npmRiskScore(assessment.findings, { published, now, tier: counted ? npmDownloadTier(counted) : null }) }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// An assessment of what of it holds whatever the time (`kept`,
|
|
210
|
+
// npmLastingRisk's) and what is told fresh: the open version's own findings
|
|
211
|
+
// (`own`, held.js npmVersionFindings, and its malware), its malware and
|
|
212
|
+
// deprecation first and the rest last, its tier as JSON holds it (tierOf),
|
|
213
|
+
// and its dependencies `added`, as addedDependencies tells them. The managed
|
|
214
|
+
// server's (assessNpm), and a test's of versions it holds.
|
|
215
|
+
const FIRST = new Set(['malware', 'deprecated'])
|
|
216
|
+
export function npmAssessmentOf(kept, own, added = kept.dependencies.added) {
|
|
217
|
+
return {
|
|
218
|
+
findings: [...own.filter(({ id }) => FIRST.has(id)), ...kept.findings,
|
|
219
|
+
...own.filter(({ id }) => !FIRST.has(id)).map(finding => finding.id === 'popularity' ? { ...finding, ...tierOf(finding.tier) } : finding)],
|
|
220
|
+
files: kept.files,
|
|
221
|
+
dependencies: { ...kept.dependencies, added },
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// What calls for a look at a version against the one compared with, as one
|
|
2
|
+
// list: what it holds, always (held.js npmHeldFindings, and its dependencies
|
|
3
|
+
// against none, npmHeldDependencyFindings), as no change hides any of it; and
|
|
4
|
+
// what changed (compare.js npmCompareFindings), each change to what it holds
|
|
5
|
+
// folded into its held finding, never lowering it: its severity the higher of
|
|
6
|
+
// the two, the files, scripts or dependencies the change brought marked
|
|
7
|
+
// (`added`); the rest (a publisher changed, provenance lost, large code
|
|
8
|
+
// added, a file become unreadable, hosts named anew) as they are.
|
|
9
|
+
import { SEVERITIES } from '../files/stamps.js'
|
|
10
|
+
import { HOST_TAGS } from '../files/hosts.js'
|
|
11
|
+
import { npmDependencyFindings } from './compare.js'
|
|
12
|
+
import { npmHostFinding } from './held.js'
|
|
13
|
+
|
|
14
|
+
// A version's dependencies that call for a look whatever came before it
|
|
15
|
+
// (compare.js npmDependencyFindings against none, `dependencies` flagged
|
|
16
|
+
// against none): `held-non-registry-deps` and `held-unused-deps`.
|
|
17
|
+
export const npmHeldDependencyFindings = dependencies => npmDependencyFindings(dependencies, null).map(finding => ({ ...finding, id: `held-${finding.id}` }))
|
|
18
|
+
|
|
19
|
+
// The more severe of two severities (stamps.js SEVERITIES).
|
|
20
|
+
const higher = (a, b) => SEVERITIES[Math.min(SEVERITIES.indexOf(a), SEVERITIES.indexOf(b))]
|
|
21
|
+
|
|
22
|
+
// What a change to what a version holds marks of its held finding's rows,
|
|
23
|
+
// by the change's id: a file's what it runs, the services it names under a
|
|
24
|
+
// hosts' tag, and an obfuscator's fingerprint new to it (`added`); the
|
|
25
|
+
// scripts added or changed and what each version ran (`flagged`, `was`); a
|
|
26
|
+
// dependency taken from elsewhere than npm's registry anew, its spec before
|
|
27
|
+
// (`was`), or added that nothing imports (`added`). Code kept running beyond
|
|
28
|
+
// itself (`kept-runs-code`) marks nothing, its files held already.
|
|
29
|
+
const byPath = (rows, key) => new Map(rows.map(row => [row.path, row[key]]))
|
|
30
|
+
const marked = (held, rows) => ({ ...held, files: held.files.map(row => rows.has(row.path) ? { ...row, added: rows.get(row.path) } : row) })
|
|
31
|
+
const FOLDS = {
|
|
32
|
+
'runs-code': ['held-runs-code', (held, change) => marked(held, byPath(change.files, 'added'))],
|
|
33
|
+
'kept-runs-code': ['held-runs-code', held => held],
|
|
34
|
+
obfuscated: ['held-obfuscated', (held, change) => marked(held, byPath(change.files, 'obfuscation'))],
|
|
35
|
+
'install-scripts': ['held-install-scripts', (held, change) => ({ ...held, flagged: change.flagged, was: [...change.was] })],
|
|
36
|
+
'non-registry-deps': ['held-non-registry-deps', (held, change) => {
|
|
37
|
+
const was = new Map(change.dependencies.map(row => [row.name, row.was]))
|
|
38
|
+
return { ...held, dependencies: held.dependencies.map(row => was.has(row.name) ? { ...row, was: was.get(row.name), added: true } : row) }
|
|
39
|
+
}],
|
|
40
|
+
'unused-deps': ['held-unused-deps', (held, change) => {
|
|
41
|
+
const added = new Set(change.dependencies.map(row => `${row.kind ?? ''}\0${row.name}`))
|
|
42
|
+
return { ...held, dependencies: held.dependencies.map(row => added.has(`${row.kind ?? ''}\0${row.name}`) ? { ...row, added: true } : row) }
|
|
43
|
+
}],
|
|
44
|
+
...Object.fromEntries([...HOST_TAGS.keys()].map(npmHostFinding).map(id => [id, [`held-${id}`, (held, change) => marked(held, byPath(change.files, 'added'))]])),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Who published the open version and how, ahead of what it holds, as a
|
|
48
|
+
// reviewer reads them; what else changed after it.
|
|
49
|
+
const FIRST = new Set(['publisher', 'provenance-lost'])
|
|
50
|
+
|
|
51
|
+
// What a version holds (`held`) with what changed against the one compared
|
|
52
|
+
// with (`changed`) folded in, as one list in the order a reviewer reads it: a
|
|
53
|
+
// publisher changed and provenance lost, what it holds, then the rest of what
|
|
54
|
+
// changed. A change whose held finding is missing is kept as it is.
|
|
55
|
+
export function npmCombinedFindings(held, changed) {
|
|
56
|
+
const combined = new Map(held.map(finding => [finding.id, finding]))
|
|
57
|
+
const rest = []
|
|
58
|
+
for (const change of changed) {
|
|
59
|
+
const [id, fold] = FOLDS[change.id] ?? []
|
|
60
|
+
const into = id && combined.get(id)
|
|
61
|
+
if (into) combined.set(id, { ...fold(into, change), severity: higher(into.severity, change.severity) })
|
|
62
|
+
else rest.push(change)
|
|
63
|
+
}
|
|
64
|
+
return [...rest.filter(({ id }) => FIRST.has(id)), ...combined.values(), ...rest.filter(({ id }) => !FIRST.has(id))]
|
|
65
|
+
}
|