@preventive/triage 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (168) hide show
  1. package/api/reap.ts +17 -0
  2. package/cli.js +6 -0
  3. package/client/finding-link.js +305 -0
  4. package/client/linked-findings.d.ts +1 -0
  5. package/client/linked-findings.js +111 -0
  6. package/common/bundle-metadata.d.ts +10 -0
  7. package/common/bundle-metadata.js +177 -0
  8. package/common/bundle-reasons.d.ts +2 -0
  9. package/common/bundle-reasons.js +21 -0
  10. package/common/bundle-sources.d.ts +3 -0
  11. package/common/bundle-sources.js +284 -0
  12. package/common/bundle-stats.js +41 -0
  13. package/common/bundle-tabs.js +1 -0
  14. package/common/code-language.js +36 -0
  15. package/common/default-scan-models.ts +30 -0
  16. package/common/finding-id.js +47 -0
  17. package/common/github-pr.ts +56 -0
  18. package/common/managed/comments.ts +34 -0
  19. package/common/managed/permissions.ts +35 -0
  20. package/common/managed/report-content.ts +42 -0
  21. package/common/managed/report-filter.ts +108 -0
  22. package/common/managed/roles.ts +28 -0
  23. package/common/managed/routes.d.ts +2 -0
  24. package/common/managed/routes.js +121 -0
  25. package/common/managed/scan-models.ts +6 -0
  26. package/common/managed/triage.ts +83 -0
  27. package/common/save-error-reason.ts +20 -7
  28. package/common/scan-server.ts +13 -0
  29. package/common/server-info.ts +33 -0
  30. package/common/utf8.d.ts +3 -0
  31. package/common/utf8.js +45 -0
  32. package/out/brotli-fallback.js +3 -3
  33. package/out/client-managed-import.js +81 -0
  34. package/out/client-managed.js +110 -0
  35. package/out/client-sync.js +16 -13
  36. package/out/graph.js +30 -4
  37. package/out/index.html +55 -8
  38. package/out/prism.js +2 -2
  39. package/out/stasis.svg +45 -0
  40. package/out/terminal.js +273 -39
  41. package/out/view.css +1 -1
  42. package/out/view.js +198 -62
  43. package/package.json +179 -55
  44. package/report/index.js +254 -0
  45. package/report/src/finding-id.js +80 -0
  46. package/report/src/finding.js +312 -0
  47. package/report/src/labels.js +33 -0
  48. package/report/src/md-structure.js +471 -0
  49. package/report/src/md-text.js +167 -0
  50. package/report/src/meta.js +76 -0
  51. package/report/src/parse-codex.js +147 -0
  52. package/report/src/parse-deepsec.js +197 -0
  53. package/report/src/parse-deepview-fields.js +375 -0
  54. package/report/src/parse-deepview-md.js +185 -0
  55. package/report/src/parse-md-id.js +137 -0
  56. package/report/src/parse-md.js +322 -0
  57. package/report/src/parse-piolium-id.js +79 -0
  58. package/report/src/parse-piolium-rows.js +131 -0
  59. package/report/src/parse-piolium-tokens.js +175 -0
  60. package/report/src/parse-piolium.js +400 -0
  61. package/report/src/security.js +63 -0
  62. package/report/src/utf8.js +21 -0
  63. package/report/src/write-md-finding.js +273 -0
  64. package/report/src/write-md.js +291 -0
  65. package/server-common/database-config.ts +16 -0
  66. package/server-common/initialize.ts +18 -0
  67. package/server-common/npm-advisories.ts +101 -0
  68. package/{server → server-common}/origin.ts +5 -5
  69. package/server-common/reap.ts +48 -0
  70. package/server-common/scan-config.ts +19 -0
  71. package/server-common/standalone.ts +29 -0
  72. package/server-common/storage-log.ts +34 -0
  73. package/server-common/vercel-blob.ts +110 -0
  74. package/server-e2e/app.ts +485 -0
  75. package/{server → server-e2e}/auth.ts +5 -1
  76. package/{server → server-e2e}/bus-receiver.ts +9 -8
  77. package/{server → server-e2e}/cli.js +9 -4
  78. package/{server → server-e2e}/config.ts +54 -39
  79. package/{server → server-e2e}/db-neon.ts +2 -2
  80. package/{server → server-e2e}/db-revision-sql.ts +7 -10
  81. package/{server → server-e2e}/db-stmt.ts +2 -2
  82. package/{server → server-e2e}/db.ts +96 -135
  83. package/{server → server-e2e}/http.ts +110 -12
  84. package/{server → server-e2e}/hub.ts +44 -14
  85. package/server-e2e/index.ts +17 -0
  86. package/server-e2e/lifecycle.ts +95 -0
  87. package/{server → server-e2e}/neon-driver.ts +2 -2
  88. package/{server → server-e2e}/npm-proxy.ts +11 -144
  89. package/{server → server-e2e}/objstore/blob-fs.ts +6 -8
  90. package/{server → server-e2e}/objstore/blob-vercel.ts +49 -141
  91. package/{server → server-e2e}/objstore/blob.ts +24 -9
  92. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  93. package/{server → server-e2e}/objstore/handlers.ts +19 -20
  94. package/{server → server-e2e}/objstore/init.ts +53 -27
  95. package/{server → server-e2e}/objstore/reaper.ts +31 -11
  96. package/server-e2e/objstore/rest-deny.ts +28 -0
  97. package/server-e2e/objstore/rest-mint.ts +224 -0
  98. package/{server → server-e2e}/objstore/rest.ts +110 -93
  99. package/{server → server-e2e}/objstore/sign.ts +105 -0
  100. package/{server → server-e2e}/objstore/store-neon.ts +10 -14
  101. package/{server → server-e2e}/objstore/store.ts +98 -118
  102. package/{server → server-e2e}/objstore/tokens.ts +9 -12
  103. package/{server → server-e2e}/peer.ts +7 -9
  104. package/{server → server-e2e}/pubsub.ts +29 -36
  105. package/{server → server-e2e}/sign.ts +12 -14
  106. package/{server → server-e2e}/sse-server.ts +105 -73
  107. package/{server → server-e2e}/sse-session.ts +30 -16
  108. package/{server → server-e2e}/static.ts +36 -29
  109. package/server-e2e/sync-handlers.ts +408 -0
  110. package/{server → server-e2e}/util.ts +9 -0
  111. package/{server → server-e2e}/ws-server.ts +29 -23
  112. package/server-managed/activity.ts +231 -0
  113. package/server-managed/avatar-store.ts +51 -0
  114. package/server-managed/blob-store.ts +66 -0
  115. package/server-managed/blob-vercel.ts +125 -0
  116. package/server-managed/brotli.ts +10 -0
  117. package/server-managed/bundle-cache.ts +185 -0
  118. package/server-managed/bundle-catalog.ts +29 -0
  119. package/server-managed/bundle-store.ts +28 -0
  120. package/server-managed/bundle-summary-cache.ts +97 -0
  121. package/server-managed/bundle.ts +39 -0
  122. package/server-managed/cache-storage.ts +40 -0
  123. package/server-managed/cli.js +13 -0
  124. package/server-managed/combined.ts +46 -0
  125. package/server-managed/comments.ts +151 -0
  126. package/server-managed/config.ts +144 -0
  127. package/server-managed/content-access.ts +15 -0
  128. package/server-managed/crypto.ts +25 -0
  129. package/server-managed/db-methods.ts +1330 -0
  130. package/server-managed/db-neon.ts +171 -0
  131. package/server-managed/db-schema.ts +203 -0
  132. package/server-managed/db-table-names.ts +22 -0
  133. package/server-managed/db.ts +109 -0
  134. package/server-managed/github-app.ts +332 -0
  135. package/server-managed/github-metadata.ts +65 -0
  136. package/server-managed/github-oauth.ts +215 -0
  137. package/server-managed/github-pulls.ts +115 -0
  138. package/server-managed/http-response.ts +18 -0
  139. package/server-managed/http.ts +2043 -0
  140. package/server-managed/import-triage.ts +48 -0
  141. package/server-managed/index.ts +135 -0
  142. package/server-managed/public-workspace.ts +150 -0
  143. package/server-managed/repo-path.ts +21 -0
  144. package/server-managed/report-migration.ts +35 -0
  145. package/server-managed/report-query.ts +4 -0
  146. package/server-managed/report-response.ts +16 -0
  147. package/server-managed/report-sources.ts +154 -0
  148. package/server-managed/repository-discovery.ts +82 -0
  149. package/server-managed/repository-policy.ts +25 -0
  150. package/server-managed/session.ts +78 -0
  151. package/server-managed/slugs.ts +38 -0
  152. package/server-managed/sql-postgres.ts +30 -0
  153. package/server-managed/sql.ts +61 -0
  154. package/server-managed/static.ts +28 -0
  155. package/server-managed/storage.ts +34 -0
  156. package/server-managed/team-catalog.ts +7 -0
  157. package/server-managed/team-feed.ts +128 -0
  158. package/server-managed/team-reports.ts +156 -0
  159. package/server-managed/triage-response.ts +16 -0
  160. package/server-managed/uploads.ts +47 -0
  161. package/server-managed/workspace-shares.ts +150 -0
  162. package/server.ts +50 -0
  163. package/server/index.ts +0 -481
  164. package/server/lifecycle.ts +0 -204
  165. package/server/sync-handlers.ts +0 -327
  166. /package/{server → server-e2e}/config.example.json +0 -0
  167. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  168. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,115 @@
