@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.
Files changed (312) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2AFQKRAM.js +51 -0
  72. package/out/chunk-2KBF5TZ7.js +2 -0
  73. package/out/chunk-2XUEZHIP.js +28 -0
  74. package/out/chunk-35VE6CDH.js +2 -0
  75. package/out/chunk-3BZ7JERB.js +1 -0
  76. package/out/chunk-4F6JSF57.js +3 -0
  77. package/out/chunk-4XCDFGDP.js +10 -0
  78. package/out/chunk-C4Z6ZBXV.js +10 -0
  79. package/out/chunk-CMQA4DM6.js +1 -0
  80. package/out/chunk-CVVJAXVY.js +2 -0
  81. package/out/chunk-DIJJNTRH.js +10 -0
  82. package/out/chunk-GJ4IUCNS.js +1 -0
  83. package/out/chunk-GNXOKIGP.js +80 -0
  84. package/out/chunk-GZ4MS3TE.js +13 -0
  85. package/out/chunk-IU3CCINR.js +3 -0
  86. package/out/chunk-K3X5EJBQ.js +2 -0
  87. package/out/chunk-KEX2ATEW.js +2 -0
  88. package/out/chunk-KVEUXIEU.js +1 -0
  89. package/out/chunk-KYCNJVB7.js +1 -0
  90. package/out/chunk-NRPC74AM.js +1 -0
  91. package/out/chunk-OOJM4CTU.js +1 -0
  92. package/out/chunk-PNHBZGDD.js +1 -0
  93. package/out/chunk-QZ3A376W.js +1 -0
  94. package/out/chunk-R5O6FAW4.js +2 -0
  95. package/out/chunk-RFZQQXSG.js +17 -0
  96. package/out/chunk-RSHMVYNS.js +1 -0
  97. package/out/chunk-TUII34K2.js +1 -0
  98. package/out/chunk-TY4RZD3Z.js +226 -0
  99. package/out/chunk-TZ5KB2Y5.js +10 -0
  100. package/out/chunk-U6OPEXIA.js +1 -0
  101. package/out/chunk-UNTRGE26.js +2 -0
  102. package/out/chunk-UXNUAIIC.js +33 -0
  103. package/out/chunk-UYAXVRVL.js +1 -0
  104. package/out/chunk-UZAZM2SX.js +2 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XL35ZV6R.js +1 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +298 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +595 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +156 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +223 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/score.js +59 -0
  154. package/risk/npm/specs.js +124 -0
  155. package/risk/npm/tarball.js +113 -0
  156. package/server-common/database-config.ts +16 -0
  157. package/server-common/initialize.ts +18 -0
  158. package/server-common/npm-advisories.ts +101 -0
  159. package/{server → server-common}/origin.ts +5 -5
  160. package/server-common/reap.ts +48 -0
  161. package/server-common/scan-config.ts +19 -0
  162. package/server-common/standalone.ts +29 -0
  163. package/server-common/storage-crypto.ts +182 -0
  164. package/server-common/storage-log.ts +38 -0
  165. package/server-common/vercel-blob.ts +103 -0
  166. package/server-e2e/app.ts +486 -0
  167. package/{server → server-e2e}/auth.ts +5 -1
  168. package/{server → server-e2e}/bus-receiver.ts +2 -1
  169. package/{server → server-e2e}/cli.js +9 -4
  170. package/{server → server-e2e}/config.ts +56 -35
  171. package/{server → server-e2e}/db-neon.ts +2 -2
  172. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  173. package/{server → server-e2e}/db-stmt.ts +2 -2
  174. package/{server → server-e2e}/db.ts +7 -7
  175. package/{server → server-e2e}/http.ts +116 -18
  176. package/{server → server-e2e}/hub.ts +40 -9
  177. package/server-e2e/index.ts +17 -0
  178. package/server-e2e/lifecycle.ts +95 -0
  179. package/{server → server-e2e}/neon-driver.ts +2 -2
  180. package/{server → server-e2e}/npm-proxy.ts +12 -145
  181. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  182. package/{server → server-e2e}/objstore/blob.ts +1 -1
  183. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  184. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  185. package/{server → server-e2e}/objstore/init.ts +35 -18
  186. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  187. package/server-e2e/objstore/rest-deny.ts +28 -0
  188. package/server-e2e/objstore/rest-mint.ts +224 -0
  189. package/{server → server-e2e}/objstore/rest.ts +27 -26
  190. package/{server → server-e2e}/objstore/sign.ts +105 -0
  191. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  192. package/{server → server-e2e}/objstore/store.ts +7 -7
  193. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  194. package/{server → server-e2e}/pubsub.ts +12 -9
  195. package/{server → server-e2e}/sign.ts +4 -4
  196. package/{server → server-e2e}/sse-server.ts +102 -68
  197. package/{server → server-e2e}/sse-session.ts +28 -8
  198. package/{server → server-e2e}/static.ts +31 -21
  199. package/server-e2e/sync-handlers.ts +408 -0
  200. package/{server → server-e2e}/ws-server.ts +20 -9
  201. package/server-managed/activity.ts +245 -0
  202. package/server-managed/avatar-store.ts +12 -0
  203. package/server-managed/blob-store.ts +17 -0
  204. package/server-managed/brotli.ts +10 -0
  205. package/server-managed/bundle-advisories.ts +120 -0
  206. package/server-managed/bundle-advisory-inventory.ts +77 -0
  207. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  208. package/server-managed/bundle-build-diagnostics.js +23 -0
  209. package/server-managed/bundle-build-leases.ts +41 -0
  210. package/server-managed/bundle-build-worker.js +111 -0
  211. package/server-managed/bundle-build.ts +198 -0
  212. package/server-managed/bundle-cache.ts +199 -0
  213. package/server-managed/bundle-catalog.ts +29 -0
  214. package/server-managed/bundle-commits.ts +140 -0
  215. package/server-managed/bundle-store.ts +24 -0
  216. package/server-managed/bundle-summary-cache.ts +130 -0
  217. package/server-managed/bundle.ts +120 -0
  218. package/server-managed/cache-storage.ts +19 -0
  219. package/server-managed/cli.js +21 -0
  220. package/server-managed/combined.ts +46 -0
  221. package/server-managed/comments.ts +151 -0
  222. package/server-managed/config.ts +158 -0
  223. package/server-managed/content-access.ts +15 -0
  224. package/server-managed/crypto.ts +25 -0
  225. package/server-managed/db-methods.ts +1725 -0
  226. package/server-managed/db-neon.ts +367 -0
  227. package/server-managed/db-schema.ts +227 -0
  228. package/server-managed/db-table-names.ts +22 -0
  229. package/server-managed/db.ts +132 -0
  230. package/server-managed/dependency-reviews.ts +248 -0
  231. package/server-managed/github-app.ts +391 -0
  232. package/server-managed/github-errors.ts +68 -0
  233. package/server-managed/github-issue-links.ts +127 -0
  234. package/server-managed/github-issue-oauth.ts +47 -0
  235. package/server-managed/github-issues.ts +152 -0
  236. package/server-managed/github-metadata.ts +213 -0
  237. package/server-managed/github-oauth.ts +235 -0
  238. package/server-managed/github-pulls.ts +227 -0
  239. package/server-managed/http-response.ts +50 -0
  240. package/server-managed/http.ts +3472 -0
  241. package/server-managed/import-triage.ts +51 -0
  242. package/server-managed/index.ts +187 -0
  243. package/server-managed/kept.ts +28 -0
  244. package/server-managed/link-reports.ts +158 -0
  245. package/server-managed/locked-packages.ts +85 -0
  246. package/server-managed/lru.ts +30 -0
  247. package/server-managed/managed-issues.ts +75 -0
  248. package/server-managed/management-catalog.ts +45 -0
  249. package/server-managed/management.ts +99 -0
  250. package/server-managed/npm-insights.ts +440 -0
  251. package/server-managed/npm-kept.ts +65 -0
  252. package/server-managed/npm-loads.ts +167 -0
  253. package/server-managed/npm-manifest.ts +115 -0
  254. package/server-managed/npm-packages.ts +378 -0
  255. package/server-managed/npm-provenance.ts +123 -0
  256. package/server-managed/npm-risk.ts +177 -0
  257. package/server-managed/object-storage-disk.ts +148 -0
  258. package/server-managed/object-storage-vercel.ts +67 -0
  259. package/server-managed/object-storage.ts +74 -0
  260. package/server-managed/package-entry-points.ts +51 -0
  261. package/server-managed/pretty-print.ts +352 -0
  262. package/server-managed/public-workspace.ts +220 -0
  263. package/server-managed/rename-diff-worker.js +26 -0
  264. package/server-managed/rename-diff.ts +270 -0
  265. package/server-managed/repo-path.ts +21 -0
  266. package/server-managed/report-migration.ts +35 -0
  267. package/server-managed/report-query.ts +10 -0
  268. package/server-managed/report-response.ts +16 -0
  269. package/server-managed/report-sources.ts +178 -0
  270. package/server-managed/report-store.ts +47 -0
  271. package/server-managed/repository-aliases.ts +91 -0
  272. package/server-managed/repository-browser.ts +153 -0
  273. package/server-managed/repository-discovery.ts +82 -0
  274. package/server-managed/repository-policy.ts +27 -0
  275. package/server-managed/repository-risk.ts +420 -0
  276. package/server-managed/revisions.ts +35 -0
  277. package/server-managed/session.ts +92 -0
  278. package/server-managed/slugs.ts +38 -0
  279. package/server-managed/solidity-entry-points.ts +69 -0
  280. package/server-managed/sql-postgres.ts +31 -0
  281. package/server-managed/sql.ts +76 -0
  282. package/server-managed/static.ts +28 -0
  283. package/server-managed/storage-command.ts +16 -0
  284. package/server-managed/storage-db.ts +175 -0
  285. package/server-managed/storage-encryption.ts +279 -0
  286. package/server-managed/storage-maintenance.ts +256 -0
  287. package/server-managed/storage-payload.ts +103 -0
  288. package/server-managed/storage-stores.ts +85 -0
  289. package/server-managed/storage.ts +78 -0
  290. package/server-managed/team-app.ts +94 -0
  291. package/server-managed/team-catalog.ts +7 -0
  292. package/server-managed/team-feed.ts +173 -0
  293. package/server-managed/team-npm-scopes.ts +77 -0
  294. package/server-managed/team-reports.ts +228 -0
  295. package/server-managed/triage-response.ts +16 -0
  296. package/server-managed/type-only-worker.js +19 -0
  297. package/server-managed/type-only.ts +103 -0
  298. package/server-managed/uploads.ts +86 -0
  299. package/server-managed/upstream-cache.ts +215 -0
  300. package/server-managed/view-as.ts +58 -0
  301. package/server-managed/workspace-shares.ts +166 -0
  302. package/server.ts +50 -0
  303. package/server/index.ts +0 -477
  304. package/server/lifecycle.ts +0 -204
  305. package/server/sync-handlers.ts +0 -320
  306. /package/{LICENSE → LICENSE-MIT} +0 -0
  307. /package/{server → server-e2e}/config.example.json +0 -0
  308. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  309. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  310. /package/{server → server-e2e}/peer.ts +0 -0
  311. /package/{server → server-e2e}/util.ts +0 -0
  312. /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
+ }
@@ -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
+ }