@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,85 @@
1
+ // Compress before the object-storage encryption boundary. Separate paths let
2
+ // legacy arbitrary report bytes remain readable without format sniffing.
3
+ import { Buffer } from 'node:buffer'
4
+ import { Readable } from 'node:stream'
5
+ import { pipeline } from 'node:stream/promises'
6
+ import { createBrotliDecompress } from 'node:zlib'
7
+ import type { BlobStore, OpenedBlob } from './blob-store.ts'
8
+ import { encodeBrotli } from './brotli.ts'
9
+
10
+ async function readBytes(stored: OpenedBlob): Promise<Buffer> {
11
+ const parts: Buffer[] = []
12
+ try {
13
+ for await (const part of stored.stream) parts.push(Buffer.from(part))
14
+ return Buffer.concat(parts)
15
+ } finally { stored.stream.destroy() }
16
+ }
17
+
18
+ // Ids whose original copy is known gone, at most this many.
19
+ const COMPRESSED_ONLY_MAX = 16_384
20
+
21
+ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
22
+ replaceLegacy: (id: string, bytes: Buffer) => Promise<unknown>): BlobStore {
23
+ // Reports this store wrote, converted, or found with only the compressed
24
+ // copy. Uploads write only that copy and nothing writes an original again,
25
+ // so their reads stop asking storage for one, each a miss (and, encrypted,
26
+ // a row read too). A compressed copy missing there asks for the original
27
+ // after all. An original left by a conversion that failed is never in it:
28
+ // the next read finds that original and resumes the conversion.
29
+ const compressedOnly = new Set<string>()
30
+ function remember(id: string): void {
31
+ compressedOnly.delete(id)
32
+ if (compressedOnly.size >= COMPRESSED_ONLY_MAX) compressedOnly.delete(compressedOnly.values().next().value!)
33
+ compressedOnly.add(id)
34
+ }
35
+ function decoded(stored: OpenedBlob): OpenedBlob {
36
+ const stream = createBrotliDecompress()
37
+ // pipeline carries storage/authentication errors into the decoded stream
38
+ // and closes the stored source when a reader leaves before EOF.
39
+ void pipeline(stored.stream, stream).catch(() => {})
40
+ return { size: null, stream }
41
+ }
42
+ async function open(id: string): Promise<OpenedBlob | null> {
43
+ if (compressedOnly.has(id)) {
44
+ const stored = await compressed.open(id)
45
+ if (stored) return decoded(stored)
46
+ compressedOnly.delete(id)
47
+ }
48
+ // Keep the original available until conversion succeeds. Concurrent reads
49
+ // can finish using an already-open stream while another reader converts it.
50
+ // Both copies are asked for at once, the original answering where it is
51
+ // still there: one round trip of storage for a report not known yet, as
52
+ // a cold instance knows none.
53
+ const [original, stored] = await Promise.allSettled([legacy.open(id), compressed.open(id)])
54
+ const unused = (copy: PromiseSettledResult<OpenedBlob | null>) => { if (copy.status === 'fulfilled') copy.value?.stream.destroy() }
55
+ if (original.status === 'rejected') { unused(stored); throw original.reason }
56
+ if (original.value) {
57
+ unused(stored)
58
+ const bytes = await readBytes(original.value)
59
+ await replaceLegacy(id, await encodeBrotli(bytes, 9))
60
+ remember(id)
61
+ return { size: bytes.length, stream: Readable.from([bytes]) }
62
+ }
63
+ if (stored.status === 'rejected') throw stored.reason
64
+ if (!stored.value) return null
65
+ remember(id)
66
+ return decoded(stored.value)
67
+ }
68
+ return {
69
+ async put(id, bytes) {
70
+ const wrapped = await compressed.put(id, await encodeBrotli(bytes, 9))
71
+ remember(id)
72
+ return wrapped
73
+ },
74
+ open,
75
+ async get(id) {
76
+ const stored = await open(id)
77
+ if (!stored) return null
78
+ return readBytes(stored)
79
+ },
80
+ async delete(id) {
81
+ compressedOnly.delete(id)
82
+ await Promise.all([compressed.delete(id), legacy.delete(id)])
83
+ },
84
+ }
85
+ }
@@ -0,0 +1,218 @@
1
+ // A repository's GitHub Actions in a risk scan (repository-risk.ts): its
2
+ // workflows and its own actions scanned (risk scanWorkflow), and the
3
+ // references they make resolved (risk resolveAction). Of a public
4
+ // repository, a reference resolved and what GitHub answers are kept for
5
+ // every scan, as many repositories use the same few actions.
6
+ import { type ActionsSource, type ResolvedAction, type ScanOptions, type WorkflowScan, dockerfileFindings, keptSource, ownFilesOf, resolveAction,
7
+ scanWorkflow, usesOfKey } from '../risk/index.js'
8
+ import type { OpenedRepository } from './repository-risk-read.ts'
9
+ import type { Lru } from './lru.ts'
10
+ import type { KeptStore } from './npm-kept.ts'
11
+ import { type Queue, queue } from './queue.ts'
12
+ import { RISK_KEPT_MS, type RiskFailure, failureOf } from './repository-risk-assess.ts'
13
+
14
+ export const WORKFLOWS_DIR = '.github/workflows'
15
+ // How many files of a repository's Actions are read, and how large.
16
+ const MAX_FILES = 200
17
+ const MAX_FILE_CHARS = 1024 * 1024
18
+ // How many references a scan resolves, how deep it follows what they name,
19
+ // and how many at once.
20
+ const MAX_ACTIONS = 1000
21
+ const MAX_DEPTH = 4
22
+ const RESOLVERS = 4
23
+
24
+ // A file of a repository's Actions: its text, or why it couldn't be had.
25
+ export type ActionsFile = { path: string; kind: 'workflow' | 'action' | 'dockerfile' } & ({ text: string } | RiskFailure)
26
+ export type WorkflowsRead = { files: ActionsFile[] } | RiskFailure
27
+
28
+ // What a scan tells of them: the workflows' paths, each file's scan, and
29
+ // each reference resolved, or why not.
30
+ export type WorkflowsLine = { type: 'workflows'; repo: number } & ({ paths: string[] } | RiskFailure)
31
+ export type WorkflowLine = { type: 'workflow'; repo: number; path: string; kind: ActionsFile['kind'] } & (WorkflowScan | RiskFailure)
32
+ export type ActionLine = { type: 'action' } & (ResolvedAction | { key: string } & RiskFailure)
33
+
34
+ // A reference to resolve, by the owner of the workflow naming it.
35
+ export interface ActionRef { key: string; owner: string | null; depth: number }
36
+
37
+ // What reading a repository's Actions asks of it.
38
+ export type ActionsReading = Required<Pick<OpenedRepository, 'workflows' | 'file'>>
39
+
40
+ // A file's text, or why not: one GitHub hasn't, or larger than read.
41
+ async function readFile(reading: ActionsReading, path: string): Promise<{ text: string } | RiskFailure> {
42
+ try {
43
+ const text = await reading.file(path)
44
+ return text.length > MAX_FILE_CHARS ? { error: 'workflow-too-large' } : { text }
45
+ } catch (error) { return failureOf(error) }
46
+ }
47
+
48
+ // Whether a read holds what any read of its commit would: no file that
49
+ // couldn't be had for a while, as GitHub's limits are.
50
+ export const lastingRead = (read: WorkflowsRead) => !('error' in read) && read.files.every(file => 'text' in file || file.error === 'workflow-too-large' || file.error === 'github-not-found')
51
+
52
+ // An action's directory, by its metadata's path.
53
+ const actionDir = (path: string) => path.replace(/\/?action\.ya?ml$/u, '')
54
+ const inRepository = (path: string) => !path.split('/').includes('..')
55
+ const notFound = (read: { text: string } | RiskFailure) => 'error' in read && read.error === 'github-not-found'
56
+
57
+ // A repository's workflows, the actions of its own they use in turn (`./`
58
+ // as the workspace has it, the repository checked out first) and those's
59
+ // Dockerfiles, MAX_FILES at most; why not, where they couldn't be listed.
60
+ export async function readActions(reading: ActionsReading): Promise<WorkflowsRead> {
61
+ let listed
62
+ try { listed = await reading.workflows() } catch (error) { return failureOf(error, 'listing its workflows') }
63
+ const files: ActionsFile[] = await Promise.all(listed.slice(0, MAX_FILES).map(async path => ({ path, kind: 'workflow' as const, ...await readFile(reading, path) })))
64
+ const seen = new Set<string>()
65
+ for (let at = 0; at < files.length; at++) {
66
+ const file = files[at]!
67
+ if (!('text' in file) || file.kind === 'dockerfile') continue
68
+ const { actions, dockerfile } = ownFilesOf(file.kind, file.text)
69
+ const path = dockerfile && [actionDir(file.path), dockerfile].filter(Boolean).join('/')
70
+ if (path && inRepository(path) && files.length < MAX_FILES) files.push({ path, kind: 'dockerfile', ...await readFile(reading, path) })
71
+ for (const dir of actions) {
72
+ if (files.length >= MAX_FILES || seen.has(dir) || !inRepository(dir) || !seen.add(dir)) continue
73
+ const named = dir === '' ? 'action' : `${dir}/action`, yml = await readFile(reading, `${named}.yml`)
74
+ const yaml = notFound(yml) ? await readFile(reading, `${named}.yaml`) : null
75
+ files.push(yaml && !notFound(yaml) ? { path: `${named}.yaml`, kind: 'action', ...yaml } : { path: `${named}.yml`, kind: 'action', ...yml })
76
+ }
77
+ }
78
+ return { files }
79
+ }
80
+
81
+ // A lockfile's packages, by name and by `name@version`, and those only
82
+ // devDependencies need.
83
+ export function lockedNames(path: string, packages: readonly { name: string; version: string | null; dev?: true }[]) {
84
+ const dev = new Map<string, boolean>()
85
+ for (const { name, version, dev: only } of packages) for (const key of version ? [name, `${name}@${version}`] : [name]) dev.set(key, (dev.get(key) ?? true) && only === true)
86
+ return { path, names: new Set(dev.keys()), dev: new Set([...dev].flatMap(([name, only]) => only ? [name] : [])) }
87
+ }
88
+
89
+ // The lines a repository's Actions read are told by, and the references
90
+ // they make to resolve. `lockfiles`, its lockfiles' packages' names by path.
91
+ export function actionsLines(repo: number, owner: string, read: WorkflowsRead, lockfiles: NonNullable<ScanOptions['lockfiles']>) {
92
+ if ('error' in read) return { lines: [{ type: 'workflows', repo, ...read } as WorkflowsLine], refs: [] as ActionRef[] }
93
+ const lines: (WorkflowsLine | WorkflowLine)[] = [{ type: 'workflows', repo, paths: read.files.filter(file => file.kind === 'workflow').map(file => file.path) }]
94
+ const refs = new Map<string, ActionRef>()
95
+ // Its own actions by their directories and workflows by their paths, as a step or a job uses one.
96
+ const actions = new Map(read.files.flatMap(file => file.kind !== 'dockerfile' && 'text' in file ? [[file.kind === 'action' ? actionDir(file.path) : file.path, file.text] as const] : []))
97
+ for (const file of read.files) {
98
+ const scan = 'error' in file ? file : file.kind === 'dockerfile' ? { uses: [], findings: dockerfileFindings(file.text) } : scanWorkflow(file.kind, file.text, { owner, lockfiles, actions })
99
+ lines.push({ type: 'workflow', repo, path: file.path, kind: file.kind, ...scan })
100
+ if ('uses' in scan) for (const uses of scan.uses) if (uses.kind === 'action' || uses.kind === 'workflow') refs.set(uses.key!, { key: uses.key!, owner, depth: 0 })
101
+ }
102
+ return { lines, refs: [...refs.values()] }
103
+ }
104
+
105
+ export type ActionAnswers = Lru<{ at: number; answer: unknown }>
106
+ // An answer's weight: an entry of a list, a kilobyte of a file.
107
+ export const answerWeight = ({ answer }: { at: number; answer: unknown }) => Array.isArray(answer) ? answer.length + 1
108
+ : typeof answer === 'string' ? Math.ceil(answer.length / 1024) + 1 : ((answer as { branches?: unknown[] } | null)?.branches?.length ?? 0) + 1
109
+
110
+ const isCommit = (text: string) => /^[\da-f]{40}$/u.test(text)
111
+
112
+ // `source`, what it answers of a public repository kept for every scan, the
113
+ // same whoever asks: of a commit while kept (but none, as one can be pushed
114
+ // later), the rest for RISK_KEPT_MS. A private one's, or one missing, never.
115
+ export function sharedSource(source: ActionsSource, answers: ActionAnswers): ActionsSource {
116
+ const keyOf = (what: string, name: string, ...more: string[]) => [what, name.toLowerCase(), ...more].join('\0')
117
+ const kept = (key: string, lasting = false) => {
118
+ const at = answers.get(key)
119
+ return at && ((lasting && at.answer !== null) || Date.now() - at.at < RISK_KEPT_MS) ? at : undefined
120
+ }
121
+ async function shared<T>(name: string, key: string, lasting: boolean, ask: () => Promise<T>): Promise<T> {
122
+ if (!kept(keyOf('repo', name))) return ask()
123
+ const at = kept(key, lasting)
124
+ if (at) return at.answer as T
125
+ const answer = await ask()
126
+ answers.set(key, { at: Date.now(), answer })
127
+ return answer
128
+ }
129
+ return {
130
+ async repo(name) {
131
+ const at = kept(keyOf('repo', name))
132
+ if (at) return at.answer as Awaited<ReturnType<ActionsSource['repo']>>
133
+ const info = await source.repo(name)
134
+ if (info?.private === false) for (const each of [name, info.fullName]) answers.set(keyOf('repo', each), { at: Date.now(), answer: info })
135
+ return info
136
+ },
137
+ tags: name => shared(name, keyOf('tags', name), false, () => source.tags(name)),
138
+ tag: (name, tag) => shared(name, keyOf('tag', name, tag), false, () => source.tag(name, tag)),
139
+ branch: (name, branch) => shared(name, keyOf('branch', name, branch), false, () => source.branch(name, branch)),
140
+ branches: name => shared(name, keyOf('branches', name), false, () => source.branches(name)),
141
+ contains: (name, base, sha) => shared(name, keyOf('contains', name, base, sha), isCommit(base), () => source.contains(name, base, sha)),
142
+ file: (name, path, commit) => shared(name, keyOf('file', name, path, commit), isCommit(commit), () => source.file(name, path, commit)),
143
+ }
144
+ }
145
+
146
+ export interface ActionsResolving {
147
+ source: ActionsSource
148
+ signal: AbortSignal
149
+ tell(line: ActionLine): void
150
+ memory: Lru<{ at: number; line: ActionLine }>
151
+ answers: ActionAnswers
152
+ store?: KeptStore<ActionLine> | undefined
153
+ }
154
+
155
+ // A reference's line: kept (`stored`, as read of the store with others),
156
+ // else resolved, and kept where it is a public repository's; why not, where
157
+ // it couldn't be.
158
+ async function resolved(resolving: ActionsResolving, source: ActionsSource, { key, owner }: ActionRef,
159
+ stored = resolving.store?.read(key)): Promise<ActionLine | null> {
160
+ const kept = resolving.memory.get(key) ?? await stored?.then(row => row && { at: row.at, line: row.value })
161
+ if (kept && Date.now() - kept.at < RISK_KEPT_MS) { resolving.memory.set(key, kept); return kept.line }
162
+ const uses = usesOfKey(key)
163
+ if (!uses) return { type: 'action', key, error: 'invalid-uses' }
164
+ let found: ResolvedAction
165
+ try { found = await resolveAction(uses, source, { owner }) } catch (error) { return resolving.signal.aborted ? null : { type: 'action', key, ...failureOf(error) } }
166
+ const line: ActionLine = { type: 'action', ...found }
167
+ if (found.repository?.private === false) {
168
+ const at = Date.now()
169
+ resolving.memory.set(key, { at, line })
170
+ await resolving.store?.write(key, line, at)
171
+ }
172
+ return line
173
+ }
174
+
175
+ // Whether a reference is named by a workflow of its repository's owner.
176
+ const ownOwners = ({ key, owner }: ActionRef) => key.split('/')[0] === owner?.toLowerCase()
177
+
178
+ // Resolves the references added, and those they name in turn, each once a
179
+ // scan, RESOLVERS at once, each told as it is; once more where another
180
+ // owner's workflow names one its own owner's did, as a repository missing is
181
+ // red then, or one is met nearer, its own followed as far as they then go. `close` settles once all are told, or the scan stops.
182
+ export function resolvingActions(resolving: ActionsResolving) {
183
+ const source = keptSource(sharedSource(resolving.source, resolving.answers))
184
+ const pending: Queue<ActionRef> = queue(resolving.signal)
185
+ const latest = new Map<string, ActionRef>()
186
+ // Those of the references added at once kept beyond this instance, read of
187
+ // the store together as they are added.
188
+ const stored = new Map<string, Promise<{ at: number; value: ActionLine } | null>>()
189
+ let closed = false, waiting = 0
190
+ const settle = () => { if (closed && waiting === 0) pending.close() }
191
+ const add = (refs: ActionRef[]) => {
192
+ for (const ref of refs) {
193
+ const first = latest.get(ref.key)
194
+ if (ref.depth > MAX_DEPTH || (first ? (!ownOwners(first) || ownOwners(ref)) && first.depth <= ref.depth : latest.size >= MAX_ACTIONS)) continue
195
+ const kept = first ? { ...ref, owner: ownOwners(first) ? ref.owner : first.owner, depth: Math.min(ref.depth, first.depth) } : ref
196
+ latest.set(ref.key, kept)
197
+ waiting++
198
+ pending.add(kept)
199
+ }
200
+ const unread = [...new Set(refs.map(ref => ref.key))].filter(key => latest.has(key) && !stored.has(key) && !resolving.memory.get(key))
201
+ const read = unread.length > 0 ? resolving.store?.readMany(unread) : undefined
202
+ for (const key of read ? unread : []) stored.set(key, read!.then(found => found.get(key) ?? null))
203
+ }
204
+ const work = async () => {
205
+ for (let ref = await pending.take(); ref !== undefined; ref = await pending.take()) {
206
+ try {
207
+ const line = await resolved(resolving, source, ref, stored.get(ref.key))
208
+ if (line && latest.get(ref.key) === ref) {
209
+ resolving.tell(line)
210
+ const owner = 'repository' in line ? (line.repository?.fullName ?? line.repo).split('/')[0]! : ref.owner
211
+ if ('uses' in line) add(line.uses.map(key => ({ key, owner, depth: ref.depth + 1 })))
212
+ }
213
+ } finally { waiting--; settle() }
214
+ }
215
+ }
216
+ const workers = Array.from({ length: RESOLVERS }, work)
217
+ return { add, async close() { closed = true; settle(); await Promise.all(workers) } }
218
+ }