@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2AFQKRAM.js +51 -0
  72. package/out/chunk-2KBF5TZ7.js +2 -0
  73. package/out/chunk-2XUEZHIP.js +28 -0
  74. package/out/chunk-35VE6CDH.js +2 -0
  75. package/out/chunk-3BZ7JERB.js +1 -0
  76. package/out/chunk-4F6JSF57.js +3 -0
  77. package/out/chunk-4XCDFGDP.js +10 -0
  78. package/out/chunk-C4Z6ZBXV.js +10 -0
  79. package/out/chunk-CMQA4DM6.js +1 -0
  80. package/out/chunk-CVVJAXVY.js +2 -0
  81. package/out/chunk-DIJJNTRH.js +10 -0
  82. package/out/chunk-GJ4IUCNS.js +1 -0
  83. package/out/chunk-GNXOKIGP.js +80 -0
  84. package/out/chunk-GZ4MS3TE.js +13 -0
  85. package/out/chunk-IU3CCINR.js +3 -0
  86. package/out/chunk-K3X5EJBQ.js +2 -0
  87. package/out/chunk-KEX2ATEW.js +2 -0
  88. package/out/chunk-KVEUXIEU.js +1 -0
  89. package/out/chunk-KYCNJVB7.js +1 -0
  90. package/out/chunk-NRPC74AM.js +1 -0
  91. package/out/chunk-OOJM4CTU.js +1 -0
  92. package/out/chunk-PNHBZGDD.js +1 -0
  93. package/out/chunk-QZ3A376W.js +1 -0
  94. package/out/chunk-R5O6FAW4.js +2 -0
  95. package/out/chunk-RFZQQXSG.js +17 -0
  96. package/out/chunk-RSHMVYNS.js +1 -0
  97. package/out/chunk-TUII34K2.js +1 -0
  98. package/out/chunk-TY4RZD3Z.js +226 -0
  99. package/out/chunk-TZ5KB2Y5.js +10 -0
  100. package/out/chunk-U6OPEXIA.js +1 -0
  101. package/out/chunk-UNTRGE26.js +2 -0
  102. package/out/chunk-UXNUAIIC.js +33 -0
  103. package/out/chunk-UYAXVRVL.js +1 -0
  104. package/out/chunk-UZAZM2SX.js +2 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XL35ZV6R.js +1 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +298 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +595 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +156 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +223 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/score.js +59 -0
  154. package/risk/npm/specs.js +124 -0
  155. package/risk/npm/tarball.js +113 -0
  156. package/server-common/database-config.ts +16 -0
  157. package/server-common/initialize.ts +18 -0
  158. package/server-common/npm-advisories.ts +101 -0
  159. package/{server → server-common}/origin.ts +5 -5
  160. package/server-common/reap.ts +48 -0
  161. package/server-common/scan-config.ts +19 -0
  162. package/server-common/standalone.ts +29 -0
  163. package/server-common/storage-crypto.ts +182 -0
  164. package/server-common/storage-log.ts +38 -0
  165. package/server-common/vercel-blob.ts +103 -0
  166. package/server-e2e/app.ts +486 -0
  167. package/{server → server-e2e}/auth.ts +5 -1
  168. package/{server → server-e2e}/bus-receiver.ts +2 -1
  169. package/{server → server-e2e}/cli.js +9 -4
  170. package/{server → server-e2e}/config.ts +56 -35
  171. package/{server → server-e2e}/db-neon.ts +2 -2
  172. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  173. package/{server → server-e2e}/db-stmt.ts +2 -2
  174. package/{server → server-e2e}/db.ts +7 -7
  175. package/{server → server-e2e}/http.ts +116 -18
  176. package/{server → server-e2e}/hub.ts +40 -9
  177. package/server-e2e/index.ts +17 -0
  178. package/server-e2e/lifecycle.ts +95 -0
  179. package/{server → server-e2e}/neon-driver.ts +2 -2
  180. package/{server → server-e2e}/npm-proxy.ts +12 -145
  181. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  182. package/{server → server-e2e}/objstore/blob.ts +1 -1
  183. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  184. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  185. package/{server → server-e2e}/objstore/init.ts +35 -18
  186. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  187. package/server-e2e/objstore/rest-deny.ts +28 -0
  188. package/server-e2e/objstore/rest-mint.ts +224 -0
  189. package/{server → server-e2e}/objstore/rest.ts +27 -26
  190. package/{server → server-e2e}/objstore/sign.ts +105 -0
  191. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  192. package/{server → server-e2e}/objstore/store.ts +7 -7
  193. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  194. package/{server → server-e2e}/pubsub.ts +12 -9
  195. package/{server → server-e2e}/sign.ts +4 -4
  196. package/{server → server-e2e}/sse-server.ts +102 -68
  197. package/{server → server-e2e}/sse-session.ts +28 -8
  198. package/{server → server-e2e}/static.ts +31 -21
  199. package/server-e2e/sync-handlers.ts +408 -0
  200. package/{server → server-e2e}/ws-server.ts +20 -9
  201. package/server-managed/activity.ts +245 -0
  202. package/server-managed/avatar-store.ts +12 -0
  203. package/server-managed/blob-store.ts +17 -0
  204. package/server-managed/brotli.ts +10 -0
  205. package/server-managed/bundle-advisories.ts +120 -0
  206. package/server-managed/bundle-advisory-inventory.ts +77 -0
  207. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  208. package/server-managed/bundle-build-diagnostics.js +23 -0
  209. package/server-managed/bundle-build-leases.ts +41 -0
  210. package/server-managed/bundle-build-worker.js +111 -0
  211. package/server-managed/bundle-build.ts +198 -0
  212. package/server-managed/bundle-cache.ts +199 -0
  213. package/server-managed/bundle-catalog.ts +29 -0
  214. package/server-managed/bundle-commits.ts +140 -0
  215. package/server-managed/bundle-store.ts +24 -0
  216. package/server-managed/bundle-summary-cache.ts +130 -0
  217. package/server-managed/bundle.ts +120 -0
  218. package/server-managed/cache-storage.ts +19 -0
  219. package/server-managed/cli.js +21 -0
  220. package/server-managed/combined.ts +46 -0
  221. package/server-managed/comments.ts +151 -0
  222. package/server-managed/config.ts +158 -0
  223. package/server-managed/content-access.ts +15 -0
  224. package/server-managed/crypto.ts +25 -0
  225. package/server-managed/db-methods.ts +1725 -0
  226. package/server-managed/db-neon.ts +367 -0
  227. package/server-managed/db-schema.ts +227 -0
  228. package/server-managed/db-table-names.ts +22 -0
  229. package/server-managed/db.ts +132 -0
  230. package/server-managed/dependency-reviews.ts +248 -0
  231. package/server-managed/github-app.ts +391 -0
  232. package/server-managed/github-errors.ts +68 -0
  233. package/server-managed/github-issue-links.ts +127 -0
  234. package/server-managed/github-issue-oauth.ts +47 -0
  235. package/server-managed/github-issues.ts +152 -0
  236. package/server-managed/github-metadata.ts +213 -0
  237. package/server-managed/github-oauth.ts +235 -0
  238. package/server-managed/github-pulls.ts +227 -0
  239. package/server-managed/http-response.ts +50 -0
  240. package/server-managed/http.ts +3472 -0
  241. package/server-managed/import-triage.ts +51 -0
  242. package/server-managed/index.ts +187 -0
  243. package/server-managed/kept.ts +28 -0
  244. package/server-managed/link-reports.ts +158 -0
  245. package/server-managed/locked-packages.ts +85 -0
  246. package/server-managed/lru.ts +30 -0
  247. package/server-managed/managed-issues.ts +75 -0
  248. package/server-managed/management-catalog.ts +45 -0
  249. package/server-managed/management.ts +99 -0
  250. package/server-managed/npm-insights.ts +440 -0
  251. package/server-managed/npm-kept.ts +65 -0
  252. package/server-managed/npm-loads.ts +167 -0
  253. package/server-managed/npm-manifest.ts +115 -0
  254. package/server-managed/npm-packages.ts +378 -0
  255. package/server-managed/npm-provenance.ts +123 -0
  256. package/server-managed/npm-risk.ts +177 -0
  257. package/server-managed/object-storage-disk.ts +148 -0
  258. package/server-managed/object-storage-vercel.ts +67 -0
  259. package/server-managed/object-storage.ts +74 -0
  260. package/server-managed/package-entry-points.ts +51 -0
  261. package/server-managed/pretty-print.ts +352 -0
  262. package/server-managed/public-workspace.ts +220 -0
  263. package/server-managed/rename-diff-worker.js +26 -0
  264. package/server-managed/rename-diff.ts +270 -0
  265. package/server-managed/repo-path.ts +21 -0
  266. package/server-managed/report-migration.ts +35 -0
  267. package/server-managed/report-query.ts +10 -0
  268. package/server-managed/report-response.ts +16 -0
  269. package/server-managed/report-sources.ts +178 -0
  270. package/server-managed/report-store.ts +47 -0
  271. package/server-managed/repository-aliases.ts +91 -0
  272. package/server-managed/repository-browser.ts +153 -0
  273. package/server-managed/repository-discovery.ts +82 -0
  274. package/server-managed/repository-policy.ts +27 -0
  275. package/server-managed/repository-risk.ts +420 -0
  276. package/server-managed/revisions.ts +35 -0
  277. package/server-managed/session.ts +92 -0
  278. package/server-managed/slugs.ts +38 -0
  279. package/server-managed/solidity-entry-points.ts +69 -0
  280. package/server-managed/sql-postgres.ts +31 -0
  281. package/server-managed/sql.ts +76 -0
  282. package/server-managed/static.ts +28 -0
  283. package/server-managed/storage-command.ts +16 -0
  284. package/server-managed/storage-db.ts +175 -0
  285. package/server-managed/storage-encryption.ts +279 -0
  286. package/server-managed/storage-maintenance.ts +256 -0
  287. package/server-managed/storage-payload.ts +103 -0
  288. package/server-managed/storage-stores.ts +85 -0
  289. package/server-managed/storage.ts +78 -0
  290. package/server-managed/team-app.ts +94 -0
  291. package/server-managed/team-catalog.ts +7 -0
  292. package/server-managed/team-feed.ts +173 -0
  293. package/server-managed/team-npm-scopes.ts +77 -0
  294. package/server-managed/team-reports.ts +228 -0
  295. package/server-managed/triage-response.ts +16 -0
  296. package/server-managed/type-only-worker.js +19 -0
  297. package/server-managed/type-only.ts +103 -0
  298. package/server-managed/uploads.ts +86 -0
  299. package/server-managed/upstream-cache.ts +215 -0
  300. package/server-managed/view-as.ts +58 -0
  301. package/server-managed/workspace-shares.ts +166 -0
  302. package/server.ts +50 -0
  303. package/server/index.ts +0 -477
  304. package/server/lifecycle.ts +0 -204
  305. package/server/sync-handlers.ts +0 -320
  306. /package/{LICENSE → LICENSE-MIT} +0 -0
  307. /package/{server → server-e2e}/config.example.json +0 -0
  308. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  309. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  310. /package/{server → server-e2e}/peer.ts +0 -0
  311. /package/{server → server-e2e}/util.ts +0 -0
  312. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,205 @@
