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

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 (311) 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-2KBF5TZ7.js +2 -0
  72. package/out/chunk-2XUEZHIP.js +28 -0
  73. package/out/chunk-35VE6CDH.js +2 -0
  74. package/out/chunk-3BZ7JERB.js +1 -0
  75. package/out/chunk-4F6JSF57.js +3 -0
  76. package/out/chunk-4XCDFGDP.js +10 -0
  77. package/out/chunk-AQ75QITE.js +1 -0
  78. package/out/chunk-B2RQY6OP.js +2 -0
  79. package/out/chunk-C4Z6ZBXV.js +10 -0
  80. package/out/chunk-CMQA4DM6.js +1 -0
  81. package/out/chunk-CVVJAXVY.js +2 -0
  82. package/out/chunk-DIJJNTRH.js +10 -0
  83. package/out/chunk-GJ4IUCNS.js +1 -0
  84. package/out/chunk-GNXOKIGP.js +80 -0
  85. package/out/chunk-GZ4MS3TE.js +13 -0
  86. package/out/chunk-IISEF2BZ.js +1 -0
  87. package/out/chunk-IU3CCINR.js +3 -0
  88. package/out/chunk-K3X5EJBQ.js +2 -0
  89. package/out/chunk-KEX2ATEW.js +2 -0
  90. package/out/chunk-KYCNJVB7.js +1 -0
  91. package/out/chunk-MYDDTA7W.js +225 -0
  92. package/out/chunk-NRPC74AM.js +1 -0
  93. package/out/chunk-OOJM4CTU.js +1 -0
  94. package/out/chunk-PNHBZGDD.js +1 -0
  95. package/out/chunk-R5O6FAW4.js +2 -0
  96. package/out/chunk-RFZQQXSG.js +17 -0
  97. package/out/chunk-RSHMVYNS.js +1 -0
  98. package/out/chunk-TGANXVTA.js +1 -0
  99. package/out/chunk-TUII34K2.js +1 -0
  100. package/out/chunk-TZ5KB2Y5.js +10 -0
  101. package/out/chunk-UN5ZSYLE.js +1 -0
  102. package/out/chunk-UNTRGE26.js +2 -0
  103. package/out/chunk-UXNUAIIC.js +33 -0
  104. package/out/chunk-UYAXVRVL.js +1 -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-XYDGU6BM.js +51 -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 +297 -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 +358 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +142 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +219 -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/specs.js +124 -0
  154. package/risk/npm/tarball.js +113 -0
  155. package/server-common/database-config.ts +16 -0
  156. package/server-common/initialize.ts +18 -0
  157. package/server-common/npm-advisories.ts +101 -0
  158. package/{server → server-common}/origin.ts +5 -5
  159. package/server-common/reap.ts +48 -0
  160. package/server-common/scan-config.ts +19 -0
  161. package/server-common/standalone.ts +29 -0
  162. package/server-common/storage-crypto.ts +182 -0
  163. package/server-common/storage-log.ts +38 -0
  164. package/server-common/vercel-blob.ts +103 -0
  165. package/server-e2e/app.ts +486 -0
  166. package/{server → server-e2e}/auth.ts +5 -1
  167. package/{server → server-e2e}/bus-receiver.ts +2 -1
  168. package/{server → server-e2e}/cli.js +9 -4
  169. package/{server → server-e2e}/config.ts +56 -35
  170. package/{server → server-e2e}/db-neon.ts +2 -2
  171. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  172. package/{server → server-e2e}/db-stmt.ts +2 -2
  173. package/{server → server-e2e}/db.ts +7 -7
  174. package/{server → server-e2e}/http.ts +116 -18
  175. package/{server → server-e2e}/hub.ts +40 -9
  176. package/server-e2e/index.ts +17 -0
  177. package/server-e2e/lifecycle.ts +95 -0
  178. package/{server → server-e2e}/neon-driver.ts +2 -2
  179. package/{server → server-e2e}/npm-proxy.ts +12 -145
  180. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  181. package/{server → server-e2e}/objstore/blob.ts +1 -1
  182. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  183. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  184. package/{server → server-e2e}/objstore/init.ts +35 -18
  185. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  186. package/server-e2e/objstore/rest-deny.ts +28 -0
  187. package/server-e2e/objstore/rest-mint.ts +224 -0
  188. package/{server → server-e2e}/objstore/rest.ts +27 -26
  189. package/{server → server-e2e}/objstore/sign.ts +105 -0
  190. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  191. package/{server → server-e2e}/objstore/store.ts +7 -7
  192. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  193. package/{server → server-e2e}/pubsub.ts +12 -9
  194. package/{server → server-e2e}/sign.ts +4 -4
  195. package/{server → server-e2e}/sse-server.ts +102 -68
  196. package/{server → server-e2e}/sse-session.ts +28 -8
  197. package/{server → server-e2e}/static.ts +31 -21
  198. package/server-e2e/sync-handlers.ts +408 -0
  199. package/{server → server-e2e}/ws-server.ts +20 -9
  200. package/server-managed/activity.ts +245 -0
  201. package/server-managed/avatar-store.ts +12 -0
  202. package/server-managed/blob-store.ts +17 -0
  203. package/server-managed/brotli.ts +10 -0
  204. package/server-managed/bundle-advisories.ts +120 -0
  205. package/server-managed/bundle-advisory-inventory.ts +77 -0
  206. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  207. package/server-managed/bundle-build-diagnostics.js +23 -0
  208. package/server-managed/bundle-build-leases.ts +41 -0
  209. package/server-managed/bundle-build-worker.js +111 -0
  210. package/server-managed/bundle-build.ts +198 -0
  211. package/server-managed/bundle-cache.ts +199 -0
  212. package/server-managed/bundle-catalog.ts +29 -0
  213. package/server-managed/bundle-commits.ts +140 -0
  214. package/server-managed/bundle-store.ts +24 -0
  215. package/server-managed/bundle-summary-cache.ts +130 -0
  216. package/server-managed/bundle.ts +120 -0
  217. package/server-managed/cache-storage.ts +19 -0
  218. package/server-managed/cli.js +21 -0
  219. package/server-managed/combined.ts +46 -0
  220. package/server-managed/comments.ts +151 -0
  221. package/server-managed/config.ts +158 -0
  222. package/server-managed/content-access.ts +15 -0
  223. package/server-managed/crypto.ts +25 -0
  224. package/server-managed/db-methods.ts +1725 -0
  225. package/server-managed/db-neon.ts +367 -0
  226. package/server-managed/db-schema.ts +227 -0
  227. package/server-managed/db-table-names.ts +22 -0
  228. package/server-managed/db.ts +132 -0
  229. package/server-managed/dependency-reviews.ts +248 -0
  230. package/server-managed/github-app.ts +391 -0
  231. package/server-managed/github-errors.ts +68 -0
  232. package/server-managed/github-issue-links.ts +127 -0
  233. package/server-managed/github-issue-oauth.ts +47 -0
  234. package/server-managed/github-issues.ts +152 -0
  235. package/server-managed/github-metadata.ts +213 -0
  236. package/server-managed/github-oauth.ts +235 -0
  237. package/server-managed/github-pulls.ts +227 -0
  238. package/server-managed/http-response.ts +50 -0
  239. package/server-managed/http.ts +3472 -0
  240. package/server-managed/import-triage.ts +51 -0
  241. package/server-managed/index.ts +187 -0
  242. package/server-managed/kept.ts +28 -0
  243. package/server-managed/link-reports.ts +158 -0
  244. package/server-managed/locked-packages.ts +85 -0
  245. package/server-managed/lru.ts +30 -0
  246. package/server-managed/managed-issues.ts +75 -0
  247. package/server-managed/management-catalog.ts +45 -0
  248. package/server-managed/management.ts +99 -0
  249. package/server-managed/npm-insights.ts +440 -0
  250. package/server-managed/npm-kept.ts +65 -0
  251. package/server-managed/npm-loads.ts +167 -0
  252. package/server-managed/npm-manifest.ts +115 -0
  253. package/server-managed/npm-packages.ts +378 -0
  254. package/server-managed/npm-provenance.ts +123 -0
  255. package/server-managed/npm-risk.ts +177 -0
  256. package/server-managed/object-storage-disk.ts +148 -0
  257. package/server-managed/object-storage-vercel.ts +67 -0
  258. package/server-managed/object-storage.ts +74 -0
  259. package/server-managed/package-entry-points.ts +51 -0
  260. package/server-managed/pretty-print.ts +352 -0
  261. package/server-managed/public-workspace.ts +220 -0
  262. package/server-managed/rename-diff-worker.js +26 -0
  263. package/server-managed/rename-diff.ts +270 -0
  264. package/server-managed/repo-path.ts +21 -0
  265. package/server-managed/report-migration.ts +35 -0
  266. package/server-managed/report-query.ts +10 -0
  267. package/server-managed/report-response.ts +16 -0
  268. package/server-managed/report-sources.ts +178 -0
  269. package/server-managed/report-store.ts +47 -0
  270. package/server-managed/repository-aliases.ts +91 -0
  271. package/server-managed/repository-browser.ts +153 -0
  272. package/server-managed/repository-discovery.ts +82 -0
  273. package/server-managed/repository-policy.ts +27 -0
  274. package/server-managed/repository-risk.ts +418 -0
  275. package/server-managed/revisions.ts +35 -0
  276. package/server-managed/session.ts +92 -0
  277. package/server-managed/slugs.ts +38 -0
  278. package/server-managed/solidity-entry-points.ts +69 -0
  279. package/server-managed/sql-postgres.ts +31 -0
  280. package/server-managed/sql.ts +76 -0
  281. package/server-managed/static.ts +28 -0
  282. package/server-managed/storage-command.ts +16 -0
  283. package/server-managed/storage-db.ts +175 -0
  284. package/server-managed/storage-encryption.ts +279 -0
  285. package/server-managed/storage-maintenance.ts +256 -0
  286. package/server-managed/storage-payload.ts +103 -0
  287. package/server-managed/storage-stores.ts +85 -0
  288. package/server-managed/storage.ts +78 -0
  289. package/server-managed/team-app.ts +94 -0
  290. package/server-managed/team-catalog.ts +7 -0
  291. package/server-managed/team-feed.ts +173 -0
  292. package/server-managed/team-npm-scopes.ts +77 -0
  293. package/server-managed/team-reports.ts +228 -0
  294. package/server-managed/triage-response.ts +16 -0
  295. package/server-managed/type-only-worker.js +19 -0
  296. package/server-managed/type-only.ts +103 -0
  297. package/server-managed/uploads.ts +86 -0
  298. package/server-managed/upstream-cache.ts +215 -0
  299. package/server-managed/view-as.ts +58 -0
  300. package/server-managed/workspace-shares.ts +166 -0
  301. package/server.ts +50 -0
  302. package/server/index.ts +0 -477
  303. package/server/lifecycle.ts +0 -204
  304. package/server/sync-handlers.ts +0 -320
  305. /package/{LICENSE → LICENSE-MIT} +0 -0
  306. /package/{server → server-e2e}/config.example.json +0 -0
  307. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  308. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  309. /package/{server → server-e2e}/peer.ts +0 -0
  310. /package/{server → server-e2e}/util.ts +0 -0
  311. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,358 @@
