@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50

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