1
+ // The hosts a version's code names, and the services malicious packages send
2
+ // what they take to, fetch what they run from or look the machine up with,
3
+ // for a reviewer: the hosts a version's scripts name that the version
4
+ // compared with's named in none of theirs, and the services a file names,
5
+ // under a tag for how much they call for (HOST_TAGS: Exfiltration, Remote,
6
+ // IP lookup), as a file's stamps say (stamps.js).
7
+ //
8
+ // A quick read, not a parse, as runs-code.js reads: a host is a URL's in a
9
+ // script's strings, regular expressions and template literals
10
+ // (JS_STRING_OR_COMMENT), its comments aside, where a link to docs is no host
11
+ // the code calls; an exfiltration host is told in any text but prose and a
12
+ // source map, wherever it is written. A host built as the code runs
13
+ // (`'https://' + name`, `${host}`) goes unseen, so a version without these
14
+ // labels is not one that sends nothing.
15
+ import { NPM_PROSE, SOURCE_MAP } from './readability.js'
16
+ import { JS_STRING_OR_COMMENT, NPM_MAX_FOUND, lineOf, npmIsScript } from './script.js'
17
+
18
+ // How many hosts a file is told naming at most, the first it names.
19
+ const NPM_MAX_HOSTS = 1000
20
+
21
+ // A URL's host: a name whose last label is letters (an internationalized
22
+ // one's too, as written or as punycode), or an IPv4 address, after its
23
+ // scheme, as a regular expression may leave its `s` optional (`https?://`),
24
+ // and any user, its case aside; whole, not the start of one built as the code
25
+ // runs (`api.service.com.${tld}`).
26
+ const LABEL = String.raw`[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?`
27
+ const URL_HOST = new RegExp(String.raw`\b(?:https?|wss?|ftp)\??:\/\/(?:[^\s/@'"\x60]*@)?(${LABEL}(?:\.${LABEL})+)(?![\p{L}\p{N}-]|\.[\p{L}\p{N}_$-])`, 'giu')
28
+ const NAMED = /\.(?:\p{L}{2,}|xn--[a-z0-9-]+)$/u
29
+ const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/u
30
+ // Hosts no code calls out to: this machine, the names kept for examples and
31
+ // tests (RFC 2606, RFC 6761) and local networks, and W3C's, whose URLs name
32
+ // XML namespaces.
33
+ const LOCAL = /^(?:localhost|.+\.(?:localhost|local|test|invalid|example|internal)|(?:.+\.)?example\.(?:com|net|org)|127(?:\.\d+){3}|0\.0\.0\.0|(?:.+\.)?w3\.org)$/u
34
+
35
+ // What a regular expression escapes in a URL, its slashes and dots
36
+ // (`/https:\/\/api\.example\.com/`), as a string may its slashes
37
+ // (`"https:\/\/api.example.com"`, as JSON can be written), read unescaped.
38
+ const ESCAPED = /\\([/.])/gu
39
+
40
+ // The pages github.com shows of a repository that a person reads, past its
41
+ // owner and name, as links and error messages name them: not its Actions,
42
+ // whose runs' artifacts and logs are downloads of what its workflows made.
43
+ const GITHUB_PAGES = new Set(['issues', 'pull', 'pulls', 'blob', 'tree', 'wiki', 'discussions', 'commit', 'commits', 'compare', 'blame',
44
+ 'security', 'projects', 'labels', 'milestone', 'milestones', 'releases', 'tags', 'graphs', 'network', 'stargazers'])
45
+ // The paths past github.com that name no repository but a page of GitHub's
46
+ // own (`github.com/sponsors/someone`, `github.com/orgs/someone/people`).
47
+ const GITHUB_OWN = new Set(['sponsors', 'orgs', 'apps', 'marketplace', 'features', 'topics', 'collections', 'settings', 'login', 'search', 'explore', 'about'])
48
+ // How a github.com URL (`url`, as written from its scheme on, the punctuation
49
+ // a sentence ends it with aside) is named among a file's hosts, read as fetch
50
+ // reads it (WHATWG URL: a port, a trailing dot, backslashes as slashes, `..`
51
+ // resolved): `github.com` for its home or a person's, a page of GitHub's own
52
+ // (GITHUB_OWN), a page of a repository a person reads (GITHUB_PAGES), a
53
+ // release's page (`releases/tag/v1`, `releases/latest`) but not its assets'
54
+ // (`releases/latest/download/…`), a file's page but not the file raw
55
+ // (`blob/…?raw=true`, any `raw` asked), and a page but not its content in another format, as
56
+ // GitHub serves one by a suffix to its path past a file's (`pull/1.patch`,
57
+ // `commit/….diff`, `commits/main.atom`). Any other (a repository's own URL,
58
+ // which git clones, `raw/…`, `archive/…`, `tarball`, `repo.git`, a path GitHub
59
+ // may add, one read otherwise) leads to whatever a repository holds: named by
60
+ // it (`github.com/owner/repo`, in lower case, as GitHub's names are any), so
61
+ // that it is never taken for one of github.com's links to code and docs
62
+ // (compare.js ALLOWED_HOSTS).
63
+ const FILE_PATHS = new Set(['blob', 'tree', 'blame', 'wiki'])
64
+ const FORMAT = /\.(?:patch|diff|atom)$/iu
65
+ const GIT = /\.git$/iu
66
+ const decoded = part => { try { return decodeURIComponent(part) } catch { return part } }
67
+ function githubNamed(url) {
68
+ let parsed = null
69
+ try { parsed = new URL(url.replace(/^(\w+)\?:/u, '$1:').replace(/[)\]}>.,;:!]+$/u, '')) } catch {}
70
+ if (!parsed) return 'github.com/'
71
+ // Its path and query decoded, as GitHub routes them (`pull/1%2Epatch`,
72
+ // `?raw=%31`).
73
+ const segments = parsed.pathname.split('/').slice(1).map(decoded)
74
+ if (segments.length > 1 && segments.at(-1) === '') segments.pop()
75
+ const [owner, repo = '', section, ...rest] = segments
76
+ const repositoryPage = owner !== '' && repo !== '' && !GIT.test(repo) && GITHUB_PAGES.has(section)
77
+ && !(section === 'releases' && rest.includes('download')) && !(section === 'blob' && parsed.searchParams.has('raw'))
78
+ && !(!FILE_PATHS.has(section) && FORMAT.test(segments.at(-1)))
79
+ const page = parsed.hostname.replace(/\.$/u, '') === 'github.com' && (segments.length === 1 || GITHUB_OWN.has(owner) || repositoryPage)
80
+ return page ? 'github.com' : `github.com/${owner}/${repo.replace(GIT, '')}`.toLowerCase()
81
+ }
82
+
83
+ // The hosts a script's strings name, each with the line it is first named on,
84
+ // in the order named, NPM_MAX_HOSTS at most; null for none, or for a file
85
+ // that is no script.
86
+ export function npmHosts(path, text) {
87
+ if (!npmIsScript(path, text) || !(text.includes('://') || text.includes(':\\/\\/'))) return null
88
+ const hosts = new Map(), line = lineOf(text)
89
+ for (const { 1: token, index } of text.matchAll(JS_STRING_OR_COMMENT)) {
90
+ if (hosts.size >= NPM_MAX_HOSTS) break
91
+ // Read unescaped, its offsets as near: on the same line but in a template
92
+ // across lines; a regular expression without its closing slash and flags,
93
+ // as a string's quote ends its URL (`/https:\/\/github.com\/a\/b\/pull\/1.patch/i`).
94
+ const body = token?.startsWith('/') ? token.replace(/\/[a-z]*$/u, '') : token
95
+ const string = body?.includes('\\') ? body.replaceAll(ESCAPED, '$1') : body
96
+ if (!string?.includes('://')) continue
97
+ for (const { 1: name, index: at } of string.matchAll(URL_HOST)) {
98
+ const host = name.toLowerCase()
99
+ const named = host === 'github.com' ? githubNamed(/^[^\s'"\u0060]*/u.exec(string.slice(at))[0]) : host
100
+ if (!hosts.has(named) && (NAMED.test(host) || IPV4.test(host)) && !LOCAL.test(host)) hosts.set(named, line(index + at))
101
+ if (hosts.size >= NPM_MAX_HOSTS) break
102
+ }
103
+ }
104
+ return hosts.size > 0 ? hosts : null
105
+ }
106
+
107
+ // A host as it is written, under another or not (`m.webhook.site`), then a
108
+ // path on it where only that path is the service (`/api/webhooks`), on its
109
+ // own: not part of a longer name (`notwebhook.site`, `webhook.sites`), a
110
+ // name under another domain (`webhook.site.evil.example`, `webhook.site.рф`),
111
+ // nor another path (`/install.sh`, `/api/webhooks.json`), though a sentence
112
+ // may end after it. Its name first, so a text is searched for that alone.
113
+ const NAME_CHAR = String.raw`[\p{L}\p{N}_-]`
114
+ const host = (name, path = '') => new RegExp(String.raw`(?<!${NAME_CHAR})${name}${path}(?!\.?${NAME_CHAR})`, 'giu')
115
+ // One so like a file's name or code (`transfer.sh`, `file.io`) only as a
116
+ // URL's host: after the `//` that starts its authority (or `\/\/` as JSON can
117
+ // write it), where the URL starts, its scheme as URL_HOST takes one after the
118
+ // text's start, a blank, a quote, `(`, `=`, `,`, `<` or `>`, or without one
119
+ // after a quote, `(`, `=` or `,` (`"//file.io/x"`): not a URL's path, whatever
120
+ // it holds (`https://example.com/a//transfer.sh`, `/http://transfer.sh`); and
121
+ // after any user, not a user itself (`https://transfer.sh@example.com`).
122
+ const inUrl = name => new RegExp(String.raw`(?<=(?:(?:^|[\s'"\x60(=,<>])(?:https?|wss?|ftp)\??:|['"\x60(=,])\\?\/\\?\/(?:[^\s/@'"\x60]*@)?(?:${NAME_CHAR}+\.)*)${name}(?!\.?${NAME_CHAR}|(?::[^\s/@'"\x60]*)?@)`, 'giu')
123
+ // The tags the services go under, each with its severity (stamps.js
124
+ // SEVERITIES), as a change between versions raises it (npmHostSeverity):
125
+ // Exfiltration, `critical` (red), where nothing but catching what is sent
126
+ // has a use for it in a package's code; Remote, `high` (orange), where it
127
+ // could be fine; and IP lookup, `moderate` (yellow), where it usually is.
128
+ export const HOST_TAGS = new Map([['Exfiltration', 'critical'], ['Remote', 'high'], ['IP lookup', 'moderate']])
129
+ const [CATCHER, REMOTE, IP_LOOKUP] = [...HOST_TAGS.keys()].map(tag => ({ tag }))
130
+ // Services malicious packages send what they take to, fetch what they run
131
+ // from, or look the machine up with, each under its tag. Exfiltration's:
132
+ // request catchers, and out-of-band testing hosts (interactsh's, Burp
133
+ // Collaborator's, Canarytokens', DNS logs). Remote's: bot and webhook APIs,
134
+ // paste sites and file drops, tunnels, Pipedream's own API (its endpoints
135
+ // are `pipedream.net`), and the install script of Bun (`bun.sh/install`, now
136
+ // `bun.com/install`), which a worm ran its payload with. And IP lookups.
137
+ export const EXFILTRATION = new Map([
138
+ ['webhook-site', { ...CATCHER, name: 'webhook.site', pattern: host(String.raw`webhook\.site`) }],
139
+ ['pipedream', { ...CATCHER, name: 'Pipedream', pattern: host(String.raw`pipedream\.net`) }],
140
+ ['requestbin', { ...CATCHER, name: 'RequestBin', pattern: host(String.raw`(?:requestbin\.(?:net|com)|requestb\.in)`) }],
141
+ ['requestcatcher', { ...CATCHER, name: 'Request Catcher', pattern: host(String.raw`requestcatcher\.com`) }],
142
+ ['requestrepo', { ...CATCHER, name: 'requestrepo', pattern: host(String.raw`requestrepo\.com`) }],
143
+ ['beeceptor', { ...CATCHER, name: 'Beeceptor', pattern: host(String.raw`beeceptor\.com`) }],
144
+ ['oast', { ...CATCHER, name: 'OAST', pattern: host(String.raw`(?:interact\.sh|interactsh\.com|oast\.(?:pro|live|site|online|fun|me)|oastify\.com|burpcollaborator\.net)`) }],
145
+ ['canarytokens', { ...CATCHER, name: 'Canarytokens', pattern: host(String.raw`canarytokens\.(?:com|org)`) }],
146
+ ['dnslog', { ...CATCHER, name: 'DNSLog', pattern: host(String.raw`dnslog\.cn`) }],
147
+ ['ceye', { ...CATCHER, name: 'CEYE', pattern: host(String.raw`ceye\.io`) }],
148
+ ['telegram', { ...REMOTE, name: 'Telegram', pattern: host(String.raw`api\.telegram\.org`) }],
149
+ ['discord', { ...REMOTE, name: 'Discord webhook', pattern: host(String.raw`discord(?:app)?\.com`, String.raw`/api(?:/v\d+)?/webhooks`) }],
150
+ ['slack', { ...REMOTE, name: 'Slack webhook', pattern: host(String.raw`hooks\.slack\.com`, '/(?:services|workflows|triggers)') }],
151
+ ['pastebin', { ...REMOTE, name: 'Pastebin', pattern: host(String.raw`pastebin\.com`) }],
152
+ ['paste-ee', { ...REMOTE, name: 'paste.ee', pattern: host(String.raw`paste\.ee`) }],
153
+ ['transfer-sh', { ...REMOTE, name: 'transfer.sh', pattern: inUrl(String.raw`transfer\.sh`) }],
154
+ ['file-io', { ...REMOTE, name: 'file.io', pattern: inUrl(String.raw`file\.io`) }],
155
+ ['0x0', { ...REMOTE, name: '0x0.st', pattern: host(String.raw`0x0\.st`) }],
156
+ ['ngrok', { ...REMOTE, name: 'ngrok', pattern: host(String.raw`ngrok(?:-free)?\.(?:io|app|dev)`) }],
157
+ ['localtunnel', { ...REMOTE, name: 'localtunnel', pattern: host(String.raw`loca\.lt`) }],
158
+ ['cloudflare-tunnel', { ...REMOTE, name: 'Cloudflare Tunnel', pattern: host(String.raw`trycloudflare\.com`) }],
159
+ ['serveo', { ...REMOTE, name: 'Serveo', pattern: host(String.raw`serveo\.net`) }],
160
+ ['pipedream-api', { ...REMOTE, name: 'Pipedream API', pattern: host(String.raw`pipedream\.com`) }],
161
+ ['bun', { ...REMOTE, name: 'Bun install', pattern: host(String.raw`bun\.(?:sh|com)`, '/install') }],
162
+ ['ipinfo', { ...IP_LOOKUP, name: 'ipinfo', pattern: host(String.raw`ipinfo\.io`) }],
163
+ ['ipify', { ...IP_LOOKUP, name: 'ipify', pattern: host(String.raw`ipify\.org`) }],
164
+ ['ifconfig-me', { ...IP_LOOKUP, name: 'ifconfig.me', pattern: host(String.raw`ifconfig\.me`) }],
165
+ ['icanhazip', { ...IP_LOOKUP, name: 'icanhazip', pattern: host(String.raw`icanhazip\.com`) }],
166
+ ['ip-api', { ...IP_LOOKUP, name: 'ip-api', pattern: host(String.raw`ip-api\.com`) }],
167
+ ['ipapi', { ...IP_LOOKUP, name: 'ipapi', pattern: host(String.raw`ipapi\.com?`) }],
168
+ ['checkip', { ...IP_LOOKUP, name: 'checkip', pattern: host(String.raw`checkip\.amazonaws\.com`) }],
169
+ ])
170
+ // Any of them, as a text may say it: most say none, told at once.
171
+ const SAYS_ANY = new RegExp([...EXFILTRATION.values()].map(({ pattern }) => pattern.source).join('|'), 'iu')
172
+
173
+ // The services a file names, each with the lines it is named on, the first
174
+ // NPM_MAX_FOUND, in EXFILTRATION's order; null for none, or for prose or a
175
+ // source map.
176
+ export function npmExfiltration(path, text) {
177
+ if (typeof text !== 'string' || NPM_PROSE.test(path) || SOURCE_MAP.test(path) || !SAYS_ANY.test(text)) return null
178
+ const found = new Map(), line = lineOf(text)
179
+ for (const [service, { pattern }] of EXFILTRATION) {
180
+ const lines = new Set()
181
+ for (const { index } of text.matchAll(pattern)) if (lines.add(line(index)).size >= NPM_MAX_FOUND) break
182
+ if (lines.size > 0) found.set(service, [...lines])
183
+ }
184
+ return found.size > 0 ? found : null
185
+ }
186
+
187
+ // The services of `found` under `tag` (HOST_TAGS), in their order; null for
188
+ // none.
189
+ export function npmHostsTagged(found, tag) {
190
+ const tagged = found && new Map([...found].filter(([service]) => EXFILTRATION.get(service).tag === tag))
191
+ return tagged?.size > 0 ? tagged : null
192
+ }
193
+
194
+ // The services a file names (`found`) by their tag, in HOST_TAGS' order:
195
+ // `[tag, services]` each.
196
+ export const npmHostTags = found => [...HOST_TAGS.keys()].map(tag => [tag, npmHostsTagged(found, tag)]).filter(([, services]) => services)
197
+
198
+ // How severe a tag's `services` are: its own severity, or with `was` (null for
199
+ // a file that held none), where one of them the file didn't name, a level
200
+ // higher, never past `critical`, as a change between versions calls for more.
201
+ export function npmHostSeverity(tag, services, was) {
202
+ const levels = [...HOST_TAGS.values()]
203
+ const at = levels.indexOf(HOST_TAGS.get(tag))
204
+ return levels[was !== undefined && [...services.keys()].some(service => !was?.has(service)) ? Math.max(0, at - 1) : at]
205
+ }
@@ -0,0 +1,171 @@
1
+ // How a file's text reads, for a reviewer: its category (binary, a source
2
+ // map, code with unexpectedly long lines or an inline map, minified, else
3
+ // ASCII or UTF-8), its control and invisible characters, its longest lines,
4
+ // an obfuscator's fingerprint, a large code file, and a file become
5
+ // unreadable between two versions.
6
+ import { CONTROLS, npmTextEncoding } from './characters.js'
7
+ import { MINIFIED_AVERAGE, minifiable, minifiedAverage } from './script.js'
8
+
9
+ // Lines longer than anyone writes by hand.
10
+ export const NPM_LONG_LINE = 1000
11
+ // Prose, which wraps where it is read: its long lines are paragraphs. By its
12
+ // extension (text, Markdown and MDX, and the markup GitHub renders a README
13
+ // from: `README.org`, `README.rdoc`), or its name alone (`LICENSE-MIT`,
14
+ // `CHANGES`; a dot in it only a version's, `LICENSE-LGPL2.1`,
15
+ // `CHANGES.2.0`; a prose extension after it is one), not code named alike
16
+ // (`lib/history.js`, `dist/notice.min.js`), which can be minified as any.
17
+ export const NPM_PROSE = /(?:\.(?:md|markdown|mdown|mkdn|mdx|txt|rst|adoc|asciidoc|asc|textile|rdoc|org|creole|mediawiki|wiki|pod)|(?:^|\/)(?:licen[cs]e|copying|notice|authors|contributors|readme|changelog|changes|history)(?:-[^/.]*)?(?:\.\d[^/.]*)*)$/iu
18
+ const SOURCE_MAP_COMMENT = /^\s*(?:\/\/|\/\*)[#@] sourceMappingURL=/u
19
+ // A line that is all one, the map in it as a data: URL, base64 or
20
+ // percent-encoded: not code that writes one.
21
+ const INLINE_SOURCE_MAP = /^\s*(?:\/\/|\/\*)[#@] sourceMappingURL=data:[^\s,]*,[\w+/=%.~-]*\s*(?:\*\/)?\s*$/u
22
+ export const SOURCE_MAP = /\.map$/iu
23
+ const MINIFIED_NAME = /\.min\.[^/.]+$/iu
24
+ // What javascript-obfuscator writes, and next to nothing else: its names,
25
+ // `_0x` and four to six hex digits, OBFUSCATED_NAMES of them or more, or the
26
+ // loop it turns its array of strings with, `while(!![])`, spaced or not.
27
+ const OBFUSCATED_NAME = /(?<![\p{ID_Continue}$])_0x[0-9a-f]{4,6}(?![\p{ID_Continue}$])/gu
28
+ export const OBFUSCATED_NAMES = 20
29
+ const OBFUSCATED_LOOP = /while\s*\(\s*!!\[\]\s*\)/u
30
+ // Names counted before a fingerprint's count stops, reading as that many or
31
+ // more: each is kept to tell it from the others, and a file can hold millions.
32
+ export const OBFUSCATED_COUNTED = 1000
33
+ // A text's obfuscator fingerprint, `{ names, loop }`: how many such names it
34
+ // has, up to OBFUSCATED_COUNTED, and whether it has the loop; null where it
35
+ // has neither enough names nor the loop.
36
+ export function npmObfuscation(text) {
37
+ const names = new Set()
38
+ for (const [name] of text.matchAll(OBFUSCATED_NAME)) {
39
+ names.add(name)
40
+ if (names.size === OBFUSCATED_COUNTED) break
41
+ }
42
+ const loop = OBFUSCATED_LOOP.test(text)
43
+ return names.size >= OBFUSCATED_NAMES || loop ? { names: names.size, loop } : null
44
+ }
45
+
46
+ // A code file this large is mostly a bundle, worth a look where a version
47
+ // adds it: JavaScript or TypeScript, not a declaration file, which holds no
48
+ // code that runs.
49
+ export const NPM_LARGE_CODE = 256 * 1024
50
+ const CODE = /\.(?:[cm]?[jt]s|[jt]sx)$/iu
51
+ const DECLARATION = /\.d\.[cm]?ts$/iu
52
+ export const npmLargeCode = (path, bytes) => typeof bytes === 'number' && bytes >= NPM_LARGE_CODE && CODE.test(path) && !DECLARATION.test(path)
53
+
54
+ // A code file that isn't text (npmFileReadability's `binary`: not UTF-8, or
55
+ // holding a NUL), by its path and how it reads, a declaration file too: no
56
+ // one can read it in review, yet what reads it by its byte order mark can
57
+ // run it, or type with it, as UTF-16 (a browser's classic script, Windows
58
+ // Script Host, TypeScript).
59
+ export const npmBinaryCode = (path, readability) => readability?.category === 'binary' && CODE.test(path)
60
+
61
+ // A file with a line over UNREADABLE_LINE characters, by how it reads
62
+ // (`now`, npmFileReadability's), `path` its path: an inline source map's line
63
+ // counts for none, and prose and source maps, whose long lines the Long lines
64
+ // tag passes over, never are, nor is a file that isn't text. Then one that
65
+ // had no line over SHORT_LINES characters (`was`) and now has one over
66
+ // UNREADABLE_LINE, by how it reads on each side.
67
+ const SHORT_LINES = 200
68
+ const UNREADABLE_LINE = 2000
69
+ export const npmUnreadable = (path, now) => Boolean(now) && now.category !== 'binary' && !NPM_PROSE.test(path) && !SOURCE_MAP.test(path)
70
+ && now.longest > UNREADABLE_LINE
71
+ export const npmBecameUnreadable = (path, was, now) => Boolean(was) && was.category !== 'binary' && was.longest <= SHORT_LINES && npmUnreadable(path, now)
72
+
73
+ // How a file reads, as its `category`, the first that holds (READABILITY):
74
+ // binary (no text), map (a source map), long (code with some lines
75
+ // longer than anyone writes), inline-map (code with its source map in it, minified or
76
+ // not), minified (code mostly on long lines, or named .min., or minified
77
+ // into shorter lines: see MINIFIED_AVERAGE), else utf8 or ascii; and
78
+ // whatever it is, the control, bidirectional or invisible characters a text
79
+ // holds (`controls`), found in one pass, which count as a label of their own
80
+ // beside it each, control and invisible (npmReadabilityLabels). Prose is
81
+ // readable whatever its lines' lengths, and an inline source map's line
82
+ // counts for none, nor for how much of the code is on long lines. With
83
+ // npmTextEncoding's `kind` and `controls`, its `longest` line's length and
84
+ // `longLines`, its code's lines' `average` length where it is minified into
85
+ // short ones (strings and comments aside, see MINIFIED_AVERAGE; 0 otherwise),
86
+ // how long its inline map is (`inlineMap`, 0 for none), and its obfuscator
87
+ // fingerprint (`obfuscation`, npmObfuscation's; null for none, and for prose
88
+ // or a source map).
89
+ export function npmFileReadability(path, text) {
90
+ const encoding = npmTextEncoding(text)
91
+ if (encoding.kind === 'binary') return { ...encoding, category: 'binary', longest: 0, longLines: 0, average: 0, inlineMap: 0, obfuscation: null }
92
+ let inlineMap = 0, longChars = 0, longLines = 0, longest = 0
93
+ for (let at = 0; at <= text.length;) {
94
+ const next = text.indexOf('\n', at)
95
+ const end = next === -1 ? text.length : next
96
+ const length = end - at - (text[end - 1] === '\r' ? 1 : 0)
97
+ if (SOURCE_MAP_COMMENT.test(text.slice(at, at + 64)) && INLINE_SOURCE_MAP.test(text.slice(at, end))) inlineMap += length
98
+ else {
99
+ if (length > NPM_LONG_LINE) {
100
+ longLines++
101
+ longChars += length
102
+ }
103
+ longest = Math.max(longest, length)
104
+ }
105
+ at = end + 1
106
+ }
107
+ // A source map holds the names and sources of the code it maps, and prose
108
+ // its examples: neither runs.
109
+ const obfuscation = NPM_PROSE.test(path) || SOURCE_MAP.test(path) ? null : npmObfuscation(text)
110
+ const read = { ...encoding, longest, longLines, average: 0, inlineMap, obfuscation }
111
+ if (SOURCE_MAP.test(path)) return { ...read, category: 'map' }
112
+ if (NPM_PROSE.test(path)) return { ...read, category: inlineMap > 0 ? 'inline-map' : encoding.kind }
113
+ if (longLines === 0) {
114
+ // None of its lines longer than MINIFIED_AVERAGE, none of its code's can
115
+ // average more: told without reading its code.
116
+ const language = minifiable(path)
117
+ const average = language !== null && longest > MINIFIED_AVERAGE ? minifiedAverage(text, language) : 0
118
+ return { ...read, average, category: inlineMap > 0 ? 'inline-map' : average > 0 ? 'minified' : encoding.kind }
119
+ }
120
+ if (longChars / (text.length - inlineMap) < .5 && !MINIFIED_NAME.test(path)) return { ...read, category: 'long' }
121
+ return { ...read, category: inlineMap > 0 ? 'inline-map' : 'minified' }
122
+ }
123
+
124
+ // The category a file's tag shows: for one with long lines, how its text is
125
+ // encoded.
126
+ export const npmTaggedCategory = readability => readability.category === 'long' ? readability.kind : readability.category
127
+
128
+ // What a file is labelled, in its tags and in the Overview's warning and
129
+ // chips, each finding it: its control characters, its invisible ones
130
+ // (characterLabels) and its long lines, where it holds them, beside its category (how its text is encoded, for one whose long
131
+ // lines are unexpected), but for ASCII or UTF-8, which a file that can't be
132
+ // read as it is counts under as none of them. A minified file's long lines
133
+ // are labelled too, as its inline map's are; prose's and a source map's
134
+ // never are.
135
+ export function npmReadabilityLabels(readability) {
136
+ const flags = [...characterLabels(readability.controls),
137
+ ...readability.longLines > 0 && ['long', 'minified', 'inline-map'].includes(readability.category) ? ['long'] : []]
138
+ const category = npmTaggedCategory(readability)
139
+ return READABILITY.get(category).mark || flags.length === 0 ? [...flags, category] : flags
140
+ }
141
+
142
+ // How a label is marked on a file: long lines as a warning where it is
143
+ // otherwise readable, as they are unexpected there, and as a notice in
144
+ // minified code, as minified code is; any other as its category is.
145
+ export const npmLabelMark = (readability, label) => label === 'long' && readability.category !== 'long' ? 'notice' : READABILITY.get(label).mark
146
+
147
+ // Each category: its name, its tag in a list of files or a file's head (and,
148
+ // `short`, in a file tree, where that is too long), and how it is marked,
149
+ // `warn` for what to look into, `notice` for what can be hard to review by
150
+ // reading it either way.
151
+ export const READABILITY = new Map([
152
+ ['binary', { name: 'Binary', tag: 'Binary', mark: 'warn' }],
153
+ ['controls', { name: 'Control chars', tag: 'Controls', mark: 'warn' }],
154
+ ['invisible', { name: 'Invisible chars', tag: 'Invisible', mark: 'warn' }],
155
+ ['long', { name: 'Long lines', tag: 'Long lines', mark: 'warn' }],
156
+ ['minified', { name: 'Minified', tag: 'Minified', short: 'Min', mark: 'notice' }],
157
+ ['map', { name: 'Source maps', tag: 'Map', mark: 'notice' }],
158
+ ['inline-map', { name: 'Inline source maps', tag: 'Inline map', mark: 'notice' }],
159
+ ['utf8', { name: 'UTF-8', tag: 'UTF-8', mark: null }],
160
+ ['ascii', { name: 'ASCII', tag: 'ASCII', mark: null }],
161
+ ])
162
+
163
+ // The characters a text holds of a label's kind, `controls` (C0, DEL, C1
164
+ // and bidirectional controls) or `invisible`, `[point, times]`, the most
165
+ // found first: those npmTextEncoding found in one pass, told apart by code
166
+ // point. The labels a text's characters take, of those two.
167
+ const CONTROL = new RegExp(CONTROLS.source, 'u')
168
+ export const charactersOf = (controls, label) => [...controls ?? []]
169
+ .filter(([point]) => CONTROL.test(String.fromCodePoint(point)) === (label === 'controls')).toSorted((a, b) => b[1] - a[1] || a[0] - b[0])
170
+ const characterLabels = controls => controls ? ['controls', 'invisible'].filter(label => charactersOf(controls, label).length > 0) : []
171
+