1
+ // What a file's code can do beyond itself, for a reviewer: run a string as
2
+ // code (eval, Function with new or without, node:vm, Module's _compile) or
3
+ // other programs (child_process), reach for credentials (a credential file's
4
+ // path, a token's name) or the whole environment, and publish to npm (`npm
5
+ // publish`, `/-/whoami`, a registry PUT), as a file's stamps say
6
+ // (stamps.js) and a version's findings name what is new to a file.
7
+ // A quick read, not a parse: a script's code with its strings, regular
8
+ // expressions and comments set aside as script.js tells them, its
9
+ // strings read for the paths, names and commands they hold, a module by the
10
+ // string naming it where it is loaded (`require('child_process')`,
11
+ // `import … from 'node:child_process'`); a declaration file (.d.ts) read as
12
+ // TypeScript, as node runs it (stripping its types, `declare function
13
+ // eval(…)` among them, a function named, not called). A call written not to
14
+ // read as one (`window['ev' + 'al']`) goes unseen, as does one in a template
15
+ // literal in another's substitution (`${`${eval(x)}`}`), whose backtick ends
16
+ // the outer one as the regular expression telling strings reads it.
17
+ //
18
+ // Labels to assist a reviewer, making the plain cases less likely to be
19
+ // missed, not a check to rely on: code can come to eval, Function,
20
+ // child_process, credentials or the registry in more ways than any reading of
21
+ // its text tells (a name built as it runs, `(…).constructor('…')`, a module
22
+ // required by a name computed, a path or URL put together, the
23
+ // registry's host read from config), so a file without a label is not one
24
+ // without them.
25
+ import { JS_STRING_OR_COMMENT, NPM_MAX_FOUND, lineOf, npmIsScript } from './script.js'
26
+
27
+ // What may come between a call's parts: blanks and comments
28
+ // (`Function/* note */(…)`), each comment ending at its first `*/`.
29
+ const COMMENT = String.raw`/\*(?:[^*]|\*(?!/)){0,100}\*/|//[^\n]{0,100}\n`
30
+ const GAP = String.raw`(?:\s|${COMMENT})*`
31
+ // Function called: as it is, optionally (`Function?.(…)`), or by its call or
32
+ // apply (`Function.call(null, …)`), each making a function of its last
33
+ // argument.
34
+ const FUNCTION_CALLED = String.raw`(?:${GAP}\??\.${GAP}(?:call|apply))?${GAP}(?:\?\.${GAP})?\(`
35
+ // But not where the function made returns `this` alone
36
+ // (`Function('return this')()`, `new Function("return this;")`): the global
37
+ // object, as older code finds it where `globalThis` may not be, lodash's and
38
+ // core-js's among them, running nothing of its own.
39
+ const GLOBAL_FOUND = String.raw`(?!${GAP}(?<quote>["'\u0060])\s*return\s+this\s*;?\s*\k<quote>${GAP}\))`
40
+ // The environment, whole: not one variable of it (`process.env.HOME`,
41
+ // `process.env[name]`).
42
+ const ENV = String.raw`process${GAP}\.${GAP}env(?![\p{ID_Continue}$])(?!${GAP}(?:\??\.|\[))`
43
+ // Its values read out whole (`JSON.stringify(process.env)`,
44
+ // `Object.entries(process.env)`, `new URLSearchParams(process.env)`), or a
45
+ // copy of them (`JSON.stringify({ ...process.env })`), not as a program is
46
+ // handed it (`{ env: process.env }`, `{ ...process.env }`), nor its
47
+ // names listed (`Object.keys(process.env)`, as debug picks its DEBUG_ ones),
48
+ // nor one variable asked for (`'HOME' in process.env`), nor its entries or
49
+ // values picked among as they are read (`Object.entries(process.env)
50
+ // .reduce(…)`, as npm's agent finds its proxy's, `.filter(…)`, `.find(…)`).
51
+ const PICKED = String.raw`${GAP}\)${GAP}\??\.${GAP}(?:filter|find|findLast|some|every|reduce)(?![\p{ID_Continue}$])`
52
+ const COPY = String.raw`(?:\{${GAP}\.\.\.${GAP})?`
53
+ const ENV_WHOLE = String.raw`(?=JSON|Object|URLSearchParams)(?<![\p{ID_Continue}$.])(?:JSON${GAP}\.${GAP}stringify|URLSearchParams)${GAP}\(${GAP}${COPY}${ENV}`
54
+ + String.raw`|(?=Object)(?<![\p{ID_Continue}$.])Object${GAP}\.${GAP}(?:values|entries)${GAP}\(${GAP}${COPY}${ENV}(?!${PICKED})`
55
+ // Any of RUNS_CODE, as a text may say it.
56
+ const SAYS_ANY = new RegExp(String.raw`eval|Function${FUNCTION_CALLED}|vm["'\x60]|_compile|child_process|\.(?:npmrc|ssh|aws)|_TOKEN|(?:stringify|values|entries|URLSearchParams)${GAP}\(${GAP}(?:\{${GAP}\.\.\.${GAP})?process${GAP}\.${GAP}env|publish|whoami|registry\.(?:npmjs\.org|yarnpkg\.com)`, 'u')
57
+
58
+ // A name read as one, not as a property's (`vm.eval`, `vm . eval`, a spread's
59
+ // `...eval` aside) or part of another's (`evaluate`), or as the global
60
+ // object's (`window.eval`, `globalThis?.eval`); looked back on only where the
61
+ // name or the global object's starts, so a long run of blanks reads in one go.
62
+ const GLOBAL = 'globalThis|window|global|self'
63
+ const named = name => String.raw`(?=${GLOBAL}|${name})(?<![\p{ID_Continue}$#])(?<!(?<!\.)\.${GAP})(?<!\bfunction${GAP})(?:(?:${GLOBAL})${GAP}\??\.${GAP})?${name}`
64
+ // A token's name, as its value is read (`process.env.GITHUB_TOKEN || null`,
65
+ // `env.NPM_TOKEN`, `||=`), not where its absence is told
66
+ // (`!process.env.GITHUB_TOKEN`, `!(process.env.GITHUB_TOKEN)`) or it is
67
+ // dropped (`delete process.env.NPM_TOKEN`), the token the whole of what is
68
+ // negated or deleted (ABSENT_ENDS: not `!process.env.GITHUB_TOKEN.trim()`,
69
+ // `!(process.env.GITHUB_TOKEN === x)`), nor where it is only set
70
+ // (`process.env.NPM_TOKEN = token`), but for a default as it is destructured
71
+ // (`{ NPM_TOKEN = '' } = process.env`, a line of its own too), nor declared
72
+ // (`const NPM_TOKEN = …`, TypeScript's `let GITHUB_TOKEN: string`), nor typed
73
+ // (a member, `GITHUB_TOKEN?: string`, `NPM_TOKEN: string | undefined`, or a
74
+ // parameter): looked back on, a few names and dots at most, only where the
75
+ // name starts.
76
+ const TOKEN_VAR = '(?:NPM|GITHUB)_TOKEN'
77
+ const SET = String.raw`\s{0,4}=(?![=>])`
78
+ // A name typed, not read: optional (`?:`), or a primitive's type after it,
79
+ // as no destructuring names what it reads (`{ NPM_TOKEN: token }`); not
80
+ // `undefined`, a ternary's choice (`x ? env.NPM_TOKEN : undefined`).
81
+ const TYPED = String.raw`\s{0,4}(?:\?:|:\s{0,4}(?:string|number|boolean|unknown|any)(?![\p{ID_Continue}$]))`
82
+ // What may follow a token negated or deleted whole: the parentheses around
83
+ // it closed, nothing read of what they hold (`!(token).trim()`, `!(token)()`), or else what
84
+ // ends an operand (`&&`, `||`, `??`, a ternary's `?`, `;`, `,`, `:`, `}`, `]`,
85
+ // the line's end).
86
+ const ABSENT_ENDS = String.raw`\s{0,4}(?:(?:\)\s{0,4}){1,4}(?![\s)]{0,8}(?:[.[(]|\?\.))|&&|\|\||\?\?|\?(?!\.)|[;,:}\]]|$)`
87
+ const TOKEN_NAME = String.raw`(?=NPM|GITHUB)(?<![\p{ID_Continue}$])(?<!\b(?:const|let|var)\s{1,4})`
88
+ + String.raw`(?:(?<!(?:!|\bdelete\s)[\s(]{0,4}(?:[\p{ID_Continue}$]{1,64}\s{0,4}\??\.\s{0,4}){0,4})|(?!${TOKEN_VAR}${ABSENT_ENDS}))`
89
+ + String.raw`${TOKEN_VAR}(?![\p{ID_Continue}$])(?!${TYPED})(?:(?!${SET})|(?<=[{,]\s{0,64}${TOKEN_VAR}))`
90
+ // A script's strings, regular expressions and comments, as script.js
91
+ // tells them, each taking its own text, and its code in none of them: eval,
92
+ // not a property TypeScript types as optional (`eval?: T`, JavaScript's `?:`
93
+ // none of its own), and Function called, but to find the global object;
94
+ // Module's _compile, called
95
+ // (`module._compile(source, filename)`) or hooked (`Module.prototype._compile
96
+ // = …`), not named in a type (`typeof Module.prototype._compile`); a token's
97
+ // name, its value read; the environment read out whole; and a PUT request's
98
+ // call (`axios.put(…)`).
99
+ const READS = new RegExp(String.raw`${JS_STRING_OR_COMMENT.source}|(?<eval>${named('eval')}(?![\p{ID_Continue}$])(?!\s*\?:))|(?<function>${named('Function')}${FUNCTION_CALLED}${GLOBAL_FOUND})`
100
+ + String.raw`|(?<compile>\.${GAP}_compile(?:${FUNCTION_CALLED}|${GAP}=(?![=>])))|(?<tokens>${TOKEN_NAME})|(?<env>${ENV_WHOLE})|(?<put>\.${GAP}put${GAP}\()`, 'gmu')
101
+ // What a string holds that a kind is: a credential file's path, npm's, SSH's
102
+ // or AWS's (`~/.npmrc`, `.ssh/id_rsa`, `.aws/credentials`, not `x.npmrc` or
103
+ // `.sshd`); a token's name where it is read, the string the name alone (a
104
+ // key, `process.env['GITHUB_TOKEN']`) or the name expanded (a shell's or
105
+ // .npmrc's `$GITHUB_TOKEN`, `${NPM_TOKEN}`, cmd's `%NPM_TOKEN%`), not where
106
+ // text names it (`requires a token in GITHUB_TOKEN`); a command publishing (`npm publish`, `pnpm
107
+ // publish …`, options before it as npm takes them, `npm --tag next publish`)
108
+ // or the registry's who-am-I, a publisher's first step with a token. And the
109
+ // registry, where a PUT request is one publishing (REGISTRY_URL).
110
+ // Told first of a string by what any of them says.
111
+ const HOLDS_ANY = /\.(?:npmrc|ssh|aws)|_TOKEN|publish|whoami|registry\./u
112
+ // Before a token's name as a key, its absence told or the token dropped, as
113
+ // TOKEN_NAME tells of one as a name (`!process.env['GITHUB_TOKEN']`,
114
+ // `delete process.env['NPM_TOKEN']`): a few names and dots at most, and
115
+ // after it, its key's bracket closed, what may follow it whole (ABSENT_ENDS,
116
+ // not `!process.env['NPM_TOKEN'].length`). After it, the token set
117
+ // (`process.env['NPM_TOKEN'] = token`).
118
+ const KEY_SET = new RegExp(String.raw`^\s{0,4}\]${SET}`, 'u')
119
+ const KEY_ENDS = new RegExp(String.raw`^\s{0,4}\]${ABSENT_ENDS}`, 'mu')
120
+ const KEY_ABSENT = /(?:!|\bdelete\s)[\s(]{0,4}(?:[\p{ID_Continue}$]{1,64}\s{0,4}\??\.\s{0,4}){0,4}[\p{ID_Continue}$]{1,64}\s{0,4}(?:\?\.\s{0,4})?\[\s{0,4}$/u
121
+ // A string's text as JavaScript reads it, its escapes decoded (`\x2f`,
122
+ // `\u002f`, `\u{2f}`, sloppy mode's octal `\057`, `\/`, `\\`, `\n`, `\t`), a
123
+ // line continued (a backslash before a line's end) joined: what a URL in it
124
+ // spells, however its separators are escaped or its lines broken.
125
+ const ESCAPE = /\\(?:x([\da-f]{2})|u([\da-f]{4})|u\{([\da-f]{1,6})\}|([0-3][0-7]{0,2}|[4-7][0-7]?)|(\r\n|[^]))/giu
126
+ const CONTROLS = new Map([['n', '\n'], ['r', '\r'], ['t', '\t'], ['b', '\b'], ['f', '\f'], ['v', '\v']])
127
+ const LINE_ENDS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029'])
128
+ const unescaped = text => text.replace(ESCAPE, (escape, hex, unit, point, octal, char) => {
129
+ if (char !== undefined) return LINE_ENDS.has(char) ? '' : CONTROLS.get(char) ?? char
130
+ const code = octal === undefined ? Number.parseInt(hex ?? unit ?? point, 16) : Number.parseInt(octal, 8)
131
+ return code <= 0x10_FFFF ? String.fromCodePoint(code) : escape
132
+ })
133
+ // The registry's host in a string, as JavaScript reads it (unescaped) and a
134
+ // URL's parsing does, its tabs and line breaks gone, as a URL's authority: after two slashes, backslashes URLs take for them too, or
135
+ // after an http(s) scheme's colon (its slashes optional there, as URLs take
136
+ // them), its userinfo between, to its last `@` before the URL's path, query or
137
+ // fragment, quotes and all, 256
138
+ // characters at most, as credentials are, so a URL's every start is read
139
+ // in bounded time (`https://registry.npmjs.org/`, `.npmrc`'s
140
+ // `//registry.npmjs.org/`, `https://${token}@registry.npmjs.org/`,
141
+ // `https:\\\\registry.npmjs.org`, `https:\x2f\x2fregistry.npmjs.org`,
142
+ // `https:registry.npmjs.org`); or starting the string, its whole or before its
143
+ // path, port, query or fragment (npm-profile's `'registry.npmjs.org'`, a URL joined,
144
+ // `'https://' + 'registry.npmjs.org/'`). Not in prose, as anywhere else
145
+ // (`registry.npmjs.org</span>`, `asks registry.npmjs.org`, `[registry.npmjs.org]`,
146
+ // `note:registry.npmjs.org`, `user@registry.npmjs.org`, `docs/registry.npmjs.org`).
147
+ const REGISTRY_HOST = String.raw`registry\.(?:npmjs\.org|yarnpkg\.com)`
148
+ const REGISTRY_URL = new RegExp(String.raw`(?:[/\\]{2}|\bhttps?:[/\\]?)(?:[^/\\?#]{0,256}@)?${REGISTRY_HOST}|^(["'\u0060])${REGISTRY_HOST}(?=\1$|[/:?#])`, 'iu')
149
+ const URL_STRIPPED = /[\t\n\r]/gu
150
+ const HOLDS = [
151
+ ['credential-files', /(?<![\w$.-])\.(?:npmrc|ssh|aws)(?![\w-])/gu],
152
+ ['tokens', new RegExp(String.raw`^["'\u0060]${TOKEN_VAR}["'\u0060]$|(?:\$\{?|%)${TOKEN_VAR}(?![\p{ID_Continue}$])`, 'gu')],
153
+ ['publish', /(?<![\w-])(?:npm|pnpm|yarn)(?:\s+-{1,2}\w[\w-]*(?:=\S*|\s+(?!-)\S+)?){0,8}?\s+publish(?![\w-])|\/-\/whoami(?![\w-])/gu],
154
+ ]
155
+ // A module a kind loads: vm, child_process, and libnpmpublish, npm's own
156
+ // publishing.
157
+ const MODULES = [
158
+ ['vm', /^(["'\u0060])(?:node:)?vm\1$/u],
159
+ ['child-process', /^(["'\u0060])(?:node:)?child_process\1$/u],
160
+ ['publish', /^(["'\u0060])libnpmpublish\1$/u],
161
+ ]
162
+ // A string a program is run with publishing (`spawn('npm', ['publish'])`),
163
+ // after the one naming it, its options and their values between
164
+ // (`['--tag', 'next', 'publish']`), and a request's method set to PUT
165
+ // (`{ method: 'PUT' }`, `options.method = 'put'`).
166
+ const PROGRAM = /^(["'\u0060])(?:npm|pnpm|yarn)\1$/u
167
+ const PUBLISH = /^(["'\u0060])publish\1$/u
168
+ const OPTION = /^(["'\u0060])-/u
169
+ const ARGUMENTS_BETWEEN = new RegExp(String.raw`^(?:[\s,[]|${COMMENT})*$`, 'u')
170
+ const PUT = /^(["'\u0060])put\1$/iu
171
+ const METHOD = new RegExp(String.raw`\bmethod["']?${GAP}[:=]${GAP}$`, 'u')
172
+ // What comes before a module's name where code loads it: `require(` (or
173
+ // `require?.(`), a bundler's `__require(` or `__non_webpack_require__(`,
174
+ // `import(`, `import` and `from`, comments allowed between
175
+ // (`import(/* webpackIgnore: true */ …)`); a name not loaded, as in a list of
176
+ // node's modules, is none, nor one a type-only import or export names
177
+ // (`import type { … } from`, `import type cp = require(`, comments allowed as
178
+ // in a load), which TypeScript erases (`import type from` imports a default
179
+ // named type, `import type = require(` one named so).
180
+ const LOADS = new RegExp(String.raw`(?:(?:require[\w$]*${GAP}(?:\?\.${GAP})?|\bimport${GAP})\(|\b(?:from|import))${GAP}$`, 'u')
181
+ const TYPE_ONLY = new RegExp(String.raw`\b(?:import|export)${GAP}type\b(?!${GAP}(?:from\b|=))[^;'"\x60]*(?:\bfrom|=${GAP}require${GAP}\()${GAP}$`, 'u')
182
+
183
+ // Each kind, in the order its tags show: its tag, what it does, its name in
184
+ // the tag's tooltip, a row of its own where kinds share the tag (a string run
185
+ // as code by any way, `eval`'s; a credential file or token, `credentials`'),
186
+ // and for one marked as worse than the others' warning, `critical`: the whole
187
+ // environment read out, and publishing.
188
+ const AS_CODE = { tag: 'eval', note: 'Runs a string as code' }
189
+ const CREDENTIALS = { tag: 'credentials', note: 'Reaches for credentials' }
190
+ export const RUNS_CODE = new Map([
191
+ ['eval', { ...AS_CODE, name: 'eval' }],
192
+ ['function', { ...AS_CODE, name: 'Function' }],
193
+ ['vm', { ...AS_CODE, name: 'vm' }],
194
+ ['compile', { ...AS_CODE, name: 'Module._compile' }],
195
+ ['child-process', { tag: 'child_process', note: 'Runs other programs', name: 'child_process' }],
196
+ ['credential-files', { ...CREDENTIALS, name: '.npmrc, .ssh or .aws' }],
197
+ ['tokens', { ...CREDENTIALS, name: 'NPM_TOKEN or GITHUB_TOKEN' }],
198
+ ['env', { tag: 'whole env', note: 'Reads out the whole environment', name: 'process.env, by JSON.stringify, Object.entries or values, URLSearchParams', mark: 'critical' }],
199
+ ['publish', { tag: 'publish', note: 'Publishes to npm', name: 'npm publish, /-/whoami or a registry PUT', mark: 'critical' }],
200
+ ])
201
+
202
+ // The kinds a file's code holds, each with the lines it is on, of the first
203
+ // NPM_MAX_FOUND places it does each, in RUNS_CODE's order; null for none, or
204
+ // for a file that is no script.
205
+ export function npmRunsCode(path, text) {
206
+ if (!npmIsScript(path, text)) return null
207
+ const found = new Map()
208
+ const at = (kind, offset) => {
209
+ const offsets = found.get(kind)
210
+ if (!offsets) found.set(kind, [offset])
211
+ else if (offsets.length < NPM_MAX_FOUND) offsets.push(offset)
212
+ }
213
+ read(text, 0, at)
214
+ // A PUT request publishing where the file names the registry.
215
+ if (found.has('registry')) for (const offset of found.get('put') ?? []) at('publish', offset)
216
+ if (![...RUNS_CODE.keys()].some(kind => found.has(kind))) return null
217
+ const line = lineOf(text)
218
+ return new Map([...RUNS_CODE.keys()].filter(kind => found.has(kind)).map(kind => [kind, [...new Set(found.get(kind).map(line))].toSorted((a, b) => a - b)]))
219
+ }
220
+
221
+ // Where code (a file's, or a template literal's substitution's, at `base` in
222
+ // the file) does a kind, as at(kind, offset), in order: its code's, what its
223
+ // strings hold, a string naming a module where it is loaded, and what a
224
+ // template literal's substitutions (`${eval(x)}`) do, code again, read as its
225
+ // own; a PUT request (`put`) and the registry's host (`registry`) as their
226
+ // own, for the file to pair. Most code says none of it at all, told before
227
+ // anything is read.
228
+ function read(code, base, at) {
229
+ if (!SAYS_ANY.test(code)) return
230
+ // Where the string before ends; whether the strings since a program's name
231
+ // are its arguments, an option or its value each, and whether the last is
232
+ // an option.
233
+ let end = 0, option = false, program = false
234
+ for (const { 0: token, 1: string, groups, index } of code.matchAll(READS)) {
235
+ if (string === undefined) {
236
+ for (const kind of CODE_KINDS) if (groups[kind]) at(kind, base + index)
237
+ continue
238
+ }
239
+ if (string[0] === '/') continue
240
+ const inner = string[0] === '`' && SAYS_ANY.test(string) ? substitutions(string) : []
241
+ for (const [from, to] of inner) read(string.slice(from, to), base + index + from, at)
242
+ // What a string holds, a template's text alone, its substitutions' code
243
+ // read as code, its comments and regular expressions aside.
244
+ const text = inner.length === 0 ? string : textOf(string, inner)
245
+ // A token's name as the string's whole (`within` 0, its quote) is a key,
246
+ // not one where its absence is told, it is dropped or only set.
247
+ const keyed = () => {
248
+ const after = code.slice(index + token.length, index + token.length + 20)
249
+ return !KEY_SET.test(after) && (!KEY_ENDS.test(after) || !KEY_ABSENT.test(code.slice(Math.max(0, index - 300), index)))
250
+ }
251
+ const held = HOLDS_ANY.test(text) ? HOLDS.flatMap(([kind, holds]) => [...text.matchAll(holds)].map(({ index: within }) => [kind, within])) : []
252
+ for (const [kind, within] of held) if (kind !== 'tokens' || within > 0 || keyed()) at(kind, base + index + within)
253
+ if (text.includes('registry.') && REGISTRY_URL.test(unescaped(text).replace(URL_STRIPPED, ''))) at('registry', base + index)
254
+ for (const [kind, name] of MODULES) if (name.test(string) && loaded(code, index)) at(kind, base + index)
255
+ if (program && ARGUMENTS_BETWEEN.test(code.slice(end, index))) {
256
+ if (PUBLISH.test(string)) at('publish', base + index)
257
+ program = OPTION.test(string) || option
258
+ option = OPTION.test(string)
259
+ } else program = false
260
+ if (PROGRAM.test(string)) [program, option] = [true, false]
261
+ if (PUT.test(string) && METHOD.test(code.slice(Math.max(0, index - 300), index))) at('put', base + index)
262
+ end = index + token.length
263
+ }
264
+ }
265
+
266
+ // The kinds code itself does, by READS' groups.
267
+ const CODE_KINDS = ['eval', 'function', 'compile', 'tokens', 'env', 'put']
268
+
269
+ // Whether the string at `index` in code names a module where it is loaded.
270
+ function loaded(code, index) {
271
+ const before = code.slice(Math.max(0, index - 300), index)
272
+ return LOADS.test(before) && !TYPE_ONLY.test(before)
273
+ }
274
+
275
+ // A template literal with its substitutions' code blanked, its text in place.
276
+ function textOf(template, inner) {
277
+ const parts = []
278
+ let from = 0
279
+ for (const [start, end] of inner) {
280
+ parts.push(template.slice(from, start), ' '.repeat(end - start))
281
+ from = end
282
+ }
283
+ parts.push(template.slice(from))
284
+ return parts.join('')
285
+ }
286
+
287
+ // Where each of a template literal's substitutions' code starts and ends, in
288
+ // it: from its `${` to the `}` that closes it.
289
+ function substitutions(template) {
290
+ const found = []
291
+ for (let i = template.indexOf('${'); i !== -1; i = template.indexOf('${', i)) {
292
+ // Escaped by an odd run of backslashes before it (`\${`, not `\\${`).
293
+ let run = 0
294
+ while (template[i - 1 - run] === '\\') run++
295
+ if (run % 2 === 1) {
296
+ i += 2
297
+ continue
298
+ }
299
+ const end = closing(template, i + 2)
300
+ found.push([i + 2, end])
301
+ i = end
302
+ }
303
+ return found
304
+ }
305
+
306
+ // A string, regular expression or comment where it starts, told as
307
+ // JS_STRING_OR_COMMENT tells them in a whole text.
308
+ const TOKEN = new RegExp(JS_STRING_OR_COMMENT.source, 'muy')
309
+
310
+ // Where the `}` closing code from `from` in a text is, or the text's end for
311
+ // none: the first its braces leave unbalanced, those in its strings, regular
312
+ // expressions and comments aside (`${/* } */ x}`, `${/}/.test(x)}`).
313
+ function closing(text, from) {
314
+ let depth = 0
315
+ for (let at = from; at < text.length; at++) {
316
+ const char = text[at]
317
+ // Tried where a token can start: a quote, a slash, or a run of blanks, a
318
+ // comment's leading blanks taken from where they start.
319
+ if ('"\'`/ \t'.includes(char)) {
320
+ TOKEN.lastIndex = at
321
+ const token = TOKEN.exec(text)
322
+ if (token) {
323
+ at += token[0].length - 1
324
+ continue
325
+ }
326
+ }
327
+ if (char === '{') depth++
328
+ else if (char === '}' && depth-- === 0) return at
329
+ }
330
+ return text.length
331
+ }
332
+
333
+ // What `found` holds (npmRunsCode's kinds, hosts.js's services) that
334
+ // `was` didn't, each with its lines: what is new to a file, as its tags say,
335
+ // `was` null for one the version compared with didn't hold, or held none in.
336
+ // Null for none.
337
+ export function npmFoundAdded(found, was) {
338
+ if (!found) return null
339
+ const added = new Map([...found].filter(([kind]) => !was?.has(kind)))
340
+ return added.size > 0 ? added : null
341
+ }
342
+
343
+ // The kinds any of `founds` holds, as their tags name them, in their order,
344
+ // a tag kinds share once: `eval, child_process`.
345
+ export const npmRunsCodeKinds = founds => [...new Set([...RUNS_CODE].filter(([kind]) => founds.some(found => found.has(kind))).map(([, { tag }]) => tag))].join(', ')
346
+
347
+ // What `found` holds (npmRunsCode's) but the kinds whose tags `tags` names,
348
+ // each with its lines; null for none.
349
+ export function npmRunsCodeWithout(found, tags) {
350
+ const kept = new Map([...found ?? []].filter(([kind]) => !tags.has(RUNS_CODE.get(kind).tag)))
351
+ return kept.size > 0 ? kept : null
352
+ }
353
+
354
+ // Its tags as a file's flags (file-flags.js), in their order, each once: a
355
+ // warning's (`high`), or for one marked worse, `critical`; none for none.
356
+ export const npmRunsCodeFlags = found => [...new Map([...found?.keys() ?? []].map(kind => RUNS_CODE.get(kind))
357
+ .map(({ tag, mark }) => [tag, { name: tag, severity: mark ?? 'high' }])).values()]
358
+
@@ -0,0 +1,114 @@
1
+ // How a script's text is read, for risk's readers of a file
2
+ // (readability.js, runs-code.js, hosts.js): JavaScript's and CSS's strings,
3
+ // regular expressions and comments, set apart from their code, what of their
4
+ // blanks a minifier drops, which files are scripts node runs, and each
5
+ // offset's line.
6
+
7
+ // Code minified into lines shorter than NPM_LONG_LINE: outside its strings and
8
+ // comments (`/* @__PURE__ */`), its lines average more than anyone writes and
9
+ // next to none of its spaces are ones a minifier drops. Told only in what
10
+ // minifiers write, as GitHub Linguist tells minified files only in JavaScript
11
+ // and CSS, each by its strings and comments (matched in one pass, so that
12
+ // neither starts inside the other, a comment with the spaces around it) and
13
+ // the spaces it can do without.
14
+ export const MINIFIED_AVERAGE = 110
15
+ const MINIFIED_SPACES = .01
16
+ // What a regular expression set aside holds.
17
+ const REGEX = '\uE000'
18
+ const JS = {
19
+ // A hashbang, the file's first line (`#!/usr/bin/env node`), a comment,
20
+ // after a byte order mark too, which node's ES module loader drops first.
21
+ // A string's or template's escape may be a line continuation (`\` before a
22
+ // line break, CRLF's whole).
23
+ // Linear on any text: a regular expression's context looked back on only
24
+ // before a `/`, a comment's leading blanks taken only from where they
25
+ // start, and what is left open read to where it would end, a string or a
26
+ // regular expression its line's end, a template or a comment the text's,
27
+ // rather than tried again from each later quote or slash.
28
+ // A regular expression first where a value starts (after `=>`, `...`, an operator or a keyword, not a property: `x.default/2`, `this.#in/2`), so a quote in it (`/["']/`) starts no string.
29
+ stringOrComment: /((?=\/)(?<=(?:^|\.\.\.|[(,=:[!&|?{};>+\-*/%^<~]|(?<![\p{ID_Continue}$.#])(?:await|case|default|delete|do|else|extends|in|instanceof|new|of|return|throw|typeof|void|yield))[ \t]*)\/(?![/*])(?:[^/\\[\n]|\\.|\[(?:[^\]\\\n]|\\.)*(?:\]|(?=\\?$)))+(?:\/[a-z]*|(?=\\?$))|"(?:[^"\\\n]|\\(?:\r\n|[\s\S]))*(?:"|$)|'(?:[^'\\\n]|\\(?:\r\n|[\s\S]))*(?:'|$)|`(?:[^`\\]|\\(?:\r\n|[\s\S]))*(?:`|(?![\s\S])))|(?<![ \t])[ \t]*(?:\/\*[\s\S]*?(?:\*\/|(?![\s\S]))|\/\/.*|(?<![\s\S])\uFEFF?#!.*)[ \t]*/gmu,
30
+ // Beside punctuation (`a, b`, `x = 1`), not between two words, as
31
+ // JavaScript tells a word's characters (`return a`, `var π`, `var \u03c0`),
32
+ droppable: /(?<![\p{ID_Continue}$\\\u200C\u200D])[ \t]+|[ \t]+(?![\p{ID_Continue}$\\\u200C\u200D])/gu,
33
+ // nor between two `+`, two `-` or two `/` (`a+ +b` is no `a++b`, `a/ /b/`
34
+ // no comment), nor after a regular expression before a word (`/a/ in b`).
35
+ needed: (code, index, run) => ('+-/'.includes(code[index - 1]) && code[index + run.length] === code[index - 1])
36
+ || (code[index - 2] === REGEX && /[\p{ID_Continue}$\\\u200C\u200D]/u.test(code[index + run.length])),
37
+ }
38
+ const CSS = {
39
+ // No `//` comments, so `url(https://…)` is code.
40
+ stringOrComment: /("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')|[ \t]*\/\*[\s\S]*?\*\/[ \t]*/gu,
41
+ // Beside braces, `;`, `,`, `>`, parentheses, after `:` (`a { color: red }`)
42
+ // and beside a selector's `+` or `~` (`.a + .b`), not those a selector or a
43
+ // value needs (`.a .b`, `1px solid #fff`, `a :hover`),
44
+ droppable: /(?<=[{};,:>(])[ \t]+|[ \t]+(?=[{};,>)!])|(?<=[+~])[ \t]+|[ \t]+(?=[+~])/gu,
45
+ // nor beside `+` or `~` in parentheses (`calc(1px + var(--x))`) or in a
46
+ // custom property's value, kept for a `calc()` to come (`--gap:1px + 2px`).
47
+ needed: (code, index, run, depth) => /[+~]/u.test(code[index - 1] + code[index + run.length])
48
+ && (depth > 0 || /^\s*--[^:]*:/u.test(code.slice(Math.max(...['{', ';', '}'].map(end => code.lastIndexOf(end, index))) + 1, index))),
49
+ }
50
+ // JavaScript's strings and regular expressions (the first group) and its
51
+ // comments, for a reader that sets them aside (runs-code.js, hosts.js).
52
+ export const JS_STRING_OR_COMMENT = JS.stringOrComment
53
+ export const minifiable = path => /\.[cm]?js$/iu.test(path) ? JS : /\.css$/iu.test(path) ? CSS : null
54
+ // How much of a text is read for how its lines are minified: their average
55
+ // is told by as much of them, and a text of millions of lines is read no
56
+ // further.
57
+ const MINIFIED_READ = 4 * 1024 * 1024
58
+ // Its code's lines' average length where they are minified into short ones,
59
+ // else 0, told by its first MINIFIED_READ characters.
60
+ export function minifiedAverage(whole, { stringOrComment, droppable, needed }) {
61
+ const text = whole.length > MINIFIED_READ ? whole.slice(0, whole.lastIndexOf('\n', MINIFIED_READ) + 1 || MINIFIED_READ) : whole
62
+ // Its lines of code alone, as long as what is on them: a comment set aside
63
+ // leaves nothing but its line breaks, a string or template `""` and its, a
64
+ // regular expression `/REGEX/`, so the code either side stays apart.
65
+ const lines = text.replaceAll(stringOrComment, (match, string) => (string === undefined ? '' : string[0] === '/' ? `/${REGEX}/` : '""') + match.replaceAll(/[^\n]/gu, ''))
66
+ .replaceAll(/^[ \t]+/gmu, '')
67
+ .split('\n').filter(line => line.trim() !== '')
68
+ const code = lines.join('\n'), length = code.length - (lines.length - 1)
69
+ // Counted by character: a run aligning `=` is as many spaces as it is wide.
70
+ let depth = 0, dropped = 0, scanned = 0
71
+ for (const { 0: run, index } of code.matchAll(droppable)) {
72
+ for (; scanned < index; scanned++) depth = Math.max(0, depth + (code[scanned] === '(' ? 1 : code[scanned] === ')' ? -1 : 0))
73
+ if (!needed(code, index, run, depth)) dropped += run.length
74
+ }
75
+ return lines.length > 0 && length > MINIFIED_AVERAGE * lines.length && dropped < MINIFIED_SPACES * length ? Math.round(length / lines.length) : 0
76
+ }
77
+
78
+ // JavaScript or TypeScript (a declaration file too, which node runs as any
79
+ // other), or a script run by node by its hashbang (`bin/cli`, with no
80
+ // extension), a byte order mark before it or not, as node runs one after it
81
+ // as an ES module (in a `"type": "module"` package).
82
+ const SCRIPT = /\.[cm]?[jt]sx?$/iu
83
+ const NODE_HASHBANG = /^\uFEFF?#!.*\bnode\b/u
84
+ // Whether a file (its path and text) is a script, as SCRIPT and NODE_HASHBANG tell.
85
+ export const npmIsScript = (path, text) => typeof text === 'string' && (SCRIPT.test(path) || NODE_HASHBANG.test(text))
86
+ // How many places a file is told doing a thing, the first ones, at most
87
+ // (runs-code.js, hosts.js): a file doing it on millions of lines is told by
88
+ // its first, and what it holds of them kept to a bound however many there
89
+ // are.
90
+ export const NPM_MAX_FOUND = 100
91
+ // Every how many line breaks one is kept where lineOf counts from.
92
+ const LINE_MARK = 1024
93
+ // Each offset in a text as its line: one more than the line breaks before it,
94
+ // counted on from the nearest of every LINE_MARKth, which alone are kept.
95
+ export function lineOf(text) {
96
+ let marks = null
97
+ return offset => {
98
+ if (!marks) {
99
+ marks = []
100
+ for (let at = text.indexOf('\n'), count = 0; at !== -1; at = text.indexOf('\n', at + 1), count++) if (count % LINE_MARK === 0) marks.push(at)
101
+ }
102
+ let high = marks.length, low = 0
103
+ while (low < high) {
104
+ const middle = (low + high) >> 1
105
+ if (marks[middle] < offset) low = middle + 1
106
+ else high = middle
107
+ }
108
+ if (low === 0) return 1
109
+ let breaks = (low - 1) * LINE_MARK + 1
110
+ for (let at = text.indexOf('\n', marks[low - 1] + 1); at !== -1 && at < offset; at = text.indexOf('\n', at + 1)) breaks++
111
+ return breaks + 1
112
+ }
113
+ }
114
+
@@ -0,0 +1,46 @@
1
+ // A file's stamps: what risk reads of one file (the readers beside this), and
2
+ // what of it calls for a look, as a list of files tags it and orders it by:
3
+ // each `{ name, severity }`, its tag's text and how severe it is (SEVERITIES).
4
+ import { READABILITY, npmFileReadability, npmLabelMark, npmLargeCode, npmReadabilityLabels, npmTaggedCategory } from './readability.js'
5
+ import { npmExfiltration, npmHostSeverity, npmHostTags, npmHosts } from './hosts.js'
6
+ import { npmRunsCode, npmRunsCodeFlags } from './runs-code.js'
7
+
8
+ // How severe a stamp or a finding is, the most first: `critical` red,
9
+ // `high` a warning's orange, `moderate` yellow, `low`, and a `notice`, under
10
+ // a low one.
11
+ export const SEVERITIES = ['critical', 'high', 'moderate', 'low', 'notice']
12
+
13
+ // A label's mark (readability.js READABILITY) as a severity: a warning's
14
+ // orange, or a notice.
15
+ const severityOf = mark => mark === 'warn' ? 'high' : 'notice'
16
+
17
+ // A file's stamps, from what was read of it: what its code does beyond
18
+ // itself (runs-code.js npmRunsCodeFlags), a warning's or, for the whole
19
+ // environment read out and publishing, critical; the services it names
20
+ // (hosts.js), a stamp a tag, at the tag's severity (Exfiltration critical,
21
+ // Remote high, IP lookup moderate); a large code file, a notice; an
22
+ // obfuscator's fingerprint, a warning's; and its control characters, long
23
+ // lines and category, by their marks, but for what reads as most files do
24
+ // (UTF-8, ASCII) and a source map.
25
+ function npmStampsOf({ path, bytes, readability, runsCode, exfiltration }) {
26
+ const category = npmTaggedCategory(readability)
27
+ const listed = category !== 'ascii' && category !== 'map' && READABILITY.get(category).mark
28
+ return [...npmRunsCodeFlags(runsCode),
29
+ ...npmHostTags(exfiltration).map(([tag, services]) => ({ name: tag, severity: npmHostSeverity(tag, services) })),
30
+ ...npmLargeCode(path, bytes) ? [{ name: 'Large', severity: 'notice' }] : [],
31
+ ...readability.obfuscation ? [{ name: 'Obfuscated', severity: 'high' }] : [],
32
+ ...npmReadabilityLabels(readability).filter(label => label === 'controls' || label === 'long')
33
+ .map(label => ({ name: READABILITY.get(label).tag, severity: severityOf(npmLabelMark(readability, label)) })),
34
+ ...listed ? [{ name: READABILITY.get(category).tag, severity: severityOf(listed) }] : []]
35
+ }
36
+
37
+ // Everything risk reads of a file (`path`, its `text`, null for one that
38
+ // isn't, and its size in `bytes`): how it reads (npmFileReadability), what
39
+ // its code does beyond itself (npmRunsCode), the services it names under the
40
+ // hosts' tags (npmExfiltration) and the hosts its code names (npmHosts), and its
41
+ // `stamps` (npmStampsOf).
42
+ export function npmFileRisk(path, text, bytes) {
43
+ const readability = npmFileReadability(path, text)
44
+ const exfiltration = npmExfiltration(path, text), runsCode = npmRunsCode(path, text)
45
+ return { readability, runsCode, exfiltration, hosts: npmHosts(path, text), stamps: npmStampsOf({ path, bytes, readability, runsCode, exfiltration }) }
46
+ }