@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,238 @@
1
+ import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
2
+ import { LINK_REPORT_SCHEMA } from './link-reports.ts'
3
+ import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
4
+ import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
5
+ import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
6
+ import { GITHUB_LINK_SCHEMA } from './github-links.ts'
7
+
8
+ // Created after the column migration, which predates it on older databases.
9
+ export const VIEW_SESSION_INDEX = 'CREATE INDEX IF NOT EXISTS managed_session_viewer_idx ON managed_session(viewer_session);'
10
+ // What a team's scopes join reports and bundles on: their repository, and the
11
+ // directory in it. Created after the column migrations, as above.
12
+ export const REPO_UPLOAD_INDEXES = `CREATE INDEX IF NOT EXISTS managed_report_repo_idx ON managed_report(repo_id, repo_directory);
13
+ CREATE INDEX IF NOT EXISTS managed_bundle_repo_idx ON managed_bundle(repo_id, repo_directory);`
14
+
15
+ export const MANAGED_SCHEMA = `
16
+ CREATE TABLE IF NOT EXISTS managed_user (
17
+ id TEXT PRIMARY KEY,
18
+ github_user_id INTEGER NOT NULL UNIQUE,
19
+ login TEXT NOT NULL,
20
+ name TEXT,
21
+ avatar_url TEXT,
22
+ role TEXT NOT NULL DEFAULT 'none',
23
+ -- GitHub user-to-server token, persisted so the repositories page can list
24
+ -- the user's repos (GET /user/repos) on demand. Refresh token + expiry are
25
+ -- null for non-expiring tokens (GitHub App with expiring tokens disabled).
26
+ gh_access_token TEXT,
27
+ gh_refresh_token TEXT,
28
+ gh_token_expires_at INTEGER,
29
+ created_at INTEGER NOT NULL,
30
+ updated_at INTEGER NOT NULL,
31
+ -- Presence is independent of admin edits and background token refreshes.
32
+ -- NULL for legacy accounts until a known session authenticates them.
33
+ last_seen_at INTEGER
34
+ ) STRICT;
35
+
36
+ CREATE TABLE IF NOT EXISTS managed_session (
37
+ id TEXT PRIMARY KEY,
38
+ user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
39
+ csrf_token TEXT NOT NULL,
40
+ created_at INTEGER NOT NULL,
41
+ expires_at INTEGER NOT NULL,
42
+ -- Private X25519 JWK that this session's uploads are sealed to (see
43
+ -- common/managed/upload-seal.ts). Created on first use.
44
+ upload_key TEXT,
45
+ -- An administrator's read-only view as user_id: the admin's own session,
46
+ -- which must stay valid. Ending that session ends the view.
47
+ viewer_session TEXT REFERENCES managed_session(id) ON DELETE CASCADE
48
+ ) STRICT;
49
+
50
+ CREATE INDEX IF NOT EXISTS managed_session_user_idx ON managed_session(user_id);
51
+ CREATE INDEX IF NOT EXISTS managed_session_expires_idx ON managed_session(expires_at);
52
+
53
+ -- Repositories selected to operate on. Keyed by GitHub's numeric repo id
54
+ -- (stable across renames). The row carries everything needed to read the repo's
55
+ -- contents server-side later: installation_id mints an App installation token
56
+ -- (Contents: Read) for PRIVATE repos — NULL means a PUBLIC repo readable without
57
+ -- the App — and full_name + default_branch locate the contents. added_by is the
58
+ -- selector, nulled (not cascaded) if that user is removed so the selection
59
+ -- survives.
60
+ CREATE TABLE IF NOT EXISTS managed_selected_repo (
61
+ repo_id INTEGER PRIMARY KEY,
62
+ full_name TEXT NOT NULL,
63
+ is_private INTEGER NOT NULL,
64
+ installation_id INTEGER,
65
+ default_branch TEXT NOT NULL,
66
+ -- Live browser hint; independent of the default discovered at selection.
67
+ cached_default_branch TEXT,
68
+ html_url TEXT NOT NULL,
69
+ added_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
70
+ added_at INTEGER NOT NULL,
71
+ updated_at INTEGER NOT NULL,
72
+ active INTEGER NOT NULL DEFAULT 1,
73
+ -- Its pull requests' dependencies reviewed (repository-dependency-reviews.ts):
74
+ -- as many published reviews required of each version, and the commit at
75
+ -- whose lockfiles' versions none is (null for none).
76
+ dependency_review INTEGER NOT NULL DEFAULT 0,
77
+ dependency_reviews_required INTEGER NOT NULL DEFAULT 1,
78
+ dependency_review_since TEXT
79
+ ) STRICT;
80
+
81
+ CREATE INDEX IF NOT EXISTS selected_repo_full_name_idx ON managed_selected_repo(full_name);
82
+
83
+ -- Bundles uploaded to the server (the "Manage bundles" page). Like reports, the
84
+ -- bytes are stored in the clear (blob-store, keyed by this opaque uuid id) for
85
+ -- the server to operate on. integrity is the content hash (sha512-<base64>,
86
+ -- byte-identical to the client's) so re-uploading the same bytes dedupes (UNIQUE);
87
+ -- it is also the key a report's bundleHashes match to auto-link.
88
+ -- uploaded_by / repo_id are the (nullable) user + repo links, both nulled (not
89
+ -- cascaded) when the referenced user / selected repo goes away.
90
+ CREATE TABLE IF NOT EXISTS managed_bundle (
91
+ id TEXT PRIMARY KEY,
92
+ slug TEXT NOT NULL,
93
+ integrity TEXT NOT NULL UNIQUE,
94
+ filename TEXT NOT NULL,
95
+ kind TEXT,
96
+ byte_size INTEGER NOT NULL,
97
+ uploaded_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
98
+ -- Durable snapshot of the uploader's login at upload time, so "who uploaded
99
+ -- this" survives the uploader being removed (when uploaded_by nulls out).
100
+ uploaded_by_login TEXT,
101
+ repo_id INTEGER REFERENCES managed_selected_repo(repo_id) ON DELETE SET NULL,
102
+ repo_directory TEXT NOT NULL DEFAULT '',
103
+ visible INTEGER NOT NULL DEFAULT 1,
104
+ uploaded_at INTEGER NOT NULL,
105
+ -- 'upload' or 'build' (generated on this server). A server build of bytes
106
+ -- already stored promotes the row to 'build'; NULL predates the column.
107
+ provenance TEXT,
108
+ -- JSON {preset, conditions, platforms} of a JS/TS server build; NULL otherwise.
109
+ build_conditions TEXT
110
+ ) STRICT;
111
+
112
+ CREATE INDEX IF NOT EXISTS managed_bundle_uploaded_at_idx ON managed_bundle(uploaded_at);
113
+
114
+ -- Reports uploaded to the server (the "Manage reports" page). A managed server
115
+ -- is TRUSTED, so the bytes are stored in the clear (blob-store, keyed by this
116
+ -- opaque uuid id) for the server to operate on later; this row carries the
117
+ -- metadata + attribution. uploaded_by / repo_id are the (nullable) user + repo
118
+ -- links, nulled (not cascaded) when the user / selected repo goes away. bundle_id
119
+ -- is the (nullable) auto-resolved link to a stored bundle; bundle_integrity is
120
+ -- the report's declared primary bundle (from its bundleHashes), kept so a later
121
+ -- bundle upload of that integrity can re-link.
122
+ CREATE TABLE IF NOT EXISTS managed_report (
123
+ id TEXT PRIMARY KEY,
124
+ slug TEXT NOT NULL,
125
+ filename TEXT NOT NULL,
126
+ content_type TEXT NOT NULL,
127
+ byte_size INTEGER NOT NULL,
128
+ sha256 TEXT NOT NULL,
129
+ uploaded_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
130
+ -- Durable snapshot of the uploader's login (see managed_bundle).
131
+ uploaded_by_login TEXT,
132
+ repo_id INTEGER REFERENCES managed_selected_repo(repo_id) ON DELETE SET NULL,
133
+ repo_directory TEXT NOT NULL DEFAULT '',
134
+ repo_embedded INTEGER NOT NULL DEFAULT 0,
135
+ analyzer TEXT,
136
+ visible INTEGER NOT NULL DEFAULT 0,
137
+ bundle_id TEXT REFERENCES managed_bundle(id) ON DELETE SET NULL,
138
+ bundle_integrity TEXT,
139
+ uploaded_at INTEGER NOT NULL
140
+ ) STRICT;
141
+
142
+ CREATE INDEX IF NOT EXISTS managed_report_uploaded_at_idx ON managed_report(uploaded_at);
143
+ CREATE INDEX IF NOT EXISTS managed_report_hash_idx ON managed_report(sha256, uploaded_at, id);
144
+ CREATE INDEX IF NOT EXISTS managed_report_bundle_integrity_idx ON managed_report(bundle_integrity);
145
+ CREATE INDEX IF NOT EXISTS managed_report_bundle_hash_idx ON managed_report(bundle_id, sha256);
146
+
147
+ -- Per-finding triage annotations — the managed (trusted-plaintext) counterpart
148
+ -- of the client's localStorage triage map, keyed the same way: by finding id
149
+ -- alone, not by report. Reports mostly repeat one another (a re-scan of the
150
+ -- same code carries the same finding ids) and a finding's triage is shared by
151
+ -- every report that carries it; which ids a viewer may read or write is
152
+ -- decided per report at the endpoint, not here. The wire shape lives in
153
+ -- common/managed/triage.ts. Whole-entry replace, NULL columns are unset;
154
+ -- flagged is tri-state: NULL unset / 1 flagged / 0 an explicit un-flag
155
+ -- tombstone. A cleared entry keeps its row with every field NULL — a
156
+ -- tombstone a reader adopts as "cleared", where a missing row means "never
157
+ -- annotated" (so a stale client copy can't resurrect a teammate's clear).
158
+ -- updated_by is the last writer (nulled when that user is removed);
159
+ -- updated_by_login is the durable login snapshot (see managed_bundle).
160
+ CREATE TABLE IF NOT EXISTS managed_finding_triage (
161
+ finding_id TEXT PRIMARY KEY,
162
+ color TEXT,
163
+ triage TEXT,
164
+ comment TEXT,
165
+ fix TEXT,
166
+ flagged INTEGER,
167
+ updated_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
168
+ updated_by_login TEXT,
169
+ updated_at INTEGER NOT NULL
170
+ ) STRICT;
171
+
172
+ -- The trail behind managed_finding_triage: one row per write that CHANGED an entry,
173
+ -- holding the entry as written (every field NULL = a clear), who wrote it and
174
+ -- when. managed_finding_triage stays the current-state projection reads hit; this is
175
+ -- walked only for a finding's history. The wire is whole-entry replace, so a
176
+ -- snapshot per write is exactly what arrived and "what changed" is the diff
177
+ -- against the previous row for the same id, computed on read. batch_id groups
178
+ -- the rows of one request. seq is the rowid: a total order that breaks ties
179
+ -- on at. actor_id / actor_login follow the uploaded_by / uploaded_by_login
180
+ -- convention (live account, durable login snapshot). Everything is kept by
181
+ -- default — the trail is the record; an operator may bound it per finding
182
+ -- (TRIAGE_HISTORY_LIMIT), in which case a finding's older events are trimmed
183
+ -- as new ones land. An id no report carries any more is the tombstone GC's
184
+ -- concern.
185
+ CREATE TABLE IF NOT EXISTS managed_finding_triage_event (
186
+ seq INTEGER PRIMARY KEY,
187
+ finding_id TEXT NOT NULL,
188
+ batch_id TEXT NOT NULL,
189
+ color TEXT,
190
+ triage TEXT,
191
+ comment TEXT,
192
+ fix TEXT,
193
+ flagged INTEGER,
194
+ actor_id TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
195
+ actor_login TEXT,
196
+ at INTEGER NOT NULL
197
+ ) STRICT;
198
+ CREATE INDEX IF NOT EXISTS finding_triage_event_finding_idx ON managed_finding_triage_event(finding_id, seq);
199
+ CREATE INDEX IF NOT EXISTS finding_triage_event_actor_at_idx ON managed_finding_triage_event(actor_id, at);
200
+
201
+ -- Teams group users + repos for access scoping. Hidden teams grant no access;
202
+ -- the two link tables below carry the many-many relations.
203
+ CREATE TABLE IF NOT EXISTS managed_team (
204
+ id TEXT PRIMARY KEY,
205
+ slug TEXT NOT NULL,
206
+ name TEXT NOT NULL UNIQUE,
207
+ hidden INTEGER NOT NULL DEFAULT 0,
208
+ dependency_review INTEGER NOT NULL DEFAULT 0,
209
+ created_at INTEGER NOT NULL,
210
+ updated_at INTEGER NOT NULL
211
+ ) STRICT;
212
+
213
+ -- Team <-> repo, many-many, with an OPTIONAL path (a subpath of the repo the
214
+ -- team is scoped to; empty path = the whole repo). Distinct paths can coexist;
215
+ -- adding the whole repo replaces them. CASCADE removes links with either side. repo_id
216
+ -- references the selected (operate-on) repos.
217
+ CREATE TABLE IF NOT EXISTS managed_team_repo (
218
+ team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
219
+ repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
220
+ path TEXT NOT NULL DEFAULT '',
221
+ PRIMARY KEY (team_id, repo_id, path)
222
+ ) STRICT;
223
+
224
+ CREATE INDEX IF NOT EXISTS team_repo_repo_idx ON managed_team_repo(repo_id);
225
+
226
+ -- Team <-> user, many-many, with per-membership visibility permissions (see
227
+ -- common/managed/permissions.ts) — view_dependencies / view_security, both
228
+ -- default 0 (off). CASCADE so the membership dies with either side.
229
+ CREATE TABLE IF NOT EXISTS managed_team_user (
230
+ team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
231
+ user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
232
+ view_dependencies INTEGER NOT NULL DEFAULT 0,
233
+ view_security INTEGER NOT NULL DEFAULT 0,
234
+ PRIMARY KEY (team_id, user_id)
235
+ ) STRICT;
236
+
237
+ CREATE INDEX IF NOT EXISTS team_user_user_idx ON managed_team_user(user_id);
238
+ ` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA + GITHUB_LINK_SCHEMA
@@ -0,0 +1,22 @@
1
+ // Table identifiers are code constants. Renames retain rows and foreign keys;
2
+ // an ambiguous old/new pair must be resolved before the server can start.
3
+ const MANAGED_TABLE_RENAMES = [
4
+ ['selected_repo', 'managed_selected_repo'],
5
+ ['team_repo', 'managed_team_repo'],
6
+ ['team_user', 'managed_team_user'],
7
+ ['finding_triage', 'managed_finding_triage'],
8
+ ['finding_triage_event', 'managed_finding_triage_event'],
9
+ ['finding_comment', 'managed_finding_comment'],
10
+ ['finding_comment_event', 'managed_finding_comment_event'],
11
+ ] as const
12
+
13
+ export function managedTableRenames(tables: Set<string>) {
14
+ // A fresh installation must not claim similarly named unrelated tables in
15
+ // a shared database. managed_user anchors an existing managed installation.
16
+ if (!tables.has('managed_user')) return []
17
+ const renames = MANAGED_TABLE_RENAMES.filter(([from]) => tables.has(from))
18
+ for (const [from, to] of renames) {
19
+ if (tables.has(to)) throw new Error(`Cannot migrate managed tables: both ${from} and ${to} exist`)
20
+ }
21
+ return renames
22
+ }
@@ -0,0 +1,142 @@
1
+ // SQLite bootstrap and legacy upgrades. Runtime methods are shared with Neon.
2
+ import { mkdirSync } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+ import { DatabaseSync } from 'node:sqlite'
5
+ import { migrateSlugs } from './slugs.ts'
6
+ import { migrateReportLocations } from './report-migration.ts'
7
+ import { initCommentMethods } from './comments.ts'
8
+ import { initActivityMethods } from './activity.ts'
9
+ import { revisionSchema } from './revisions.ts'
10
+ import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
11
+ import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
12
+ import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
13
+ import { DEPENDENCY_REVIEW_COLUMNS } from './dependency-reviews.ts'
14
+ import { STORAGE_SCHEMA } from './storage-db.ts'
15
+ import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
16
+ import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
17
+ import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
18
+ import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
19
+ import { managedTableRenames } from './db-table-names.ts'
20
+ import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
21
+ import { createSqliteDriver, scopeManagedMethods } from './sql.ts'
22
+ export type * from './db-methods.ts'
23
+
24
+ function migrateManagedTableNames(db: DatabaseSync): void {
25
+ db.exec('BEGIN IMMEDIATE')
26
+ try {
27
+ db.exec('CREATE TABLE IF NOT EXISTS managed_schema_version (version INTEGER PRIMARY KEY) STRICT;')
28
+ if (!db.prepare('SELECT version FROM managed_schema_version WHERE version = 3').get()) {
29
+ const rows = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all() as { name: string }[]
30
+ for (const [from, to] of managedTableRenames(new Set(rows.map(row => row.name)))) {
31
+ db.exec(`ALTER TABLE ${from} RENAME TO ${to}`)
32
+ }
33
+ db.exec('INSERT INTO managed_schema_version VALUES (3)')
34
+ }
35
+ db.exec('COMMIT')
36
+ } catch (err) { db.exec('ROLLBACK'); throw err }
37
+ }
38
+
39
+ // Preserve the legacy single-path links while adding path to their identity.
40
+ function migrateTeamRepoPaths(db: DatabaseSync): void {
41
+ const columns = db.prepare('PRAGMA table_info(managed_team_repo)').all() as { name: string; pk: number }[]
42
+ if (columns.some(column => column.name === 'path' && column.pk === 3)) return
43
+ db.exec('BEGIN IMMEDIATE')
44
+ try {
45
+ db.exec(`CREATE TABLE managed_team_repo_paths (
46
+ team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
47
+ repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
48
+ path TEXT NOT NULL DEFAULT '', PRIMARY KEY (team_id, repo_id, path)
49
+ ) STRICT;
50
+ INSERT INTO managed_team_repo_paths SELECT team_id, repo_id, COALESCE(path, '') FROM managed_team_repo;
51
+ DROP TABLE managed_team_repo;
52
+ ALTER TABLE managed_team_repo_paths RENAME TO managed_team_repo;
53
+ CREATE INDEX team_repo_repo_idx ON managed_team_repo(repo_id);`)
54
+ db.exec('COMMIT')
55
+ } catch (err) { db.exec('ROLLBACK'); throw err }
56
+ }
57
+
58
+ // Add `column` to `table` if it's missing (a lightweight migration for DBs that
59
+ // predate the column; `table`/`column` are code constants, never user input).
60
+ function ensureColumn(db: DatabaseSync, table: string, column: string, type: string): boolean {
61
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[]
62
+ if (cols.some((c) => c.name === column)) return false
63
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`)
64
+ return true
65
+ }
66
+
67
+ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}): ManagedDb {
68
+ mkdirSync(dirname(path), { recursive: true })
69
+ const db = new DatabaseSync(path)
70
+ try {
71
+ db.exec('PRAGMA journal_mode = WAL;')
72
+ db.exec('PRAGMA synchronous = FULL;')
73
+ db.exec('PRAGMA foreign_keys = ON;')
74
+ migrateManagedTableNames(db)
75
+ db.exec(MANAGED_SCHEMA + GITHUB_METADATA_SCHEMA + GITHUB_COMMIT_SCHEMA + MANAGED_ISSUE_SCHEMA + STORAGE_SCHEMA + BUNDLE_BUILD_LEASE_SCHEMA + UPSTREAM_CACHE_SCHEMA)
76
+ ensureColumn(db, 'managed_selected_repo', 'cached_default_branch', 'TEXT')
77
+ ensureColumn(db, 'managed_session', 'upload_key', 'TEXT')
78
+ ensureColumn(db, 'managed_session', 'viewer_session', 'TEXT REFERENCES managed_session(id) ON DELETE CASCADE')
79
+ db.exec(VIEW_SESSION_INDEX)
80
+ for (const table of ['managed_report', 'managed_bundle']) {
81
+ ensureColumn(db, table, 'data_key', 'TEXT')
82
+ ensureColumn(db, table, 'storage_encrypted', 'INTEGER NOT NULL DEFAULT 0')
83
+ }
84
+ ensureColumn(db, 'managed_user', 'gh_tokens_encrypted', 'INTEGER NOT NULL DEFAULT 0')
85
+ // Existing names are the sign-in accounts' own: none was chosen yet.
86
+ for (const [column, type] of GITHUB_LINK_USER_COLUMNS) {
87
+ if (ensureColumn(db, 'managed_user', column, type) && column === 'github_name') db.exec('UPDATE managed_user SET github_name = name')
88
+ }
89
+ ensureColumn(db, 'managed_github_metadata', 'state_reason', GITHUB_STATE_REASON_COLUMN)
90
+ ensureColumn(db, 'managed_github_metadata', 'attempted_at', 'INTEGER')
91
+ // Existing public links must opt in too; never preserve an implicit grant.
92
+ ensureColumn(db, 'managed_workspace_share', 'dependencies', 'INTEGER NOT NULL DEFAULT 0')
93
+ ensureColumn(db, 'managed_workspace_share', 'security', 'INTEGER NOT NULL DEFAULT 0')
94
+ migrateTeamRepoPaths(db)
95
+ migrateSlugs(db)
96
+ // Migrate DBs created before a column existed (CREATE TABLE IF NOT EXISTS
97
+ // never alters an already-present table). Idempotent — skipped on fresh DBs.
98
+ const addedLastSeen = ensureColumn(db, 'managed_user', 'last_seen_at', 'INTEGER')
99
+ if (addedLastSeen) {
100
+ // Session creation is known authentication activity. updated_at is not:
101
+ // role changes and token refreshes also wrote it. Keep unknowns NULL.
102
+ db.exec(`UPDATE managed_user SET last_seen_at = (
103
+ SELECT MAX(s.created_at) FROM managed_session s WHERE s.user_id = managed_user.id
104
+ )`)
105
+ }
106
+ ensureColumn(db, 'managed_report', 'uploaded_by_login', 'TEXT')
107
+ ensureColumn(db, 'managed_report', 'repo_embedded', 'INTEGER NOT NULL DEFAULT 0')
108
+ migrateReportLocations(db, join(dirname(path), 'reports'))
109
+ ensureColumn(db, 'managed_report', 'analyzer', 'TEXT')
110
+ // Existing rows predate publication controls and were already visible.
111
+ // Backfill only when adding the column; newly inserted rows still use the
112
+ // hidden default and must be explicitly published.
113
+ const addedVisible = ensureColumn(db, 'managed_report', 'visible', 'INTEGER NOT NULL DEFAULT 0')
114
+ if (addedVisible) db.prepare('UPDATE managed_report SET visible = 1 WHERE visible = 0').run()
115
+ ensureColumn(db, 'managed_bundle', 'uploaded_by_login', 'TEXT')
116
+ ensureColumn(db, 'managed_bundle', 'repo_directory', "TEXT NOT NULL DEFAULT ''")
117
+ db.exec(REPO_UPLOAD_INDEXES)
118
+ ensureColumn(db, 'managed_bundle', 'visible', 'INTEGER NOT NULL DEFAULT 1')
119
+ // Existing rows stay NULL (unknown): some may have been built on the server.
120
+ ensureColumn(db, 'managed_bundle', 'provenance', 'TEXT')
121
+ ensureColumn(db, 'managed_bundle', 'build_conditions', 'TEXT')
122
+ ensureColumn(db, 'managed_team', 'hidden', 'INTEGER NOT NULL DEFAULT 0')
123
+ ensureColumn(db, 'managed_team', 'dependency_review', 'INTEGER NOT NULL DEFAULT 0')
124
+ ensureColumn(db, 'managed_finding_issue', 'auto_fix_url', 'TEXT')
125
+ ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
126
+ ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
127
+ for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
128
+ for (const [column, type] of DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_dependency_review', column, type)
129
+ ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
130
+ ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
131
+ ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
132
+ initCommentMethods(db)
133
+ initActivityMethods(db)
134
+ db.exec(revisionSchema())
135
+ } catch (err) {
136
+ try { db.close() } catch {}
137
+ throw err
138
+ }
139
+
140
+ const driver = createSqliteDriver(db)
141
+ return scopeManagedMethods(createManagedMethods(driver, options), driver)
142
+ }
@@ -0,0 +1,60 @@
1
+ // A repository's Dependabot alerts for a repository risk scan, read with the
2
+ // App's installation token where the admin's own access lets them see them on
3
+ // GitHub, as doc/bundles.md's repository risk scan section tells.
4
+ import type { ManagedConfig } from './config.ts'
5
+ import type { SelectedRepo } from './db.ts'
6
+ import { type DependabotAlert, dependabotAlerts } from './dependabot-alerts.ts'
7
+ import { GithubApiError, type GithubIdentity, githubRepoPermission, repoAccessToken, repositoryInstallation } from './github-app.ts'
8
+
9
+ // The levels GitHub lets see a repository's Dependabot alerts by default
10
+ // (maintain's reads `write`).
11
+ const SEES_ALERTS: ReadonlySet<string> = new Set(['admin', 'write'])
12
+ // Failures of verifying the sign-in that are the sign-in's own: no token, or
13
+ // one of another account (repository-browser.ts getIdentity), or one GitHub no
14
+ // longer takes. Any other, as GitHub failing to answer, is told as it is.
15
+ const UNVERIFIED: ReadonlySet<string> = new Set(['no-repository', 'github-unauthorized'])
16
+
17
+ // What of a repository's reader (repository-browser.ts) the alerts are asked
18
+ // with: the user's verified identity, and their permission level where its
19
+ // access gate asked GitHub of it already.
20
+ export interface AlertsReader {
21
+ identity(): Promise<GithubIdentity>
22
+ permission(): string | null
23
+ }
24
+
25
+ // Alerts read, and their `recheck`: the user's access to them checked again,
26
+ // asked of GitHub afresh, just before they are told (repository-risk.ts), as
27
+ // access to the repository is, as it may be withdrawn meanwhile.
28
+ export interface AlertsRead { alerts: DependabotAlert[]; recheck(): Promise<void> }
29
+
30
+ // `repo`'s Dependabot alerts (dependabot-alerts.ts), read with the token of
31
+ // the App's installation GitHub tells covers it now, its token checked to read
32
+ // it (github-app.ts repositoryInstallation), never the one its connection
33
+ // keeps, which may no longer cover it, once the user's own permission on it
34
+ // lets them see them; else refused, the alerts not asked:
35
+ // `dependabot-not-installed`, `dependabot-unverified`, or
36
+ // `dependabot-forbidden`, naming their permission. The token is asked each
37
+ // time of repoAccessToken, which renews it before it expires, as the recheck
38
+ // may come long after the alerts are read.
39
+ export async function appDependabotAlerts(config: ManagedConfig, repo: Pick<SelectedRepo, 'repoId' | 'fullName'>, reader: AlertsReader,
40
+ fetchImpl: typeof fetch = globalThis.fetch): Promise<AlertsRead> {
41
+ const id = await repositoryInstallation(config, repo.repoId, repo.fullName, fetchImpl)
42
+ const tokenNow = async () => {
43
+ const token = await repoAccessToken(config, id, fetchImpl)
44
+ if (token == null) throw new GithubApiError(403, 'dependabot-not-installed')
45
+ return token
46
+ }
47
+ const token = await tokenNow()
48
+ let who
49
+ try { who = await reader.identity() } catch (error) {
50
+ throw error instanceof GithubApiError && UNVERIFIED.has(error.message) ? new GithubApiError(403, 'dependabot-unverified') : error
51
+ }
52
+ const seen = (permission: string) => {
53
+ if (!SEES_ALERTS.has(permission)) {
54
+ throw new GithubApiError(403, 'dependabot-forbidden', null, `@${who.login}'s access to this repository (${permission}) doesn't let them see its Dependabot alerts on GitHub`)
55
+ }
56
+ }
57
+ seen(reader.permission() ?? await githubRepoPermission(repo.fullName, token, who, fetchImpl))
58
+ const alerts = await dependabotAlerts(repo.fullName, token, fetchImpl)
59
+ return { alerts, recheck: async () => { seen(await githubRepoPermission(repo.fullName, await tokenNow(), who, fetchImpl)) } }
60
+ }
@@ -0,0 +1,185 @@
1
+ // A repository's Dependabot alerts of npm's ecosystem, open or dismissed, as a
2
+ // repository risk scan blends them into npm's advisories (repository-risk.ts),
3
+ // a dismissed one with who dismissed it, when and why: GitHub's
4
+ // `GET /repos/:repo/dependabot/alerts`, a page of 100 at a time, each found by
5
+ // the cursor the one before's Link header names, MAX_PAGES at most. Asked with
6
+ // the App's installation token, once the admin's own access to the repository
7
+ // is found to let them see the alerts on GitHub (dependabot-access.ts): where
8
+ // the installation hasn't Dependabot alerts: read, or the repository has them
9
+ // off, that is thrown as its code, never GitHub's text, for the scan to tell
10
+ // and go on.
11
+ import { isExactVersion, satisfies, validRange } from '@preventive/upstream/semver.js'
12
+ import { isNpmPackageName } from '../common/managed/npm-packages.js'
13
+ import { GithubApiError, githubUnreachable, throwGithubResponseError } from './github-errors.ts'
14
+ import type { LockedPackage } from './locked-packages.ts'
15
+
16
+ // An alert, as GitHub has it: its `number` in its repository and its page
17
+ // there (`url`, built, never fetched), the manifest or lockfile it is of
18
+ // (`manifest`, from the repository's root), its package and advisory, the
19
+ // versions it affects as GitHub writes them (`range`, `>= 1.0.0, < 1.2.6`)
20
+ // and the first that isn't (`patched`), `dev` where GitHub tells the
21
+ // dependency is only for development, and `dismissed` where it is: by whom,
22
+ // when, why and what they said, or by one of the repository's rules (`auto`).
23
+ export interface DependabotAlert {
24
+ number: number
25
+ url: string
26
+ manifest: string
27
+ name: string
28
+ ghsa: string
29
+ // The advisory's other ids, as its CVE.
30
+ aliases: string[]
31
+ title?: string
32
+ // `critical`, `high`, `moderate`, `low`: GitHub's `medium` reads `moderate`.
33
+ severity?: string
34
+ range?: string
35
+ patched?: string
36
+ cwe: string[]
37
+ dev?: true
38
+ dismissed?: AlertDismissal
39
+ }
40
+
41
+ // GitHub's `dismissed_reason`: `fix_started`, `inaccurate`, `no_bandwidth`,
42
+ // `not_used` or `tolerable_risk`; `at` as GitHub writes it (ISO 8601).
43
+ export interface AlertDismissal { auto?: true; reason?: string; at?: string; by?: string; comment?: string }
44
+
45
+ const API = 'https://api.github.com'
46
+ const API_VERSION = '2022-11-28'
47
+ const USER_AGENT = 'deepview-triage'
48
+ const PER_PAGE = 100
49
+ // Up to how many pages a listing asks: 2,000 alerts of npm's in a repository,
50
+ // past which the rest are left out; where open and dismissed ones are more,
51
+ // the open ones are listed again on their own (dependabotAlerts).
52
+ const MAX_PAGES = 20
53
+ // A page holds each advisory's whole text: given longer than githubJson's.
54
+ const PAGE_TIMEOUT_MS = 20_000
55
+ const GHSA = /^GHSA(?:-[23456789cfghjmpqrvwx]{4}){3}$/u
56
+ const SEVERITIES = new Map([['critical', 'critical'], ['high', 'high'], ['medium', 'moderate'], ['moderate', 'moderate'], ['low', 'low']])
57
+ // A path from a repository's root, as git takes one: no empty, `.` or `..`
58
+ // component, nor a control character.
59
+ const isRepoPath = (path: unknown): path is string => typeof path === 'string' && path.length <= 1024 && !/\p{Cc}/u.test(path)
60
+ && path.split('/').every(part => part !== '' && part !== '.' && part !== '..')
61
+
62
+ // The states asked: a fixed alert's dependency is no longer what it was.
63
+ const STATES = ['open', 'dismissed', 'auto_dismissed']
64
+ const REASONS = new Set(['fix_started', 'inaccurate', 'no_bandwidth', 'not_used', 'tolerable_risk'])
65
+ const LOGIN = /^[\dA-Za-z](?:[\dA-Za-z-]{0,38})(?:\[bot\])?$/u
66
+
67
+ const text = (value: unknown, most = 500) => typeof value === 'string' && value.trim() ? value.trim().slice(0, most) : undefined
68
+
69
+ // How a dismissed alert was dismissed, as far as GitHub tells it soundly.
70
+ function dismissalOf(state: string, raw: Record<string, any>): AlertDismissal {
71
+ const at = state === 'auto_dismissed' ? raw['auto_dismissed_at'] : raw['dismissed_at']
72
+ const when = typeof at === 'string' && at.length <= 40 && !Number.isNaN(Date.parse(at)) ? { at } : {}
73
+ if (state === 'auto_dismissed') return { auto: true, ...when }
74
+ const by = raw['dismissed_by']?.login, comment = text(raw['dismissed_comment'], 280), reason = raw['dismissed_reason']
75
+ return { ...REASONS.has(reason) ? { reason } : {}, ...when, ...typeof by === 'string' && LOGIN.test(by) ? { by } : {}, ...comment === undefined ? {} : { comment } }
76
+ }
77
+
78
+ // An alert as GitHub answers it, or null for one that isn't an open or
79
+ // dismissed one of npm's, of a package and GHSA npm and GitHub would name.
80
+ function alertOf(fullName: string, raw: unknown): DependabotAlert | null {
81
+ if (raw == null || typeof raw !== 'object') return null
82
+ const { number, state, dependency, security_advisory: advisory, security_vulnerability: vulnerability } = raw as Record<string, any>
83
+ if (!Number.isSafeInteger(number) || number <= 0 || !STATES.includes(state)) return null
84
+ const ghsa = advisory?.ghsa_id, manifest = dependency?.manifest_path, name = dependency?.package?.name
85
+ if (dependency?.package?.ecosystem !== 'npm' || !isNpmPackageName(name) || !isRepoPath(manifest) || typeof ghsa !== 'string' || !GHSA.test(ghsa)) return null
86
+ const severity = SEVERITIES.get(vulnerability?.severity ?? advisory?.severity)
87
+ const patched = text(vulnerability?.first_patched_version?.identifier, 100), range = text(vulnerability?.vulnerable_version_range, 200), title = text(advisory?.summary)
88
+ const ids = Array.isArray(advisory?.identifiers) ? advisory.identifiers as { value?: unknown }[] : []
89
+ const cwes = Array.isArray(advisory?.cwes) ? advisory.cwes as { cwe_id?: unknown }[] : []
90
+ return {
91
+ number, url: `https://github.com/${fullName.split('/').map(encodeURIComponent).join('/')}/security/dependabot/${number}`, manifest, name, ghsa,
92
+ aliases: [...new Set(ids.flatMap(id => typeof id?.value === 'string' && /^[\w.:-]{1,64}$/u.test(id.value) && id.value !== ghsa ? [id.value] : []))],
93
+ ...title === undefined ? {} : { title }, ...severity === undefined ? {} : { severity },
94
+ ...range === undefined ? {} : { range }, ...patched === undefined ? {} : { patched },
95
+ cwe: [...new Set(cwes.flatMap(cwe => typeof cwe?.cwe_id === 'string' && /^CWE-\d{1,6}$/u.test(cwe.cwe_id) ? [cwe.cwe_id] : []))],
96
+ ...dependency.scope === 'development' ? { dev: true as const } : {},
97
+ ...state === 'open' ? {} : { dismissed: dismissalOf(state, raw as Record<string, any>) },
98
+ }
99
+ }
100
+
101
+ // The next page's cursor, as the Link header's `next` names it: its `after`,
102
+ // or its `page`, all that is taken of it, the rest of its URL left as asked.
103
+ function nextPage(link: string | null): [string, string] | null {
104
+ const next = link?.split(',').map(part => /^\s*<([^>]*)>\s*;\s*rel="next"\s*$/u.exec(part)?.[1]).find(Boolean)
105
+ if (!next) return null
106
+ let url
107
+ try { url = new URL(next) } catch { return null }
108
+ if (url.origin !== API) return null
109
+ const after = url.searchParams.get('after'), page = url.searchParams.get('page')
110
+ return after ? ['after', after] : page && /^\d{1,4}$/u.test(page) ? ['page', page] : null
111
+ }
112
+
113
+ // A page asked of GitHub: its alerts and the headers they came with. A 403
114
+ // that isn't a rate limit is told as the alerts being off
115
+ // (`dependabot-disabled`) or not to be read by the App's installation
116
+ // (`dependabot-app-permission`), as a 404 is, the repository found just
117
+ // before; the rest as githubJson tells them.
118
+ async function askPage(url: string, token: string, fetchImpl: typeof fetch): Promise<{ alerts: unknown[]; link: string | null }> {
119
+ const started = Date.now()
120
+ let res: Response
121
+ try {
122
+ res = await fetchImpl(url, {
123
+ signal: AbortSignal.timeout(PAGE_TIMEOUT_MS), redirect: 'error',
124
+ headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json', 'user-agent': USER_AGENT, 'x-github-api-version': API_VERSION },
125
+ })
126
+ } catch (error) { throw githubUnreachable(error, started) }
127
+ if (res.status === 403) {
128
+ const message = (await res.clone().json().catch(() => null) as { message?: unknown } | null)?.message
129
+ const limited = res.headers.get('x-ratelimit-remaining') === '0' || res.headers.has('retry-after')
130
+ || (typeof message === 'string' && /rate limit|abuse detection/iu.test(message))
131
+ if (!limited) throw new GithubApiError(403, typeof message === 'string' && /disabled/iu.test(message) ? 'dependabot-disabled' : 'dependabot-app-permission')
132
+ }
133
+ if (res.status === 404) throw new GithubApiError(403, 'dependabot-app-permission')
134
+ if (!res.ok) await throwGithubResponseError(res)
135
+ let body
136
+ try { body = await res.json() } catch (error) {
137
+ throw error instanceof SyntaxError ? new GithubApiError(502, 'github-malformed') : githubUnreachable(error, started)
138
+ }
139
+ if (!Array.isArray(body)) throw new GithubApiError(502, 'github-malformed')
140
+ return { alerts: body, link: res.headers.get('link') }
141
+ }
142
+
143
+ // `fullName`'s alerts in `states`, MAX_PAGES at most, in GitHub's order (the
144
+ // newest first), and whether that is all of them.
145
+ async function listAlerts(fullName: string, states: readonly string[], token: string, fetchImpl: typeof fetch) {
146
+ const base = `${API}/repos/${fullName.split('/').map(encodeURIComponent).join('/')}/dependabot/alerts`
147
+ const alerts: DependabotAlert[] = []
148
+ const seen = new Set<number>()
149
+ let cursor: [string, string] | null = null
150
+ for (let pages = 0; pages < MAX_PAGES; pages++) {
151
+ const query = new URLSearchParams({ state: states.join(','), ecosystem: 'npm', per_page: String(PER_PAGE), ...cursor ? { [cursor[0]]: cursor[1] } : {} })
152
+ const answer = await askPage(`${base}?${query}`, token, fetchImpl)
153
+ for (const raw of answer.alerts) {
154
+ const alert = alertOf(fullName, raw)
155
+ if (alert && !seen.has(alert.number)) { seen.add(alert.number); alerts.push(alert) }
156
+ }
157
+ cursor = answer.alerts.length === 0 ? null : nextPage(answer.link)
158
+ if (!cursor) return { alerts, whole: true }
159
+ }
160
+ return { alerts, whole: false }
161
+ }
162
+
163
+ // `fullName`'s Dependabot alerts of npm's ecosystem, open or dismissed, as
164
+ // `token`, the App's installation's (dependabot-access.ts), may see them, in
165
+ // GitHub's order. Asked at once, but where they are more than a listing holds,
166
+ // as newer dismissed ones would leave older open ones out, the open ones are
167
+ // listed again on their own, and the dismissed ones had kept after them.
168
+ export async function dependabotAlerts(fullName: string, token: string, fetchImpl: typeof fetch = globalThis.fetch): Promise<DependabotAlert[]> {
169
+ const all = await listAlerts(fullName, STATES, token, fetchImpl)
170
+ if (all.whole) return all.alerts
171
+ const open = await listAlerts(fullName, ['open'], token, fetchImpl)
172
+ const told = new Set(open.alerts.map(alert => alert.number))
173
+ return [...open.alerts, ...all.alerts.filter(alert => alert.dismissed && !told.has(alert.number))]
174
+ }
175
+
176
+ // The versions of an alert's package that a lockfile it is of locks
177
+ // (`packages`) and its range covers, GitHub's read as npm's semver reads its
178
+ // own (`>= 1.0.0, < 1.2.6` as `>= 1.0.0 < 1.2.6`); none where it is of no
179
+ // lockfile read, or its range is none semver reads.
180
+ export function alertVersions(alert: Pick<DependabotAlert, 'name' | 'range'>, packages: readonly LockedPackage[] | undefined): string[] {
181
+ const range = alert.range === undefined ? null : validRange(alert.range.replaceAll(',', ' '))
182
+ if (!packages || range === null) return []
183
+ return [...new Set(packages.flatMap(({ name, version, source }) => name === alert.name && (source === 'registry' || source === 'mirror')
184
+ && isExactVersion(version) && satisfies(version, range) ? [version] : []))]
185
+ }