@preventive/triage 1.0.0-alpha.45 → 1.0.0-alpha.47
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/client/linked-findings.js +3 -0
- package/common/bundle-metadata.d.ts +1 -0
- package/common/bundle-metadata.js +1 -1
- package/common/bundle-sources.js +5 -1
- package/common/line-diff.js +5 -4
- package/common/managed/agent-tasks.ts +5 -3
- package/common/managed/github-agents.ts +28 -10
- package/out/activity.js +4 -0
- package/out/{chunk-NPW52BM2.js → chunk-4HI2MSAV.js} +13 -13
- package/out/chunk-5AHIQFFG.js +36 -0
- package/out/chunk-6ANSHRFQ.js +6 -0
- package/out/chunk-6WF3KNTL.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/{chunk-HMAJDVXN.js → chunk-FH24VHSJ.js} +1 -1
- package/out/chunk-NOZJULWS.js +52 -0
- package/out/{chunk-OKRLLE5I.js → chunk-PWFUOLZK.js} +1 -1
- package/out/chunk-S6U2J62U.js +232 -0
- package/out/chunk-SPEFPML6.js +2 -0
- package/out/chunk-UENXT2HK.js +3 -0
- package/out/chunk-VL43BTSO.js +1 -0
- package/out/{chunk-3PS4KWD2.js → chunk-WCPJDAYA.js} +1 -1
- package/out/chunk-ZCS4UYHI.js +1 -0
- package/out/chunk-ZGNMDGSB.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +15 -15
- package/out/client-sync.js +2 -2
- package/out/compare.js +12 -12
- package/out/dialogs.js +28 -28
- package/out/graph.js +11 -11
- package/out/index.html +11 -11
- package/out/scan.js +2 -2
- package/out/terminal.js +6 -6
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +4 -1
- package/risk/actions/findings.js +14 -9
- package/risk/actions/scripts/shell.js +5 -1
- package/risk/index.d.ts +9 -4
- package/risk/npm/assess.js +15 -4
- package/risk/npm/registry.js +6 -1
- package/server-e2e/app.ts +5 -4
- package/server-e2e/http.ts +5 -2
- package/server-managed/activity.ts +24 -11
- package/server-managed/app-agents.ts +10 -21
- package/server-managed/bundle-build-worker.js +7 -3
- package/server-managed/bundle-build.ts +5 -3
- package/server-managed/bundle-cache.ts +16 -4
- package/server-managed/bundle.ts +3 -4
- package/server-managed/combined.ts +3 -2
- package/server-managed/commit-coauthors.ts +5 -9
- package/server-managed/commit-lockfiles.ts +21 -11
- package/server-managed/config.ts +6 -0
- package/server-managed/db-methods.ts +321 -156
- package/server-managed/db-neon.ts +71 -24
- package/server-managed/db-schema.ts +4 -0
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-reviews.ts +13 -3
- package/server-managed/github-app.ts +12 -6
- package/server-managed/github-issue-links.ts +11 -5
- package/server-managed/github-pulls.ts +2 -2
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/http-response.ts +10 -2
- package/server-managed/http.ts +269 -143
- package/server-managed/index.ts +8 -7
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/link-reports.ts +5 -1
- package/server-managed/npm-delta.ts +11 -6
- package/server-managed/npm-kept.ts +51 -7
- package/server-managed/npm-loads.ts +117 -35
- package/server-managed/npm-packages.ts +107 -14
- package/server-managed/npm-risk.ts +68 -18
- package/server-managed/pretty-print.ts +112 -35
- package/server-managed/pull-dependencies.ts +29 -9
- package/server-managed/pull-dependency-reviews.ts +8 -8
- package/server-managed/queue.ts +16 -0
- package/server-managed/rename-diff.ts +22 -13
- package/server-managed/report-sources.ts +1 -1
- package/server-managed/report-store.ts +50 -12
- package/server-managed/repository-actions.ts +15 -6
- package/server-managed/repository-agents.ts +1 -1
- package/server-managed/repository-browser.ts +123 -33
- package/server-managed/repository-code.ts +28 -11
- package/server-managed/repository-dependency-reviews.ts +41 -19
- package/server-managed/repository-history.ts +24 -8
- package/server-managed/repository-pulse.ts +9 -2
- package/server-managed/repository-risk-audit.ts +13 -0
- package/server-managed/repository-risk-read.ts +11 -6
- package/server-managed/repository-risk.ts +27 -20
- package/server-managed/session.ts +18 -4
- package/server-managed/solidity-entry-points.ts +5 -3
- package/server-managed/sql-postgres.ts +3 -0
- package/server-managed/sql.ts +6 -2
- package/server-managed/storage-db.ts +11 -3
- package/server-managed/storage-encryption.ts +7 -4
- package/server-managed/storage.ts +8 -4
- package/server-managed/team-feed.ts +3 -1
- package/server-managed/team-reports.ts +30 -13
- package/server-managed/uploads.ts +29 -9
- package/server-managed/upstream-cache.ts +12 -0
- package/server.ts +14 -5
- package/out/chunk-5DHL5NG3.js +0 -2
- package/out/chunk-6HNXNIQY.js +0 -52
- package/out/chunk-AFLZH7WG.js +0 -1
- package/out/chunk-NV7ES4EW.js +0 -1
- package/out/chunk-PTGDYLNS.js +0 -36
- package/out/chunk-Q6MVYGYV.js +0 -3
- package/out/chunk-QRD6GIG5.js +0 -229
- package/out/chunk-WI7Z5A2T.js +0 -6
- package/out/chunk-WQ74ZBZF.js +0 -7
- package/out/chunk-XVBEOGOS.js +0 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LinkReportStore, linkReportMethods, linkRevision } from './link-reports.ts'
|
|
1
|
+
import { type LinkReportStore, linkReportMethods, linkRevision, linkRevisionOf } from './link-reports.ts'
|
|
2
2
|
import { type RepositoryAliasStore, repositoryAliasMethods } from './repository-aliases.ts'
|
|
3
3
|
import { type DependencyReviewStore, dependencyReviewMethods } from './dependency-reviews.ts'
|
|
4
4
|
import { type RepositoryDependencyReviewStore, repositoryDependencyReviewMethods } from './repository-dependency-reviews.ts'
|
|
@@ -13,7 +13,7 @@ import { preferredSlug } from './slugs.ts'
|
|
|
13
13
|
import { type CommentStore, commentMethods } from './comments.ts'
|
|
14
14
|
import { type ActivityStore, activityMethods } from './activity.ts'
|
|
15
15
|
import { type GithubCommitStore, type GithubMetadataStore, githubCommitMethods, githubMetadataMethods } from './github-metadata.ts'
|
|
16
|
-
import { type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
16
|
+
import { type ManagedIssue, type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
17
17
|
import type { IssueFixUpdate } from './github-issue-links.ts'
|
|
18
18
|
import { type BundleBuildLeaseStore, bundleBuildLeaseMethods } from './bundle-build-leases.ts'
|
|
19
19
|
import { type UpstreamCacheStore, upstreamCacheMethods } from './upstream-cache.ts'
|
|
@@ -23,7 +23,7 @@ import { type WorkspaceShareStore, workspaceShareMethods } from './workspace-sha
|
|
|
23
23
|
import { ManagedMutationError, type ManagementStore, managementMethods } from './management.ts'
|
|
24
24
|
import { type ManagementCatalogStore, managementCatalogMethods } from './management-catalog.ts'
|
|
25
25
|
import { type StorageKey } from '../server-common/storage-crypto.ts'
|
|
26
|
-
import { type StorageDb, type StoredTokens, decodeStorageTokens, encodeStorageTokens, storageMethods, storageState, validateStorageUpload } from './storage-db.ts'
|
|
26
|
+
import { type StorageDb, type StoredTokens, decodeStorageTokens, encodeStorageTokens, storageMethods, storageState, storageStateColumns, storageStateOf, validateStorageUpload } from './storage-db.ts'
|
|
27
27
|
|
|
28
28
|
// A managed user identity (the subset of GitHub's `GET /user` we keep). Input
|
|
29
29
|
// to the upsert; `githubUserId` is the provider lookup key, never exposed to
|
|
@@ -392,12 +392,19 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
392
392
|
// A view session opened by `viewerSessionId`, while that session is current
|
|
393
393
|
// and its user is still an admin. Presence is the admin's, not the viewed user's.
|
|
394
394
|
viewSessionWithUser(id: string, viewerSessionId: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
395
|
+
// The two above as they read, recording no presence: for a read again in a
|
|
396
|
+
// request whose first read recorded it.
|
|
397
|
+
getSessionWithUser(id: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
398
|
+
getViewSessionWithUser(id: string, viewerSessionId: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
395
399
|
// Store `candidate` unless the session already has an upload key; returns
|
|
396
400
|
// the stored key, or null when the session no longer exists.
|
|
397
401
|
ensureSessionUploadKey(id: string, candidate: string): Promise<string | null>
|
|
398
402
|
deleteSession(id: string): Promise<void>
|
|
399
403
|
deleteExpiredSessions(now: number): Promise<number>
|
|
400
404
|
listUsers(): Promise<AdminUser[]>
|
|
405
|
+
// One user by id, as their sessions read them, or null; for an action naming
|
|
406
|
+
// one, where listUsers would work out every user's latest activity.
|
|
407
|
+
getUser(id: string): Promise<StoredUser | null>
|
|
401
408
|
// Set a user's role; resolves true iff a matching user row was updated.
|
|
402
409
|
setUserRole(id: string, role: Role): Promise<boolean>
|
|
403
410
|
// Persist / read a user's GitHub token (for on-demand repo listing).
|
|
@@ -413,6 +420,8 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
413
420
|
deselectRepo(repoId: number): Promise<boolean>
|
|
414
421
|
listSelectedRepos(): Promise<SelectedRepo[]>
|
|
415
422
|
listAllRepos(): Promise<ManagedRepo[]>
|
|
423
|
+
// A selected repository's full name, active or not; null for none.
|
|
424
|
+
getRepoFullName(repoId: number): Promise<string | null>
|
|
416
425
|
deactivateRepo(repoId: number): Promise<boolean>
|
|
417
426
|
reactivateRepo(repoId: number): Promise<boolean>
|
|
418
427
|
deleteRepo(repoId: number): Promise<boolean>
|
|
@@ -459,6 +468,8 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
459
468
|
// stamp nor the trail. Every change also appends to managed_finding_triage_event;
|
|
460
469
|
// listTriageHistory walks one finding's trail, newest first.
|
|
461
470
|
listTriage(findingIds: readonly string[]): Promise<TriageRow[]>
|
|
471
|
+
// The distinct Fix links of those, without the rest of their entries.
|
|
472
|
+
listTriageFixes(findingIds: readonly string[]): Promise<string[]>
|
|
462
473
|
getAnnotations(findingIds: readonly string[]): Promise<{ triage: TriageRow[]; comments: Awaited<ReturnType<CommentStore['listComments']>>; issues: Awaited<ReturnType<ManagedIssueStore['listManagedIssues']>> }>
|
|
463
474
|
applyManagedIssueFixes(sessionId: string, snapshot: TeamReportAccessSnapshot, updates: IssueFixUpdate[]): Promise<boolean>
|
|
464
475
|
// A content-free fingerprint of current annotations, read in one snapshot.
|
|
@@ -527,6 +538,9 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
527
538
|
// Whether `userId` may read `reportId`: true iff the report's repo belongs to
|
|
528
539
|
// a non-hidden team the user is a member of. Backs the team-scoped report view endpoint.
|
|
529
540
|
userCanReadReport(userId: string, reportId: string): Promise<boolean>
|
|
541
|
+
// What viewing `reportId` turns on, in one read: who uploaded it, whether it
|
|
542
|
+
// is published, and whether userCanReadReport; null where there is none.
|
|
543
|
+
getReportView(userId: string, reportId: string): Promise<{ uploadedBy: string | null; visible: boolean; readable: boolean } | null>
|
|
530
544
|
// The viewer's effective visibility permissions for a report, OR'd across the
|
|
531
545
|
// teams (holding the report's repo) the user belongs to — drives the content
|
|
532
546
|
// filter (a viewer without a permission has those findings stripped).
|
|
@@ -543,9 +557,22 @@ type SessionRow = {
|
|
|
543
557
|
uid: string; login: string; name: string | null; avatar: string | null; role: Role
|
|
544
558
|
}
|
|
545
559
|
type ViewSessionRow = SessionRow & { viewerId: string; viewerLogin: string; viewerName: string | null }
|
|
560
|
+
const ownSession = (row: SessionRow) => ({
|
|
561
|
+
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey },
|
|
562
|
+
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
563
|
+
})
|
|
564
|
+
const viewSession = (row: ViewSessionRow) => ({
|
|
565
|
+
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey,
|
|
566
|
+
viewer: { id: row.viewerId, login: row.viewerLogin, name: row.viewerName } },
|
|
567
|
+
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
568
|
+
})
|
|
546
569
|
|
|
547
570
|
type UserRow = { id: string; login: string; name: string | null; role: Role; created: number; lastSeen: number | null; lastActivity: number | null }
|
|
548
571
|
|
|
572
|
+
// Rows a statement gathered into one JSON array value (json_group_array): its
|
|
573
|
+
// text on SQLite, parsed already on Postgres, where none gather as NULL.
|
|
574
|
+
const jsonRows = <T>(value: unknown): T[] => value == null ? [] : (typeof value === 'string' ? JSON.parse(value) : value) as T[]
|
|
575
|
+
|
|
549
576
|
// Shared by listing, access checks, and permission aggregation. A team sees
|
|
550
577
|
// reports rooted at its path or below it. Literal substring comparison keeps
|
|
551
578
|
// separators, case, and SQL wildcard characters significant.
|
|
@@ -555,6 +582,46 @@ const REPORT_IN_TEAM_PATH_SQL = `(tr.path IS NULL OR tr.path = ''
|
|
|
555
582
|
|
|
556
583
|
const BUNDLE_IN_TEAM_PATH_SQL = REPORT_IN_TEAM_PATH_SQL.replaceAll('r.repo_directory', 'b.repo_directory')
|
|
557
584
|
|
|
585
|
+
// What a user's team catalog reads (listTeamsForUser), for `user`: a
|
|
586
|
+
// placeholder, or a column of a statement reading it with more. Their teams,
|
|
587
|
+
// `dependencyReview` where the user reviews dependencies in the team
|
|
588
|
+
// (dependency-reviews.ts reviewsIn): one that reviews them, and a member
|
|
589
|
+
// who manages, or triages and may see its dependencies; an admin in any.
|
|
590
|
+
const USER_TEAMS_SQL = (user: string) => `SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
591
|
+
CASE WHEN t.dependency_review = 1 AND (u.role IN ('admin', 'manage') OR (u.role = 'triage' AND tu.view_dependencies = 1))
|
|
592
|
+
THEN 1 ELSE 0 END AS dependencyReview,
|
|
593
|
+
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
594
|
+
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id JOIN managed_user u ON u.id = tu.user_id
|
|
595
|
+
WHERE tu.user_id = ${user} AND t.hidden = 0`
|
|
596
|
+
// Their teams' repository scopes.
|
|
597
|
+
const USER_TEAM_SCOPES_SQL = (user: string) => `SELECT tr.team_id AS teamId, tr.repo_id AS repoId, tr.path AS path, sr.full_name AS fullName
|
|
598
|
+
FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
599
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
600
|
+
JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
601
|
+
WHERE tu.user_id = ${user}`
|
|
602
|
+
// Reports attached to the repos of their teams, tagged by team (a report
|
|
603
|
+
// shows under every team whose repo it's attached to).
|
|
604
|
+
const USER_TEAM_REPORTS_SQL = (user: string) => `SELECT DISTINCT tr.team_id AS teamId, r.id AS id, r.slug AS slug, r.filename AS filename, r.analyzer, r.visible, r.uploaded_at,
|
|
605
|
+
r.sha256 AS sha256, r.bundle_id AS bundleId, r.repo_id AS repoId, r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
606
|
+
tu.view_dependencies AS dependencies, tu.view_security AS security,
|
|
607
|
+
(SELECT role FROM managed_user WHERE id = tu.user_id) AS role
|
|
608
|
+
FROM managed_team_user tu
|
|
609
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
610
|
+
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
611
|
+
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
612
|
+
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
613
|
+
WHERE tu.user_id = ${user} AND (r.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))`
|
|
614
|
+
// Bundles, scoped through the same team -> repository links as reports.
|
|
615
|
+
// Hidden bundles remain available to managers within their team scopes.
|
|
616
|
+
const USER_TEAM_BUNDLES_SQL = (user: string) => `SELECT DISTINCT tr.team_id AS teamId, b.id AS id, b.slug AS slug, b.integrity AS integrity,
|
|
617
|
+
b.filename AS filename, b.kind, b.visible, b.byte_size AS byteSize, b.repo_id AS repoId, b.repo_directory AS repoDirectory, sr.full_name AS repoFullName, b.uploaded_at, b.provenance
|
|
618
|
+
FROM managed_team_user tu
|
|
619
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
620
|
+
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
621
|
+
JOIN managed_bundle b ON b.repo_id = tr.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
622
|
+
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
623
|
+
WHERE tu.user_id = ${user} AND (b.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))`
|
|
624
|
+
|
|
558
625
|
// Whether a repeated upload/build renames the stored row. Uploads never rename
|
|
559
626
|
// a built row, or one whose provenance is unknown; builds rename either. The
|
|
560
627
|
// kind must stay readable from its blob store (sourcemaps are stored apart):
|
|
@@ -563,6 +630,49 @@ const BUNDLE_RENAMED_SQL = `(:rename = 1 AND (provenance = 'upload' OR (:provena
|
|
|
563
630
|
AND (COALESCE(kind, '') = COALESCE(:kind, '') OR (kind IS NULL AND :kind = 'stasis'))
|
|
564
631
|
AND (filename <> :filename OR kind IS DISTINCT FROM :kind))`
|
|
565
632
|
|
|
633
|
+
// The rows a user's team catalog is built of (USER_TEAMS_SQL and the rest).
|
|
634
|
+
type UserTeamRows = {
|
|
635
|
+
teams: { id: string; slug: string; name: string; dependencyReview: number; dependencies: number; security: number }[]
|
|
636
|
+
scopes: { teamId: string; repoId: number; path: string; fullName: string }[]
|
|
637
|
+
reports: {
|
|
638
|
+
teamId: string; id: string; slug: string; filename: string; analyzer: string | null; sha256: string; bundleId: string | null; repoId: number
|
|
639
|
+
repoDirectory: string; repoFullName: string | null; visible: number; dependencies: number; security: number; role: string
|
|
640
|
+
}[]
|
|
641
|
+
bundles: (Omit<UserTeamBundle, 'visible'> & { teamId: string; visible: number })[]
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// A user's team catalog from its rows, `links` the link reports' revision.
|
|
645
|
+
function userTeams(teams: UserTeamRows['teams'], scopes: UserTeamRows['scopes'], links: string, reports: UserTeamRows['reports'], bundles: UserTeamRows['bundles']): UserTeam[] {
|
|
646
|
+
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
647
|
+
for (const r of reports) {
|
|
648
|
+
const list = reportsByTeam.get(r.teamId) ?? []
|
|
649
|
+
const cacheKey = createHash('sha256').update(JSON.stringify([
|
|
650
|
+
r.sha256, r.bundleId, r.filename, r.repoId, r.repoDirectory, r.repoFullName, r.dependencies, r.security, r.role, links,
|
|
651
|
+
])).digest('base64url')
|
|
652
|
+
list.push({ id: r.id, slug: r.slug, filename: r.filename, visible: r.visible === 1, analyzer: r.analyzer, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory, cacheKey })
|
|
653
|
+
reportsByTeam.set(r.teamId, list)
|
|
654
|
+
}
|
|
655
|
+
const bundlesByTeam = new Map<string, UserTeamBundle[]>()
|
|
656
|
+
for (const b of bundles) {
|
|
657
|
+
const list = bundlesByTeam.get(b.teamId) ?? []
|
|
658
|
+
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename, visible: b.visible === 1,
|
|
659
|
+
kind: b.kind, byteSize: b.byteSize, repoId: b.repoId, repoDirectory: b.repoDirectory, repoFullName: b.repoFullName, provenance: b.provenance })
|
|
660
|
+
bundlesByTeam.set(b.teamId, list)
|
|
661
|
+
}
|
|
662
|
+
return teams.map((t) => ({
|
|
663
|
+
id: t.id, slug: t.slug, name: t.name,
|
|
664
|
+
...(t.dependencyReview === 1 ? { dependencyReview: true } : {}),
|
|
665
|
+
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
666
|
+
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
667
|
+
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
668
|
+
])).digest('base64url'),
|
|
669
|
+
repos: scopes.filter(scope => scope.teamId === t.id).map(scope => ({ repoId: scope.repoId, fullName: scope.fullName, path: scope.path || null }))
|
|
670
|
+
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
671
|
+
reports: reportsByTeam.get(t.id) ?? [],
|
|
672
|
+
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
673
|
+
}))
|
|
674
|
+
}
|
|
675
|
+
|
|
566
676
|
// Prepare every statement the store uses, returned as a bag the factory
|
|
567
677
|
// destructures — keeps openSqliteManagedDb itself small (one place per query).
|
|
568
678
|
function prepareStatements(db: ManagedSql) {
|
|
@@ -601,6 +711,15 @@ function prepareStatements(db: ManagedSql) {
|
|
|
601
711
|
JOIN managed_user u ON u.id = s.user_id
|
|
602
712
|
WHERE s.id = ? AND s.expires_at > ? AND s.viewer_session IS NULL`,
|
|
603
713
|
),
|
|
714
|
+
// selectOwnSessionStmt with touchUserSeenStmt in one statement: the user of
|
|
715
|
+
// a live own session seen, and returned with it.
|
|
716
|
+
touchOwnSessionStmt: db.prepare(
|
|
717
|
+
`UPDATE managed_user SET last_seen_at = CASE WHEN last_seen_at IS NULL OR last_seen_at < ? THEN ? ELSE last_seen_at END
|
|
718
|
+
WHERE id = (SELECT user_id FROM managed_session WHERE id = ? AND expires_at > ? AND viewer_session IS NULL)
|
|
719
|
+
RETURNING (SELECT id FROM managed_session WHERE id = ?) AS id, (SELECT csrf_token FROM managed_session WHERE id = ?) AS csrf,
|
|
720
|
+
(SELECT expires_at FROM managed_session WHERE id = ?) AS exp, (SELECT upload_key FROM managed_session WHERE id = ?) AS uploadKey,
|
|
721
|
+
id AS uid, login AS login, name AS name, avatar_url AS avatar, role AS role`,
|
|
722
|
+
),
|
|
604
723
|
selectViewSessionStmt: db.prepare(
|
|
605
724
|
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
606
725
|
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role,
|
|
@@ -634,6 +753,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
634
753
|
) AS events) AS lastActivity
|
|
635
754
|
FROM managed_user u ORDER BY u.created_at ASC, u.login ASC`,
|
|
636
755
|
),
|
|
756
|
+
selectUserStmt: db.prepare(`SELECT id, login, name, avatar_url AS avatarUrl, role FROM managed_user WHERE id = ?`),
|
|
637
757
|
touchUserSeenStmt: db.prepare(
|
|
638
758
|
`UPDATE managed_user SET last_seen_at = ? WHERE id = ? AND (last_seen_at IS NULL OR last_seen_at < ?)`,
|
|
639
759
|
),
|
|
@@ -643,9 +763,14 @@ function prepareStatements(db: ManagedSql) {
|
|
|
643
763
|
SET gh_access_token = ?, gh_refresh_token = ?, gh_token_expires_at = ?, gh_tokens_encrypted = ?, updated_at = ?
|
|
644
764
|
WHERE id = ?`,
|
|
645
765
|
),
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
766
|
+
// A user's GitHub tokens with storage-db.ts storageState's read, in one
|
|
767
|
+
// statement.
|
|
768
|
+
selectTokensStateStmt: db.prepare(
|
|
769
|
+
`SELECT u.id AS userId, u.gh_access_token AS access, u.gh_refresh_token AS refresh, u.gh_token_expires_at AS exp, u.gh_tokens_encrypted AS encrypted,
|
|
770
|
+
e.id AS stateId, ${storageStateColumns('e')}
|
|
771
|
+
FROM (SELECT 1 AS one) AS o
|
|
772
|
+
LEFT JOIN managed_user u ON u.id = ?
|
|
773
|
+
LEFT JOIN managed_storage_encryption e ON e.id = 1`,
|
|
649
774
|
),
|
|
650
775
|
// Concurrent first requests keep whichever key was stored first.
|
|
651
776
|
ensureUploadKeyStmt: db.prepare(
|
|
@@ -690,6 +815,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
690
815
|
added_by AS addedBy, added_at AS addedAt, active AS active
|
|
691
816
|
FROM managed_selected_repo WHERE active = 1 ORDER BY full_name ASC`,
|
|
692
817
|
),
|
|
818
|
+
selectRepoFullNameStmt: db.prepare(`SELECT full_name AS fullName FROM managed_selected_repo WHERE repo_id = ?`),
|
|
693
819
|
selectAllReposStmt: db.prepare(
|
|
694
820
|
`SELECT repo_id AS repoId, full_name AS fullName, is_private AS priv,
|
|
695
821
|
installation_id AS installId, default_branch AS branch, cached_default_branch AS cachedDefaultBranch, html_url AS htmlUrl,
|
|
@@ -734,42 +860,68 @@ function prepareStatements(db: ManagedSql) {
|
|
|
734
860
|
`SELECT id FROM managed_report WHERE sha256 = ? AND COALESCE(analyzer, '') = COALESCE(?, '')
|
|
735
861
|
ORDER BY uploaded_at, id LIMIT 1`,
|
|
736
862
|
),
|
|
737
|
-
|
|
738
|
-
|
|
863
|
+
// A report access snapshot's reads in one statement, so one snapshot of
|
|
864
|
+
// the database answers them all: the session's user; those of the
|
|
865
|
+
// requested reports (a JSON array of ids) they may read, each with the
|
|
866
|
+
// grants their teams give them (a JSON array); and the enabled link
|
|
867
|
+
// reports (linkRevision).
|
|
868
|
+
selectReportSnapshotStmt: db.prepare(
|
|
869
|
+
`WITH viewer AS (
|
|
870
|
+
SELECT u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
871
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
872
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
873
|
+
), requested AS (SELECT value AS id FROM json_each(?)), report_grants AS (
|
|
739
874
|
SELECT r.id AS id, MAX(tu.view_dependencies) AS dependencies, MAX(tu.view_security) AS security
|
|
740
875
|
FROM managed_report r JOIN requested q ON q.id = r.id
|
|
741
876
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
742
|
-
JOIN managed_team_user tu ON tu.team_id = tr.team_id AND tu.user_id =
|
|
877
|
+
JOIN managed_team_user tu ON tu.team_id = tr.team_id AND tu.user_id = (SELECT uid FROM viewer)
|
|
743
878
|
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
744
879
|
GROUP BY r.id
|
|
745
880
|
)
|
|
746
|
-
SELECT
|
|
747
|
-
r.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
881
|
+
SELECT v.uid AS uid, v.login AS login, v.name AS name, v.avatar AS avatar, v.role AS role,
|
|
882
|
+
(SELECT json_group_array(json_object('id', r.id, 'filename', r.filename, 'byteSize', r.byte_size, 'sha256', r.sha256,
|
|
883
|
+
'repoDirectory', r.repo_directory, 'repoFullName', sr.full_name,
|
|
884
|
+
'dependencies', COALESCE(g.dependencies, 0), 'security', COALESCE(g.security, 0)) ORDER BY r.id)
|
|
885
|
+
FROM managed_report r JOIN requested q ON q.id = r.id
|
|
886
|
+
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
887
|
+
LEFT JOIN report_grants g ON g.id = r.id
|
|
888
|
+
WHERE v.role = 'admin' OR (v.role = 'manage' AND r.uploaded_by = v.uid)
|
|
889
|
+
OR (g.id IS NOT NULL AND (v.role = 'manage' OR r.visible = 1))) AS reports,
|
|
890
|
+
(SELECT json_group_array(json_object('id', id, 'sha256', sha256) ORDER BY id) FROM managed_link_report WHERE enabled = 1) AS links
|
|
891
|
+
FROM viewer v`,
|
|
754
892
|
),
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
893
|
+
// A team snapshot's reads in one statement, so one snapshot of the
|
|
894
|
+
// database answers them all: the session's user; whether they see the
|
|
895
|
+
// team; the team's visible reports in the scopes it grants them, with the
|
|
896
|
+
// grants, and the hidden one asked for where their role may preview it;
|
|
897
|
+
// the team's repositories (each a JSON array); and the enabled link
|
|
898
|
+
// reports (linkRevision).
|
|
899
|
+
selectTeamSnapshotStmt: db.prepare(
|
|
900
|
+
`WITH viewer AS (
|
|
901
|
+
SELECT u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
902
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
903
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
904
|
+
)
|
|
905
|
+
SELECT v.uid AS uid, v.login AS login, v.name AS name, v.avatar AS avatar, v.role AS role,
|
|
906
|
+
EXISTS(SELECT 1 FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
907
|
+
WHERE tu.user_id = v.uid AND tu.team_id = ? AND t.hidden = 0) AS member,
|
|
908
|
+
(SELECT json_group_array(json_object('id', x.id, 'filename', x.filename, 'byteSize', x.byteSize, 'sha256', x.sha256,
|
|
909
|
+
'visible', x.visible, 'repoDirectory', x.repoDirectory, 'repoFullName', x.repoFullName,
|
|
910
|
+
'dependencies', x.dependencies, 'security', x.security) ORDER BY x.id)
|
|
911
|
+
FROM (SELECT DISTINCT r.id AS id, r.filename AS filename, r.byte_size AS byteSize, r.sha256 AS sha256, r.visible AS visible,
|
|
912
|
+
r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
913
|
+
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
914
|
+
FROM managed_team_user tu
|
|
915
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
916
|
+
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
917
|
+
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
918
|
+
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
919
|
+
WHERE tu.user_id = v.uid AND tu.team_id = ? AND (r.visible = 1 OR (v.role IN ('admin', 'manage') AND r.id = ?))) AS x) AS reports,
|
|
920
|
+
(SELECT json_group_array(json_object('repoId', tr.repo_id, 'github', sr.full_name, 'path', tr.path) ORDER BY tr.repo_id, tr.path)
|
|
921
|
+
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
922
|
+
WHERE tr.team_id = ?) AS repositories,
|
|
923
|
+
(SELECT json_group_array(json_object('id', id, 'sha256', sha256) ORDER BY id) FROM managed_link_report WHERE enabled = 1) AS links
|
|
924
|
+
FROM viewer v`,
|
|
773
925
|
),
|
|
774
926
|
reportFilenamesWithBundleHashStmt: db.prepare(`SELECT DISTINCT filename FROM managed_report WHERE bundle_id = ? AND sha256 = ?`),
|
|
775
927
|
deleteReportStmt: db.prepare(`DELETE FROM managed_report WHERE id = ?`),
|
|
@@ -820,6 +972,28 @@ function prepareStatements(db: ManagedSql) {
|
|
|
820
972
|
WHERE t.finding_id IN (SELECT value FROM json_each(?))
|
|
821
973
|
ORDER BY t.finding_id ASC`,
|
|
822
974
|
),
|
|
975
|
+
// A set of findings' triage (selectTriageStmt), comments (comments.ts
|
|
976
|
+
// listComments) and issues (managed-issues.ts listManagedIssues), each
|
|
977
|
+
// a JSON array of the rows those read, in their order, from one snapshot.
|
|
978
|
+
selectAnnotationsStmt: db.prepare(
|
|
979
|
+
`WITH requested AS (SELECT value AS id FROM json_each(?))
|
|
980
|
+
SELECT (SELECT json_group_array(json_object('findingId', t.finding_id, 'color', t.color, 'triage', t.triage, 'comment', t.comment,
|
|
981
|
+
'fix', t.fix, 'flagged', t.flagged, 'updatedByLogin', COALESCE(u.login, t.updated_by_login), 'updatedAt', t.updated_at)
|
|
982
|
+
ORDER BY t.finding_id ASC)
|
|
983
|
+
FROM managed_finding_triage t LEFT JOIN managed_user u ON u.id = t.updated_by
|
|
984
|
+
WHERE t.finding_id IN (SELECT id FROM requested)) AS triage,
|
|
985
|
+
(SELECT json_group_array(json_object('id', c.id, 'findingId', c.finding_id, 'body', c.body, 'authorId', c.author_id,
|
|
986
|
+
'authorLogin', COALESCE(u.login, c.author_login), 'createdAt', c.created_at, 'updatedAt', c.updated_at, 'version', c.version)
|
|
987
|
+
ORDER BY c.created_at NULLS FIRST, c.id)
|
|
988
|
+
FROM managed_finding_comment c LEFT JOIN managed_user u ON u.id = c.author_id
|
|
989
|
+
WHERE c.finding_id IN (SELECT id FROM requested)) AS comments,
|
|
990
|
+
(SELECT json_group_array(json_object('findingId', i.finding_id, 'repoId', i.repo_id, 'repository', i.repository, 'requestId', i.request_id,
|
|
991
|
+
'issueUrl', i.issue_url, 'autoFixUrl', i.auto_fix_url, 'autoFixCheckedAt', i.auto_fix_checked_at,
|
|
992
|
+
'createdBy', i.created_by, 'createdAt', i.created_at) ORDER BY i.finding_id)
|
|
993
|
+
FROM managed_finding_issue i WHERE i.finding_id IN (SELECT id FROM requested)) AS issues`,
|
|
994
|
+
),
|
|
995
|
+
selectTriageFixesStmt: db.prepare(`SELECT DISTINCT fix FROM managed_finding_triage
|
|
996
|
+
WHERE finding_id IN (SELECT value FROM json_each(?)) AND fix IS NOT NULL ORDER BY fix`),
|
|
823
997
|
annotationTriageRevisionStmt: db.prepare(`
|
|
824
998
|
SELECT t.finding_id AS id,
|
|
825
999
|
(SELECT e.batch_id FROM managed_finding_triage_event e
|
|
@@ -919,51 +1093,39 @@ function prepareStatements(db: ManagedSql) {
|
|
|
919
1093
|
renameTeamStmt: db.prepare(`UPDATE managed_team SET name = ?, updated_at = ? WHERE id = ?`),
|
|
920
1094
|
setTeamHiddenStmt: db.prepare(`UPDATE managed_team SET hidden = ?, updated_at = ? WHERE id = ?`),
|
|
921
1095
|
selectTeamsStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team ORDER BY name ASC`),
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
// Bundles are scoped through the same team -> repository links as reports.
|
|
956
|
-
// Hidden bundles remain available to managers within their team scopes.
|
|
957
|
-
selectUserTeamBundlesStmt: db.prepare(
|
|
958
|
-
`SELECT DISTINCT tr.team_id AS teamId, b.id AS id, b.slug AS slug, b.integrity AS integrity,
|
|
959
|
-
b.filename AS filename, b.kind, b.visible, b.byte_size AS byteSize, b.repo_id AS repoId, b.repo_directory AS repoDirectory, sr.full_name AS repoFullName, b.uploaded_at, b.provenance
|
|
960
|
-
FROM managed_team_user tu
|
|
961
|
-
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
962
|
-
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
963
|
-
JOIN managed_bundle b ON b.repo_id = tr.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
964
|
-
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
965
|
-
WHERE tu.user_id = ? AND (b.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))
|
|
966
|
-
ORDER BY b.uploaded_at DESC, b.filename ASC, b.id ASC`,
|
|
1096
|
+
selectTeamsForUserStmt: db.prepare(`${USER_TEAMS_SQL('?')} ORDER BY t.name ASC, t.id ASC`),
|
|
1097
|
+
selectUserTeamScopesStmt: db.prepare(`${USER_TEAM_SCOPES_SQL('?')} ORDER BY tr.team_id, tr.repo_id, tr.path`),
|
|
1098
|
+
selectUserTeamReportsStmt: db.prepare(`${USER_TEAM_REPORTS_SQL('?')} ORDER BY r.uploaded_at DESC, r.filename ASC, r.id ASC`),
|
|
1099
|
+
selectUserTeamBundlesStmt: db.prepare(`${USER_TEAM_BUNDLES_SQL('?')} ORDER BY b.uploaded_at DESC, b.filename ASC, b.id ASC`),
|
|
1100
|
+
// getUserTeamFeedSnapshot's reads in one statement, so one snapshot of the
|
|
1101
|
+
// database answers them all: the session's user and the change revisions
|
|
1102
|
+
// (getFeedState), and listTeamsForUser's lists, each a JSON array in the
|
|
1103
|
+
// order those statements read it.
|
|
1104
|
+
selectUserTeamFeedStmt: db.prepare(
|
|
1105
|
+
`WITH viewer AS (
|
|
1106
|
+
SELECT u.id AS id, u.role AS role, r.catalog AS catalog, r.annotations AS annotations
|
|
1107
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id JOIN managed_change_revision r ON r.id = 1
|
|
1108
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
1109
|
+
)
|
|
1110
|
+
SELECT v.id AS id, v.role AS role, v.catalog AS catalog, v.annotations AS annotations,
|
|
1111
|
+
(SELECT json_group_array(json_object('id', x.id, 'slug', x.slug, 'name', x.name, 'dependencyReview', x.dependencyReview,
|
|
1112
|
+
'dependencies', x.dependencies, 'security', x.security) ORDER BY x.name ASC, x.id ASC)
|
|
1113
|
+
FROM (${USER_TEAMS_SQL('v.id')}) AS x) AS teams,
|
|
1114
|
+
(SELECT json_group_array(json_object('teamId', x.teamId, 'repoId', x.repoId, 'path', x.path, 'fullName', x.fullName)
|
|
1115
|
+
ORDER BY x.teamId, x.repoId, x.path)
|
|
1116
|
+
FROM (${USER_TEAM_SCOPES_SQL('v.id')}) AS x) AS scopes,
|
|
1117
|
+
(SELECT json_group_array(json_object('id', id, 'sha256', sha256) ORDER BY id) FROM managed_link_report WHERE enabled = 1) AS links,
|
|
1118
|
+
(SELECT json_group_array(json_object('teamId', x.teamId, 'id', x.id, 'slug', x.slug, 'filename', x.filename, 'analyzer', x.analyzer,
|
|
1119
|
+
'visible', x.visible, 'sha256', x.sha256, 'bundleId', x.bundleId, 'repoId', x.repoId, 'repoDirectory', x.repoDirectory,
|
|
1120
|
+
'repoFullName', x.repoFullName, 'dependencies', x.dependencies, 'security', x.security, 'role', x.role)
|
|
1121
|
+
ORDER BY x.uploaded_at DESC, x.filename ASC, x.id ASC)
|
|
1122
|
+
FROM (${USER_TEAM_REPORTS_SQL('v.id')}) AS x) AS reports,
|
|
1123
|
+
(SELECT json_group_array(json_object('teamId', x.teamId, 'id', x.id, 'slug', x.slug, 'integrity', x.integrity, 'filename', x.filename,
|
|
1124
|
+
'kind', x.kind, 'visible', x.visible, 'byteSize', x.byteSize, 'repoId', x.repoId, 'repoDirectory', x.repoDirectory,
|
|
1125
|
+
'repoFullName', x.repoFullName, 'provenance', x.provenance)
|
|
1126
|
+
ORDER BY x.uploaded_at DESC, x.filename ASC, x.id ASC)
|
|
1127
|
+
FROM (${USER_TEAM_BUNDLES_SQL('v.id')}) AS x) AS bundles
|
|
1128
|
+
FROM viewer v`,
|
|
967
1129
|
),
|
|
968
1130
|
selectUserRepoScopesStmt: db.prepare(
|
|
969
1131
|
`SELECT DISTINCT tr.repo_id AS repoId, NULLIF(tr.path, '') AS path
|
|
@@ -979,6 +1141,14 @@ function prepareStatements(db: ManagedSql) {
|
|
|
979
1141
|
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
980
1142
|
WHERE r.id = ? AND tu.user_id = ? LIMIT 1`,
|
|
981
1143
|
),
|
|
1144
|
+
selectReportViewStmt: db.prepare(
|
|
1145
|
+
`SELECT r.uploaded_by AS uploadedBy, r.visible AS visible,
|
|
1146
|
+
EXISTS(SELECT 1 FROM managed_team_repo tr
|
|
1147
|
+
JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
1148
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1149
|
+
WHERE tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL} AND tu.user_id = ?) AS readable
|
|
1150
|
+
FROM managed_report r WHERE r.id = ?`,
|
|
1151
|
+
),
|
|
982
1152
|
selectBundleSecurityStmt: db.prepare(
|
|
983
1153
|
`SELECT 1 FROM managed_bundle b
|
|
984
1154
|
JOIN managed_team_repo tr ON tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
@@ -1065,6 +1235,9 @@ function selectedRepoMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
1065
1235
|
const rows = (await selectAllReposStmt.all()) as RepoRow[]
|
|
1066
1236
|
return rows.map(readManagedRepo)
|
|
1067
1237
|
},
|
|
1238
|
+
async getRepoFullName(repoId: number): Promise<string | null> {
|
|
1239
|
+
return ((await stmts.selectRepoFullNameStmt.get(repoId)) as { fullName: string } | undefined)?.fullName ?? null
|
|
1240
|
+
},
|
|
1068
1241
|
async deactivateRepo(repoId: number): Promise<boolean> {
|
|
1069
1242
|
return Number((await deactivateRepoStmt.run(repoId)).changes) > 0
|
|
1070
1243
|
},
|
|
@@ -1167,39 +1340,40 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1167
1340
|
return await stmts.findingCatalogReportsStmt.all(after, limit) as Pick<ReportRecord, 'id' | 'byteSize'>[]
|
|
1168
1341
|
},
|
|
1169
1342
|
async getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string, reportId: string | null = null): Promise<TeamReportAccessSnapshot | null> {
|
|
1170
|
-
const session = await stmts.
|
|
1343
|
+
const session = await stmts.selectTeamSnapshotStmt.get(sessionId, now, teamId, teamId, reportId, teamId) as
|
|
1344
|
+
SessionRow & { member: unknown; reports: unknown; repositories: unknown; links: unknown } | undefined
|
|
1171
1345
|
if (!session) return null
|
|
1172
1346
|
const user: StoredUser = { id: session.uid, login: session.login, name: session.name, avatarUrl: session.avatar, role: session.role }
|
|
1173
|
-
if (user.role === 'none' || !
|
|
1347
|
+
if (user.role === 'none' || !session.member) return { user, teamId: null, reports: [], repositories: [] }
|
|
1174
1348
|
const whole = user.role === 'admin' || user.role === 'manage'
|
|
1175
|
-
const rows =
|
|
1349
|
+
const rows = jsonRows<{
|
|
1176
1350
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1177
1351
|
repoFullName: string | null; dependencies: number; security: number; visible: number
|
|
1178
|
-
}
|
|
1352
|
+
}>(session.reports)
|
|
1179
1353
|
// Only privileged callers can receive a hidden row from this query. Give
|
|
1180
1354
|
// that individual preview its own capacity; published report selections
|
|
1181
1355
|
// still use the complete workspace for cross-report classification.
|
|
1182
1356
|
const hidden = rows.find(row => row.id === reportId && row.visible === 0)
|
|
1183
1357
|
const selected = hidden ? [hidden] : rows
|
|
1184
|
-
const repositories =
|
|
1185
|
-
return { user, teamId, reportId, hiddenReportId: hidden?.id ?? null, repositories, linkRevision:
|
|
1358
|
+
const repositories = jsonRows<TeamReportAccessSnapshot['repositories'][number]>(session.repositories)
|
|
1359
|
+
return { user, teamId, reportId, hiddenReportId: hidden?.id ?? null, repositories, linkRevision: linkRevisionOf(jsonRows(session.links)), reports: selected.map(row => ({
|
|
1186
1360
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1187
1361
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1188
1362
|
permissions: { dependencies: whole || row.dependencies === 1, security: whole || row.security === 1 },
|
|
1189
1363
|
})) }
|
|
1190
1364
|
},
|
|
1191
1365
|
async getReportAccessSnapshot(sessionId: string, now: number, ids: readonly string[]): Promise<ReportAccessSnapshot | null> {
|
|
1192
|
-
const session = await stmts.
|
|
1366
|
+
const session = await stmts.selectReportSnapshotStmt.get(sessionId, now, JSON.stringify([...new Set(ids)])) as
|
|
1367
|
+
SessionRow & { reports: unknown; links: unknown } | undefined
|
|
1193
1368
|
if (!session) return null
|
|
1194
1369
|
const user: StoredUser = { id: session.uid, login: session.login, name: session.name, avatarUrl: session.avatar, role: session.role }
|
|
1195
1370
|
if (user.role === 'none' || ids.length === 0) return { user, reports: [] }
|
|
1196
1371
|
const admin = user.role === 'admin', manage = user.role === 'manage'
|
|
1197
|
-
const rows =
|
|
1198
|
-
admin ? 1 : 0, manage ? 1 : 0, user.id, manage ? 1 : 0) as {
|
|
1372
|
+
const rows = jsonRows<{
|
|
1199
1373
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1200
1374
|
repoFullName: string | null; dependencies: number; security: number
|
|
1201
|
-
}
|
|
1202
|
-
return { user, linkRevision:
|
|
1375
|
+
}>(session.reports)
|
|
1376
|
+
return { user, linkRevision: linkRevisionOf(jsonRows(session.links)), reports: rows.map(row => ({
|
|
1203
1377
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1204
1378
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1205
1379
|
permissions: { dependencies: admin || manage || row.dependencies === 1, security: admin || manage || row.security === 1 },
|
|
@@ -1226,6 +1400,11 @@ type TriageDbRow = {
|
|
|
1226
1400
|
comment: string | null; fix: string | null; flagged: number | null
|
|
1227
1401
|
updatedByLogin: string | null; updatedAt: number
|
|
1228
1402
|
}
|
|
1403
|
+
const triageRow = (r: TriageDbRow): TriageRow => ({
|
|
1404
|
+
findingId: r.findingId, color: r.color, triage: r.triage, comment: r.comment, fix: r.fix,
|
|
1405
|
+
flagged: r.flagged == null ? null : r.flagged === 1,
|
|
1406
|
+
updatedByLogin: r.updatedByLogin, updatedAt: r.updatedAt,
|
|
1407
|
+
})
|
|
1229
1408
|
type TriageStateDbRow = { color: string | null; triage: string | null; comment: string | null; fix: string | null; flagged: number | null }
|
|
1230
1409
|
type TriageEventDbRow = TriageStateDbRow & {
|
|
1231
1410
|
seq: number; findingId: string; batchId: string; actorId: string | null; actorLogin: string | null; actorName: string | null; at: number
|
|
@@ -1289,14 +1468,13 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1289
1468
|
actorId: r.actorId, actorLogin: r.actorLogin, actorName: r.actorName, at: r.at,
|
|
1290
1469
|
}))
|
|
1291
1470
|
},
|
|
1471
|
+
async listTriageFixes(findingIds: readonly string[]): Promise<string[]> {
|
|
1472
|
+
if (findingIds.length === 0) return []
|
|
1473
|
+
return ((await stmts.selectTriageFixesStmt.all(JSON.stringify(findingIds))) as { fix: string }[]).map(row => row.fix)
|
|
1474
|
+
},
|
|
1292
1475
|
async listTriage(findingIds: readonly string[]): Promise<TriageRow[]> {
|
|
1293
1476
|
if (findingIds.length === 0) return []
|
|
1294
|
-
|
|
1295
|
-
return rows.map((r) => ({
|
|
1296
|
-
findingId: r.findingId, color: r.color, triage: r.triage, comment: r.comment, fix: r.fix,
|
|
1297
|
-
flagged: r.flagged == null ? null : r.flagged === 1,
|
|
1298
|
-
updatedByLogin: r.updatedByLogin, updatedAt: r.updatedAt,
|
|
1299
|
-
}))
|
|
1477
|
+
return ((await selectTriageStmt.all(JSON.stringify(findingIds))) as TriageDbRow[]).map(triageRow)
|
|
1300
1478
|
},
|
|
1301
1479
|
async setTriage(findingId: string, entry: TriageEntryPatch | null, updatedBy: string | null, updatedByLogin: string | null, now: number): Promise<void> {
|
|
1302
1480
|
// A single write is its own batch. The row and its event go together.
|
|
@@ -1498,40 +1676,8 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1498
1676
|
return (await selectUserRepoScopesStmt.all(userId)) as { repoId: number; path: string | null }[]
|
|
1499
1677
|
},
|
|
1500
1678
|
async listTeamsForUser(userId: string): Promise<UserTeam[]> {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
const links = await linkRevision(db)
|
|
1504
|
-
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
1505
|
-
for (const r of (await selectUserTeamReportsStmt.all(userId)) as {
|
|
1506
|
-
teamId: string; id: string; slug: string; filename: string; analyzer: string | null; sha256: string; bundleId: string | null; repoId: number
|
|
1507
|
-
repoDirectory: string; repoFullName: string | null; visible: number; dependencies: number; security: number; role: string
|
|
1508
|
-
}[]) {
|
|
1509
|
-
const list = reportsByTeam.get(r.teamId) ?? []
|
|
1510
|
-
const cacheKey = createHash('sha256').update(JSON.stringify([
|
|
1511
|
-
r.sha256, r.bundleId, r.filename, r.repoId, r.repoDirectory, r.repoFullName, r.dependencies, r.security, r.role, links,
|
|
1512
|
-
])).digest('base64url')
|
|
1513
|
-
list.push({ id: r.id, slug: r.slug, filename: r.filename, visible: r.visible === 1, analyzer: r.analyzer, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory, cacheKey })
|
|
1514
|
-
reportsByTeam.set(r.teamId, list)
|
|
1515
|
-
}
|
|
1516
|
-
const bundlesByTeam = new Map<string, UserTeamBundle[]>()
|
|
1517
|
-
for (const b of (await selectUserTeamBundlesStmt.all(userId)) as (Omit<UserTeamBundle, 'visible'> & { teamId: string; visible: number })[]) {
|
|
1518
|
-
const list = bundlesByTeam.get(b.teamId) ?? []
|
|
1519
|
-
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename, visible: b.visible === 1,
|
|
1520
|
-
kind: b.kind, byteSize: b.byteSize, repoId: b.repoId, repoDirectory: b.repoDirectory, repoFullName: b.repoFullName, provenance: b.provenance })
|
|
1521
|
-
bundlesByTeam.set(b.teamId, list)
|
|
1522
|
-
}
|
|
1523
|
-
return teams.map((t) => ({
|
|
1524
|
-
id: t.id, slug: t.slug, name: t.name,
|
|
1525
|
-
...(t.dependencyReview === 1 ? { dependencyReview: true } : {}),
|
|
1526
|
-
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
1527
|
-
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1528
|
-
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
1529
|
-
])).digest('base64url'),
|
|
1530
|
-
repos: scopes.filter(scope => scope.teamId === t.id).map(scope => ({ repoId: scope.repoId, fullName: scope.fullName, path: scope.path || null }))
|
|
1531
|
-
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
1532
|
-
reports: reportsByTeam.get(t.id) ?? [],
|
|
1533
|
-
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
1534
|
-
}))
|
|
1679
|
+
return userTeams(await selectTeamsForUserStmt.all(userId) as UserTeamRows['teams'], await stmts.selectUserTeamScopesStmt.all(userId) as UserTeamRows['scopes'],
|
|
1680
|
+
await linkRevision(db), await selectUserTeamReportsStmt.all(userId) as UserTeamRows['reports'], await selectUserTeamBundlesStmt.all(userId) as UserTeamRows['bundles'])
|
|
1535
1681
|
},
|
|
1536
1682
|
async getFeedState(sessionId: string, now: number) {
|
|
1537
1683
|
const row = await db.prepare(`SELECT u.id, u.role, r.catalog, r.annotations FROM managed_session s
|
|
@@ -1540,16 +1686,22 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1540
1686
|
return row ? { user: { id: row.id, role: row.role }, catalog: row.catalog, annotations: row.annotations } : null
|
|
1541
1687
|
},
|
|
1542
1688
|
async getUserTeamFeedSnapshot(sessionId: string, now: number) {
|
|
1543
|
-
// One
|
|
1544
|
-
//
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1689
|
+
// One statement, shared by SQLite and Postgres. Never touch last-seen
|
|
1690
|
+
// timestamps or include other users' memberships.
|
|
1691
|
+
const row = await stmts.selectUserTeamFeedStmt.get(sessionId, now) as {
|
|
1692
|
+
id: string; role: Role; catalog: number; annotations: number; teams: unknown; scopes: unknown; links: unknown; reports: unknown; bundles: unknown
|
|
1693
|
+
} | undefined
|
|
1694
|
+
if (!row) return null
|
|
1695
|
+
const teams = row.role === 'none' ? [] : userTeams(jsonRows(row.teams), jsonRows(row.scopes), linkRevisionOf(jsonRows(row.links)), jsonRows(row.reports), jsonRows(row.bundles))
|
|
1696
|
+
return { user: { id: row.id, role: row.role }, catalog: row.catalog, teams, revision: teamCatalogRevision(teams) }
|
|
1549
1697
|
},
|
|
1550
1698
|
async userCanReadReport(userId: string, reportId: string): Promise<boolean> {
|
|
1551
1699
|
return (await selectReportReadableStmt.get(reportId, userId)) != null
|
|
1552
1700
|
},
|
|
1701
|
+
async getReportView(userId: string, reportId: string) {
|
|
1702
|
+
const row = await stmts.selectReportViewStmt.get(userId, reportId) as { uploadedBy: string | null; visible: number; readable: unknown } | undefined
|
|
1703
|
+
return row ? { uploadedBy: row.uploadedBy, visible: row.visible === 1, readable: Boolean(row.readable) } : null
|
|
1704
|
+
},
|
|
1553
1705
|
async userCanReadBundleAdvisories(userId: string, bundleId: string, teamId: string | null): Promise<boolean> {
|
|
1554
1706
|
return (await stmts.selectBundleSecurityStmt.get(bundleId, userId, teamId, teamId)) != null
|
|
1555
1707
|
},
|
|
@@ -1614,10 +1766,16 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1614
1766
|
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0, db)
|
|
1615
1767
|
const {
|
|
1616
1768
|
upsertUserStmt, selectLinkedStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1617
|
-
touchUserSeenStmt, updateRoleStmt, updateTokensStmt,
|
|
1769
|
+
selectUserStmt, touchOwnSessionStmt, touchUserSeenStmt, updateRoleStmt, updateTokensStmt, ensureUploadKeyStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1618
1770
|
selectViewSessionStmt, deleteOtherViewsStmt, insertViewSessionStmt, deleteDemotedViewsStmt,
|
|
1619
1771
|
} = stmts
|
|
1620
1772
|
|
|
1773
|
+
// Claimed at every automatic reap: prepared once.
|
|
1774
|
+
const claimLeaseStmt = db.prepare(`INSERT INTO managed_maintenance_lease (id, owner, expires_at) VALUES (?, ?, ?)
|
|
1775
|
+
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
1776
|
+
WHERE managed_maintenance_lease.expires_at <= ? RETURNING id`)
|
|
1777
|
+
const finishLeaseStmt = db.prepare('UPDATE managed_maintenance_lease SET expires_at = ? WHERE owner = ?')
|
|
1778
|
+
|
|
1621
1779
|
const methods: Omit<ManagedDb, keyof ManagementStore | keyof ManagementCatalogStore> = {
|
|
1622
1780
|
...storageMethods(db, key),
|
|
1623
1781
|
...linkReportMethods(db, key),
|
|
@@ -1629,12 +1787,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1629
1787
|
...bundleBuildLeaseMethods(db),
|
|
1630
1788
|
...upstreamCacheMethods(db),
|
|
1631
1789
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
1632
|
-
return !!await
|
|
1633
|
-
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
1634
|
-
WHERE managed_maintenance_lease.expires_at <= ? RETURNING id`).get(migration ? 2 : 1, owner, until, now)
|
|
1790
|
+
return !!await claimLeaseStmt.get(migration ? 2 : 1, owner, until, now)
|
|
1635
1791
|
},
|
|
1636
1792
|
async finishMaintenanceLease(owner, until) {
|
|
1637
|
-
await
|
|
1793
|
+
await finishLeaseStmt.run(until, owner)
|
|
1638
1794
|
},
|
|
1639
1795
|
async upsertUser(user, now, initialAdminGithubId = null) {
|
|
1640
1796
|
if (await selectLinkedStmt.get(user.githubUserId)) return null
|
|
@@ -1655,13 +1811,12 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1655
1811
|
await touchUserSeenStmt.run(now, session.userId, now)
|
|
1656
1812
|
},
|
|
1657
1813
|
async sessionWithUser(id, now) {
|
|
1814
|
+
const row = (await touchOwnSessionStmt.get(now, now, id, now, id, id, id, id)) as SessionRow | undefined
|
|
1815
|
+
return row == null ? null : ownSession(row)
|
|
1816
|
+
},
|
|
1817
|
+
async getSessionWithUser(id, now) {
|
|
1658
1818
|
const row = (await selectOwnSessionStmt.get(id, now)) as SessionRow | undefined
|
|
1659
|
-
|
|
1660
|
-
await touchUserSeenStmt.run(now, row.uid, now)
|
|
1661
|
-
return {
|
|
1662
|
-
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey },
|
|
1663
|
-
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
1664
|
-
}
|
|
1819
|
+
return row == null ? null : ownSession(row)
|
|
1665
1820
|
},
|
|
1666
1821
|
async createViewSession(view, now) {
|
|
1667
1822
|
if (Number((await insertViewSessionStmt.run(view.id, view.csrfToken, now, view.userId, view.viewerSessionId, now)).changes) === 0) return false
|
|
@@ -1672,11 +1827,11 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1672
1827
|
const row = (await selectViewSessionStmt.get(id, viewerSessionId, now, now)) as ViewSessionRow | undefined
|
|
1673
1828
|
if (row == null) return null
|
|
1674
1829
|
await touchUserSeenStmt.run(now, row.viewerId, now)
|
|
1675
|
-
return
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1830
|
+
return viewSession(row)
|
|
1831
|
+
},
|
|
1832
|
+
async getViewSessionWithUser(id, viewerSessionId, now) {
|
|
1833
|
+
const row = (await selectViewSessionStmt.get(id, viewerSessionId, now, now)) as ViewSessionRow | undefined
|
|
1834
|
+
return row == null ? null : viewSession(row)
|
|
1680
1835
|
},
|
|
1681
1836
|
async ensureSessionUploadKey(id, candidate) {
|
|
1682
1837
|
const row = (await ensureUploadKeyStmt.get(candidate, id)) as { uploadKey: string } | undefined
|
|
@@ -1692,6 +1847,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1692
1847
|
const rows = (await selectUsersStmt.all()) as UserRow[]
|
|
1693
1848
|
return rows.map((r) => ({ id: r.id, login: r.login, name: r.name, role: r.role, createdAt: r.created, lastSeenAt: r.lastSeen, lastActivityAt: r.lastActivity }))
|
|
1694
1849
|
},
|
|
1850
|
+
async getUser(id) {
|
|
1851
|
+
const row = (await selectUserStmt.get(id)) as StoredUser | undefined
|
|
1852
|
+
return row == null ? null : { id: row.id, login: row.login, name: row.name, avatarUrl: row.avatarUrl, role: row.role }
|
|
1853
|
+
},
|
|
1695
1854
|
async setUserRole(id, role) {
|
|
1696
1855
|
if (role !== 'admin') await deleteDemotedViewsStmt.run(id)
|
|
1697
1856
|
return Number((await updateRoleStmt.run(role, Date.now(), id)).changes) > 0
|
|
@@ -1702,8 +1861,11 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1702
1861
|
await updateTokensStmt.run(value.access, value.refresh, tokens.expiresAt, state ? 1 : 0, Date.now(), id)
|
|
1703
1862
|
},
|
|
1704
1863
|
async getUserTokens(id) {
|
|
1705
|
-
|
|
1706
|
-
|
|
1864
|
+
// The tokens and the storage encryption state they were kept under, in
|
|
1865
|
+
// one read: a user missing reads as no tokens, no encryption row as none.
|
|
1866
|
+
const read = (await stmts.selectTokensStateStmt.get(id)) as StoredTokens & Parameters<typeof storageStateOf>[0] & { userId: string | null; stateId: number | null }
|
|
1867
|
+
const state = storageStateOf(read.stateId == null ? undefined : read, key)
|
|
1868
|
+
const row = read.userId == null ? undefined : read
|
|
1707
1869
|
if (row == null || row.access == null) return null
|
|
1708
1870
|
if (row.encrypted && !state) throw new Error('Encrypted tokens without storage encryption state')
|
|
1709
1871
|
if (!row.encrypted && state?.complete) throw new Error('Unexpected plaintext GitHub tokens')
|
|
@@ -1721,7 +1883,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1721
1883
|
...activity,
|
|
1722
1884
|
...comments,
|
|
1723
1885
|
async getAnnotations(ids) {
|
|
1724
|
-
|
|
1886
|
+
// listTriage, listComments and listManagedIssues in one statement.
|
|
1887
|
+
const row = await stmts.selectAnnotationsStmt.get(JSON.stringify(ids)) as { triage: unknown; comments: unknown; issues: unknown }
|
|
1888
|
+
return { triage: ids.length > 0 ? jsonRows<TriageDbRow>(row.triage).map(triageRow) : [], comments: ids.length > 0 ? jsonRows<Awaited<ReturnType<CommentStore['listComments']>>[number]>(row.comments) : [],
|
|
1889
|
+
issues: jsonRows<ManagedIssue>(row.issues) }
|
|
1725
1890
|
},
|
|
1726
1891
|
async applyManagedIssueFixes(sessionId, snapshot, updates) {
|
|
1727
1892
|
// Recheck access inside the same write transaction as the derived field.
|