@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,285 @@
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, npmUnusedDependencyCounted } 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, npmUnusedDependencySeverity } from './new-dependency.js'
16
+ import { SEVERITIES } from '../files/stamps.js'
17
+ import { npmRiskScore } from './score.js'
18
+
19
+ // Which reading of a version an assessment kept (`risk/npm/assessments`) is:
20
+ // one more whenever a heuristic changes what it finds, so one kept by an
21
+ // older reading is read anew.
22
+ const RISK_VERSION = 18
23
+
24
+ // How long an assessment kept may be as JSON, and what risk reads of either
25
+ // version's files: a package past it, whose files name thousands of hosts or
26
+ // do what they run on thousands of lines each, is refused as too large as its
27
+ // files are read, before more of them is held (filesRisk), and the assessment
28
+ // once told, rather than kept and answered. A package's files tell a few
29
+ // hundred characters each, a few MiB for the most files npm's viewer reads.
30
+ const MAX_ASSESSMENT_LENGTH = 16 * 1024 * 1024
31
+ const tooLarge = what => new NpmRiskError('package-too-large', `what risk tells of ${what} is past ${MAX_ASSESSMENT_LENGTH} characters`)
32
+
33
+ // A value as JSON holds it: a Map as an object, its keys as strings.
34
+ const plain = value => value instanceof Map ? Object.fromEntries([...value].map(([key, item]) => [key, plain(item)]))
35
+ : Array.isArray(value) ? value.map(plain)
36
+ : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)])) : value
37
+
38
+ // A tier among npm's packages as an assessment says it: `{ tier, level }`, the
39
+ // rank a package is among (null past the last, unpopular) and its tint
40
+ // (downloads.js npmTierLevel).
41
+ const tierOf = tier => ({ tier: Number.isFinite(tier) ? tier : null, level: npmTierLevel(tier) })
42
+
43
+ // Each of a version's files' stamps and what risk read of it (stamps.js
44
+ // npmFileRisk), by path, as JSON holds them: read a file at a time and given
45
+ // up on, as too large, once they are past MAX_ASSESSMENT_LENGTH as JSON
46
+ // altogether, so that no more of them is held than that.
47
+ function filesRisk(version, what) {
48
+ let length = 0
49
+ return Object.fromEntries(version.json.sources.map(path => {
50
+ const { readability, runsCode, exfiltration, hosts, stamps } = npmVersionFileRisk(version, path)
51
+ const risk = plain({ stamps, readability, runsCode, exfiltration, hosts })
52
+ length += path.length + JSON.stringify(risk).length
53
+ if (length > MAX_ASSESSMENT_LENGTH) throw tooLarge(what)
54
+ return [path, risk]
55
+ }))
56
+ }
57
+
58
+ // What of an assessment holds whatever the time, of the open version
59
+ // (`other`) against the one compared with (`base`), or alone without one,
60
+ // each as compare.js takes a version, as JSON holds it: what it holds,
61
+ // always (held.js npmHeldFindings, and its dependencies against none,
62
+ // combined.js npmHeldDependencyFindings), and against `base` what its files,
63
+ // dependencies, install scripts, publisher and provenance call for anew
64
+ // (compare.js npmCompareFindings), folded into what it holds, never lowering
65
+ // it (combined.js npmCombinedFindings); each file's stamps and what risk read
66
+ // of it (filesRisk); and its dependencies added and changed, each with its
67
+ // flags (dependencies.js), against `base` each it holds too (`held`). Its own findings are told fresh
68
+ // (npmAssessmentOf). The managed server's assessments keep it (`lasting`),
69
+ // and a test can tell what one says of versions it holds. Each publisher's
70
+ // standing on the package is told where `standing` is given (release.js
71
+ // npmAccountStanding, of the open version).
72
+ export const npmLastingRisk = (other, base = null, standing = null) => lastingOf(other, base, filesRisk(other, `${other.npm.name}@${other.npm.version}`), standing)
73
+
74
+ // npmLastingRisk's, the open version's files read within bounds (`files`,
75
+ // filesRisk's), as the one compared with's are where `base` has them, before
76
+ // the findings read them all.
77
+ function lastingOf(other, base, files, standing) {
78
+ if (base?.json) filesRisk(base, `${base.npm.name}@${base.npm.version}`)
79
+ const all = npmFlagDependencyChanges(npmDependencyChanges(undefined, other.npm.manifest), null, other)
80
+ const held = [...npmHeldFindings(other), ...npmHeldDependencyFindings(all)]
81
+ const { added, changed } = base ? npmFlagDependencyChanges(npmDependencyChanges(base.npm.manifest, other.npm.manifest), base, other) : all
82
+ const findings = base ? npmCombinedFindings(held, npmCompareFindings(base, other, { added, changed }, standing)) : held
83
+ const kept = { findings: plain(findings), files, dependencies: plain({ added, changed, ...base ? { held: all.added } : {} }) }
84
+ if (JSON.stringify(kept).length > MAX_ASSESSMENT_LENGTH) throw tooLarge(`${other.npm.name}@${other.npm.version}`)
85
+ return kept
86
+ }
87
+
88
+ // What of an assessment holds whatever the time (npmLastingRisk), kept once a
89
+ // reading and pair, its versions' files read from npm only where it isn't:
90
+ // the open version's first, then the one compared with's only where they
91
+ // could change what is found (compare.js npmBaseFilesTell), that one else
92
+ // told by what npm's document of it says alone; and where another account
93
+ // published it, each one's standing on the package (release.js
94
+ // npmAccountStanding), by its versions as `facts` read them, or as its
95
+ // document is read again where they were kept. A standing changes as npm
96
+ // removes a version, its publisher then unknown, and never else, so such a
97
+ // pair is kept by how many npm has removed (`~<removals>`), read anew as it
98
+ // removes another.
99
+ async function lasting(name, version, oldVersion, facts, context, fromEmpty) {
100
+ const old = oldVersion === null ? null : facts.versions.get(oldVersion)
101
+ const publishers = [old, facts.versions.get(version)].map(fact => fact?.manifest.publisher)
102
+ const changed = publishers.every(Boolean) && publishers[0] !== publishers[1]
103
+ const key = `${RISK_VERSION}/${name}@${version}${fromEmpty ? '..(empty)' : oldVersion === null ? '' : `..${oldVersion}`}${changed ? `~${facts.removals}` : ''}`
104
+ const hit = context.cache ? await context.cache.read('risk/npm/assessments', key) : null
105
+ if (hit) return hit
106
+ const other = await npmVersionFiles(name, version, facts.versions.get(version), context)
107
+ const files = filesRisk(other, `${name}@${version}`)
108
+ const base = fromEmpty ? { npm: { name, version: '', manifest: {} }, json: { sources: [], sourcesContent: [] } } : old === null ? null : npmBaseFilesTell(other, old.manifest) ? await npmVersionFiles(name, oldVersion, old, context)
109
+ : { npm: { name, version: oldVersion, manifest: old.manifest, published: old.published }, json: null }
110
+ const standing = changed ? npmAccountStanding(facts.releases ?? await npmPackageReleases(name, context), other.npm.published) : null
111
+ const kept = lastingOf(other, base, files, standing)
112
+ if (context.cache) await context.cache.write('risk/npm/assessments', key, kept)
113
+ return kept
114
+ }
115
+
116
+ // How many dependencies added are told of at most, however many a version
117
+ // adds, as each costs npm a request or two: the rest are left as they are,
118
+ // their packages unknown.
119
+ const MAX_TOLD_DEPENDENCIES = 100
120
+
121
+ // Each dependency `added` that npm installs from its registry, up to
122
+ // MAX_TOLD_DEPENDENCIES of them, but for the caller's own packages (`owned`,
123
+ // as its own scopes take, few downloading them, as new as its releases),
124
+ // with what risk tells of its package
125
+ // (new-dependency.js npmAddedDependency): its tier, its last whole month's
126
+ // downloads by what its tier goes by where it has one (`month`, `{ total,
127
+ // from }`), `uncounted` where npm counts none of them, `created` and `gap`
128
+ // where it is new since the version compared with (`published`, when that
129
+ // and the open version were), and for one nothing imports (flagged so, or
130
+ // among `unusedKeys`) that risk counts (dependencies.js
131
+ // npmUnusedDependencyCounted), the version its spec installs (`installs`,
132
+ // registry.js npmPublicFacts); `missing` where
133
+ // its package was looked up and npm has none of it for anyone, as once
134
+ // unpublished or taken down; four asked at a time, whether each is the
135
+ // caller's own among them, none more once one fails or MAX_TOLD_DEPENDENCIES
136
+ // not the caller's own were taken, however many a version adds.
137
+ async function addedDependencies(added, published, context, owned, unusedKeys = new Set()) {
138
+ const told = [...added]
139
+ const asked = told.flatMap((row, at) => row.opens ? [at] : [])
140
+ let next = 0, taken = 0
141
+ const tell = async row => {
142
+ const downloads = await npmDownloads(row.opens, context)
143
+ const unused = (row.flags.some(flag => flag.check === 'unimported') || unusedKeys.has(keyOf(row))) && npmUnusedDependencyCounted(row)
144
+ let facts = npmAddedDependency({ downloads, published, unused }, context.now), missing = false
145
+ if (facts.wanted) {
146
+ const listed = await npmPublicFacts(row.opens, context, unused ? row.range : null)
147
+ missing = listed === null
148
+ facts = npmAddedDependency({ downloads, listed: listed && { ...listed, private: false }, published, unused }, context.now)
149
+ }
150
+ const settled = npmSettledDownloads(downloads, context.now)
151
+ const month = npmDownloadMonths(settled).at(-1)
152
+ return { ...row, ...facts.tier === null ? {} : { ...tierOf(facts.tier), ...settled ? month && { month: { total: month.total, from: month.from.toISOString().slice(0, 10) } } : { uncounted: true } },
153
+ ...facts.created ? { created: facts.created, gap: facts.gap } : {}, ...facts.installs ? { installs: facts.installs } : {}, ...missing ? { missing } : {} }
154
+ }
155
+ await Promise.all(Array.from({ length: 4 }, async () => {
156
+ while (next < asked.length && taken < MAX_TOLD_DEPENDENCIES) {
157
+ const at = asked[next++]
158
+ try {
159
+ if (await owned(told[at].opens)) continue
160
+ if (taken >= MAX_TOLD_DEPENDENCIES) break
161
+ taken++
162
+ told[at] = await tell(told[at])
163
+ } catch (error) { next = asked.length; throw error }
164
+ }
165
+ }))
166
+ return told
167
+ }
168
+
169
+ // What calls for a look at `version` of package `name`, as JSON holds it:
170
+ // malware first (malware.js npmMalwareFinding: its package taken down by
171
+ // npm, and npm's advisories of malware naming it where `advisories`, true
172
+ // unless given false, has them asked, OSV's too where `osv` is, true unless
173
+ // given false, as a private package's name is never sent it); against `oldVersion` where given,
174
+ // true for the one of its versions most likely free of what an attack
175
+ // injects, as risk picks it by npm's document (old-version.js npmOldVersion;
176
+ // none where none is below it), the one it picked said as `oldVersion`
177
+ // (compare.js npmCompareFindings's order), else what it holds (held.js
178
+ // npmHeldFindings), a deprecation first, and last
179
+ // one younger than `minimumReleaseAge` (two weeks unless given; 0 for none, as
180
+ // a caller's own scopes take), and the package's tier past the top 20,000
181
+ // (`popularity`, held.js npmVersionFindings, unless `popularity` is given
182
+ // false, as a caller's own scopes take); `files`, each file's stamps and what risk read of it; and
183
+ // `dependencies`, those added and changed against it, each with its flags,
184
+ // an added one with its package's tier and age where `downloads` (true unless
185
+ // given false) has them asked, and `dependencies` (true unless given false)
186
+ // too, but where `owned` says its package is the caller's own, as its own scopes
187
+ // take (none unless given). `cache` keeps what is read (registry.js),
188
+ // `packument` asks for a package's whole document and `tarball` for a
189
+ // version's tarball in risk's stead (registry.js), `now` is when it is asked, `signal` gives up on what is
190
+ // still asked of npm. A version npm removed, as it removes every one it finds
191
+ // malware in, is told of by its malware alone (`removed`), nothing of it left
192
+ // to read. Throws an NpmRiskError (registry.js), its `code` saying why, where
193
+ // npm has no such package or version (nor malware explaining one removed),
194
+ // answers past risk's bounds, or can't be asked.
195
+ 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 } = {}) {
196
+ const context = { cache, packument, tarball, now, signal }
197
+ const facts = await npmPackageFacts(name, version, oldVersion, context)
198
+ const old = facts.oldVersion
199
+ if (old !== null && !facts.versions.has(old)) throw new NpmRiskError('version-not-found', `npm has no ${name}@${old}`)
200
+ // Its advisories, its lasting risk and its downloads need none of each
201
+ // other, so all are asked at once, and waited on in this order, so that a
202
+ // failure fails it as the first of them failing would.
203
+ const listed = facts.versions.has(version)
204
+ const advised = settledLater(advisories ? npmVersionAdvisories(name, version, osv, context) : Promise.resolve([]))
205
+ const keeping = listed ? settledLater(lasting(name, version, old, facts, context, fromEmpty)) : null
206
+ const counting = listed && downloads && popularity ? settledLater(npmDownloads(name, context)) : null
207
+ const malware = npmMalwareFinding({ takenDown: facts.takenDown, advisories: await advised })
208
+ if (!listed) {
209
+ if (!malware) throw new NpmRiskError('version-not-found', `npm has no ${name}@${version}`)
210
+ return { name, version, oldVersion: null, published: facts.removed.get(version), removed: true, findings: [malware], files: {}, dependencies: { added: [], changed: [] },
211
+ score: npmRiskScore([malware]) }
212
+ }
213
+ const { published, manifest } = facts.versions.get(version)
214
+ const kept = await keeping
215
+ const counted = await counting
216
+ const own = npmVersionFindings({ deprecated: facts.deprecated[version], published, minimumReleaseAge, now, known: Boolean(manifest.knownAccounts), scope: npmKnownScope(name),
217
+ provenance: Boolean(manifest.provenance), downloads: counted })
218
+ const was = old === null ? null : facts.versions.get(old).published
219
+ // Those it holds that nothing imports by another spec than the version
220
+ // compared with's, told by their new spec as an added one nothing imports
221
+ // is (`respecified`), and an added one nothing imports however it was
222
+ // flagged, as one moved from another kind of dependency is.
223
+ const unusedKeys = unusedKeysOf(kept)
224
+ const respecified = kept.dependencies.changed.filter(row => unusedKeys.has(keyOf(row))).map(row => ({ ...row, range: row.to }))
225
+ const told = downloads && dependencies ? await addedDependencies([...kept.dependencies.added, ...respecified], [was, published], context, owned, unusedKeys)
226
+ : [...kept.dependencies.added, ...respecified]
227
+ const added = told.slice(0, kept.dependencies.added.length)
228
+ const assessment = npmAssessmentOf(kept, malware ? [malware, ...own] : own, added, told.slice(added.length))
229
+ return { name, version, oldVersion: old, published, ...assessment, score: npmRiskScore(assessment.findings, { published, now, tier: counted ? npmDownloadTier(counted) : null }) }
230
+ }
231
+
232
+ // A dependency's key among a version's, by its kind and name.
233
+ const keyOf = row => `${row.kind ?? ''}\0${row.name}`
234
+
235
+ // `promise`, waited on later: its failure, if it fails before then, not
236
+ // thrown as unhandled meanwhile.
237
+ const settledLater = promise => { promise.catch(() => {}); return promise }
238
+
239
+ // The keys of the dependencies a version holds that nothing of it imports
240
+ // (`kept`, npmLastingRisk's: its `held-unused-deps`), those risk counts
241
+ // (dependencies.js npmUnusedDependencyCounted).
242
+ const unusedKeysOf = kept => new Set((kept.findings.find(({ id }) => id === 'held-unused-deps')?.dependencies ?? []).filter(npmUnusedDependencyCounted).map(keyOf))
243
+
244
+ // The dependencies nothing of a version imports (`held-unused-deps`), each
245
+ // graded by what risk tells of its package fresh (`told`, as
246
+ // addedDependencies tells it; new-dependency.js
247
+ // npmUnusedDependencySeverity), with its tier, when its package was first
248
+ // published and the version its spec installs where told: kept from the
249
+ // version compared with where there was one (`compared`) and it named it as
250
+ // the same kind of dependency by the same spec: not one marked new, nor
251
+ // `added` (its kind and name new, as one moved from another kind is), nor
252
+ // `changed` (each said with the spec it had, `was`); one risk doesn't count
253
+ // (dependencies.js npmUnusedDependencyCounted) a notice, told all the same.
254
+ // The finding as its most severe.
255
+ function unusedGraded(finding, told, compared, { added, changed }) {
256
+ const facts = new Map(told.map(row => [keyOf(row), row])), was = new Map(changed.map(row => [keyOf(row), row.from]))
257
+ const anew = new Set(added.map(keyOf))
258
+ const dependencies = finding.dependencies.map(row => {
259
+ const of = facts.get(keyOf(row))
260
+ return { ...row, ...was.has(keyOf(row)) ? { was: was.get(keyOf(row)) } : {}, severity: npmUnusedDependencyCounted(row)
261
+ ? npmUnusedDependencySeverity(of, compared && !row.added && !anew.has(keyOf(row)) && !was.has(keyOf(row))) : 'notice',
262
+ ...of?.level ? { tier: of.tier, level: of.level } : {}, ...of?.created ? { created: of.created, gap: of.gap } : {}, ...of?.installs ? { installs: of.installs } : {},
263
+ ...of?.missing ? { missing: true } : {} }
264
+ })
265
+ return { ...finding, severity: SEVERITIES[Math.min(...dependencies.map(row => SEVERITIES.indexOf(row.severity)))], dependencies }
266
+ }
267
+
268
+ // An assessment of what of it holds whatever the time (`kept`,
269
+ // npmLastingRisk's) and what is told fresh: the open version's own findings
270
+ // (`own`, held.js npmVersionFindings, and its malware), its malware and
271
+ // deprecation first and the rest last, its tier as JSON holds it (tierOf),
272
+ // and its dependencies `added`, as addedDependencies tells them, those it
273
+ // holds by another spec that nothing imports beside them (`respecified`),
274
+ // for its unused dependencies' grades. The managed
275
+ // server's (assessNpm), and a test's of versions it holds.
276
+ const FIRST = new Set(['malware', 'deprecated'])
277
+ export function npmAssessmentOf(kept, own, added = kept.dependencies.added, respecified = []) {
278
+ return {
279
+ findings: [...own.filter(({ id }) => FIRST.has(id)), ...kept.findings.map(finding => finding.id === 'held-unused-deps'
280
+ ? unusedGraded(finding, [...added, ...respecified], Boolean(kept.dependencies.held), kept.dependencies) : finding),
281
+ ...own.filter(({ id }) => !FIRST.has(id)).map(finding => finding.id === 'popularity' ? { ...finding, ...tierOf(finding.tier) } : finding)],
282
+ files: kept.files,
283
+ dependencies: { ...kept.dependencies, added },
284
+ }
285
+ }
@@ -0,0 +1,68 @@
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 or reaching for credentials (`kept-runs-code`, `kept-credentials`)
29
+ // marks nothing, its files held already.
30
+ const byPath = (rows, key) => new Map(rows.map(row => [row.path, row[key]]))
31
+ const marked = (held, rows) => ({ ...held, files: held.files.map(row => rows.has(row.path) ? { ...row, added: rows.get(row.path) } : row) })
32
+ const FOLDS = {
33
+ 'runs-code': ['held-runs-code', (held, change) => marked(held, byPath(change.files, 'added'))],
34
+ 'kept-runs-code': ['held-runs-code', held => held],
35
+ credentials: ['held-credentials', (held, change) => marked(held, byPath(change.files, 'added'))],
36
+ 'kept-credentials': ['held-credentials', held => held],
37
+ obfuscated: ['held-obfuscated', (held, change) => marked(held, byPath(change.files, 'obfuscation'))],
38
+ 'install-scripts': ['held-install-scripts', (held, change) => ({ ...held, flagged: change.flagged, was: [...change.was] })],
39
+ 'non-registry-deps': ['held-non-registry-deps', (held, change) => {
40
+ const was = new Map(change.dependencies.map(row => [row.name, row.was]))
41
+ return { ...held, dependencies: held.dependencies.map(row => was.has(row.name) ? { ...row, was: was.get(row.name), added: true } : row) }
42
+ }],
43
+ 'unused-deps': ['held-unused-deps', (held, change) => {
44
+ const added = new Set(change.dependencies.map(row => `${row.kind ?? ''}\0${row.name}`))
45
+ return { ...held, dependencies: held.dependencies.map(row => added.has(`${row.kind ?? ''}\0${row.name}`) ? { ...row, added: true } : row) }
46
+ }],
47
+ ...Object.fromEntries([...HOST_TAGS.keys()].map(npmHostFinding).map(id => [id, [`held-${id}`, (held, change) => marked(held, byPath(change.files, 'added'))]])),
48
+ }
49
+
50
+ // Who published the open version and how, ahead of what it holds, as a
51
+ // reviewer reads them; what else changed after it.
52
+ const FIRST = new Set(['publisher', 'provenance-lost'])
53
+
54
+ // What a version holds (`held`) with what changed against the one compared
55
+ // with (`changed`) folded in, as one list in the order a reviewer reads it: a
56
+ // publisher changed and provenance lost, what it holds, then the rest of what
57
+ // changed. A change whose held finding is missing is kept as it is.
58
+ export function npmCombinedFindings(held, changed) {
59
+ const combined = new Map(held.map(finding => [finding.id, finding]))
60
+ const rest = []
61
+ for (const change of changed) {
62
+ const [id, fold] = FOLDS[change.id] ?? []
63
+ const into = id && combined.get(id)
64
+ if (into) combined.set(id, { ...fold(into, change), severity: higher(into.severity, change.severity) })
65
+ else rest.push(change)
66
+ }
67
+ return [...rest.filter(({ id }) => FIRST.has(id)), ...combined.values(), ...rest.filter(({ id }) => !FIRST.has(id))]
68
+ }
@@ -0,0 +1,257 @@
1
+ // What calls for a look at a package version against the one before it, as
2
+ // findings: `{ id, severity, ... }` each (stamps.js SEVERITIES), what it found
3
+ // beside. Each version as risk holds one (`{ kind: 'sourcemap', npm, json,
4
+ // fileSizes? }`: npm's facts, `npm.manifest` as manifest.js reads a version's
5
+ // document, its files as a source map lists them, `json.sources` and
6
+ // `json.sourcesContent`, a text or null each, and their sizes by path where
7
+ // known), and `files` how they differ (diff.js npmFilesDiff): `onlyOther`, the
8
+ // files the open version adds, and `changed`, those it changes, each `{ path,
9
+ // renamedFrom?, modified? }`, by its path in the open version.
10
+ import { bundleFileSizes, bundleFilesAsMap } from '../../common/bundle-sources.js'
11
+ import { npmLoadedScripts, npmVersionFileRisk, npmVersionHosts } from './files.js'
12
+ import { npmHostFinding } from './held.js'
13
+ import { HOST_TAGS, npmHostSeverity, npmHostsTagged } from '../files/hosts.js'
14
+ import { npmBecameUnreadable, npmBinaryCode, npmLargeCode, npmUnreadable } from '../files/readability.js'
15
+ import { npmIsScript } from '../files/script.js'
16
+ import { SEVERITIES } from '../files/stamps.js'
17
+ import { CREDENTIAL_TAGS, npmFoundAdded, npmRunsCodeFlags, npmRunsCodeTagged, npmRunsCodeWithout } from '../files/runs-code.js'
18
+ import { npmDependencySpecs } from './dependencies.js'
19
+ import { npmFilesDiff } from './diff.js'
20
+ import { npmInstallScripts } from './install-scripts.js'
21
+ import { npmInstallScriptsChange, npmProvenanceLost, npmPublisherChange } from './release.js'
22
+
23
+ // The files the open version (`other`) runs as scripts that the one compared
24
+ // with (`base`) held, changed or not, but didn't run, by their names
25
+ // (script.js npmIsScript, a `.js` renamed `.txt`) or as code loaded them
26
+ // (npm/files.js npmLoadedScripts), as its code now loads them, each a row as npmFilesDiff's
27
+ // `changed` holds one, its own where it has one: what node runs of them is
28
+ // new, as a changed file's is.
29
+ function npmWokenFiles(base, other, files) {
30
+ const added = new Set(files.onlyOther.map(row => row.path)), rows = new Map(files.changed.map(row => [row.path, row]))
31
+ const before = bundleFilesAsMap(base), loadedBefore = npmLoadedScripts(base)
32
+ // A file that isn't text by its name alone (`payload.js`).
33
+ const ran = path => loadedBefore.has(path) || (before.has(path) && npmIsScript(path, typeof before.get(path) === 'string' ? before.get(path) : ''))
34
+ return [...npmLoadedScripts(other)].filter(path => !added.has(path)).map(path => rows.get(path) ?? { path })
35
+ .filter(row => !ran(row.renamedFrom ?? row.path))
36
+ }
37
+
38
+ // What the open version does to its files that calls for a look, as red
39
+ // findings: code that runs beyond itself (runs-code.js), new to a file it
40
+ // adds or changes, the kinds with their lines (`runs-code`), those reaching
41
+ // for credentials apart (CREDENTIAL_TAGS, `credentials`); a service under
42
+ // one of the hosts' tags new to a file it adds or changes (hosts.js
43
+ // npmExfiltration), a finding a tag a level higher than its own
44
+ // (npmHostSeverity: `exfiltration` and `remote` red, `ip-lookup` orange); a
45
+ // code file of 256 KiB or more it adds, one node runs as a script whatever
46
+ // its name too (npmLargeCode, `large-code`); a file
47
+ // it changes that had no line over 200 characters and now has one over 2,000
48
+ // (npmBecameUnreadable, `now-unreadable`); a file it adds or changes with
49
+ // an obfuscator's fingerprint where the one compared with had none
50
+ // (`obfuscated`); and a code file it adds or changes that isn't text, where
51
+ // the one compared with's was text, or no code, or none (npmBinaryCode,
52
+ // `binary-code`). Then in orange what any of its files runs beyond itself that
53
+ // `runs-code` doesn't name, kept from the version compared with
54
+ // (`kept-runs-code`, `kept-credentials`). A renamed file by its old path; one renamed alone,
55
+ // unchanged, holds what it held. A file become unreadable calls for less
56
+ // where its source map was rebuilt with it (`rebuilt`, rebuiltWithMap), as a
57
+ // build minifies a file and writes its map together, and in a `major`
58
+ // release (npmMajorRelease): `now-unreadable` orange where every such file is
59
+ // either, else red. A file it held, changed or not, that node now runs as a
60
+ // script (npmWokenFiles) counts as one it changes, large code where it is
61
+ // one, and unreadable now where it wasn't as no script, whatever its lines
62
+ // were.
63
+ function npmFileFindings(base, other, files, major) {
64
+ const added = new Set(files.onlyOther)
65
+ const after = row => npmVersionFileRisk(other, row.path)
66
+ const before = row => added.has(row) ? null : npmVersionFileRisk(base, row.renamedFrom ?? row.path)
67
+ const changed = files.changed.filter(row => row.renamedFrom == null || row.modified)
68
+ const woken = npmWokenFiles(base, other, files)
69
+ const touched = [...new Set([...files.onlyOther, ...changed, ...woken])]
70
+ const sizes = bundleFileSizes(other)
71
+ // The files that hold what they didn't before, each with that.
72
+ const gained = key => touched.map(row => ({ path: row.path, added: npmFoundAdded(after(row)[key], before(row)?.[key] ?? null) })).filter(row => row.added)
73
+ const exfiltration = gained('exfiltration'), runsCode = gained('runsCode')
74
+ const flagged = new Set(runsCode.flatMap(row => npmRunsCodeFlags(row.added).map(({ name }) => name)))
75
+ const kept = [...bundleFilesAsMap(other).keys()].map(path => ({ path, found: npmRunsCodeWithout(npmVersionFileRisk(other, path).runsCode, flagged) })).filter(row => row.found)
76
+ // Code that runs beyond itself, or of the kinds reaching for credentials.
77
+ const ofKinds = (rows, key, of) => rows.map(row => ({ ...row, [key]: of(row[key], CREDENTIAL_TAGS) })).filter(row => row[key])
78
+ return [
79
+ { id: 'runs-code', severity: 'critical', files: ofKinds(runsCode, 'added', npmRunsCodeWithout) },
80
+ { id: 'credentials', severity: 'critical', files: ofKinds(runsCode, 'added', npmRunsCodeTagged) },
81
+ ...[...HOST_TAGS.keys()].map(tag => {
82
+ const rows = exfiltration.map(row => ({ path: row.path, added: npmHostsTagged(row.added, tag) })).filter(row => row.added)
83
+ return { id: npmHostFinding(tag), severity: rows.length > 0 ? npmHostSeverity(tag, rows[0].added, null) : HOST_TAGS.get(tag), files: rows }
84
+ }),
85
+ { id: 'large-code', severity: 'critical', files: [...files.onlyOther, ...woken].filter(row => npmLargeCode(row.path, sizes.get(row.path), npmLoadedScripts(other).has(row.path)))
86
+ .map(row => ({ path: row.path, bytes: sizes.get(row.path) })) },
87
+ { id: 'now-unreadable', ...unreadableFinding(major, [...new Set([...changed.filter(row => npmBecameUnreadable(row.path, before(row)?.readability, after(row).readability, npmLoadedScripts(other).has(row.path))),
88
+ ...woken.filter(row => npmUnreadable(row.path, after(row).readability, true) && !npmUnreadable(row.renamedFrom ?? row.path, before(row).readability))])]
89
+ .map(row => ({ path: row.path, was: before(row).readability.longest, now: after(row).readability.longest,
90
+ ...rebuiltWithMap(base, other, row.path, row.renamedFrom ?? row.path) ? { rebuilt: true } : {} }))) },
91
+ { id: 'obfuscated', severity: 'critical', files: touched.filter(row => after(row).readability.obfuscation && !before(row)?.readability.obfuscation)
92
+ .map(row => ({ path: row.path, obfuscation: after(row).readability.obfuscation })) },
93
+ { id: 'binary-code', severity: 'critical', files: touched.filter(row => npmBinaryCode(row.path, after(row).readability, npmLoadedScripts(other).has(row.path))
94
+ && !npmBinaryCode(row.renamedFrom ?? row.path, before(row)?.readability, npmLoadedScripts(base).has(row.renamedFrom ?? row.path)))
95
+ .map(row => ({ path: row.path, bytes: sizes.get(row.path) })) },
96
+ { id: 'kept-runs-code', severity: 'high', files: ofKinds(kept, 'found', npmRunsCodeWithout) },
97
+ { id: 'kept-credentials', severity: 'high', files: ofKinds(kept, 'found', npmRunsCodeTagged) },
98
+ ].filter(finding => finding.files.length > 0)
99
+ }
100
+
101
+ // Hosts whose naming anew calls for no look: where packages keep their code
102
+ // and docs, as their links and error messages name them, the most a version
103
+ // names anew of popular packages a year apart (lit.dev, Lit's), and none
104
+ // whose content anyone may publish (a wiki's raw pages) but github.com's
105
+ // pages, as hosts.js githubNamed names them: a repository's own URL, its
106
+ // files and releases are named by it. Each as it is written, not a host
107
+ // under it, so one an attack sends to or fetches from there
108
+ // (`api.github.com`, `raw.githubusercontent.com`) is still named; and no link
109
+ // shortener (`git.io`, `git.new`), which can lead anywhere.
110
+ const ALLOWED_HOSTS = new Set(['github.com', 'lit.dev', 'developer.mozilla.org', 'json-schema.org', 'nodejs.org', 'babeljs.io', 'tseslint.com',
111
+ 'devblogs.microsoft.com', 'semver.org'])
112
+
113
+ // A severity one level lower (stamps.js SEVERITIES).
114
+ const lower = severity => SEVERITIES[Math.min(SEVERITIES.indexOf(severity) + 1, SEVERITIES.length - 1)]
115
+
116
+ // Whether the open version (`now`) is a major release past the one compared
117
+ // with (`was`): its major number higher, or its minor for 0.x. A rewrite
118
+ // changes how a package's files read and who publishes it, and npm's `^`
119
+ // ranges don't take a major release by themselves, as a hijacked release
120
+ // wants them to.
121
+ function npmMajorRelease(was, now) {
122
+ const [a, b] = [was, now].map(version => /^v?(\d+)\.(\d+)\./u.exec(version ?? '')?.slice(1).map(Number))
123
+ return Boolean(a && b) && (b[0] > a[0] || (a[0] === 0 && b[0] === 0 && b[1] > a[1]))
124
+ }
125
+
126
+ // Whether the open version's file at `path` (`basePath` in the one compared
127
+ // with) had its source map rebuilt with it: its last line, a
128
+ // `sourceMappingURL` comment and no more, names a map file of the package by
129
+ // a plain path from the file's own (MAP_NAME), a source map
130
+ // (its `mappings`, or each of an indexed map's `sections`, told), new or
131
+ // changed from the one at its path, or beside the file's own before it was
132
+ // renamed, as a build writes them together. An attack editing a built file
133
+ // leaves its map as it was.
134
+ const SOURCE_MAP_FILE = /^\s*(?:\/\/[#@] sourceMappingURL=(\S+)|\/\*[#@] sourceMappingURL=(\S+?)\s*\*\/)\s*$/u
135
+ // A map name a build writes (`a.js.map`, `../maps/a.map`): segments of
136
+ // letters, digits and `._~@+-`, as a browser reads them alike. One at a URL,
137
+ // from a site's root, or with an escape or a backslash names none.
138
+ const MAP_NAME = /^(?:[\w.~@+-]+\/)*[\w.~@+-]+\.map$/u
139
+ function besideFile(file, name) {
140
+ const parts = file.split('/').slice(0, -1)
141
+ for (const part of name.split('/')) {
142
+ if (part === '..' && parts.length === 0) return null
143
+ if (part === '..') parts.pop()
144
+ else if (part !== '.') parts.push(part)
145
+ }
146
+ return parts.join('/')
147
+ }
148
+ function rebuiltWithMap(base, other, path, basePath) {
149
+ const text = bundleFilesAsMap(other).get(path)
150
+ const directive = typeof text === 'string' ? SOURCE_MAP_FILE.exec(text.trimEnd().split('\n').at(-1)) : null
151
+ const name = directive?.[1] ?? directive?.[2]
152
+ if (!name || !MAP_NAME.test(name)) return false
153
+ const [map, was] = [bundleFilesAsMap(other).get(besideFile(path, name)), bundleFilesAsMap(base)]
154
+ return isSourceMap(map) && map !== was.get(besideFile(path, name)) && map !== was.get(besideFile(basePath, name))
155
+ }
156
+ const isSourceMap = text => {
157
+ try {
158
+ const json = JSON.parse(text)
159
+ return typeof json?.mappings === 'string'
160
+ || (Array.isArray(json?.sections) && json.sections.length > 0 && json.sections.every(section => typeof section?.map?.mappings === 'string'))
161
+ } catch { return false }
162
+ }
163
+
164
+ // The files become unreadable as a finding says them: red, but orange in a
165
+ // major release, or where each had its source map rebuilt with it.
166
+ const unreadableFinding = (major, files) => ({ severity: major || files.every(row => row.rebuilt) ? 'high' : 'critical', ...major ? { major } : {}, files })
167
+
168
+ // The hosts the open version's code names in its URLs that the one compared
169
+ // with's named in none of its, ALLOWED_HOSTS aside, each `{ host, path, line,
170
+ // places }`: the file it is first named in and its line, and `places` every
171
+ // file that names it, `{ path, line }` each, the line it is first named on
172
+ // there. A file unchanged names what it named, so the files the open version
173
+ // adds or changes hold all it names anew.
174
+ function npmNewHosts(base, other, files) {
175
+ const found = new Map(), known = npmVersionHosts(base)
176
+ for (const path of new Set([...files.onlyOther, ...files.changed, ...npmWokenFiles(base, other, files)].map(row => row.path))) {
177
+ for (const [host, line] of npmVersionFileRisk(other, path).hosts ?? []) {
178
+ if (known.has(host) || ALLOWED_HOSTS.has(host)) continue
179
+ const place = { path, line }
180
+ if (found.has(host)) found.get(host).places.push(place)
181
+ else found.set(host, { host, ...place, places: [place] })
182
+ }
183
+ }
184
+ return [...found.values()]
185
+ }
186
+
187
+ // Whether the files of the version compared with could change what
188
+ // npmCompareFindings finds of the open version (`other`), told before they
189
+ // are read, by the open version's files and the manifest npm's document of
190
+ // the one compared with gives (`manifest`): what its files call for
191
+ // (npmFileFindings, npmNewHosts) is only ever what the open version's own
192
+ // files hold that the one compared with's didn't, so where none of them runs
193
+ // code beyond itself, names a service under one of the hosts' tags or a host
194
+ // past ALLOWED_HOSTS, bears an obfuscator's fingerprint, has a line no one
195
+ // writes (npmUnreadable), is large code or is code that isn't text
196
+ // (npmBinaryCode), nothing is found of them whatever
197
+ // the one compared with holds. Nor do the one compared with's install scripts
198
+ // differ by a binding.gyp among its files, but where its manifest names
199
+ // neither `preinstall` nor `install`, and the open version has some.
200
+ export function npmBaseFilesTell(other, manifest) {
201
+ if (npmInstallScripts(other.npm.manifest, other.json.sources).length > 0
202
+ && npmInstallScripts(manifest).length !== npmInstallScripts(manifest, ['binding.gyp']).length) return true
203
+ const sizes = bundleFileSizes(other)
204
+ return [...bundleFilesAsMap(other).keys()].some(path => {
205
+ const { runsCode, exfiltration, readability, hosts } = npmVersionFileRisk(other, path)
206
+ return Boolean(runsCode || exfiltration || readability.obfuscation) || npmUnreadable(path, readability, npmLoadedScripts(other).has(path)) || npmLargeCode(path, sizes.get(path), npmLoadedScripts(other).has(path))
207
+ || npmBinaryCode(path, readability, npmLoadedScripts(other).has(path))
208
+ || [...hosts?.keys() ?? []].some(host => !ALLOWED_HOSTS.has(host))
209
+ })
210
+ }
211
+
212
+ // Of the dependencies the open version adds and changes against `base`, or
213
+ // alone, none given, each with its flags (dependencies.js
214
+ // npmFlagDependencyChanges): those it takes from elsewhere than npm's
215
+ // registry, where the one compared with didn't, or by another spec
216
+ // (`non-registry-deps`, each `{ name, was, now }`, its specs); and those it
217
+ // adds that nothing of it imports, which npm installs all the same, its
218
+ // install scripts and all, as a dependency added to run them would be
219
+ // (`unused-deps`, each `{ name, kind, range }`).
220
+ export function npmDependencyFindings({ added, changed }, base) {
221
+ const flagged = check => [...added, ...changed].filter(row => row.flags.some(flag => flag.check === check))
222
+ const was = base ? npmDependencySpecs(base.npm.manifest) : null
223
+ const fetched = flagged('source').map(row => ({ name: row.name, was: was?.get(row.name) ?? null, now: row.to ?? row.range }))
224
+ const unimported = flagged('unimported').map(({ name, kind, range }) => ({ name, kind, range }))
225
+ return [
226
+ ...fetched.length > 0 ? [{ id: 'non-registry-deps', severity: 'high', dependencies: fetched }] : [],
227
+ ...unimported.length > 0 ? [{ id: 'unused-deps', severity: 'high', dependencies: unimported }] : [],
228
+ ]
229
+ }
230
+
231
+ // Everything that calls for a look at the open version (`other`) against the
232
+ // one compared with (`base`), whatever the time, in the order a reviewer reads
233
+ // them: a publisher that changed (release.js npmPublisherChange,
234
+ // `publisher`, each account's standing on the package told where `standing`
235
+ // is given), a level lower in a major release (npmMajorRelease, `major`),
236
+ // as a package changing hands with a rewrite does; provenance lost (`provenance-lost`); install scripts added or
237
+ // changed (`install-scripts`); its `dependencies` added and changed, as
238
+ // flagged (npmDependencyFindings); its files, by how they differ (diff.js
239
+ // npmFilesDiff, npmFileFindings); and the hosts its code names anew
240
+ // (npmNewHosts, `new-hosts`). A `base` without its files (`json` null),
241
+ // where npmBaseFilesTell says they would change nothing, is found nothing of
242
+ // by them.
243
+ export function npmCompareFindings(base, other, dependencies, standing = null) {
244
+ const major = npmMajorRelease(base.npm.version, other.npm.version)
245
+ const publisher = npmPublisherChange(base.npm.manifest, other.npm.manifest, standing)
246
+ const scripts = npmInstallScriptsChange(...[base, other].map(version => ({ manifest: version.npm.manifest, paths: version.json?.sources })))
247
+ const files = base.json ? npmFilesDiff(base, other) : null
248
+ const hosts = files ? npmNewHosts(base, other, files) : []
249
+ return [
250
+ ...publisher ? [{ id: 'publisher', ...publisher, ...major ? { severity: lower(publisher.severity), major } : {} }] : [],
251
+ ...npmProvenanceLost(base.npm.manifest, other.npm.manifest) ? [{ id: 'provenance-lost', severity: 'critical' }] : [],
252
+ ...scripts ? [{ id: 'install-scripts', severity: 'high', ...scripts }] : [],
253
+ ...npmDependencyFindings(dependencies, base),
254
+ ...files ? npmFileFindings(base, other, files, major) : [],
255
+ ...hosts.length > 0 ? [{ id: 'new-hosts', severity: 'high', hosts }] : [],
256
+ ]
257
+ }