@preventive/triage 1.0.0-alpha.23 → 1.0.0-alpha.26
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/common/server-info.ts +1 -0
- package/out/client-managed-import.js +41 -41
- package/out/client-managed.js +79 -63
- package/out/graph.js +6 -6
- package/out/prism.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +187 -186
- package/package.json +4 -1
- package/report/index.js +7 -1
- package/report/src/finding-id.js +1 -1
- package/report/src/finding.js +6 -4
- package/report/src/labels.js +3 -1
- package/report/src/parse-codex.js +3 -1
- package/report/src/parse-deepsec.js +3 -1
- package/report/src/parse-deepview-md.js +2 -1
- package/report/src/parse-md.js +3 -2
- package/report/src/parse-piolium-rows.js +7 -12
- package/report/src/parse-piolium-tokens.js +12 -2
- package/report/src/parse-piolium.js +7 -8
- package/report/src/severity.js +18 -0
- package/server-common/vercel-blob.ts +7 -18
- package/server-e2e/config.ts +4 -0
- package/server-e2e/objstore/blob-vercel.ts +4 -4
- package/server-managed/blob-vercel.ts +10 -8
- package/server-managed/config.ts +2 -1
- package/server-managed/db-methods.ts +41 -14
- package/server-managed/db-neon.ts +11 -4
- package/server-managed/http.ts +112 -90
- package/server-managed/import-triage.ts +4 -1
- package/server-managed/index.ts +30 -8
- package/server-managed/management.ts +86 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/public-workspace.ts +1 -11
- package/server-managed/report-query.ts +6 -0
- package/server-managed/repository-browser.ts +6 -1
- package/server-managed/sql.ts +5 -0
- package/server-managed/uploads.ts +28 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto'
|
|
2
2
|
import { teamCatalogRevision } from './team-catalog.ts'
|
|
3
|
-
import type
|
|
3
|
+
import { type Role, roleAtLeast } from '../common/managed/roles.ts'
|
|
4
4
|
import type { TeamUserPermissions } from '../common/managed/permissions.ts'
|
|
5
5
|
import type { TriageEntryPatch } from '../common/managed/triage.ts'
|
|
6
6
|
import { preferredSlug } from './slugs.ts'
|
|
@@ -11,6 +11,7 @@ import { type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts
|
|
|
11
11
|
import { type ImportTriageStore, importTriageMethods } from './import-triage.ts'
|
|
12
12
|
import type { ManagedSql } from './sql.ts'
|
|
13
13
|
import { type WorkspaceShareStore, workspaceShareMethods } from './workspace-shares.ts'
|
|
14
|
+
import { ManagedMutationError, type ManagementStore, managementMethods } from './management.ts'
|
|
14
15
|
|
|
15
16
|
// A managed user identity (the subset of GitHub's `GET /user` we keep). Input
|
|
16
17
|
// to the upsert; `githubUserId` is the provider lookup key, never exposed to
|
|
@@ -203,7 +204,9 @@ export interface TriageEventRow {
|
|
|
203
204
|
comment: string | null
|
|
204
205
|
fix: string | null
|
|
205
206
|
flagged: boolean | null
|
|
207
|
+
actorId: string | null
|
|
206
208
|
actorLogin: string | null
|
|
209
|
+
actorName: string | null
|
|
207
210
|
at: number
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -307,7 +310,7 @@ export interface UserTeam {
|
|
|
307
310
|
}
|
|
308
311
|
|
|
309
312
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
310
|
-
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, WorkspaceShareStore, ImportTriageStore {
|
|
313
|
+
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, WorkspaceShareStore, ImportTriageStore, ManagementStore {
|
|
311
314
|
// Upsert the identity; returns the user's opaque id (stable across logins).
|
|
312
315
|
// Initial-admin approval comes only from trusted login configuration. It
|
|
313
316
|
// promotes a matching No access identity only while it is the sole user.
|
|
@@ -349,9 +352,11 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
349
352
|
insertReport(report: ReportRecordInput, now: number): Promise<void>
|
|
350
353
|
// Content reuse is atomic with insertion, including across server instances.
|
|
351
354
|
// Existing copies are kept; the oldest matching row is the stable identity.
|
|
352
|
-
|
|
355
|
+
// HTTP uploads pass sessionId to recheck role/destination in that transaction.
|
|
356
|
+
insertOrReuseReport(report: ReportRecordInput, now: number, sessionId?: string): Promise<ReportRecord>
|
|
353
357
|
getReportByHash(sha256: string, analyzer: string | null): Promise<ReportRecord | null>
|
|
354
358
|
listReports(userId?: string): Promise<AdminReport[]>
|
|
359
|
+
listFindingCatalogReports(after: string, limit: number): Promise<Pick<ReportRecord, 'id' | 'byteSize'>[]>
|
|
355
360
|
getReport(id: string): Promise<ReportRecord | null>
|
|
356
361
|
getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string): Promise<TeamReportAccessSnapshot | null>
|
|
357
362
|
getReportAccessSnapshot(sessionId: string, now: number, ids: readonly string[]): Promise<ReportAccessSnapshot | null>
|
|
@@ -385,7 +390,7 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
385
390
|
// (referencing reports' bundle_id null out via the FK). linkReportsToBundle
|
|
386
391
|
// attaches a freshly-stored bundle to the (still-unlinked) reports that
|
|
387
392
|
// declared its integrity.
|
|
388
|
-
insertBundle(bundle: BundleInput, now: number): Promise<void>
|
|
393
|
+
insertBundle(bundle: BundleInput, now: number, sessionId?: string): Promise<void>
|
|
389
394
|
getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
|
|
390
395
|
getBundle(id: string): Promise<ManagedBundle | null>
|
|
391
396
|
listBundles(userId?: string): Promise<AdminBundle[]>
|
|
@@ -569,6 +574,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
569
574
|
WHERE tr.repo_id = r.repo_id AND tu.user_id = ? AND ${REPORT_IN_TEAM_PATH_SQL}))
|
|
570
575
|
ORDER BY r.uploaded_at DESC, r.filename ASC`,
|
|
571
576
|
),
|
|
577
|
+
findingCatalogReportsStmt: db.prepare(`SELECT id, byte_size AS byteSize FROM managed_report WHERE id > ? ORDER BY id LIMIT ?`),
|
|
572
578
|
selectReportStmt: db.prepare(
|
|
573
579
|
`SELECT id, slug, filename, content_type AS contentType, byte_size AS byteSize,
|
|
574
580
|
sha256, uploaded_by AS uploadedBy, uploaded_at AS uploadedAt,
|
|
@@ -644,7 +650,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
644
650
|
selectTriageHistoryStmt: db.prepare(
|
|
645
651
|
`SELECT e.seq AS seq, e.finding_id AS findingId, e.batch_id AS batchId, e.color AS color, e.triage AS triage,
|
|
646
652
|
e.comment AS comment, e.fix AS fix, e.flagged AS flagged,
|
|
647
|
-
COALESCE(u.login, e.actor_login) AS actorLogin, e.at AS at
|
|
653
|
+
u.id AS actorId, COALESCE(u.login, e.actor_login) AS actorLogin, u.name AS actorName, e.at AS at
|
|
648
654
|
FROM managed_finding_triage_event e
|
|
649
655
|
LEFT JOIN managed_user u ON u.id = e.actor_id
|
|
650
656
|
WHERE e.finding_id = ?
|
|
@@ -904,8 +910,20 @@ type ReportRow = {
|
|
|
904
910
|
bundleId: string | null
|
|
905
911
|
}
|
|
906
912
|
|
|
907
|
-
//
|
|
908
|
-
//
|
|
913
|
+
// Byte storage may outlast a logout or grant change. HTTP uploads reauthorize
|
|
914
|
+
// inside the metadata writer transaction after their bytes have been stored.
|
|
915
|
+
async function authorizeUpload(stmts: ReturnType<typeof prepareStatements>, sessionId: string | undefined,
|
|
916
|
+
item: { uploadedBy: string | null; repoId: number | null; repoDirectory?: string }) {
|
|
917
|
+
if (sessionId === undefined) return // Trusted imports and fixtures use the raw store API.
|
|
918
|
+
const user = await stmts.selectSessionStmt.get(sessionId, Date.now()) as SessionRow | undefined
|
|
919
|
+
if (!user) throw new ManagedMutationError(401, 'unauthenticated')
|
|
920
|
+
if (user.uid !== item.uploadedBy || !roleAtLeast(user.role, 'manage')) throw new ManagedMutationError(403, 'forbidden')
|
|
921
|
+
if (item.repoId === null) return
|
|
922
|
+
if (!(await stmts.selectReposStmt.all() as RepoRow[]).some(repo => repo.repoId === item.repoId)) throw new ManagedMutationError(400, 'repo-not-selected')
|
|
923
|
+
if (user.role !== 'admin' && !await stmts.selectRepoPathReadableStmt.get(item.repoDirectory ?? '', item.repoId, user.uid)) throw new ManagedMutationError(403, 'repo-forbidden')
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// The report slice of ManagedDb closes over its prepared statements.
|
|
909
927
|
function reportMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
910
928
|
const { insertReportStmt, selectReportsStmt, selectReportStmt, deleteReportStmt, setReportRepoStmt, setReportVisibleStmt } = stmts
|
|
911
929
|
async function getReport(id: string): Promise<ReportRecord | null> {
|
|
@@ -929,7 +947,8 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
929
947
|
insertReport,
|
|
930
948
|
getReport,
|
|
931
949
|
getReportByHash,
|
|
932
|
-
async insertOrReuseReport(report: ReportRecordInput, now: number): Promise<ReportRecord> {
|
|
950
|
+
async insertOrReuseReport(report: ReportRecordInput, now: number, sessionId?: string): Promise<ReportRecord> {
|
|
951
|
+
await authorizeUpload(stmts, sessionId, report)
|
|
933
952
|
// The driver holds its writer lock for this whole operation. Checking
|
|
934
953
|
// again here closes the race between the HTTP lookup and blob upload.
|
|
935
954
|
const existing = await getReportByHash(report.sha256, report.analyzer ?? null)
|
|
@@ -948,6 +967,9 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
948
967
|
uploadedAt: r.uploadedAt,
|
|
949
968
|
}))
|
|
950
969
|
},
|
|
970
|
+
async listFindingCatalogReports(after: string, limit: number): Promise<Pick<ReportRecord, 'id' | 'byteSize'>[]> {
|
|
971
|
+
return await stmts.findingCatalogReportsStmt.all(after, limit) as Pick<ReportRecord, 'id' | 'byteSize'>[]
|
|
972
|
+
},
|
|
951
973
|
async getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string): Promise<TeamReportAccessSnapshot | null> {
|
|
952
974
|
const session = await stmts.selectSessionStmt.get(sessionId, now) as SessionRow | undefined
|
|
953
975
|
if (!session) return null
|
|
@@ -1004,7 +1026,9 @@ type TriageDbRow = {
|
|
|
1004
1026
|
updatedByLogin: string | null; updatedAt: number
|
|
1005
1027
|
}
|
|
1006
1028
|
type TriageStateDbRow = { color: string | null; triage: string | null; comment: string | null; fix: string | null; flagged: number | null }
|
|
1007
|
-
type TriageEventDbRow = TriageStateDbRow & {
|
|
1029
|
+
type TriageEventDbRow = TriageStateDbRow & {
|
|
1030
|
+
seq: number; findingId: string; batchId: string; actorId: string | null; actorLogin: string | null; actorName: string | null; at: number
|
|
1031
|
+
}
|
|
1008
1032
|
|
|
1009
1033
|
// The per-finding triage slice of ManagedDb. Closes over its prepared
|
|
1010
1034
|
// statements (and the handle, for the batch write's transaction). A
|
|
@@ -1059,7 +1083,7 @@ function triageMethods( stmts: ReturnType<typeof prepareStatements>, historyLimi
|
|
|
1059
1083
|
seq: r.seq, findingId: r.findingId, batchId: r.batchId,
|
|
1060
1084
|
color: r.color, triage: r.triage, comment: r.comment, fix: r.fix,
|
|
1061
1085
|
flagged: r.flagged == null ? null : r.flagged === 1,
|
|
1062
|
-
actorLogin: r.actorLogin, at: r.at,
|
|
1086
|
+
actorId: r.actorId, actorLogin: r.actorLogin, actorName: r.actorName, at: r.at,
|
|
1063
1087
|
}))
|
|
1064
1088
|
},
|
|
1065
1089
|
async listTriage(findingIds: readonly string[]): Promise<TriageRow[]> {
|
|
@@ -1108,7 +1132,8 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
1108
1132
|
selectBundlesStmt, deleteBundleStmt, setBundleRepoStmt, linkReportsToBundleStmt, selectBundleReadableStmt, selectRepoReadableStmt, selectRepoPathReadableStmt,
|
|
1109
1133
|
} = stmts
|
|
1110
1134
|
return {
|
|
1111
|
-
async insertBundle(bundle: BundleInput, now: number): Promise<void> {
|
|
1135
|
+
async insertBundle(bundle: BundleInput, now: number, sessionId?: string): Promise<void> {
|
|
1136
|
+
await authorizeUpload(stmts, sessionId, bundle)
|
|
1112
1137
|
await insertBundleStmt.run(
|
|
1113
1138
|
bundle.id, preferredSlug(bundle.id), bundle.integrity, bundle.filename, bundle.kind,
|
|
1114
1139
|
bundle.byteSize, bundle.uploadedBy, bundle.uploadedByLogin ?? null, bundle.repoId, bundle.repoId == null ? '' : bundle.repoDirectory ?? '', now,
|
|
@@ -1291,13 +1316,14 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1291
1316
|
const comments = commentMethods(db)
|
|
1292
1317
|
const activity = activityMethods(db)
|
|
1293
1318
|
const stmts = prepareStatements(db)
|
|
1319
|
+
const reports = reportMethods(stmts)
|
|
1294
1320
|
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0)
|
|
1295
1321
|
const {
|
|
1296
1322
|
upsertUserStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectSessionStmt, selectUsersStmt,
|
|
1297
1323
|
touchUserSeenStmt, updateRoleStmt, updateTokensStmt, selectTokensStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1298
1324
|
} = stmts
|
|
1299
1325
|
|
|
1300
|
-
|
|
1326
|
+
const methods: Omit<ManagedDb, keyof ManagementStore> = {
|
|
1301
1327
|
async upsertUser(user, now, initialAdminGithubId = null) {
|
|
1302
1328
|
// New row → a fresh id; ON CONFLICT(github_user_id) keeps an existing
|
|
1303
1329
|
// user's id (DO UPDATE leaves it untouched), so re-read to return it.
|
|
@@ -1355,13 +1381,14 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1355
1381
|
...activity,
|
|
1356
1382
|
...comments,
|
|
1357
1383
|
...workspaceShareMethods(db),
|
|
1358
|
-
...
|
|
1384
|
+
...reports,
|
|
1359
1385
|
...triage,
|
|
1360
|
-
...importTriageMethods({ ...triage, ...comments }),
|
|
1386
|
+
...importTriageMethods({ getReport: reports.getReport, ...triage, ...comments }),
|
|
1361
1387
|
...bundleMethods(stmts),
|
|
1362
1388
|
...teamMethods(stmts),
|
|
1363
1389
|
async close() {
|
|
1364
1390
|
await db.close()
|
|
1365
1391
|
},
|
|
1366
1392
|
}
|
|
1393
|
+
return { ...methods, ...managementMethods(methods) }
|
|
1367
1394
|
}
|
|
@@ -6,7 +6,7 @@ import { GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-met
|
|
|
6
6
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
7
7
|
import { COMMENT_SCHEMA } from './comments.ts'
|
|
8
8
|
import { ACTIVITY_SCHEMA } from './activity.ts'
|
|
9
|
-
import { type ManagedSqlDriver, scopeManagedMethods } from './sql.ts'
|
|
9
|
+
import { ManagedCommitError, type ManagedSqlDriver, scopeManagedMethods } from './sql.ts'
|
|
10
10
|
import { postgresSchema, postgresSql } from './sql-postgres.ts'
|
|
11
11
|
import { managedTableRenames } from './db-table-names.ts'
|
|
12
12
|
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
@@ -144,16 +144,23 @@ export async function openPostgresManagedDb(connect: PgConnect, options: Managed
|
|
|
144
144
|
async scope(write, work) {
|
|
145
145
|
if (closed) throw new Error('Managed database is closed')
|
|
146
146
|
const db = await connect()
|
|
147
|
+
let commitAttempted = false
|
|
148
|
+
let result
|
|
147
149
|
try {
|
|
148
150
|
await db.query(write ? 'BEGIN' : 'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY')
|
|
149
151
|
if (write) await db.query(LOCK)
|
|
150
|
-
|
|
152
|
+
result = await context.run(db, work)
|
|
153
|
+
commitAttempted = true
|
|
151
154
|
await db.query('COMMIT')
|
|
152
|
-
return result
|
|
153
155
|
} catch (err) {
|
|
154
156
|
await db.query('ROLLBACK').catch(() => {})
|
|
157
|
+
await db.release().catch(() => {})
|
|
158
|
+
if (write && commitAttempted) throw new ManagedCommitError(err)
|
|
155
159
|
throw err
|
|
156
|
-
}
|
|
160
|
+
}
|
|
161
|
+
try { await db.release() }
|
|
162
|
+
catch (err) { if (write) throw new ManagedCommitError(err); throw err }
|
|
163
|
+
return result
|
|
157
164
|
},
|
|
158
165
|
close() { closed = true },
|
|
159
166
|
}
|