@preventive/triage 1.0.0-alpha.2 → 1.0.0-alpha.21
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.
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +111 -0
- package/common/bundle-metadata.d.ts +10 -0
- package/common/bundle-metadata.js +177 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-sources.d.ts +3 -0
- package/common/bundle-sources.js +284 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +36 -0
- package/common/default-scan-models.ts +30 -0
- package/common/finding-id.js +47 -0
- package/common/github-pr.ts +56 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/permissions.ts +35 -0
- package/common/managed/report-content.ts +42 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +121 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/triage.ts +83 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +33 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/out/brotli-fallback.js +3 -3
- package/out/client-managed-import.js +81 -0
- package/out/client-managed.js +110 -0
- package/out/client-sync.js +16 -13
- package/out/graph.js +30 -4
- package/out/index.html +55 -8
- package/out/prism.js +2 -2
- package/out/stasis.svg +45 -0
- package/out/terminal.js +273 -39
- package/out/view.css +1 -1
- package/out/view.js +198 -62
- package/package.json +179 -55
- package/report/index.js +254 -0
- package/report/src/finding-id.js +80 -0
- package/report/src/finding.js +312 -0
- package/report/src/labels.js +33 -0
- package/report/src/md-structure.js +471 -0
- package/report/src/md-text.js +167 -0
- package/report/src/meta.js +76 -0
- package/report/src/parse-codex.js +147 -0
- package/report/src/parse-deepsec.js +197 -0
- package/report/src/parse-deepview-fields.js +375 -0
- package/report/src/parse-deepview-md.js +185 -0
- package/report/src/parse-md-id.js +137 -0
- package/report/src/parse-md.js +322 -0
- package/report/src/parse-piolium-id.js +79 -0
- package/report/src/parse-piolium-rows.js +131 -0
- package/report/src/parse-piolium-tokens.js +175 -0
- package/report/src/parse-piolium.js +400 -0
- package/report/src/security.js +63 -0
- package/report/src/utf8.js +21 -0
- package/report/src/write-md-finding.js +273 -0
- package/report/src/write-md.js +291 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-log.ts +34 -0
- package/server-common/vercel-blob.ts +110 -0
- package/server-e2e/app.ts +485 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +9 -8
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +54 -39
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +7 -10
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +96 -135
- package/{server → server-e2e}/http.ts +110 -12
- package/{server → server-e2e}/hub.ts +44 -14
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +11 -144
- package/{server → server-e2e}/objstore/blob-fs.ts +6 -8
- package/{server → server-e2e}/objstore/blob-vercel.ts +49 -141
- package/{server → server-e2e}/objstore/blob.ts +24 -9
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +19 -20
- package/{server → server-e2e}/objstore/init.ts +53 -27
- package/{server → server-e2e}/objstore/reaper.ts +31 -11
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +110 -93
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +10 -14
- package/{server → server-e2e}/objstore/store.ts +98 -118
- package/{server → server-e2e}/objstore/tokens.ts +9 -12
- package/{server → server-e2e}/peer.ts +7 -9
- package/{server → server-e2e}/pubsub.ts +29 -36
- package/{server → server-e2e}/sign.ts +12 -14
- package/{server → server-e2e}/sse-server.ts +105 -73
- package/{server → server-e2e}/sse-session.ts +30 -16
- package/{server → server-e2e}/static.ts +36 -29
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/util.ts +9 -0
- package/{server → server-e2e}/ws-server.ts +29 -23
- package/server-managed/activity.ts +231 -0
- package/server-managed/avatar-store.ts +51 -0
- package/server-managed/blob-store.ts +66 -0
- package/server-managed/blob-vercel.ts +125 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-cache.ts +185 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-store.ts +28 -0
- package/server-managed/bundle-summary-cache.ts +97 -0
- package/server-managed/bundle.ts +39 -0
- package/server-managed/cache-storage.ts +40 -0
- package/server-managed/cli.js +13 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +144 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1336 -0
- package/server-managed/db-neon.ts +171 -0
- package/server-managed/db-schema.ts +203 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +109 -0
- package/server-managed/github-app.ts +332 -0
- package/server-managed/github-metadata.ts +65 -0
- package/server-managed/github-oauth.ts +215 -0
- package/server-managed/github-pulls.ts +115 -0
- package/server-managed/http-response.ts +18 -0
- package/server-managed/http.ts +2043 -0
- package/server-managed/import-triage.ts +48 -0
- package/server-managed/index.ts +135 -0
- package/server-managed/public-workspace.ts +150 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +4 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +154 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/session.ts +78 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/sql-postgres.ts +30 -0
- package/server-managed/sql.ts +61 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage.ts +34 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +128 -0
- package/server-managed/team-reports.ts +156 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/uploads.ts +47 -0
- package/server-managed/workspace-shares.ts +150 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -481
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -327
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /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
|
+
|