1
+ import { type GithubFixRef, type GithubFixResult, type GithubFixStatus, githubIssueClosedReason, isGithubRepoName, parseGithubFixUrl } from '../common/github-pr.ts'
2
+ import type { ManagedConfig } from './config.ts'
3
+ import type { ManagedDb, TeamReportAccessSnapshot } from './db.ts'
4
+ import type { GithubMetadata } from './github-metadata.ts'
5
+ import { ensureUserAccessToken } from './github-oauth.ts'
6
+
7
+ type Metadata = Pick<GithubMetadata, 'title' | 'description' | 'status' | 'stateReason'>
8
+ const MAX_GITHUB_LOOKUPS = 200
9
+ const GITHUB_LOOKUP_TIMEOUT_MS = 10_000
10
+ const GITHUB_METADATA_TTL_MS = 60_000
11
+
12
+ // ref.repo is always the canonical name from managed_selected_repo, never an
13
+ // owner/repo/path taken from a Fix URL. Redirects cannot move the user token.
14
+ async function fetchMetadata(ref: GithubFixRef, token: string, fetchImpl: typeof fetch): Promise<Metadata | null> {
15
+ try {
16
+ const response = await fetchImpl(`https://api.github.com/repos/${ref.repo}/${ref.kind === 'pull' ? 'pulls' : 'issues'}/${ref.number}`, {
17
+ headers: {
18
+ authorization: `Bearer ${token}`, accept: 'application/vnd.github+json',
19
+ 'user-agent': 'deepview-triage', 'x-github-api-version': '2022-11-28',
20
+ },
21
+ redirect: 'error',
22
+ })
23
+ if (!response.ok) return null
24
+ const body = await response.json() as { number?: unknown; title?: unknown; body?: unknown; state?: unknown; merged?: unknown;
25
+ draft?: unknown; base?: { repo?: { full_name?: unknown } }; repository_url?: unknown; pull_request?: unknown; state_reason?: unknown } | null
26
+ if (body?.number !== ref.number || typeof body.title !== 'string' || !body.title.trim() || body.title.length > 1024
27
+ || !['open', 'closed'].includes(String(body.state)) || (body.body != null && typeof body.body !== 'string')) return null
28
+ let status: GithubFixStatus
29
+ if (ref.kind === 'pull') {
30
+ if (typeof body.merged !== 'boolean' || typeof body.base?.repo?.full_name !== 'string'
31
+ || body.base.repo.full_name.toLowerCase() !== ref.repo.toLowerCase()) return null
32
+ status = body.merged ? 'merged' : body.state === 'closed' ? 'closed' : body.draft === true ? 'draft' : 'open'
33
+ } else {
34
+ // The issues API also accepts PR numbers. Do not cache a PR as a closed
35
+ // ordinary issue: only the pulls API can establish its merged state.
36
+ if (body.pull_request != null || typeof body.repository_url !== 'string'
37
+ || body.repository_url.toLowerCase() !== `https://api.github.com/repos/${ref.repo}`.toLowerCase()) return null
38
+ status = body.state === 'closed' ? 'closed' : 'open'
39
+ }
40
+ const stateReason = ref.kind === 'issue' && status === 'closed' ? githubIssueClosedReason(body.state_reason) : null
41
+ return { title: body.title, description: body.body ?? null, status, stateReason }
42
+ } catch { return null }
43
+ }
44
+
45
+ // The workspace handler checks user -> team membership and finding visibility.
46
+ // Every shared cache read is additionally restricted to that team’s repositories,
47
+ // including for admins; cached metadata is never itself an access grant.
48
+ export async function lookupFixes(config: ManagedConfig, db: ManagedDb, snapshot: TeamReportAccessSnapshot, urls: string[], fetchImpl: typeof fetch = globalThis.fetch): Promise<GithubFixResult[]> {
49
+ const allowed = new Map(snapshot.repositories.filter(repo => isGithubRepoName(repo.github)).map(repo => [repo.github.toLowerCase(), repo]))
50
+ const parsed = urls.map(parseGithubFixUrl)
51
+ const jobs = new Map<string, GithubFixRef>()
52
+ const keys = parsed.map(ref => {
53
+ const repo = ref && allowed.get(ref.repo.toLowerCase())
54
+ if (!ref || !repo) return null
55
+ const key = `${repo.repoId}:${ref.kind}:${ref.number}`
56
+ jobs.set(key, { ...ref, repo: repo.github })
57
+ return key
58
+ })
59
+ const metadata = new Map((await db.listGithubMetadata([...jobs.keys()])).map(entry => [entry.key, entry]))
60
+ const now = Date.now()
61
+ const lastCheckedAt = (key: string) => {
62
+ const cached = metadata.get(key)
63
+ return Math.max(cached?.fetchedAt ?? 0, cached?.attemptedAt ?? 0)
64
+ }
65
+ const pending = [...jobs.entries()].filter(([key, ref]) => {
66
+ const cached = metadata.get(key)
67
+ // Backfill closed issues cached before closure reasons existed, within the
68
+ // same authorized/capped queue. Even an unknown reason completes backfill.
69
+ return !cached || (ref.kind === 'issue' && cached.status === 'closed' && cached.stateReason == null)
70
+ || (['open', 'draft'].includes(cached.status) && cached.fetchedAt + GITHUB_METADATA_TTL_MS <= now)
71
+ }).toSorted(([a], [b]) => lastCheckedAt(a) - lastCheckedAt(b)).slice(0, MAX_GITHUB_LOOKUPS)
72
+ // Only stale/missing, distinct, authorized entries consume the request budget.
73
+ // Prioritize missing/oldest entries so a large workspace makes progress.
74
+ if (pending.length > 0) {
75
+ const signal = AbortSignal.timeout(GITHUB_LOOKUP_TIMEOUT_MS)
76
+ const fetchWithinDeadline: typeof fetch = (input, init) => {
77
+ signal.throwIfAborted()
78
+ return fetchImpl(input, { ...init, signal })
79
+ }
80
+ const token = await ensureUserAccessToken(config, db, snapshot.user.id, now, fetchWithinDeadline)
81
+ const fresh: Omit<GithubMetadata, 'attemptedAt'>[] = []
82
+ const attempted: string[] = []
83
+ if (token) {
84
+ // Token refresh and all waves share one deadline, with at most four
85
+ // upstream calls in flight. Failure retains even stale cached metadata.
86
+ for (let i = 0; i < pending.length && !signal.aborted; i += 4) {
87
+ await Promise.all(pending.slice(i, i + 4).map(async ([key, ref]) => {
88
+ if (signal.aborted) return
89
+ if (metadata.has(key)) attempted.push(key)
90
+ const result = await fetchMetadata(ref, token, fetchWithinDeadline)
91
+ if (result) fresh.push({ key, ...result, fetchedAt: Date.now() })
92
+ }))
93
+ }
94
+ }
95
+ // Only started cached reads rotate. Missing entries retain priority, and
96
+ // jobs skipped by the cap, deadline or absent token keep their place.
97
+ if (attempted.length > 0) await db.recordGithubMetadataAttempts(attempted, Date.now())
98
+ if (fresh.length > 0) {
99
+ await db.setGithubMetadata(fresh)
100
+ // A concurrent merged result must win over a slower open/closed read.
101
+ for (const entry of await db.listGithubMetadata(fresh.map(row => row.key))) metadata.set(entry.key, entry)
102
+ }
103
+ }
104
+ // The handler rechecks workspace access and saved Fix links before returning.
105
+ return urls.flatMap((url, index): GithubFixResult[] => {
106
+ const key = keys[index]
107
+ if (!key) return []
108
+ const found = metadata.get(key)
109
+ return [found ? { url, title: found.title, description: found.description, status: found.status, stateReason: found.stateReason } : { url, error: 'unavailable' }]
110
+ })
111
+ }
112
+
113
+ export function storedFixUrls(entries: { fix: string | null }[]): string[] {
114
+ return [...new Set(entries.flatMap(entry => entry.fix && parseGithubFixUrl(entry.fix) ? [entry.fix] : []))].toSorted()
115
+ }
@@ -0,0 +1,18 @@
1
+ import type { ServerResponse } from 'node:http'
2
+ import { Buffer } from 'node:buffer'
3
+ import { UPLOAD_CHUNK_BYTES } from './uploads.ts'
4
+
5
+ // Writing body chunks enables the function's streamed-response path. Data is
6
+ // already materialized by the authorization/filtering layer before we get here.
7
+ export function writeResponse(res: ServerResponse, body: string | Buffer): void {
8
+ const bytes = typeof body === 'string' ? Buffer.from(body) : body
9
+ if (bytes.length <= UPLOAD_CHUNK_BYTES) { res.end(body); return }
10
+ for (let offset = 0; offset < bytes.length; offset += 64 * 1024) res.write(bytes.subarray(offset, offset + 64 * 1024))
11
+ res.end()
12
+ }
13
+
14
+ export function sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {
15
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store', ...headers })
16
+ writeResponse(res, JSON.stringify(body))
17
+ }
18
+