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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +3 -0
  6. package/client/dependency-paths.js +36 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +115 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +357 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-compare-versions.js +172 -0
  17. package/common/bundle-comparison.d.ts +8 -0
  18. package/common/bundle-comparison.js +62 -0
  19. package/common/bundle-metadata.d.ts +15 -0
  20. package/common/bundle-metadata.js +226 -0
  21. package/common/bundle-pkg-of.d.ts +1 -0
  22. package/common/bundle-pkg-of.js +90 -0
  23. package/common/bundle-reasons.d.ts +2 -0
  24. package/common/bundle-reasons.js +21 -0
  25. package/common/bundle-source-links.d.ts +6 -0
  26. package/common/bundle-source-links.js +216 -0
  27. package/common/bundle-sourcemap.d.ts +10 -0
  28. package/common/bundle-sourcemap.js +100 -0
  29. package/common/bundle-sources.d.ts +7 -0
  30. package/common/bundle-sources.js +333 -0
  31. package/common/bundle-stats.js +41 -0
  32. package/common/bundle-tabs.js +1 -0
  33. package/common/code-language.js +65 -0
  34. package/common/default-scan-models.ts +31 -0
  35. package/common/dependency-reviews.d.ts +18 -0
  36. package/common/dependency-reviews.js +64 -0
  37. package/common/finding-duplicates.js +58 -0
  38. package/common/finding-filters.js +274 -0
  39. package/common/finding-id.js +47 -0
  40. package/common/github-issue-labels.d.ts +1 -0
  41. package/common/github-issue-labels.js +11 -0
  42. package/common/github-pr.ts +56 -0
  43. package/common/gzip.js +59 -0
  44. package/common/line-diff.d.ts +8 -0
  45. package/common/line-diff.js +57 -0
  46. package/common/linked-workspace-groups.js +63 -0
  47. package/common/managed/agent-tasks.ts +292 -0
  48. package/common/managed/comments.ts +34 -0
  49. package/common/managed/finding-detail.d.ts +2 -0
  50. package/common/managed/finding-detail.js +32 -0
  51. package/common/managed/github-agents.ts +114 -0
  52. package/common/managed/github-logins.ts +49 -0
  53. package/common/managed/npm-packages.d.ts +5 -0
  54. package/common/managed/npm-packages.js +34 -0
  55. package/common/managed/permissions.ts +31 -0
  56. package/common/managed/report-content.ts +72 -0
  57. package/common/managed/report-filter.ts +109 -0
  58. package/common/managed/repository-alias.ts +65 -0
  59. package/common/managed/roles.ts +35 -0
  60. package/common/managed/routes.d.ts +2 -0
  61. package/common/managed/routes.js +347 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +5 -0
  64. package/common/managed/team-app.js +31 -0
  65. package/common/managed/triage.ts +83 -0
  66. package/common/managed/upload-seal.ts +126 -0
  67. package/common/pretty-print.d.ts +5 -0
  68. package/common/pretty-print.js +24 -0
  69. package/common/revalidation-conflicts.js +48 -0
  70. package/common/revalidation-input-groups.js +56 -0
  71. package/common/save-error-reason.ts +20 -7
  72. package/common/scan-server.ts +13 -0
  73. package/common/security-links.d.ts +5 -0
  74. package/common/security-links.js +33 -0
  75. package/common/server-info.ts +46 -0
  76. package/common/utf8.d.ts +3 -0
  77. package/common/utf8.js +45 -0
  78. package/common/workspace-app.js +39 -0
  79. package/common/workspace-groups.js +176 -0
  80. package/out/activity.js +4 -0
  81. package/out/brotli-fallback.js +3 -3
  82. package/out/chunk-2WDZPXVL.js +3 -0
  83. package/out/chunk-2XUEZHIP.js +28 -0
  84. package/out/chunk-2ZDZWLFY.js +1 -0
  85. package/out/chunk-4HI2MSAV.js +17 -0
  86. package/out/chunk-4XMP7YVH.js +36 -0
  87. package/out/chunk-6LNQTKAV.js +52 -0
  88. package/out/chunk-6S2U2UYG.js +232 -0
  89. package/out/chunk-75YLHBCM.js +3 -0
  90. package/out/chunk-7BKKYVWR.js +7 -0
  91. package/out/chunk-7C7DWL7I.js +1 -0
  92. package/out/chunk-7VAWINQF.js +6 -0
  93. package/out/chunk-C4Z6ZBXV.js +10 -0
  94. package/out/chunk-FC65TPUB.js +1 -0
  95. package/out/chunk-FKBXNUW4.js +1 -0
  96. package/out/chunk-GDPCLSOS.js +2 -0
  97. package/out/chunk-GO7FT4OD.js +1 -0
  98. package/out/chunk-HTBYHSPG.js +1 -0
  99. package/out/chunk-IYPY47MC.js +1 -0
  100. package/out/chunk-IZ7POWI3.js +18 -0
  101. package/out/chunk-JCIIBLKR.js +1 -0
  102. package/out/chunk-NRPC74AM.js +1 -0
  103. package/out/chunk-OOJM4CTU.js +1 -0
  104. package/out/chunk-PBZPYIG7.js +1 -0
  105. package/out/chunk-PNHBZGDD.js +1 -0
  106. package/out/chunk-PWFUOLZK.js +17 -0
  107. package/out/chunk-PXK7EU4V.js +1 -0
  108. package/out/chunk-QB4CBOX2.js +1 -0
  109. package/out/chunk-R3NZFXVR.js +1 -0
  110. package/out/chunk-R5O6FAW4.js +2 -0
  111. package/out/chunk-SXV2T5C4.js +80 -0
  112. package/out/chunk-TQJDYHVE.js +2 -0
  113. package/out/chunk-TUII34K2.js +1 -0
  114. package/out/chunk-UK7RGJT6.js +3 -0
  115. package/out/chunk-VFWMQ3LG.js +2 -0
  116. package/out/chunk-VH4XMZIW.js +2 -0
  117. package/out/chunk-WCPJDAYA.js +10 -0
  118. package/out/chunk-WFYLMF6Z.js +10 -0
  119. package/out/chunk-WYLQWYLA.js +1 -0
  120. package/out/client-managed-import.js +4 -0
  121. package/out/client-managed.js +33 -0
  122. package/out/client-sync.js +11 -17
  123. package/out/compare.js +35 -0
  124. package/out/dialogs.js +55 -0
  125. package/out/github-issue-authorized.html +3 -0
  126. package/out/github-issue-authorized.js +1 -0
  127. package/out/graph.js +17 -4
  128. package/out/index.html +90 -8
  129. package/out/prism.js +14 -10
  130. package/out/scan.js +2 -0
  131. package/out/stasis.svg +20 -0
  132. package/out/terminal.js +366 -39
  133. package/out/view.css +1 -1
  134. package/out/view.js +19 -144
  135. package/package.json +362 -59
  136. package/risk/actions/findings.js +161 -0
  137. package/risk/actions/resolve.js +219 -0
  138. package/risk/actions/scripts/commands.js +182 -0
  139. package/risk/actions/scripts/grade.js +113 -0
  140. package/risk/actions/scripts/packages.js +123 -0
  141. package/risk/actions/scripts/shell.js +169 -0
  142. package/risk/actions/uses.js +76 -0
  143. package/risk/actions/workflow.js +110 -0
  144. package/risk/files/characters.js +196 -0
  145. package/risk/files/hosts.js +205 -0
  146. package/risk/files/readability.js +177 -0
  147. package/risk/files/runs-code.js +615 -0
  148. package/risk/files/script.js +118 -0
  149. package/risk/files/stamps.js +47 -0
  150. package/risk/index.d.ts +254 -0
  151. package/risk/index.js +11 -0
  152. package/risk/npm/assess.js +285 -0
  153. package/risk/npm/combined.js +68 -0
  154. package/risk/npm/compare.js +257 -0
  155. package/risk/npm/dependencies.js +172 -0
  156. package/risk/npm/diff.js +79 -0
  157. package/risk/npm/downloads.js +84 -0
  158. package/risk/npm/files.js +445 -0
  159. package/risk/npm/held.js +63 -0
  160. package/risk/npm/install-scripts.js +34 -0
  161. package/risk/npm/malware.js +36 -0
  162. package/risk/npm/manifest.js +65 -0
  163. package/risk/npm/new-dependency.js +60 -0
  164. package/risk/npm/old-version.js +108 -0
  165. package/risk/npm/registry.js +388 -0
  166. package/risk/npm/release.js +85 -0
  167. package/risk/npm/score.js +59 -0
  168. package/risk/npm/specs.js +124 -0
  169. package/risk/npm/tarball.js +96 -0
  170. package/server-common/database-config.ts +16 -0
  171. package/server-common/initialize.ts +18 -0
  172. package/server-common/npm-advisories.ts +101 -0
  173. package/{server → server-common}/origin.ts +5 -5
  174. package/server-common/reap.ts +48 -0
  175. package/server-common/scan-config.ts +19 -0
  176. package/server-common/standalone.ts +29 -0
  177. package/server-common/storage-crypto.ts +182 -0
  178. package/server-common/storage-log.ts +38 -0
  179. package/server-common/vercel-blob.ts +103 -0
  180. package/server-e2e/app.ts +487 -0
  181. package/{server → server-e2e}/bus-receiver.ts +2 -1
  182. package/{server → server-e2e}/cli.js +9 -4
  183. package/{server → server-e2e}/config.ts +50 -42
  184. package/{server → server-e2e}/db-neon.ts +2 -2
  185. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  186. package/{server → server-e2e}/db-stmt.ts +2 -2
  187. package/{server → server-e2e}/db.ts +7 -7
  188. package/{server → server-e2e}/http.ts +44 -13
  189. package/{server → server-e2e}/hub.ts +40 -9
  190. package/server-e2e/index.ts +17 -0
  191. package/server-e2e/lifecycle.ts +95 -0
  192. package/{server → server-e2e}/neon-driver.ts +2 -2
  193. package/{server → server-e2e}/npm-proxy.ts +12 -145
  194. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  195. package/{server → server-e2e}/objstore/blob.ts +1 -1
  196. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  197. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  198. package/{server → server-e2e}/objstore/init.ts +19 -27
  199. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  200. package/server-e2e/objstore/rest-deny.ts +28 -0
  201. package/server-e2e/objstore/rest-mint.ts +224 -0
  202. package/{server → server-e2e}/objstore/rest.ts +3 -3
  203. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  204. package/{server → server-e2e}/objstore/store.ts +7 -7
  205. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  206. package/{server → server-e2e}/pubsub.ts +12 -9
  207. package/{server → server-e2e}/sign.ts +4 -4
  208. package/{server → server-e2e}/sse-server.ts +60 -33
  209. package/{server → server-e2e}/static.ts +31 -21
  210. package/{server → server-e2e}/sync-handlers.ts +64 -30
  211. package/{server → server-e2e}/ws-server.ts +20 -9
  212. package/server-managed/activity.ts +258 -0
  213. package/server-managed/agent-merges.ts +109 -0
  214. package/server-managed/app-agents.ts +21 -0
  215. package/server-managed/avatar-store.ts +13 -0
  216. package/server-managed/blob-store.ts +17 -0
  217. package/server-managed/brotli.ts +10 -0
  218. package/server-managed/bundle-advisories.ts +120 -0
  219. package/server-managed/bundle-advisory-inventory.ts +77 -0
  220. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  221. package/server-managed/bundle-build-diagnostics.js +23 -0
  222. package/server-managed/bundle-build-leases.ts +41 -0
  223. package/server-managed/bundle-build-worker.js +115 -0
  224. package/server-managed/bundle-build.ts +200 -0
  225. package/server-managed/bundle-cache.ts +211 -0
  226. package/server-managed/bundle-catalog.ts +29 -0
  227. package/server-managed/bundle-commits.ts +140 -0
  228. package/server-managed/bundle-store.ts +24 -0
  229. package/server-managed/bundle-summary-cache.ts +130 -0
  230. package/server-managed/bundle.ts +119 -0
  231. package/server-managed/cache-storage.ts +20 -0
  232. package/server-managed/cli.js +21 -0
  233. package/server-managed/combined.ts +47 -0
  234. package/server-managed/comments.ts +151 -0
  235. package/server-managed/commit-coauthors.ts +106 -0
  236. package/server-managed/commit-lockfiles.ts +73 -0
  237. package/server-managed/comparison.ts +41 -0
  238. package/server-managed/config.ts +164 -0
  239. package/server-managed/content-access.ts +15 -0
  240. package/server-managed/crypto.ts +25 -0
  241. package/server-managed/db-methods.ts +1923 -0
  242. package/server-managed/db-neon.ts +458 -0
  243. package/server-managed/db-schema.ts +238 -0
  244. package/server-managed/db-table-names.ts +22 -0
  245. package/server-managed/db.ts +142 -0
  246. package/server-managed/dependabot-access.ts +60 -0
  247. package/server-managed/dependabot-alerts.ts +185 -0
  248. package/server-managed/dependency-review-comments.ts +100 -0
  249. package/server-managed/dependency-reviews.ts +346 -0
  250. package/server-managed/github-accounts.ts +60 -0
  251. package/server-managed/github-actions.ts +71 -0
  252. package/server-managed/github-app.ts +415 -0
  253. package/server-managed/github-errors.ts +70 -0
  254. package/server-managed/github-graphql.ts +39 -0
  255. package/server-managed/github-issue-links.ts +140 -0
  256. package/server-managed/github-issue-oauth.ts +47 -0
  257. package/server-managed/github-issues.ts +152 -0
  258. package/server-managed/github-link-oauth.ts +69 -0
  259. package/server-managed/github-links.ts +204 -0
  260. package/server-managed/github-metadata.ts +213 -0
  261. package/server-managed/github-oauth.ts +238 -0
  262. package/server-managed/github-pulls.ts +227 -0
  263. package/server-managed/github-requests.ts +39 -0
  264. package/server-managed/github-users.ts +30 -0
  265. package/server-managed/http-response.ts +76 -0
  266. package/server-managed/http.ts +5329 -0
  267. package/server-managed/import-triage.ts +51 -0
  268. package/server-managed/index.ts +192 -0
  269. package/server-managed/kept-briefly.ts +37 -0
  270. package/server-managed/kept.ts +28 -0
  271. package/server-managed/link-reports.ts +162 -0
  272. package/server-managed/locked-packages.ts +129 -0
  273. package/server-managed/lockfile-scope.ts +49 -0
  274. package/server-managed/lru.ts +30 -0
  275. package/server-managed/managed-issues.ts +78 -0
  276. package/server-managed/management-catalog.ts +45 -0
  277. package/server-managed/management.ts +99 -0
  278. package/server-managed/npm-delta-worker.js +23 -0
  279. package/server-managed/npm-delta.ts +194 -0
  280. package/server-managed/npm-insights.ts +449 -0
  281. package/server-managed/npm-kept.ts +215 -0
  282. package/server-managed/npm-loads.ts +264 -0
  283. package/server-managed/npm-manifest.ts +115 -0
  284. package/server-managed/npm-packages.ts +454 -0
  285. package/server-managed/npm-provenance.ts +123 -0
  286. package/server-managed/npm-risk.ts +285 -0
  287. package/server-managed/object-storage-disk.ts +148 -0
  288. package/server-managed/object-storage-vercel.ts +67 -0
  289. package/server-managed/object-storage.ts +75 -0
  290. package/server-managed/package-entry-points.ts +51 -0
  291. package/server-managed/pretty-print.ts +434 -0
  292. package/server-managed/public-workspace.ts +238 -0
  293. package/server-managed/pull-comment-reviews.ts +161 -0
  294. package/server-managed/pull-dependencies.ts +348 -0
  295. package/server-managed/pull-dependency-reviews.ts +328 -0
  296. package/server-managed/pull-reviews.ts +205 -0
  297. package/server-managed/queue.ts +39 -0
  298. package/server-managed/release-commits.ts +84 -0
  299. package/server-managed/rename-diff-worker.js +26 -0
  300. package/server-managed/rename-diff.ts +279 -0
  301. package/server-managed/repo-path.ts +21 -0
  302. package/server-managed/report-migration.ts +35 -0
  303. package/server-managed/report-query.ts +10 -0
  304. package/server-managed/report-response.ts +16 -0
  305. package/server-managed/report-sources.ts +178 -0
  306. package/server-managed/report-store.ts +85 -0
  307. package/server-managed/repository-actions.ts +218 -0
  308. package/server-managed/repository-agents.ts +355 -0
  309. package/server-managed/repository-aliases.ts +91 -0
  310. package/server-managed/repository-browser.ts +294 -0
  311. package/server-managed/repository-changes.ts +284 -0
  312. package/server-managed/repository-code.ts +215 -0
  313. package/server-managed/repository-dependency-reviews.ts +165 -0
  314. package/server-managed/repository-discovery.ts +82 -0
  315. package/server-managed/repository-history.ts +432 -0
  316. package/server-managed/repository-issues.ts +195 -0
  317. package/server-managed/repository-policy.ts +27 -0
  318. package/server-managed/repository-pulls.ts +156 -0
  319. package/server-managed/repository-pulse.ts +460 -0
  320. package/server-managed/repository-risk-assess.ts +105 -0
  321. package/server-managed/repository-risk-audit.ts +79 -0
  322. package/server-managed/repository-risk-read.ts +170 -0
  323. package/server-managed/repository-risk.ts +491 -0
  324. package/server-managed/repository-squashes.ts +168 -0
  325. package/server-managed/request-timing.ts +63 -0
  326. package/server-managed/reviewed-dependencies.ts +150 -0
  327. package/server-managed/revisions.ts +35 -0
  328. package/server-managed/scan-downloads.ts +87 -0
  329. package/server-managed/session.ts +132 -0
  330. package/server-managed/slugs.ts +38 -0
  331. package/server-managed/solidity-entry-points.ts +71 -0
  332. package/server-managed/sql-postgres.ts +34 -0
  333. package/server-managed/sql.ts +88 -0
  334. package/server-managed/static.ts +28 -0
  335. package/server-managed/storage-command.ts +16 -0
  336. package/server-managed/storage-db.ts +183 -0
  337. package/server-managed/storage-encryption.ts +290 -0
  338. package/server-managed/storage-maintenance.ts +259 -0
  339. package/server-managed/storage-payload.ts +121 -0
  340. package/server-managed/storage-stores.ts +91 -0
  341. package/server-managed/storage.ts +82 -0
  342. package/server-managed/team-app.ts +134 -0
  343. package/server-managed/team-catalog.ts +7 -0
  344. package/server-managed/team-feed.ts +180 -0
  345. package/server-managed/team-npm-scopes.ts +106 -0
  346. package/server-managed/team-reports.ts +307 -0
  347. package/server-managed/triage-response.ts +16 -0
  348. package/server-managed/type-only-worker.js +19 -0
  349. package/server-managed/type-only.ts +105 -0
  350. package/server-managed/uploads.ts +106 -0
  351. package/server-managed/upstream-cache.ts +243 -0
  352. package/server-managed/view-as.ts +58 -0
  353. package/server-managed/workspace-shares.ts +169 -0
  354. package/server.ts +70 -0
  355. package/server/index.ts +0 -480
  356. package/server/lifecycle.ts +0 -208
  357. /package/{LICENSE → LICENSE-MIT} +0 -0
  358. /package/{server → server-e2e}/auth.ts +0 -0
  359. /package/{server → server-e2e}/config.example.json +0 -0
  360. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  361. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  362. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  363. /package/{server → server-e2e}/peer.ts +0 -0
  364. /package/{server → server-e2e}/sse-session.ts +0 -0
  365. /package/{server → server-e2e}/util.ts +0 -0
  366. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,615 @@
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, `gh auth token`, the cloud's instance metadata, the
5
+ // GitHub Actions runner's process or another process's memory, trufflehog)
6
+ // or the whole environment, publish to npm (`npm publish`, `/-/whoami`, a
7
+ // registry PUT), run a shell command fetching or handing off code (`curl …
8
+ // https://…`, `| sh`, `powershell -…`, rundll32, osascript, `/dev/tcp/`),
9
+ // and name a place code runs from again later (a shell's rc file,
10
+ // LaunchAgents, systemd's user units, `.claude/settings.json`,
11
+ // `.vscode/tasks.json`), as a file's stamps say (stamps.js) and a version's
12
+ // findings name what is new to a file. A shell script (`.sh`, `.bat`, `.ps1`
13
+ // …, or a shell's hashbang) is read whole for the last two and the
14
+ // credentials a command reaches for, its text its commands.
15
+ // A quick read, not a parse: a script's code with its strings, regular
16
+ // expressions and comments set aside as script.js tells them, its
17
+ // strings read for the paths, names and commands they hold, a module by the
18
+ // string naming it where it is loaded (`require('child_process')`,
19
+ // `import … from 'node:child_process'`); a declaration file (.d.ts) read as
20
+ // TypeScript, as node runs it (stripping its types, `declare function
21
+ // eval(…)` among them, a function named, not called). A call written not to
22
+ // read as one (`window['ev' + 'al']`) goes unseen, as does one in a template
23
+ // literal in another's substitution (`${`${eval(x)}`}`), whose backtick ends
24
+ // the outer one as the regular expression telling strings reads it.
25
+ //
26
+ // Labels to assist a reviewer, making the plain cases less likely to be
27
+ // missed, not a check to rely on: code can come to eval, Function,
28
+ // child_process, credentials or the registry in more ways than any reading of
29
+ // its text tells (a name built as it runs, `(…).constructor('…')`, a module
30
+ // required by a name computed, a path or URL put together, the
31
+ // registry's host read from config), so a file without a label is not one
32
+ // without them.
33
+ import { JS_STRING_OR_COMMENT, NPM_MAX_FOUND, lineOf, npmIsScript } from './script.js'
34
+
35
+ // What may come between a call's parts: blanks and comments
36
+ // (`Function/* note */(…)`), each comment ending at its first `*/`.
37
+ const COMMENT = String.raw`/\*(?:[^*]|\*(?!/)){0,100}\*/|//[^\n]{0,100}\n`
38
+ const GAP = String.raw`(?:\s|${COMMENT})*`
39
+ // Function called: as it is, optionally (`Function?.(…)`), or by its call or
40
+ // apply (`Function.call(null, …)`), each making a function of its last
41
+ // argument.
42
+ const FUNCTION_CALLED = String.raw`(?:${GAP}\??\.${GAP}(?:call|apply))?${GAP}(?:\?\.${GAP})?\(`
43
+ // But not where the function made returns `this` alone
44
+ // (`Function('return this')()`, `new Function("return this;")`): the global
45
+ // object, as older code finds it where `globalThis` may not be, lodash's and
46
+ // core-js's among them, running nothing of its own.
47
+ const GLOBAL_FOUND = String.raw`(?!${GAP}(?<quote>["'\u0060])\s*return\s+this\s*;?\s*\k<quote>${GAP}\))`
48
+ // The environment, whole: not one variable of it (`process.env.HOME`,
49
+ // `process.env[name]`).
50
+ const ENV = String.raw`process${GAP}\.${GAP}env(?![\p{ID_Continue}$])(?!${GAP}(?:\??\.|\[))`
51
+ // Its values read out whole (`JSON.stringify(process.env)`,
52
+ // `Object.entries(process.env)`, `new URLSearchParams(process.env)`), or a
53
+ // copy of them (`JSON.stringify({ ...process.env })`), not as a program is
54
+ // handed it (`{ env: process.env }`, `{ ...process.env }`), nor its
55
+ // names listed (`Object.keys(process.env)`, as debug picks its DEBUG_ ones),
56
+ // nor one variable asked for (`'HOME' in process.env`), nor its entries or
57
+ // values picked among as they are read (`Object.entries(process.env)
58
+ // .reduce(…)`, as npm's agent finds its proxy's, `.filter(…)`, `.find(…)`).
59
+ const PICKED = String.raw`${GAP}\)${GAP}\??\.${GAP}(?:filter|find|findLast|some|every|reduce)(?![\p{ID_Continue}$])`
60
+ const COPY = String.raw`(?:\{${GAP}\.\.\.${GAP})?`
61
+ const ENV_WHOLE = String.raw`(?=JSON|Object|URLSearchParams)(?<![\p{ID_Continue}$.])(?:JSON${GAP}\.${GAP}stringify|URLSearchParams)${GAP}\(${GAP}${COPY}${ENV}`
62
+ + String.raw`|(?=Object)(?<![\p{ID_Continue}$.])Object${GAP}\.${GAP}(?:values|entries)${GAP}\(${GAP}${COPY}${ENV}(?!${PICKED})`
63
+ // What a shell command, and a place code runs from again later, may say
64
+ // (SHELL, PERSISTENCE), and what of the credentials is told in any case:
65
+ // trufflehog (TRUFFLEHOG), GCP's metadata host, and a string naming gh
66
+ // (`'gh'`, `'/usr/bin/gh'`, `'GH.EXE'`) as a program run with its arguments
67
+ // apart is, whatever is between them (`'gh', ['auth', /* … */ 'token']`).
68
+ // A shell, as a hashbang or a pipe names it (`#!/bin/ksh`, `| fish`,
69
+ // BusyBox's `ash`, `| powershell`); after a pipe, by its path or env's too (`| /bin/sh`,
70
+ // `| /usr/bin/env bash`), env's options, their values, and settings before
71
+ // it on its line (`env -S`, `env -u NAME`, `env MODE=x`).
72
+ export const SHELLS = String.raw`(?:(?:ba|z|da|k|mk|c|tc|fi|a)?sh|pwsh|powershell)`
73
+ const SHELL_PATH = String.raw`(?:(?:\/[\w.-]{1,40}){0,8}\/)?(?:env(?:[^\S\n]+(?:-[\w-]{1,20}(?:=\S{0,100}|[^\S\n]+(?!-)[^\s=]{1,100})?|\w{1,40}=\S{0,100})){0,6}[^\S\n]+)?`
74
+ const SHELL_OR_PLACE = String.raw`curl|wget|\|\s*(?:sudo|${SHELL_PATH}${SHELLS})|powershell|pwsh|rundll32|osascript|mshta|certutil|bitsadmin|\/dev\/(?:tcp|udp)|iex|invoke-expression`
75
+ + String.raw`|\$profile|profile\.ps1|config\.fish|conf\.d|bashrc|zshrc|zshenv|zlogin|bash_profile|bash_login|zprofile|\.profile|Launch(?:Agents|Daemons)|systemd|systemctl|\.claude|\.vscode|trufflehog|metadata\.google\.internal|["'\x60\/\\]gh(?:\.exe)?["'\x60]`
76
+ // What says one of the credentials a command, an endpoint or a process holds
77
+ // (HOLDS' gh-token, cloud-metadata, runner-worker and process-memory), as
78
+ // code and its strings may say it, in the case it is written: `gh auth
79
+ // token`'s command, lowercase as gh reads it, the metadata endpoint's
80
+ // address, `Runner.Worker` and `/proc/`; what is told in any case is
81
+ // SHELL_OR_PLACE's.
82
+ const SAYS_CREDENTIALS = String.raw`auth(?:[^\S\n]|[\\^\x60]\r?\n)+token|169\.254\.169\.254|Runner\.Worker|\/proc\/`
83
+ // Any of RUNS_CODE, as a text may say it: a shell command or a place in any
84
+ // case, as Windows' are written (`PowerShell`, `RUNDLL32`).
85
+ const SAYS_SHELL_OR_PLACE = new RegExp(SHELL_OR_PLACE, 'iu')
86
+ const SAYS_CODE = new RegExp(String.raw`eval|Function${FUNCTION_CALLED}|vm["'\x60]|_compile|child_process|\.(?:npmrc|ssh|aws)|_TOKEN|${SAYS_CREDENTIALS}|(?:stringify|values|entries|URLSearchParams)${GAP}\(${GAP}(?:\{${GAP}\.\.\.${GAP})?process${GAP}\.${GAP}env|publish|whoami|registry\.(?:npmjs\.org|yarnpkg\.com)`, 'u')
87
+ const SAYS_ANY = { test: text => SAYS_CODE.test(text) || SAYS_SHELL_OR_PLACE.test(text) }
88
+
89
+ // A name read as one, not as a property's (`vm.eval`, `vm . eval`, a spread's
90
+ // `...eval` aside) or part of another's (`evaluate`), or as the global
91
+ // object's (`window.eval`, `globalThis?.eval`); looked back on only where the
92
+ // name or the global object's starts, so a long run of blanks reads in one go.
93
+ const GLOBAL = 'globalThis|window|global|self'
94
+ const named = name => String.raw`(?=${GLOBAL}|${name})(?<![\p{ID_Continue}$#])(?<!(?<!\.)\.${GAP})(?<!\bfunction${GAP})(?:(?:${GLOBAL})${GAP}\??\.${GAP})?${name}`
95
+ // A token's name, as its value is read (`process.env.GITHUB_TOKEN || null`,
96
+ // `env.NPM_TOKEN`, `||=`), not where its absence is told
97
+ // (`!process.env.GITHUB_TOKEN`, `!(process.env.GITHUB_TOKEN)`) or it is
98
+ // dropped (`delete process.env.NPM_TOKEN`), the token the whole of what is
99
+ // negated or deleted (ABSENT_ENDS: not `!process.env.GITHUB_TOKEN.trim()`,
100
+ // `!(process.env.GITHUB_TOKEN === x)`), nor where it is only set
101
+ // (`process.env.NPM_TOKEN = token`), but for a default as it is destructured
102
+ // (`{ NPM_TOKEN = '' } = process.env`, a line of its own too), nor declared
103
+ // (`const NPM_TOKEN = …`, TypeScript's `let GITHUB_TOKEN: string`), nor typed
104
+ // (a member, `GITHUB_TOKEN?: string`, `NPM_TOKEN: string | undefined`, or a
105
+ // parameter): looked back on, a few names and dots at most, only where the
106
+ // name starts.
107
+ const TOKEN_VAR = '(?:NPM|GITHUB)_TOKEN'
108
+ const SET = String.raw`\s{0,4}=(?![=>])`
109
+ // A name typed, not read: optional (`?:`), or a primitive's type after it,
110
+ // as no destructuring names what it reads (`{ NPM_TOKEN: token }`); not
111
+ // `undefined`, a ternary's choice (`x ? env.NPM_TOKEN : undefined`).
112
+ const TYPED = String.raw`\s{0,4}(?:\?:|:\s{0,4}(?:string|number|boolean|unknown|any)(?![\p{ID_Continue}$]))`
113
+ // What may follow a token negated or deleted whole: the parentheses around
114
+ // it closed, nothing read of what they hold (`!(token).trim()`, `!(token)()`), or else what
115
+ // ends an operand (`&&`, `||`, `??`, a ternary's `?`, `;`, `,`, `:`, `}`, `]`,
116
+ // the line's end).
117
+ const ABSENT_ENDS = String.raw`\s{0,4}(?:(?:\)\s{0,4}){1,4}(?![\s)]{0,8}(?:[.[(]|\?\.))|&&|\|\||\?\?|\?(?!\.)|[;,:}\]]|$)`
118
+ const TOKEN_NAME = String.raw`(?=NPM|GITHUB)(?<![\p{ID_Continue}$])(?<!\b(?:const|let|var)\s{1,4})`
119
+ + String.raw`(?:(?<!(?:!|\bdelete\s)[\s(]{0,4}(?:[\p{ID_Continue}$]{1,64}\s{0,4}\??\.\s{0,4}){0,4})|(?!${TOKEN_VAR}${ABSENT_ENDS}))`
120
+ + String.raw`${TOKEN_VAR}(?![\p{ID_Continue}$])(?!${TYPED})(?:(?!${SET})|(?<=[{,]\s{0,64}${TOKEN_VAR}))`
121
+ // eval as a key, not read: a property's name or a member typed, coming
122
+ // first or after another (`{ eval: x }`, WebAuthn's `extensions: { prf: {
123
+ // eval: { first } } }`, `const { eval: run } = vm`, `{ a: 1, eval: 2 }`, `a;
124
+ // eval: T`); not where anything else comes before it, as a ternary's choice
125
+ // does, whole or last of its branch (`c ? eval : f`, `c ? x && eval : f`),
126
+ // nor a worker told to run its string as code (`new Worker(code, { eval:
127
+ // true })`, minified `{eval:!0}`), nor eval destructured from the global
128
+ // object (`const { eval: run } = globalThis`, `({ a, eval: run } = window)`).
129
+ const KEYED = String.raw`(?<=[{,;]${GAP}eval)\s*:(?!${GAP}(?:true|!0)(?![\p{ID_Continue}$]))`
130
+ + String.raw`(?![^{};]{0,200}\}${GAP}=${GAP}(?:${GLOBAL})(?![\p{ID_Continue}$]))`
131
+ // A script's strings, regular expressions and comments, as script.js
132
+ // tells them, each taking its own text, and its code in none of them: eval,
133
+ // not a property TypeScript types as optional (`eval?: T`, JavaScript's `?:`
134
+ // none of its own) nor a key (KEYED), and Function called, but to find the
135
+ // global object;
136
+ // Module's _compile, called
137
+ // (`module._compile(source, filename)`) or hooked (`Module.prototype._compile
138
+ // = …`), not named in a type (`typeof Module.prototype._compile`); a token's
139
+ // name, its value read; the environment read out whole; and a PUT request's
140
+ // call (`axios.put(…)`).
141
+ const READS = new RegExp(String.raw`${JS_STRING_OR_COMMENT.source}|(?<eval>${named('eval')}(?![\p{ID_Continue}$])(?!\s*\?:)(?!${KEYED}))|(?<function>${named('Function')}${FUNCTION_CALLED}${GLOBAL_FOUND})`
142
+ + String.raw`|(?<compile>\.${GAP}_compile(?:${FUNCTION_CALLED}|${GAP}=(?![=>])))|(?<tokens>${TOKEN_NAME})|(?<env>${ENV_WHOLE})|(?<put>\.${GAP}put${GAP}\()`, 'gmu')
143
+ // What a string holds that a kind is: a credential file's path, npm's, SSH's
144
+ // or AWS's (`~/.npmrc`, `.ssh/id_rsa`, `.aws/credentials`, not `x.npmrc` or
145
+ // `.sshd`); a token's name where it is read, the string the name alone (a
146
+ // key, `process.env['GITHUB_TOKEN']`) or the name expanded (a shell's or
147
+ // .npmrc's `$GITHUB_TOKEN`, `${NPM_TOKEN}`, cmd's `%NPM_TOKEN%`), not where
148
+ // text names it (`requires a token in GITHUB_TOKEN`); a credential a
149
+ // command, an endpoint or a process holds (`gh auth token`, the cloud's
150
+ // metadata endpoint, `Runner.Worker`, `/proc/<pid>/mem`, trufflehog, as
151
+ // HOLDS tells them); a command publishing (`npm publish`, `pnpm
152
+ // publish …`, options before it as npm takes them, `npm --tag next publish`)
153
+ // or the registry's who-am-I, a publisher's first step with a token. And the
154
+ // registry, where a PUT request is one publishing (REGISTRY_URL).
155
+ // Told first of a string by what any of them says.
156
+ const HOLDS_CODE = new RegExp(String.raw`\.(?:npmrc|ssh|aws)|_TOKEN|${SAYS_CREDENTIALS}|publish|whoami|registry\.`, 'u')
157
+ const HOLDS_ANY = { test: text => HOLDS_CODE.test(text) || SAYS_SHELL_OR_PLACE.test(text) }
158
+ // Before a token's name as a key, its absence told or the token dropped, as
159
+ // TOKEN_NAME tells of one as a name (`!process.env['GITHUB_TOKEN']`,
160
+ // `delete process.env['NPM_TOKEN']`): a few names and dots at most, and
161
+ // after it, its key's bracket closed, what may follow it whole (ABSENT_ENDS,
162
+ // not `!process.env['NPM_TOKEN'].length`). After it, the token set
163
+ // (`process.env['NPM_TOKEN'] = token`).
164
+ const KEY_SET = new RegExp(String.raw`^\s{0,4}\]${SET}`, 'u')
165
+ const KEY_ENDS = new RegExp(String.raw`^\s{0,4}\]${ABSENT_ENDS}`, 'mu')
166
+ 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
167
+ // A string's text as JavaScript reads it, its escapes decoded (`\x2f`,
168
+ // `\u002f`, `\u{2f}`, sloppy mode's octal `\057`, `\/`, `\\`, `\n`, `\t`), a
169
+ // line continued (a backslash before a line's end) joined: what a URL in it
170
+ // spells, however its separators are escaped or its lines broken.
171
+ 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
172
+ const CONTROLS = new Map([['n', '\n'], ['r', '\r'], ['t', '\t'], ['b', '\b'], ['f', '\f'], ['v', '\v']])
173
+ const LINE_ENDS = new Set(['\n', '\r', '\r\n', '\u2028', '\u2029'])
174
+ const unescaped = text => text.replace(ESCAPE, (escape, hex, unit, point, octal, char) => {
175
+ if (char !== undefined) return LINE_ENDS.has(char) ? '' : CONTROLS.get(char) ?? char
176
+ const code = octal === undefined ? Number.parseInt(hex ?? unit ?? point, 16) : Number.parseInt(octal, 8)
177
+ return code <= 0x10_FFFF ? String.fromCodePoint(code) : escape
178
+ })
179
+ // The registry's host in a string, as JavaScript reads it (unescaped) and a
180
+ // 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
181
+ // after an http(s) scheme's colon (its slashes optional there, as URLs take
182
+ // them), its userinfo between, to its last `@` before the URL's path, query or
183
+ // fragment, quotes and all, 256
184
+ // characters at most, as credentials are, so a URL's every start is read
185
+ // in bounded time (`https://registry.npmjs.org/`, `.npmrc`'s
186
+ // `//registry.npmjs.org/`, `https://${token}@registry.npmjs.org/`,
187
+ // `https:\\\\registry.npmjs.org`, `https:\x2f\x2fregistry.npmjs.org`,
188
+ // `https:registry.npmjs.org`); or starting the string, its whole or before its
189
+ // path, port, query or fragment (npm-profile's `'registry.npmjs.org'`, a URL joined,
190
+ // `'https://' + 'registry.npmjs.org/'`). Not in prose, as anywhere else
191
+ // (`registry.npmjs.org</span>`, `asks registry.npmjs.org`, `[registry.npmjs.org]`,
192
+ // `note:registry.npmjs.org`, `user@registry.npmjs.org`, `docs/registry.npmjs.org`).
193
+ const REGISTRY_HOST = String.raw`registry\.(?:npmjs\.org|yarnpkg\.com)`
194
+ const REGISTRY_URL = new RegExp(String.raw`(?:[/\\]{2}|\bhttps?:[/\\]?)(?:[^/\\?#]{0,256}@)?${REGISTRY_HOST}|^(["'\u0060])${REGISTRY_HOST}(?=\1$|[/:?#])`, 'iu')
195
+ const URL_STRIPPED = /[\t\n\r]/gu
196
+ // A shell command fetching or handing off code: curl or wget given a URL
197
+ // (`curl -fsSL https://…`, `ftp://`, `https:\/\/` as a string may escape
198
+ // it), long options before it too (`-H "Authorization: …"`), a quoted
199
+ // argument's `;`, `|` and `&` no command's end (`-d "a=1&b=2"`), its line
200
+ // continued too (`curl -fsSL \⏎ https://…`, `curl\⏎`, cmd's `^`,
201
+ // PowerShell's backtick), not the next line's, the next command's (`curl
202
+ // --version; echo https://…`) nor the next curl's, a download piped to a
203
+ // shell (`… | sh`, `payload|sudo -E -- bash`, `| ksh`, `| /bin/sh`, `cat
204
+ // hash | sh`, not a list of shells, `bash|zsh|sh`, a regular expression's
205
+ // alternatives, `(x|ss|sh)$`, nor `|| sh`, nor an escaped pipe, grep's
206
+ // `'bash\|zsh'`), PowerShell given options (`powershell -nop -enc …`),
207
+ // rundll32, osascript and mshta, certutil fetching (`certutil -urlcache -f
208
+ // http://…`) and bitsadmin's `/transfer`, and bash's network redirection
209
+ // (`/dev/tcp/host/port`), and PowerShell running what it fetches (`iex (iwr
210
+ // https://…).Content`, `irm https://… | iex`, long options before the pipe
211
+ // too, `iwr … -Headers @{…} | iex`, `IEX (New-Object
212
+ // Net.WebClient).DownloadString(…)`), its line continued by a backtick too,
213
+ // not the next pipeline's, `iex $x && iwr …`); in any case, as Windows' are
214
+ // written, and by its path (`/usr/bin/curl`).
215
+ const CONTINUED = String.raw`[\\^\u0060]\r?\n`
216
+ const ON_ITS_LINE = String.raw`(?:[^\n;|&]|${CONTINUED})`
217
+ // PowerShell's own, its backtick alone (not `\⏎`, a new statement there).
218
+ const POWERSHELL_CONTINUED = String.raw`\u0060\r?\n`
219
+ // A command's text up to its end, its arguments' quotes kept, a quote left
220
+ // open (`it's`) as any other character.
221
+ const ARGUMENTS = String.raw`(?:[^\n;|&"']|${CONTINUED}|"[^"\n]*"|'[^'\n]*'|"(?![^"\n]*")|'(?![^'\n]*'))`
222
+ const APART = String.raw`(?:[^\S\n]|${CONTINUED})`
223
+ // A command's options, a few, and their values (`-E`, `-u root`, `--now`),
224
+ // on its line or one continued.
225
+ const OPTIONS = String.raw`(?:${APART}+--?\w[\w-]*(?:=\S*|${APART}+(?!-)\S+)?){0,6}?`
226
+ const SHELL = String.raw`(?<![\w.-])(?:curl|wget)(?:\.exe)?${APART}(?:(?!(?:curl|wget)(?![\w-]))${ARGUMENTS}){0,2000}?["']?(?:https?|s?ftps?|scp):\\?\/\\?\/|(?<![|\\]|(?<![\w.\/-])${SHELLS}\s*)\|\s*(?:sudo${OPTIONS}(?:${APART}+--)?${APART}+)?${SHELL_PATH}${SHELLS}(?![\w)|$-])`
227
+ + String.raw`|(?<![\w.-])(?:powershell|pwsh)(?:\.exe)?[^\S\n]+-\w|(?<![\w.-])(?:rundll32|osascript|mshta)(?:\.exe)?(?![\w-])`
228
+ + String.raw`|(?<![\w.-])certutil(?:\.exe)?${APART}${ON_ITS_LINE}{0,100}?-urlcache|(?<![\w.-])bitsadmin(?:\.exe)?${APART}${ON_ITS_LINE}{0,100}?\/transfer|\/dev\/(?:tcp|udp)\/`
229
+ + String.raw`|(?<![\w$.-])(?:iex|invoke-expression)(?![\w-])(?:[^\n;|&]|${POWERSHELL_CONTINUED}){0,200}?(?:(?<![\w$.-])(?:iwr|irm|invoke-webrequest|invoke-restmethod)(?![\w-])|\.downloadstring\s*\()`
230
+ + String.raw`|(?<![\w$.-])(?:iwr|irm|invoke-webrequest|invoke-restmethod)(?![\w-])(?:(?!(?:iwr|irm|invoke-webrequest|invoke-restmethod)(?![\w-]))(?:[^\n;|&]|${POWERSHELL_CONTINUED})){0,2000}\|\s*(?:iex|invoke-expression)(?![\w-])`
231
+ // A place code runs from again later: a shell's rc file (`~/.bashrc`,
232
+ // `.zshrc`, `.zshenv`, `.zlogin`, `.bash_profile`, `.bash_login`,
233
+ // `.zprofile`, and `.profile` in a path, `~/.profile`, not a selector's
234
+ // class, nor one after another, `.nav`), PowerShell's profile (`$PROFILE`,
235
+ // `Microsoft.PowerShell_profile.ps1`, all hosts' `PowerShell\Profile.ps1`,
236
+ // `$PSHOME\Profile.ps1`, not another folder's), macOS's LaunchAgents and
237
+ // LaunchDaemons, fish's (`~/.config/fish/config.fish`, `conf.d`), systemd's
238
+ // user units (`~/.config/systemd/user`, `systemctl --user enable`, other
239
+ // options before it, `--now`, or `--user` after it in its command,
240
+ // `systemctl enable --user`; not `start`, which runs a unit once), and the
241
+ // settings an AI agent or an editor runs commands from on its own
242
+ // (`.claude/settings.json`, `.vscode/tasks.json`); in any case, as Windows'
243
+ // and macOS's paths are, a path's separator a slash or a backslash, escaped
244
+ // in a string as code writes it (`"\\.claude\\settings.json"`, `\/`).
245
+ const SEPARATOR = String.raw`(?:\\?\/|\\{1,2})`
246
+ const PERSISTENCE = String.raw`(?<![\w.-])\.(?:bashrc|zshrc|zshenv|zlogin|bash_profile|bash_login|zprofile)(?![\w-])|(?<=[\/\\])(?<!\.[\w-]*[\/\\])\.profile(?![\w-])|(?<![\w-])Launch(?:Agents|Daemons)(?![\w-])|systemd${SEPARATOR}user(?![\w-])|fish${SEPARATOR}(?:config\.fish|conf\.d)(?![\w-])`
247
+ + String.raw`|(?<![\w$])\$profile(?![\w-])|(?<![\w.-])Microsoft\.\w+_profile\.ps1(?![\w-])|(?:PowerShell|\$pshome)${SEPARATOR}profile\.ps1(?![\w-])|systemctl${OPTIONS}${APART}+--user${OPTIONS}${APART}+enable(?![\w-])|systemctl${OPTIONS}${APART}+enable(?:${APART}+[^\s;|&]+){0,6}?${APART}+--user(?![\w-])|(?<![\w.-])\.claude${SEPARATOR}settings(?:\.local)?\.json|(?<![\w.-])\.vscode${SEPARATOR}tasks\.json`
248
+ // Credentials a command, an endpoint or a process holds, as recent npm
249
+ // malware reached for them: the GitHub CLI printing its token (`gh auth
250
+ // token`, by its path too, `gh.exe`, its name in any case as Windows runs
251
+ // it, `GH.EXE`, its line continued as a shell command's, `gh auth \⏎
252
+ // token`), not its other commands (`gh auth status`); the cloud's
253
+ // instance metadata endpoint, AWS's, Azure's and GCP's
254
+ // (`http://169.254.169.254/latest/meta-data/…`, `metadata.google.internal`,
255
+ // in any case as DNS reads it), as a host whole (HOST_START, HOST_END): not
256
+ // an address or a host either starts or ends (`169.254.169.2540`,
257
+ // `foo-169.254.169.254`, `169.254.169.254.example.com`,
258
+ // `metadata.google.internal.example.com`; a trailing dot,
259
+ // `metadata.google.internal.`, its host still), nor in a URL's path or
260
+ // userinfo (`https://example.com/169.254.169.254`,
261
+ // `https://169.254.169.254@example.com`; WHATWG's `\@` a path's, its host
262
+ // the address still), nor Azure's instance metadata
263
+ // (`169.254.169.254/metadata/instance/compute`, after a port too, `:80`, its
264
+ // slashes escaped too, `\/`), a machine's region and size, not its
265
+ // identity's token, as msal asks it; the GitHub Actions runner's process
266
+ // (`Runner.Worker`, `Runner.Worker.exe`, `GitHub.Runner.Worker`), whose
267
+ // memory holds a job's secrets, not in another name (`MyRunner.Worker`,
268
+ // `Runner.Workers`) nor case (`runner.worker`); another process's memory, by
269
+ // its pid's digits or a glob (`/proc/1234/mem`, `/proc/*/mem`,
270
+ // `/proc/[0-9]*/mem`) or a substitution (`/proc/${pid}/mem`, a shell's
271
+ // `$pid`, `$(pgrep …)` and backticks, Python's `{pid}`, a format's `%d`),
272
+ // not a process's own (`/proc/self/mem`) nor what else /proc tells of one
273
+ // (`/proc/1234/status`); and trufflehog, a scanner for secrets, in any case
274
+ // (`TruffleHog`), not in another name (`trufflehogs`, `TRUFFLEHOG_VERSION`).
275
+ const GH_TOKEN = String.raw`(?<![\w.-])[gG][hH](?:\.[eE][xX][eE])?${APART}+auth${APART}+token(?![\w-])`
276
+ const PID = String.raw`[\d*?[\]-]{1,20}|\$\{[^}\n]{0,100}\}|\$\([^)\n]{0,200}\)|\u0060[^\u0060\n]{0,200}\u0060|\$\w{1,64}|\{\w{0,64}\}|%d`
277
+ // A host whole, a URL's or alone: not after a word, a dot or a hyphen, nor a
278
+ // path's slash (`https://example.com/169.254.169.254`), a URL's two
279
+ // (`//`, `\/\/`) its start after a scheme's colon or alone
280
+ // (`//169.254.169.254`), not after a path's character
281
+ // (`https://example.com//169.254.169.254`); nor before more of a name or
282
+ // userinfo's `@` (`https://169.254.169.254@example.com`, `…:password@`).
283
+ const HOST_START = String.raw`(?<![\w.-]|(?<![\/\\])\/|[\w.~%-](?:\\?\/){2})`
284
+ const HOST_END = String.raw`(?![\w-]|\.[\w-]|(?::[^\s\/@'"\x60]{0,64})?@)`
285
+ const TRUFFLEHOG = String.raw`(?<![\p{ID_Continue}$-])trufflehog(?![\p{ID_Continue}$-])`
286
+ const HOLDS = [
287
+ ['credential-files', /(?<![\w$.-])\.(?:npmrc|ssh|aws)(?![\w-])/gu],
288
+ ['tokens', new RegExp(String.raw`^["'\u0060]${TOKEN_VAR}["'\u0060]$|(?:\$\{?|%)${TOKEN_VAR}(?![\p{ID_Continue}$])`, 'gu')],
289
+ ['gh-token', new RegExp(GH_TOKEN, 'gu')],
290
+ ['cloud-metadata', new RegExp(String.raw`${HOST_START}169\.254\.169\.254${HOST_END}(?!(?::\d{1,5})?\\?\/metadata\\?\/instance(?![\w-]))|${HOST_START}metadata\.google\.internal${HOST_END}`, 'giu')],
291
+ ['runner-worker', /(?<![\w$-])Runner\.Worker(?![\w$-])/gu],
292
+ ['process-memory', new RegExp(String.raw`\/proc\/(?:${PID})\/mem(?![\w-])`, 'gu')],
293
+ ['trufflehog', new RegExp(TRUFFLEHOG, 'giu')],
294
+ ['publish', /(?<![\w-])(?:npm|pnpm|yarn)(?:\s+-{1,2}\w[\w-]*(?:=\S*|\s+(?!-)\S+)?){0,8}?\s+publish(?![\w-])|\/-\/whoami(?![\w-])/gu],
295
+ ['shell', new RegExp(SHELL, 'giu')],
296
+ ['persistence', new RegExp(PERSISTENCE, 'giu')],
297
+ ]
298
+ // What a shell script holds, read whole: its commands, the places they write
299
+ // to, and the credentials a command, a process or its memory holds (`gh auth
300
+ // token`, `Runner.Worker`, `/proc/<pid>/mem`, trufflehog); not the cloud's
301
+ // metadata endpoint, whose fetching is a shell command already (`curl
302
+ // http://169.254.169.254/…`) and which a script provisioning a machine asks
303
+ // its region and id, nor a credential file or a token, which a script
304
+ // publishing a package names.
305
+ const SHELL_KINDS = new Set(['gh-token', 'runner-worker', 'process-memory', 'trufflehog', 'shell', 'persistence'])
306
+ const SHELL_HOLDS = HOLDS.filter(([kind]) => SHELL_KINDS.has(kind))
307
+ // A shell script, by its extension, its name a shell's startup file's
308
+ // (`.bashrc`, `.profile`), or its hashbang: a shell's, by its path or env's,
309
+ // env's options and settings before it (`#!/bin/ksh`, `#!/usr/bin/env -S
310
+ // bash -e`, `#!/usr/bin/env -S MODE=x bash`, `#!/usr/bin/env pwsh`), past a
311
+ // byte order mark.
312
+ const SHELL_SCRIPT = /(?:\.(?:sh|bash|zsh|ksh|mksh|dash|ash|csh|tcsh|fish|command|bat|cmd|ps1|psm1)|(?:^|[\\/])\.(?:bashrc|zshrc|zshenv|zlogin|bash_profile|bash_login|zprofile|profile))$/iu
313
+ // A shell script's comment, a line's whole (`# curl … | sh`) or its end
314
+ // after a command (`echo ok # don't curl …`, `echo ok;# …`) where the quotes
315
+ // before it are closed, escapes read (`"a \" b"`, PowerShell's by a
316
+ // backtick), not in one (`"a # b"`), and no other `#` (`$#`, `<#`) is;
317
+ // PowerShell's, by its extension or hashbang, its blocks too (`<# … #>`,
318
+ // opened where a command or value may start, `$x=<# … #>1`, not in a
319
+ // string, `"<#"`, `"a <#"`, to the end where none closes); a batch file's, cmd's `::`
320
+ // and `REM` (`@REM`, and `REM` after `&` or `||`, `echo ok & REM …`); as
321
+ // none of its commands.
322
+ const SHELL_COMMENT = /^[ \t]*#.*$|^((?:[^'"\n#\\]|\\.|'[^'\n]*'|"(?:[^"\n\\]|\\.)*")*?[ \t;&|])#.*$/gmu
323
+ const POWERSHELL = /\.psm?1$/iu
324
+ const POWERSHELL_HASHBANG = /^\uFEFF?#!\s*\S*\/(?:env\s+(?:(?:-\S+|\w+=\S*)\s+)*)?pwsh(?![\w-])/u
325
+ const POWERSHELL_COMMENT = /^((?:[^'"\n#\u0060]|\u0060.|'[^'\n]*'|"(?:[^"\n\u0060]|\u0060.)*")*?)(?:(?<=^|[\s;({=,])<#[\s\S]*?(?:#>|(?![\s\S]))|(?<=^|[ \t;&|])#.*$)/gmu
326
+ const BATCH = /\.(?:bat|cmd)$/iu
327
+ const BATCH_COMMENT = /^[ \t]*@?(?:::|rem(?![\w-])).*$|(?<=&|\|\|)[ \t]*@?rem(?![\w-]).*$/gimu
328
+ const SHELL_HASHBANG = new RegExp(String.raw`^\uFEFF?#!\s*\S*\/(?:env\s+(?:(?:-\S+|\w+=\S*)\s+)*)?${SHELLS}(?![\w-])`, 'u')
329
+ // A directory and a file in it named apart, as path.join takes them
330
+ // (`join(home, '.claude', 'settings.json')`): a place code runs from again
331
+ // later where a file names both.
332
+ const PARTS = [
333
+ ['claude', /^(["'\u0060])\.claude\1$/iu, /^(["'\u0060])settings(?:\.local)?\.json\1$/iu],
334
+ ['vscode', /^(["'\u0060])\.vscode\1$/iu, /^(["'\u0060])tasks\.json\1$/iu],
335
+ ]
336
+ // `.profile` named apart where the home directory is the part before it
337
+ // (`join(os.homedir(), '.profile')`, `resolve(process.env.HOME, …)`, `'~'`),
338
+ // not a selector's class alone.
339
+ const PROFILE = /^(["'\u0060])\.profile\1$/iu
340
+ const HOME_BEFORE = /(?:homedir\s*\(\s*\)|\b(?:HOME|USERPROFILE)|(["'\u0060])~\1)\s*,\s*$/u
341
+ // A module a kind loads: vm, child_process, and libnpmpublish, npm's own
342
+ // publishing.
343
+ const MODULES = [
344
+ ['vm', /^(["'\u0060])(?:node:)?vm\1$/u],
345
+ ['child-process', /^(["'\u0060])(?:node:)?child_process\1$/u],
346
+ ['publish', /^(["'\u0060])libnpmpublish\1$/u],
347
+ ]
348
+ // A string a program is run with publishing (`spawn('npm', ['publish'])`),
349
+ // after the one naming it, its options and their values between
350
+ // (`['--tag', 'next', 'publish']`), and a request's method set to PUT
351
+ // (`{ method: 'PUT' }`, `options.method = 'put'`).
352
+ const PROGRAM = /^(["'\u0060])(?:npm|pnpm|yarn)\1$/u
353
+ const PUBLISH = /^(["'\u0060])publish\1$/u
354
+ const OPTION = /^(["'\u0060])-/u
355
+ const ARGUMENTS_BETWEEN = new RegExp(String.raw`^(?:[\s,[]|${COMMENT})*$`, 'u')
356
+ const PUT = /^(["'\u0060])put\1$/iu
357
+ const METHOD = new RegExp(String.raw`\bmethod["']?${GAP}[:=]${GAP}$`, 'u')
358
+ // A program run with its arguments apart, by its path too, a Windows one's
359
+ // from its drive with spaces in it (`spawn('curl', ['-fsSL', 'https://…'])`,
360
+ // `execFile('powershell.exe', ['-NoP', '-enc', …])`, `spawn('systemctl',
361
+ // ['--user', 'enable', …])`, `spawnSync('gh', ['auth', 'token'])`,
362
+ // `execFileSync('C:\\Program Files\\GitHub CLI\\gh.exe', …)`): its command
363
+ // line, its arguments' strings after its name, read as a shell command is, a
364
+ // variable's or a property's value between (`['-o', output, 'https://…']`,
365
+ // `outputs[i]`, `options?.out`), its first 20 arguments, however long.
366
+ const COMMAND = /^(["'\u0060])(?:\S*[\\/]|[a-z]:[\\/][^\n]*[\\/])?(?:curl|wget|certutil|bitsadmin|powershell|pwsh|systemctl|gh)(?:\.exe)?\1$/iu
367
+ // An argument a shell would need quoted, its separators no command's end
368
+ // (`'Cookie: a=b; c=d'`), its own double quotes single then.
369
+ const SEPARATES = /[;|&]/u
370
+ const VALUES_BETWEEN = new RegExp(String.raw`^(?:[\s,[\]\w$.?]|${COMMENT})*$`, 'u')
371
+ // A place named by its parts one after the other, as path.join takes them
372
+ // (`join(home, '.config', 'systemd', 'user')`), read as a path is.
373
+ const PLACE = new RegExp(PERSISTENCE, 'iu')
374
+ // What comes before a module's name where code loads it: `require(` (or
375
+ // `require?.(`), a bundler's `__require(` or `__non_webpack_require__(`,
376
+ // `import(`, `import` and `from`, comments allowed between
377
+ // (`import(/* webpackIgnore: true */ …)`); a name not loaded, as in a list of
378
+ // node's modules, is none, nor one a type-only import or export names
379
+ // (`import type { … } from`, `import type cp = require(`, comments allowed as
380
+ // in a load), which TypeScript erases (`import type from` imports a default
381
+ // named type, `import type = require(` one named so).
382
+ const LOADS = new RegExp(String.raw`(?:(?:require[\w$]*${GAP}(?:\?\.${GAP})?|\bimport${GAP})\(|\b(?:from|import))${GAP}$`, 'u')
383
+ const TYPE_ONLY = new RegExp(String.raw`\b(?:import|export)${GAP}type\b(?!${GAP}(?:from\b|=))[^;'"\x60]*(?:\bfrom|=${GAP}require${GAP}\()${GAP}$`, 'u')
384
+
385
+ // Each kind, in the order its tags show: its tag, what it does, its name in
386
+ // the tag's tooltip, a row of its own where kinds share the tag (a string run
387
+ // as code by any way, `eval`'s; a credential file, a token, a command, an
388
+ // endpoint or a process, `credentials`'), and for one marked as worse than
389
+ // the others' warning, `critical`: the whole environment read out,
390
+ // publishing, and a place code runs from again later.
391
+ const AS_CODE = { tag: 'eval', note: 'Runs a string as code' }
392
+ const CREDENTIALS = { tag: 'credentials', note: 'Reaches for credentials' }
393
+ export const RUNS_CODE = new Map([
394
+ ['eval', { ...AS_CODE, name: 'eval' }],
395
+ ['function', { ...AS_CODE, name: 'Function' }],
396
+ ['vm', { ...AS_CODE, name: 'vm' }],
397
+ ['compile', { ...AS_CODE, name: 'Module._compile' }],
398
+ ['child-process', { tag: 'child_process', note: 'Runs other programs', name: 'child_process' }],
399
+ ['credential-files', { ...CREDENTIALS, name: '.npmrc, .ssh or .aws' }],
400
+ ['tokens', { ...CREDENTIALS, name: 'NPM_TOKEN or GITHUB_TOKEN' }],
401
+ ['gh-token', { ...CREDENTIALS, name: 'gh auth token' }],
402
+ ['cloud-metadata', { ...CREDENTIALS, name: '169.254.169.254 or metadata.google.internal' }],
403
+ ['runner-worker', { ...CREDENTIALS, name: 'Runner.Worker' }],
404
+ ['process-memory', { ...CREDENTIALS, name: '/proc/<pid>/mem' }],
405
+ ['trufflehog', { ...CREDENTIALS, name: 'trufflehog' }],
406
+ ['env', { tag: 'whole env', note: 'Reads out the whole environment', name: 'process.env, by JSON.stringify, Object.entries or values, URLSearchParams', mark: 'critical' }],
407
+ ['publish', { tag: 'publish', note: 'Publishes to npm', name: 'npm publish, /-/whoami or a registry PUT', mark: 'critical' }],
408
+ ['shell', { tag: 'shell', note: 'Runs shell commands fetching or handing off code', name: 'curl or wget a URL, | sh, powershell -…, iex, rundll32, osascript, /dev/tcp' }],
409
+ ['persistence', { tag: 'persistence', note: 'Names a place code runs from again later', name: '.bashrc, .zshrc, $PROFILE, LaunchAgents, systemd user units, .claude/settings.json, .vscode/tasks.json', mark: 'critical' }],
410
+ ])
411
+
412
+ // The kinds a file's code holds, each with the lines it is on, of the first
413
+ // NPM_MAX_FOUND places it does each, in RUNS_CODE's order; null for none, or
414
+ // for a file that is no script, by its name or as node runs it (`nodeRuns`,
415
+ // npmIsScript's `script`). A shell script's, read whole, are its shell
416
+ // commands and the places it names; one with a shell's hashbang is one
417
+ // whatever its extension, and read as JavaScript too where it is that, as
418
+ // a launcher (`#!/bin/sh` then `exec node "$0"`) is both.
419
+ export function npmRunsCode(path, text, nodeRuns = false) {
420
+ const script = npmIsScript(path, text, nodeRuns)
421
+ const shell = typeof text === 'string' && (SHELL_SCRIPT.test(path) || SHELL_HASHBANG.test(text))
422
+ if (!script && !shell) return null
423
+ const found = new Map()
424
+ const at = (kind, offset) => {
425
+ const offsets = found.get(kind)
426
+ if (!offsets) found.set(kind, [offset])
427
+ else if (offsets.length < NPM_MAX_FOUND) offsets.push(offset)
428
+ }
429
+ if (script) read(text, 0, at)
430
+ if (shell && SAYS_ANY.test(text)) {
431
+ // Its comments blanked, its lines where they were.
432
+ const comments = BATCH.test(path) ? BATCH_COMMENT : POWERSHELL.test(path) || POWERSHELL_HASHBANG.test(text) ? POWERSHELL_COMMENT : SHELL_COMMENT
433
+ const commands = text.replace(comments, (comment, command = '') => command + comment.slice(command.length).replaceAll(/[^\n]/gu, ' '))
434
+ for (const [kind, holds] of SHELL_HOLDS) for (const { index } of commands.matchAll(holds)) at(kind, index)
435
+ }
436
+ // A PUT request publishing where the file names the registry; a place
437
+ // named by its directory and its file apart.
438
+ if (found.has('registry')) for (const offset of found.get('put') ?? []) at('publish', offset)
439
+ for (const [part] of PARTS) if (found.has(`${part}-file`)) for (const offset of found.get(`${part}-dir`) ?? []) at('persistence', offset)
440
+ if (![...RUNS_CODE.keys()].some(kind => found.has(kind))) return null
441
+ const line = lineOf(text)
442
+ 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)]))
443
+ }
444
+
445
+ // Where code (a file's, or a template literal's substitution's, at `base` in
446
+ // the file) does a kind, as at(kind, offset), in order: its code's, what its
447
+ // strings hold, a string naming a module where it is loaded, and what a
448
+ // template literal's substitutions (`${eval(x)}`) do, code again, read as its
449
+ // own; a PUT request (`put`) and the registry's host (`registry`) as their
450
+ // own, for the file to pair. Most code says none of it at all, told before
451
+ // anything is read.
452
+ function read(code, base, at) {
453
+ if (!SAYS_ANY.test(code)) return
454
+ // The string before, and where, a place's part maybe; the command line
455
+ // since a program's name, and where it is named; where the string before
456
+ // ends; whether the strings since a program's name are its arguments, an
457
+ // option or its value each, and whether the last is an option.
458
+ let before = null, command = null, end = 0, option = false, program = false
459
+ for (const { 0: token, 1: string, groups, index } of code.matchAll(READS)) {
460
+ if (string === undefined) {
461
+ for (const kind of CODE_KINDS) if (groups[kind]) at(kind, base + index)
462
+ continue
463
+ }
464
+ if (string[0] === '/') continue
465
+ const inner = string[0] === '`' && SAYS_ANY.test(string) ? substitutions(string) : []
466
+ for (const [from, to] of inner) read(string.slice(from, to), base + index + from, at)
467
+ // What a string holds, a template's text alone, its substitutions' code
468
+ // read as code, its comments and regular expressions aside.
469
+ const text = inner.length === 0 ? string : textOf(string, inner)
470
+ // A token's name as the string's whole (`within` 0, its quote) is a key,
471
+ // not one where its absence is told, it is dropped or only set.
472
+ const keyed = () => {
473
+ const after = code.slice(index + token.length, index + token.length + 20)
474
+ return !KEY_SET.test(after) && (!KEY_ENDS.test(after) || !KEY_ABSENT.test(code.slice(Math.max(0, index - 300), index)))
475
+ }
476
+ // Each kind searched for before its places are listed, as matchAll copies
477
+ // its regular expression each time.
478
+ const held = HOLDS_ANY.test(text) ? HOLDS.flatMap(([kind, holds]) => text.search(holds) === -1 ? [] : [...text.matchAll(holds)].map(({ index: within }) => [kind, within])) : []
479
+ for (const [kind, within] of held) if (kind !== 'tokens' || within > 0 || keyed()) at(kind, base + index + within)
480
+ if (text.includes('registry.') && REGISTRY_URL.test(unescaped(text).replace(URL_STRIPPED, ''))) at('registry', base + index)
481
+ for (const [kind, name] of MODULES) if (name.test(string) && loaded(code, index)) at(kind, base + index)
482
+ if (PROFILE.test(string) && HOME_BEFORE.test(code.slice(Math.max(0, index - 100), index))) at('persistence', base + index)
483
+ for (const [part, dir, file] of PARTS) {
484
+ if (dir.test(string)) at(`${part}-dir`, base + index)
485
+ if (file.test(string)) at(`${part}-file`, base + index)
486
+ }
487
+ if (program && ARGUMENTS_BETWEEN.test(code.slice(end, index))) {
488
+ if (PUBLISH.test(string)) at('publish', base + index)
489
+ program = OPTION.test(string) || option
490
+ option = OPTION.test(string)
491
+ } else program = false
492
+ if (PROGRAM.test(string)) [program, option] = [true, false]
493
+ if (command && command.arguments < 20 && VALUES_BETWEEN.test(code.slice(end, index))) {
494
+ const argument = text.slice(1, -1)
495
+ command.line += ` ${SEPARATES.test(argument) ? `"${argument.replaceAll('"', "'")}"` : argument}`
496
+ command.arguments++
497
+ const { line } = command
498
+ const hit = SHELL_HOLDS.find(([, holds]) => line.search(holds) !== -1)
499
+ if (hit) {
500
+ at(hit[0], command.at)
501
+ command = null
502
+ }
503
+ } else command = null
504
+ if (COMMAND.test(string)) command = { arguments: 0, at: base + index, line: string.slice(1, -1) }
505
+ if (before && before.text.length + text.length < 200 && ARGUMENTS_BETWEEN.test(code.slice(end, index))) {
506
+ const [first, second] = [before.text.slice(1, -1), text.slice(1, -1)]
507
+ if (!PLACE.test(first) && !PLACE.test(second) && PLACE.test(`${first}/${second}`)) at('persistence', before.at)
508
+ }
509
+ before = { text, at: base + index }
510
+ if (PUT.test(string) && METHOD.test(code.slice(Math.max(0, index - 300), index))) at('put', base + index)
511
+ end = index + token.length
512
+ }
513
+ }
514
+
515
+ // The kinds code itself does, by READS' groups.
516
+ const CODE_KINDS = ['eval', 'function', 'compile', 'tokens', 'env', 'put']
517
+
518
+ // Whether the string at `index` in code names a module where it is loaded.
519
+ function loaded(code, index) {
520
+ const before = code.slice(Math.max(0, index - 300), index)
521
+ return LOADS.test(before) && !TYPE_ONLY.test(before)
522
+ }
523
+
524
+ // A template literal with its substitutions' code blanked, its text in place.
525
+ function textOf(template, inner) {
526
+ const parts = []
527
+ let from = 0
528
+ for (const [start, end] of inner) {
529
+ parts.push(template.slice(from, start), ' '.repeat(end - start))
530
+ from = end
531
+ }
532
+ parts.push(template.slice(from))
533
+ return parts.join('')
534
+ }
535
+
536
+ // Where each of a template literal's substitutions' code starts and ends, in
537
+ // it: from its `${` to the `}` that closes it.
538
+ function substitutions(template) {
539
+ const found = []
540
+ for (let i = template.indexOf('${'); i !== -1; i = template.indexOf('${', i)) {
541
+ // Escaped by an odd run of backslashes before it (`\${`, not `\\${`).
542
+ let run = 0
543
+ while (template[i - 1 - run] === '\\') run++
544
+ if (run % 2 === 1) {
545
+ i += 2
546
+ continue
547
+ }
548
+ const end = closing(template, i + 2)
549
+ found.push([i + 2, end])
550
+ i = end
551
+ }
552
+ return found
553
+ }
554
+
555
+ // A string, regular expression or comment where it starts, told as
556
+ // JS_STRING_OR_COMMENT tells them in a whole text.
557
+ const TOKEN = new RegExp(JS_STRING_OR_COMMENT.source, 'muy')
558
+
559
+ // Where the `}` closing code from `from` in a text is, or the text's end for
560
+ // none: the first its braces leave unbalanced, those in its strings, regular
561
+ // expressions and comments aside (`${/* } */ x}`, `${/}/.test(x)}`).
562
+ function closing(text, from) {
563
+ let depth = 0
564
+ for (let at = from; at < text.length; at++) {
565
+ const char = text[at]
566
+ // Tried where a token can start: a quote, a slash, or a run of blanks, a
567
+ // comment's leading blanks taken from where they start.
568
+ if ('"\'`/ \t'.includes(char)) {
569
+ TOKEN.lastIndex = at
570
+ const token = TOKEN.exec(text)
571
+ if (token) {
572
+ at += token[0].length - 1
573
+ continue
574
+ }
575
+ }
576
+ if (char === '{') depth++
577
+ else if (char === '}' && depth-- === 0) return at
578
+ }
579
+ return text.length
580
+ }
581
+
582
+ // What `found` holds (npmRunsCode's kinds, hosts.js's services) that
583
+ // `was` didn't, each with its lines: what is new to a file, as its tags say,
584
+ // `was` null for one the version compared with didn't hold, or held none in.
585
+ // Null for none.
586
+ export function npmFoundAdded(found, was) {
587
+ if (!found) return null
588
+ const added = new Map([...found].filter(([kind]) => !was?.has(kind)))
589
+ return added.size > 0 ? added : null
590
+ }
591
+
592
+ // The kinds any of `founds` holds, as their tags name them, in their order,
593
+ // a tag kinds share once: `eval, child_process`.
594
+ export const npmRunsCodeKinds = founds => [...new Set([...RUNS_CODE].filter(([kind]) => founds.some(found => found.has(kind))).map(([, { tag }]) => tag))].join(', ')
595
+
596
+ // What `found` holds (npmRunsCode's) but the kinds whose tags `tags` names
597
+ // (npmRunsCodeWithout), or of those kinds alone (npmRunsCodeTagged), each
598
+ // with its lines; null for none.
599
+ function npmRunsCodeOf(found, kept) {
600
+ const of = new Map([...found ?? []].filter(([kind]) => kept(RUNS_CODE.get(kind).tag)))
601
+ return of.size > 0 ? of : null
602
+ }
603
+ export const npmRunsCodeWithout = (found, tags) => npmRunsCodeOf(found, tag => !tags.has(tag))
604
+ export const npmRunsCodeTagged = (found, tags) => npmRunsCodeOf(found, tag => tags.has(tag))
605
+
606
+ // The kinds that reach for credentials, by their tag: a version's finding of
607
+ // their own (risk/npm/held.js `held-credentials`), apart from the code its
608
+ // files run beyond themselves.
609
+ export const CREDENTIAL_TAGS = new Set([CREDENTIALS.tag])
610
+
611
+ // Its tags as a file's flags (file-flags.js), in their order, each once: a
612
+ // warning's (`high`), or for one marked worse, `critical`; none for none.
613
+ export const npmRunsCodeFlags = found => [...new Map([...found?.keys() ?? []].map(kind => RUNS_CODE.get(kind))
614
+ .map(({ tag, mark }) => [tag, { name: tag, severity: mark ?? 'high' }])).values()]
615
+