@preventive/triage 1.0.0-alpha.33 → 1.0.0-alpha.34
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 +5 -4
- package/common/bundle-metadata.js +4 -3
- package/common/bundle-source-links.d.ts +2 -1
- package/common/bundle-source-links.js +133 -6
- package/common/bundle-sources.d.ts +2 -0
- package/common/bundle-sources.js +2 -4
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/routes.js +20 -1
- package/common/managed/triage.ts +2 -2
- package/out/client-managed-import.js +51 -46
- package/out/client-managed.js +252 -72
- package/out/client-sync.js +14 -14
- package/out/graph.js +11 -11
- package/out/index.html +1 -0
- package/out/terminal.js +169 -169
- package/out/view.css +1 -1
- package/out/view.js +410 -202
- package/package.json +22 -17
- package/server-managed/activity.ts +15 -1
- package/server-managed/bundle-advisories.ts +8 -3
- package/server-managed/bundle-advisory-inventory.ts +27 -25
- package/server-managed/bundle-build-worker.js +45 -9
- package/server-managed/bundle-build.ts +35 -11
- package/server-managed/bundle-cache.ts +1 -1
- package/server-managed/bundle.ts +17 -1
- package/server-managed/config.ts +5 -0
- package/server-managed/db-methods.ts +168 -49
- package/server-managed/db-neon.ts +59 -4
- package/server-managed/db-schema.ts +12 -3
- package/server-managed/db.ts +9 -1
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issues.ts +1 -1
- package/server-managed/http-response.ts +7 -1
- package/server-managed/http.ts +248 -80
- package/server-managed/index.ts +4 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/managed-issues.ts +31 -3
- package/server-managed/management.ts +17 -4
- package/server-managed/public-workspace.ts +5 -2
- package/server-managed/report-sources.ts +6 -5
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/revisions.ts +2 -2
- package/server-managed/sql.ts +3 -2
- package/server-managed/storage.ts +17 -0
- package/server-managed/team-feed.ts +3 -3
- package/server-managed/team-reports.ts +24 -15
- package/server-managed/upstream-cache.ts +73 -0
- package/server-managed/workspace-shares.ts +16 -12
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type LinkReportStore, linkReportMethods, linkRevision } from './link-reports.ts'
|
|
2
|
+
import { type RepositoryAliasStore, repositoryAliasMethods } from './repository-aliases.ts'
|
|
1
3
|
import { createHash, randomUUID } from 'node:crypto'
|
|
2
4
|
import { teamCatalogRevision } from './team-catalog.ts'
|
|
3
5
|
import { type Role, roleAtLeast } from '../common/managed/roles.ts'
|
|
@@ -8,7 +10,9 @@ import { type CommentStore, commentMethods } from './comments.ts'
|
|
|
8
10
|
import { type ActivityStore, activityMethods } from './activity.ts'
|
|
9
11
|
import { type GithubMetadataStore, githubMetadataMethods } from './github-metadata.ts'
|
|
10
12
|
import { type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
13
|
+
import type { IssueFixUpdate } from './github-issue-links.ts'
|
|
11
14
|
import { type BundleBuildLeaseStore, bundleBuildLeaseMethods } from './bundle-build-leases.ts'
|
|
15
|
+
import { type UpstreamCacheStore, upstreamCacheMethods } from './upstream-cache.ts'
|
|
12
16
|
import { type ImportTriageStore, importTriageMethods } from './import-triage.ts'
|
|
13
17
|
import type { ManagedSql } from './sql.ts'
|
|
14
18
|
import { type WorkspaceShareStore, workspaceShareMethods } from './workspace-shares.ts'
|
|
@@ -126,10 +130,13 @@ export interface ReportAccessRecord {
|
|
|
126
130
|
}
|
|
127
131
|
export interface TeamReportAccessSnapshot extends ReportAccessSnapshot {
|
|
128
132
|
teamId: string | null
|
|
133
|
+
reportId?: string | null
|
|
134
|
+
hiddenReportId?: string | null
|
|
129
135
|
repositories: { repoId: number; github: string; path: string | null }[]
|
|
130
136
|
}
|
|
131
137
|
export interface ReportAccessSnapshot {
|
|
132
138
|
user: StoredUser
|
|
139
|
+
linkRevision?: string
|
|
133
140
|
reports: ReportAccessRecord[]
|
|
134
141
|
}
|
|
135
142
|
|
|
@@ -218,10 +225,19 @@ export interface TriageEventRow {
|
|
|
218
225
|
at: number
|
|
219
226
|
}
|
|
220
227
|
|
|
228
|
+
// How a bundle's bytes arrived: uploaded, or generated by this server from a
|
|
229
|
+
// repository. null predates the distinction (possibly built).
|
|
230
|
+
export type BundleProvenance = 'upload' | 'build'
|
|
231
|
+
|
|
232
|
+
// The preset, export conditions and Metro platforms a server build of a JS/TS
|
|
233
|
+
// bundle resolved with, as the build request named them. Stored as JSON.
|
|
234
|
+
export interface BundleBuildConditions { preset: 'node' | 'browser' | 'metro'; conditions: string[]; platforms: string[] }
|
|
235
|
+
|
|
221
236
|
// A stored bundle's metadata. Bytes live in the blob-store keyed by `id`;
|
|
222
237
|
// `integrity` (sha512-<base64>) is the content-addressed identity (UNIQUE),
|
|
223
238
|
// matched against a report's bundleHashes to auto-link.
|
|
224
239
|
export interface ManagedBundle {
|
|
240
|
+
visible: boolean
|
|
225
241
|
id: string
|
|
226
242
|
slug: string
|
|
227
243
|
integrity: string
|
|
@@ -232,15 +248,25 @@ export interface ManagedBundle {
|
|
|
232
248
|
repoDirectory: string
|
|
233
249
|
repoId: number | null
|
|
234
250
|
uploadedAt: number
|
|
251
|
+
provenance: BundleProvenance | null
|
|
252
|
+
// null for uploads, Solidity builds, and builds predating the column.
|
|
253
|
+
buildConditions: BundleBuildConditions | null
|
|
235
254
|
}
|
|
236
255
|
|
|
237
256
|
// What the upload handler supplies to record a bundle; the store stamps
|
|
238
257
|
// uploaded_at. `uploadedByLogin` is the durable uploader-login snapshot.
|
|
239
|
-
export type BundleInput = Omit<ManagedBundle, 'uploadedAt' | 'slug' | 'repoDirectory'
|
|
258
|
+
export type BundleInput = Omit<ManagedBundle, 'uploadedAt' | 'slug' | 'repoDirectory' | 'visible' | 'provenance' | 'buildConditions'> & {
|
|
259
|
+
visible?: boolean; uploadedByLogin: string | null; repoDirectory?: string; dataKey?: string | null; provenance?: BundleProvenance | null
|
|
260
|
+
buildConditions?: BundleBuildConditions | null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// A repeated upload or build of stored bytes (see reuseBundle).
|
|
264
|
+
export interface BundleReuse { provenance: BundleProvenance; filename: string; kind: string | null; rename: boolean; buildConditions?: BundleBuildConditions | null }
|
|
240
265
|
|
|
241
266
|
// A bundle row for the "Manage bundles" list — adds the uploader login + repo
|
|
242
267
|
// full name display joins (null when absent / since removed).
|
|
243
268
|
export interface AdminBundle {
|
|
269
|
+
visible: boolean
|
|
244
270
|
id: string
|
|
245
271
|
slug: string
|
|
246
272
|
integrity: string
|
|
@@ -252,6 +278,7 @@ export interface AdminBundle {
|
|
|
252
278
|
repoId: number | null
|
|
253
279
|
repoFullName: string | null
|
|
254
280
|
uploadedAt: number
|
|
281
|
+
provenance: BundleProvenance | null
|
|
255
282
|
}
|
|
256
283
|
|
|
257
284
|
// A team's repo link (with its optional subpath) and member (with resolved
|
|
@@ -271,6 +298,7 @@ export interface AdminTeam {
|
|
|
271
298
|
id: string
|
|
272
299
|
slug: string
|
|
273
300
|
name: string
|
|
301
|
+
hidden: boolean
|
|
274
302
|
repos: TeamRepoLink[]
|
|
275
303
|
members: TeamMember[]
|
|
276
304
|
}
|
|
@@ -289,6 +317,7 @@ export type UserOption = {
|
|
|
289
317
|
// bundles attached to the team's repos (id + filename, newest first). An item
|
|
290
318
|
// shows under a team when its repo is one of the team's linked repos.
|
|
291
319
|
export interface UserTeamReport {
|
|
320
|
+
visible: boolean
|
|
292
321
|
id: string
|
|
293
322
|
slug: string
|
|
294
323
|
filename: string
|
|
@@ -298,6 +327,7 @@ export interface UserTeamReport {
|
|
|
298
327
|
cacheKey: string
|
|
299
328
|
}
|
|
300
329
|
export interface UserTeamBundle {
|
|
330
|
+
visible: boolean
|
|
301
331
|
id: string
|
|
302
332
|
slug: string
|
|
303
333
|
integrity: string
|
|
@@ -307,6 +337,7 @@ export interface UserTeamBundle {
|
|
|
307
337
|
repoDirectory: string
|
|
308
338
|
repoId: number
|
|
309
339
|
repoFullName: string
|
|
340
|
+
provenance: BundleProvenance | null
|
|
310
341
|
}
|
|
311
342
|
export interface UserTeam {
|
|
312
343
|
id: string
|
|
@@ -319,7 +350,7 @@ export interface UserTeam {
|
|
|
319
350
|
}
|
|
320
351
|
|
|
321
352
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
322
|
-
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, BundleBuildLeaseStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb {
|
|
353
|
+
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore {
|
|
323
354
|
claimMaintenanceLease(owner: string, now: number, until: number, migration?: boolean): Promise<boolean>
|
|
324
355
|
finishMaintenanceLease(owner: string, until: number): Promise<void>
|
|
325
356
|
getFeedState(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; catalog: number; annotations: number } | null>
|
|
@@ -375,7 +406,7 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
375
406
|
getReport(id: string): Promise<ReportRecord | null>
|
|
376
407
|
// Writer-locked reconciliation waits for an uncertain earlier insert.
|
|
377
408
|
resolveReportUpload(id: string): Promise<ReportRecord | null>
|
|
378
|
-
getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string): Promise<TeamReportAccessSnapshot | null>
|
|
409
|
+
getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string, reportId?: string | null): Promise<TeamReportAccessSnapshot | null>
|
|
379
410
|
getReportAccessSnapshot(sessionId: string, now: number, ids: readonly string[]): Promise<ReportAccessSnapshot | null>
|
|
380
411
|
listReportFilenamesWithBundleHash(bundleId: string, sha256: string): Promise<string[]>
|
|
381
412
|
deleteReport(id: string): Promise<boolean>
|
|
@@ -395,7 +426,8 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
395
426
|
// stamp nor the trail. Every change also appends to managed_finding_triage_event;
|
|
396
427
|
// listTriageHistory walks one finding's trail, newest first.
|
|
397
428
|
listTriage(findingIds: readonly string[]): Promise<TriageRow[]>
|
|
398
|
-
getAnnotations(findingIds: readonly string[]): Promise<{ triage: TriageRow[]; comments: Awaited<ReturnType<CommentStore['listComments']>> }>
|
|
429
|
+
getAnnotations(findingIds: readonly string[]): Promise<{ triage: TriageRow[]; comments: Awaited<ReturnType<CommentStore['listComments']>>; issues: Awaited<ReturnType<ManagedIssueStore['listManagedIssues']>> }>
|
|
430
|
+
applyManagedIssueFixes(sessionId: string, snapshot: TeamReportAccessSnapshot, updates: IssueFixUpdate[]): Promise<boolean>
|
|
399
431
|
// A content-free fingerprint of current annotations, read in one snapshot.
|
|
400
432
|
getAnnotationRevision(findingIds: readonly string[]): Promise<string>
|
|
401
433
|
setTriage(findingId: string, entry: TriageEntryPatch | null, updatedBy: string | null, updatedByLogin: string | null, now: number): Promise<void>
|
|
@@ -407,8 +439,14 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
407
439
|
// uploader login + repo; deleteBundle resolves true iff a row was removed
|
|
408
440
|
// (referencing reports' bundle_id null out via the FK). linkReportsToBundle
|
|
409
441
|
// attaches a freshly-stored bundle to the (still-unlinked) reports that
|
|
410
|
-
// declared its integrity.
|
|
442
|
+
// declared its integrity. reuseBundle records a repeated upload or build of
|
|
443
|
+
// stored bytes: a build always marks the row built and records its conditions
|
|
444
|
+
// where the row has none, and with `rename` an uploaded row takes the new
|
|
445
|
+
// filename/kind. A built row is never renamed; one predating provenance is
|
|
446
|
+
// renamed only by a build. Resolves true iff changed.
|
|
447
|
+
// The caller authorizes `rename` (see reuseBundleUpload).
|
|
411
448
|
insertBundle(bundle: BundleInput, now: number, sessionId?: string): Promise<void>
|
|
449
|
+
reuseBundle(id: string, reuse: BundleReuse): Promise<boolean>
|
|
412
450
|
getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
|
|
413
451
|
resolveBundleUpload(integrity: string): Promise<ManagedBundle | null>
|
|
414
452
|
getBundle(id: string): Promise<ManagedBundle | null>
|
|
@@ -427,31 +465,34 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
427
465
|
// Set a bundle's repository and normalized directory (repoId null detaches
|
|
428
466
|
// and clears the directory). The caller validates the location and access.
|
|
429
467
|
setBundleRepo(id: string, repoId: number | null, directory?: string): Promise<boolean>
|
|
468
|
+
setBundleVisible(id: string, visible: boolean): Promise<boolean>
|
|
430
469
|
linkReportsToBundle(integrity: string, bundleId: string, userId?: string): Promise<void>
|
|
431
470
|
// Teams ("Manage teams"). createTeam inserts a team (false iff the name is
|
|
432
471
|
// taken); renameTeam changes a team's name ('name-taken' iff another team
|
|
433
472
|
// already has it, 'not-found' iff no such team, 'ok' otherwise — same name is
|
|
434
473
|
// idempotent); deleteTeam drops it (cascading its links); listTeams returns
|
|
435
|
-
// every team with its repos + members inlined; listUserOptions is the id+login+name
|
|
474
|
+
// every team (including hidden ones) with its repos + members inlined; listUserOptions is the id+login+name
|
|
436
475
|
// set for the member picker. The set*/remove* pairs maintain the link tables:
|
|
437
476
|
// setTeamRepo adds a path (whole repo replaces paths); setTeamMember upserts a
|
|
438
477
|
// membership + its visibility permissions (each resolves true iff a row was
|
|
439
478
|
// written / removed; the caller validates the team/repo/user exist first).
|
|
440
479
|
createTeam(id: string, name: string, now: number): Promise<boolean>
|
|
441
480
|
renameTeam(id: string, name: string, now: number): Promise<'ok' | 'name-taken' | 'not-found'>
|
|
481
|
+
// Hiding preserves links but disables all grants and workspace access.
|
|
482
|
+
setTeamHidden(id: string, hidden: boolean, now: number): Promise<boolean>
|
|
442
483
|
deleteTeam(id: string): Promise<boolean>
|
|
443
|
-
getTeam(id: string): Promise<{ id: string; slug: string; name: string } | null>
|
|
484
|
+
getTeam(id: string): Promise<{ id: string; slug: string; name: string; hidden: boolean } | null>
|
|
444
485
|
listTeams(): Promise<AdminTeam[]>
|
|
445
486
|
listUserOptions(): Promise<UserOption[]>
|
|
446
487
|
// Current grants for manager content reads, writes, and repository pickers.
|
|
447
488
|
listRepoScopesForUser(userId: string): Promise<{ repoId: number; path: string | null }[]>
|
|
448
|
-
// The teams a given user belongs to (name-sorted), each with reports and
|
|
489
|
+
// The non-hidden teams a given user belongs to (name-sorted), each with reports and
|
|
449
490
|
// bundles attached to that team's repos — for that user's own sidebar Teams section.
|
|
450
491
|
// Any user; only their own memberships.
|
|
451
492
|
listTeamsForUser(userId: string): Promise<UserTeam[]>
|
|
452
493
|
getUserTeamFeedSnapshot(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; revision: string; teams: UserTeam[]; catalog: number } | null>
|
|
453
494
|
// Whether `userId` may read `reportId`: true iff the report's repo belongs to
|
|
454
|
-
// a team the user is a member of. Backs the team-scoped report view endpoint.
|
|
495
|
+
// a non-hidden team the user is a member of. Backs the team-scoped report view endpoint.
|
|
455
496
|
userCanReadReport(userId: string, reportId: string): Promise<boolean>
|
|
456
497
|
// The viewer's effective visibility permissions for a report, OR'd across the
|
|
457
498
|
// teams (holding the report's repo) the user belongs to — drives the content
|
|
@@ -480,6 +521,14 @@ const REPORT_IN_TEAM_PATH_SQL = `(tr.path IS NULL OR tr.path = ''
|
|
|
480
521
|
|
|
481
522
|
const BUNDLE_IN_TEAM_PATH_SQL = REPORT_IN_TEAM_PATH_SQL.replaceAll('r.repo_directory', 'b.repo_directory')
|
|
482
523
|
|
|
524
|
+
// Whether a repeated upload/build renames the stored row. Uploads never rename
|
|
525
|
+
// a built row, or one whose provenance is unknown; builds rename either. The
|
|
526
|
+
// kind must stay readable from its blob store (sourcemaps are stored apart):
|
|
527
|
+
// keep it, or let an unrecognized archive become Stasis — never downgrade.
|
|
528
|
+
const BUNDLE_RENAMED_SQL = `(:rename = 1 AND (provenance = 'upload' OR (:provenance = 'build' AND provenance IS NULL))
|
|
529
|
+
AND (COALESCE(kind, '') = COALESCE(:kind, '') OR (kind IS NULL AND :kind = 'stasis'))
|
|
530
|
+
AND (filename <> :filename OR kind IS DISTINCT FROM :kind))`
|
|
531
|
+
|
|
483
532
|
// Prepare every statement the store uses, returned as a bag the factory
|
|
484
533
|
// destructures — keeps openSqliteManagedDb itself small (one place per query).
|
|
485
534
|
function prepareStatements(db: ManagedSql) {
|
|
@@ -598,6 +647,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
598
647
|
LEFT JOIN managed_bundle b ON b.id = r.bundle_id
|
|
599
648
|
WHERE (? IS NULL OR r.uploaded_by = ? OR EXISTS (
|
|
600
649
|
SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
650
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
601
651
|
WHERE tr.repo_id = r.repo_id AND tu.user_id = ? AND ${REPORT_IN_TEAM_PATH_SQL}))
|
|
602
652
|
ORDER BY r.uploaded_at DESC, r.filename ASC`,
|
|
603
653
|
),
|
|
@@ -619,6 +669,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
619
669
|
FROM managed_report r JOIN requested q ON q.id = r.id
|
|
620
670
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
621
671
|
JOIN managed_team_user tu ON tu.team_id = tr.team_id AND tu.user_id = ?
|
|
672
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
622
673
|
GROUP BY r.id
|
|
623
674
|
)
|
|
624
675
|
SELECT r.id AS id, r.filename AS filename, r.byte_size AS byteSize, r.sha256 AS sha256,
|
|
@@ -630,21 +681,23 @@ function prepareStatements(db: ManagedSql) {
|
|
|
630
681
|
WHERE ? = 1 OR (? = 1 AND r.uploaded_by = ?)
|
|
631
682
|
OR (g.id IS NOT NULL AND (? = 1 OR r.visible = 1))`,
|
|
632
683
|
),
|
|
633
|
-
selectTeamAccessStmt: db.prepare(`SELECT 1 FROM managed_team_user
|
|
684
|
+
selectTeamAccessStmt: db.prepare(`SELECT 1 FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
685
|
+
WHERE tu.user_id = ? AND tu.team_id = ? AND t.hidden = 0`),
|
|
634
686
|
selectTeamRepositoriesStmt: db.prepare(
|
|
635
687
|
`SELECT tr.repo_id AS repoId, sr.full_name AS github, tr.path AS path
|
|
636
688
|
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
637
689
|
WHERE tr.team_id = ? ORDER BY tr.repo_id, tr.path`,
|
|
638
690
|
),
|
|
639
691
|
selectTeamReportAccessStmt: db.prepare(
|
|
640
|
-
`SELECT DISTINCT r.id AS id, r.filename AS filename, r.byte_size AS byteSize, r.sha256 AS sha256,
|
|
692
|
+
`SELECT DISTINCT r.id AS id, r.filename AS filename, r.byte_size AS byteSize, r.sha256 AS sha256, r.visible AS visible,
|
|
641
693
|
r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
642
694
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
643
695
|
FROM managed_team_user tu
|
|
696
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
644
697
|
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
645
698
|
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
646
699
|
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
647
|
-
WHERE tu.user_id = ? AND tu.team_id = ? AND (r.visible = 1 OR ? = 1)
|
|
700
|
+
WHERE tu.user_id = ? AND tu.team_id = ? AND (r.visible = 1 OR (? = 1 AND r.id = ?))
|
|
648
701
|
ORDER BY r.id`,
|
|
649
702
|
),
|
|
650
703
|
reportFilenamesWithBundleHashStmt: db.prepare(`SELECT DISTINCT filename FROM managed_report WHERE bundle_id = ? AND sha256 = ?`),
|
|
@@ -707,58 +760,80 @@ function prepareStatements(db: ManagedSql) {
|
|
|
707
760
|
WHERE finding_id IN (SELECT value FROM json_each(?)) ORDER BY id`),
|
|
708
761
|
insertBundleStmt: db.prepare(
|
|
709
762
|
`WITH candidate(id, slug) AS (VALUES (?, ?))
|
|
710
|
-
INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, uploaded_at, data_key, storage_encrypted)
|
|
763
|
+
INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, visible, uploaded_at, data_key, storage_encrypted, provenance, build_conditions)
|
|
711
764
|
SELECT id, CASE WHEN EXISTS (SELECT 1 FROM managed_bundle WHERE slug = candidate.slug) THEN id ELSE slug END,
|
|
712
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM candidate`,
|
|
765
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM candidate`,
|
|
766
|
+
),
|
|
767
|
+
// SET reads the pre-update row. Unchanged rows match nothing, so identical
|
|
768
|
+
// retries do not bump the catalog revision. Conditions that rebuild the same
|
|
769
|
+
// bytes describe them equally, so the first recorded ones stay.
|
|
770
|
+
reuseBundleStmt: db.prepare(
|
|
771
|
+
`UPDATE managed_bundle SET
|
|
772
|
+
filename = CASE WHEN ${BUNDLE_RENAMED_SQL} THEN :filename ELSE filename END,
|
|
773
|
+
kind = CASE WHEN ${BUNDLE_RENAMED_SQL} THEN :kind ELSE kind END,
|
|
774
|
+
provenance = CASE WHEN :provenance = 'build' THEN 'build' ELSE provenance END,
|
|
775
|
+
build_conditions = CASE WHEN :provenance = 'build' THEN COALESCE(build_conditions, :buildConditions) ELSE build_conditions END
|
|
776
|
+
WHERE id = :id AND (${BUNDLE_RENAMED_SQL} OR (:provenance = 'build' AND (provenance IS DISTINCT FROM 'build'
|
|
777
|
+
OR build_conditions IS DISTINCT FROM COALESCE(build_conditions, :buildConditions))))`,
|
|
713
778
|
),
|
|
714
779
|
selectBundleByIntegrityStmt: db.prepare(
|
|
715
780
|
`SELECT id, slug, integrity, filename, kind, byte_size AS byteSize,
|
|
716
|
-
uploaded_by AS uploadedBy, repo_id AS repoId, repo_directory AS repoDirectory, uploaded_at AS uploadedAt
|
|
781
|
+
uploaded_by AS uploadedBy, repo_id AS repoId, repo_directory AS repoDirectory, visible, uploaded_at AS uploadedAt, provenance,
|
|
782
|
+
build_conditions AS buildConditions
|
|
717
783
|
FROM managed_bundle WHERE integrity = ?`,
|
|
718
784
|
),
|
|
719
785
|
selectBundleStmt: db.prepare(
|
|
720
786
|
`SELECT id, slug, integrity, filename, kind, byte_size AS byteSize,
|
|
721
|
-
uploaded_by AS uploadedBy, repo_id AS repoId, repo_directory AS repoDirectory, uploaded_at AS uploadedAt
|
|
787
|
+
uploaded_by AS uploadedBy, repo_id AS repoId, repo_directory AS repoDirectory, visible, uploaded_at AS uploadedAt, provenance,
|
|
788
|
+
build_conditions AS buildConditions
|
|
722
789
|
FROM managed_bundle WHERE id = ?`,
|
|
723
790
|
),
|
|
724
791
|
selectBundlesStmt: db.prepare(
|
|
725
792
|
`SELECT b.id AS id, b.slug AS slug, b.integrity AS integrity, b.filename AS filename, b.kind AS kind,
|
|
726
793
|
b.byte_size AS byteSize, COALESCE(u.login, b.uploaded_by_login) AS uploadedByLogin,
|
|
727
|
-
b.repo_id AS repoId, b.repo_directory AS repoDirectory, sr.full_name AS repoFullName, b.uploaded_at AS uploadedAt
|
|
794
|
+
b.repo_id AS repoId, b.repo_directory AS repoDirectory, b.visible, sr.full_name AS repoFullName, b.uploaded_at AS uploadedAt, b.provenance
|
|
728
795
|
FROM managed_bundle b
|
|
729
796
|
LEFT JOIN managed_user u ON u.id = b.uploaded_by
|
|
730
797
|
LEFT JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
731
798
|
WHERE (? IS NULL OR (b.uploaded_by = ? OR EXISTS (
|
|
732
799
|
SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
800
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
733
801
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?)))
|
|
734
802
|
ORDER BY b.uploaded_at DESC, b.filename ASC`,
|
|
735
803
|
),
|
|
736
804
|
selectBundleReadableStmt: db.prepare(
|
|
737
805
|
`SELECT 1 FROM managed_bundle b WHERE b.id = ? AND (b.uploaded_by = ? OR EXISTS (
|
|
738
806
|
SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
807
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
739
808
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?))`,
|
|
740
809
|
),
|
|
741
810
|
selectReadableBundleIdsStmt: db.prepare(
|
|
742
811
|
`SELECT b.id FROM managed_bundle b WHERE b.id IN (SELECT value FROM json_each(?)) AND (b.uploaded_by = ? OR EXISTS (
|
|
743
812
|
SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
813
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
744
814
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?))`,
|
|
745
815
|
),
|
|
746
816
|
selectRepoReadableStmt: db.prepare(
|
|
747
817
|
`SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
818
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
748
819
|
WHERE tr.repo_id = ? AND tu.user_id = ? LIMIT 1`,
|
|
749
820
|
),
|
|
750
821
|
selectRepoPathReadableStmt: db.prepare(
|
|
751
822
|
`WITH r(repo_directory) AS (VALUES (?))
|
|
752
823
|
SELECT 1 FROM r JOIN managed_team_repo tr ON tr.repo_id = ? AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
753
|
-
JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
824
|
+
JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
825
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
826
|
+
WHERE tu.user_id = ? LIMIT 1`,
|
|
754
827
|
),
|
|
755
828
|
deleteBundleStmt: db.prepare(`DELETE FROM managed_bundle WHERE id = ?`),
|
|
756
829
|
setBundleRepoStmt: db.prepare(`UPDATE managed_bundle SET repo_id = ?, repo_directory = ? WHERE id = ?`),
|
|
830
|
+
setBundleVisibleStmt: db.prepare(`UPDATE managed_bundle SET visible = ? WHERE id = ?`),
|
|
757
831
|
// Attach a freshly-stored bundle to the reports that declared its integrity
|
|
758
832
|
// but haven't been linked yet (bundle uploaded after the report).
|
|
759
833
|
linkReportsToBundleStmt: db.prepare(
|
|
760
834
|
`UPDATE managed_report AS r SET bundle_id = ? WHERE bundle_integrity = ? AND bundle_id IS NULL
|
|
761
835
|
AND (? IS NULL OR r.uploaded_by = ? OR EXISTS (SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
836
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
762
837
|
WHERE tu.user_id = ? AND tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}))`,
|
|
763
838
|
),
|
|
764
839
|
// OR IGNORE: a duplicate name (UNIQUE) is the "taken" signal (0 changes); the
|
|
@@ -768,30 +843,33 @@ function prepareStatements(db: ManagedSql) {
|
|
|
768
843
|
SELECT id, CASE WHEN EXISTS (SELECT 1 FROM managed_team WHERE slug = candidate.slug) THEN id ELSE slug END,
|
|
769
844
|
?, ?, ? FROM candidate`),
|
|
770
845
|
deleteTeamStmt: db.prepare(`DELETE FROM managed_team WHERE id = ?`),
|
|
771
|
-
selectTeamStmt: db.prepare(`SELECT id, slug, name FROM managed_team WHERE id = ?`),
|
|
846
|
+
selectTeamStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team WHERE id = ?`),
|
|
772
847
|
selectTeamByNameStmt: db.prepare(`SELECT id FROM managed_team WHERE name = ?`),
|
|
773
848
|
renameTeamStmt: db.prepare(`UPDATE managed_team SET name = ?, updated_at = ? WHERE id = ?`),
|
|
774
|
-
|
|
849
|
+
setTeamHiddenStmt: db.prepare(`UPDATE managed_team SET hidden = ?, updated_at = ? WHERE id = ?`),
|
|
850
|
+
selectTeamsStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team ORDER BY name ASC`),
|
|
775
851
|
selectTeamsForUserStmt: db.prepare(
|
|
776
852
|
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
777
853
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
778
854
|
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
779
|
-
WHERE tu.user_id = ? ORDER BY t.name ASC, t.id ASC`,
|
|
855
|
+
WHERE tu.user_id = ? AND t.hidden = 0 ORDER BY t.name ASC, t.id ASC`,
|
|
780
856
|
),
|
|
781
857
|
selectUserTeamScopesStmt: db.prepare(
|
|
782
858
|
`SELECT tr.team_id AS teamId, tr.repo_id AS repoId, tr.path AS path, sr.full_name AS fullName
|
|
783
859
|
FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
860
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
784
861
|
JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
785
862
|
WHERE tu.user_id = ? ORDER BY tr.team_id, tr.repo_id, tr.path`,
|
|
786
863
|
),
|
|
787
864
|
// Reports attached to the repos of the user's teams, tagged by team (a report
|
|
788
865
|
// shows under every team whose repo it's attached to). Newest first.
|
|
789
866
|
selectUserTeamReportsStmt: db.prepare(
|
|
790
|
-
`SELECT DISTINCT tr.team_id AS teamId, r.id AS id, r.slug AS slug, r.filename AS filename, r.analyzer, r.uploaded_at,
|
|
867
|
+
`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,
|
|
791
868
|
r.sha256 AS sha256, r.bundle_id AS bundleId, r.repo_id AS repoId, r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
792
869
|
tu.view_dependencies AS dependencies, tu.view_security AS security,
|
|
793
870
|
(SELECT role FROM managed_user WHERE id = tu.user_id) AS role
|
|
794
871
|
FROM managed_team_user tu
|
|
872
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
795
873
|
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
796
874
|
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
797
875
|
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
@@ -799,33 +877,37 @@ function prepareStatements(db: ManagedSql) {
|
|
|
799
877
|
ORDER BY r.uploaded_at DESC, r.filename ASC, r.id ASC`,
|
|
800
878
|
),
|
|
801
879
|
// Bundles are scoped through the same team -> repository links as reports.
|
|
802
|
-
//
|
|
803
|
-
// team repository scope to appear in that team's sidebar.
|
|
880
|
+
// Hidden bundles remain available to managers within their team scopes.
|
|
804
881
|
selectUserTeamBundlesStmt: db.prepare(
|
|
805
882
|
`SELECT DISTINCT tr.team_id AS teamId, b.id AS id, b.slug AS slug, b.integrity AS integrity,
|
|
806
|
-
b.filename AS filename, b.kind, b.byte_size AS byteSize, b.repo_id AS repoId, b.repo_directory AS repoDirectory, sr.full_name AS repoFullName, b.uploaded_at
|
|
883
|
+
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
|
|
807
884
|
FROM managed_team_user tu
|
|
885
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
808
886
|
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
809
887
|
JOIN managed_bundle b ON b.repo_id = tr.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
810
888
|
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
811
|
-
WHERE tu.user_id = ?
|
|
889
|
+
WHERE tu.user_id = ? AND (b.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))
|
|
812
890
|
ORDER BY b.uploaded_at DESC, b.filename ASC, b.id ASC`,
|
|
813
891
|
),
|
|
814
892
|
selectUserRepoScopesStmt: db.prepare(
|
|
815
893
|
`SELECT DISTINCT tr.repo_id AS repoId, NULLIF(tr.path, '') AS path
|
|
816
|
-
FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
894
|
+
FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
895
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
896
|
+
WHERE tu.user_id = ?`,
|
|
817
897
|
),
|
|
818
898
|
// A report is readable iff one of the user's team scopes contains it.
|
|
819
899
|
selectReportReadableStmt: db.prepare(
|
|
820
900
|
`SELECT 1 FROM managed_report r
|
|
821
901
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
822
902
|
JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
903
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
823
904
|
WHERE r.id = ? AND tu.user_id = ? LIMIT 1`,
|
|
824
905
|
),
|
|
825
906
|
selectBundleSecurityStmt: db.prepare(
|
|
826
907
|
`SELECT 1 FROM managed_bundle b
|
|
827
908
|
JOIN managed_team_repo tr ON tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
828
909
|
JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
910
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
829
911
|
WHERE b.id = ? AND tu.user_id = ? AND tu.view_security = 1
|
|
830
912
|
AND (? IS NULL OR tr.team_id = ?) LIMIT 1`,
|
|
831
913
|
),
|
|
@@ -837,6 +919,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
837
919
|
FROM managed_report r
|
|
838
920
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
839
921
|
JOIN managed_team_user tu ON tu.team_id = tr.team_id AND tu.user_id = ?
|
|
922
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
840
923
|
WHERE r.id = ?`,
|
|
841
924
|
),
|
|
842
925
|
selectUserOptionsStmt: db.prepare(`SELECT id, login, name, role FROM managed_user ORDER BY login ASC`),
|
|
@@ -1007,18 +1090,23 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1007
1090
|
async listFindingCatalogReports(after: string, limit: number): Promise<Pick<ReportRecord, 'id' | 'byteSize'>[]> {
|
|
1008
1091
|
return await stmts.findingCatalogReportsStmt.all(after, limit) as Pick<ReportRecord, 'id' | 'byteSize'>[]
|
|
1009
1092
|
},
|
|
1010
|
-
async getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string): Promise<TeamReportAccessSnapshot | null> {
|
|
1093
|
+
async getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string, reportId: string | null = null): Promise<TeamReportAccessSnapshot | null> {
|
|
1011
1094
|
const session = await stmts.selectSessionStmt.get(sessionId, now) as SessionRow | undefined
|
|
1012
1095
|
if (!session) return null
|
|
1013
1096
|
const user: StoredUser = { id: session.uid, login: session.login, name: session.name, avatarUrl: session.avatar, role: session.role }
|
|
1014
1097
|
if (user.role === 'none' || !await stmts.selectTeamAccessStmt.get(user.id, teamId)) return { user, teamId: null, reports: [], repositories: [] }
|
|
1015
1098
|
const whole = user.role === 'admin' || user.role === 'manage'
|
|
1016
|
-
const rows = await stmts.selectTeamReportAccessStmt.all(user.id, teamId, whole ? 1 : 0) as {
|
|
1099
|
+
const rows = await stmts.selectTeamReportAccessStmt.all(user.id, teamId, whole ? 1 : 0, reportId) as {
|
|
1017
1100
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1018
|
-
repoFullName: string | null; dependencies: number; security: number
|
|
1101
|
+
repoFullName: string | null; dependencies: number; security: number; visible: number
|
|
1019
1102
|
}[]
|
|
1103
|
+
// Only privileged callers can receive a hidden row from this query. Give
|
|
1104
|
+
// that individual preview its own capacity; published report selections
|
|
1105
|
+
// still use the complete workspace for cross-report classification.
|
|
1106
|
+
const hidden = rows.find(row => row.id === reportId && row.visible === 0)
|
|
1107
|
+
const selected = hidden ? [hidden] : rows
|
|
1020
1108
|
const repositories = await stmts.selectTeamRepositoriesStmt.all(teamId) as TeamReportAccessSnapshot['repositories']
|
|
1021
|
-
return { user, teamId, repositories, reports:
|
|
1109
|
+
return { user, teamId, reportId, hiddenReportId: hidden?.id ?? null, repositories, linkRevision: await linkRevision(db), reports: selected.map(row => ({
|
|
1022
1110
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1023
1111
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1024
1112
|
permissions: { dependencies: whole || row.dependencies === 1, security: whole || row.security === 1 },
|
|
@@ -1035,7 +1123,7 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1035
1123
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1036
1124
|
repoFullName: string | null; dependencies: number; security: number
|
|
1037
1125
|
}[]
|
|
1038
|
-
return { user, reports: rows.map(row => ({
|
|
1126
|
+
return { user, linkRevision: await linkRevision(db), reports: rows.map(row => ({
|
|
1039
1127
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1040
1128
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1041
1129
|
permissions: { dependencies: admin || manage || row.dependencies === 1, security: admin || manage || row.security === 1 },
|
|
@@ -1102,6 +1190,8 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1102
1190
|
return JSON.stringify([
|
|
1103
1191
|
await stmts.annotationTriageRevisionStmt.all(ids),
|
|
1104
1192
|
await stmts.annotationCommentRevisionStmt.all(ids),
|
|
1193
|
+
await db.prepare(`SELECT finding_id, issue_url, auto_fix_url FROM managed_finding_issue
|
|
1194
|
+
WHERE finding_id IN (SELECT value FROM json_each(?)) AND issue_url IS NOT NULL ORDER BY finding_id`).all(ids),
|
|
1105
1195
|
])
|
|
1106
1196
|
},
|
|
1107
1197
|
async deleteTriage(findingIds: readonly string[]): Promise<number> {
|
|
@@ -1183,18 +1273,23 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1183
1273
|
}
|
|
1184
1274
|
|
|
1185
1275
|
type BundleRow = {
|
|
1276
|
+
visible: number
|
|
1186
1277
|
id: string; slug: string; integrity: string; filename: string; kind: string | null; byteSize: number
|
|
1187
|
-
uploadedBy: string | null; repoId: number | null; repoDirectory: string; uploadedAt: number
|
|
1278
|
+
uploadedBy: string | null; repoId: number | null; repoDirectory: string; uploadedAt: number; provenance: BundleProvenance | null
|
|
1279
|
+
buildConditions: string | null
|
|
1188
1280
|
}
|
|
1189
1281
|
type BundleListRow = {
|
|
1282
|
+
visible: number
|
|
1190
1283
|
id: string; slug: string; integrity: string; filename: string; kind: string | null; byteSize: number
|
|
1191
1284
|
uploadedByLogin: string | null; repoId: number | null; repoFullName: string | null; repoDirectory: string; uploadedAt: number
|
|
1285
|
+
provenance: BundleProvenance | null
|
|
1192
1286
|
}
|
|
1193
1287
|
|
|
1194
1288
|
function mapBundle(r: BundleRow): ManagedBundle {
|
|
1195
1289
|
return {
|
|
1196
1290
|
id: r.id, slug: r.slug, integrity: r.integrity, filename: r.filename, kind: r.kind,
|
|
1197
|
-
byteSize: r.byteSize, uploadedBy: r.uploadedBy, repoId: r.repoId, repoDirectory: r.repoDirectory, uploadedAt: r.uploadedAt,
|
|
1291
|
+
byteSize: r.byteSize, uploadedBy: r.uploadedBy, repoId: r.repoId, repoDirectory: r.repoDirectory, uploadedAt: r.uploadedAt, visible: r.visible === 1,
|
|
1292
|
+
provenance: r.provenance, buildConditions: r.buildConditions == null ? null : JSON.parse(r.buildConditions) as BundleBuildConditions,
|
|
1198
1293
|
}
|
|
1199
1294
|
}
|
|
1200
1295
|
|
|
@@ -1214,9 +1309,14 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1214
1309
|
await validateStorageUpload(db, key, 'bundle', bundle.id, bundle.dataKey)
|
|
1215
1310
|
await insertBundleStmt.run(
|
|
1216
1311
|
bundle.id, preferredSlug(bundle.id), bundle.integrity, bundle.filename, bundle.kind,
|
|
1217
|
-
bundle.byteSize, bundle.uploadedBy, bundle.uploadedByLogin ?? null, bundle.repoId, bundle.repoId == null ? '' : bundle.repoDirectory ?? '', now, bundle.dataKey ?? null, bundle.dataKey ? 1 : 0,
|
|
1312
|
+
bundle.byteSize, bundle.uploadedBy, bundle.uploadedByLogin ?? null, bundle.repoId, bundle.repoId == null ? '' : bundle.repoDirectory ?? '', bundle.visible === false ? 0 : 1, now, bundle.dataKey ?? null, bundle.dataKey ? 1 : 0,
|
|
1313
|
+
bundle.provenance ?? null, bundle.buildConditions == null ? null : JSON.stringify(bundle.buildConditions),
|
|
1218
1314
|
)
|
|
1219
1315
|
},
|
|
1316
|
+
async reuseBundle(id: string, { provenance, filename, kind, rename, buildConditions }: BundleReuse): Promise<boolean> {
|
|
1317
|
+
return Number((await stmts.reuseBundleStmt.run({ id, provenance, filename, kind, rename: rename ? 1 : 0,
|
|
1318
|
+
buildConditions: buildConditions == null ? null : JSON.stringify(buildConditions) })).changes) > 0
|
|
1319
|
+
},
|
|
1220
1320
|
getBundleByIntegrity,
|
|
1221
1321
|
// The distinct method name selects a writer-locked reconciliation scope.
|
|
1222
1322
|
resolveBundleUpload: getBundleByIntegrity,
|
|
@@ -1232,6 +1332,7 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1232
1332
|
const row = await selectBundleStmt.get(id) as BundleRow | undefined
|
|
1233
1333
|
if (!row) return denied
|
|
1234
1334
|
const manager = roleAtLeast(session.role, 'manage')
|
|
1335
|
+
if (!manager && row.visible !== 1) return denied
|
|
1235
1336
|
if (session.role !== 'admin') {
|
|
1236
1337
|
const allowed = manager ? await selectBundleReadableStmt.get(id, session.uid, session.uid)
|
|
1237
1338
|
: row.repoId !== null && await selectRepoPathReadableStmt.get(row.repoDirectory, row.repoId, session.uid)
|
|
@@ -1245,8 +1346,8 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1245
1346
|
const rows = (await selectBundlesStmt.all(userId ?? null, userId ?? null, userId ?? null)) as BundleListRow[]
|
|
1246
1347
|
return rows.map((r) => ({
|
|
1247
1348
|
id: r.id, slug: r.slug, integrity: r.integrity, filename: r.filename, kind: r.kind, byteSize: r.byteSize,
|
|
1248
|
-
uploadedByLogin: r.uploadedByLogin, repoId: r.repoId, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory,
|
|
1249
|
-
uploadedAt: r.uploadedAt,
|
|
1349
|
+
uploadedByLogin: r.uploadedByLogin, repoId: r.repoId, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory, visible: r.visible === 1,
|
|
1350
|
+
uploadedAt: r.uploadedAt, provenance: r.provenance,
|
|
1250
1351
|
}))
|
|
1251
1352
|
},
|
|
1252
1353
|
async userCanReadBundle(userId: string, id: string): Promise<boolean> {
|
|
@@ -1266,6 +1367,9 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1266
1367
|
async deleteBundle(id: string): Promise<boolean> {
|
|
1267
1368
|
return Number((await deleteBundleStmt.run(id)).changes) > 0
|
|
1268
1369
|
},
|
|
1370
|
+
async setBundleVisible(id: string, visible: boolean): Promise<boolean> {
|
|
1371
|
+
return Number((await stmts.setBundleVisibleStmt.run(visible ? 1 : 0, id)).changes) > 0
|
|
1372
|
+
},
|
|
1269
1373
|
async setBundleRepo(id: string, repoId: number | null, directory?: string): Promise<boolean> {
|
|
1270
1374
|
return Number((await setBundleRepoStmt.run(repoId, repoId == null ? '' : directory ?? '', id)).changes) > 0
|
|
1271
1375
|
},
|
|
@@ -1275,7 +1379,7 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1275
1379
|
}
|
|
1276
1380
|
}
|
|
1277
1381
|
|
|
1278
|
-
type TeamRow = { id: string; slug: string; name: string }
|
|
1382
|
+
type TeamRow = { id: string; slug: string; name: string; hidden: number }
|
|
1279
1383
|
type TeamRepoRow = { teamId: string; repoId: number; fullName: string; path: string | null }
|
|
1280
1384
|
type TeamMemberRow = { teamId: string; userId: string; login: string; viewDependencies: number; viewSecurity: number }
|
|
1281
1385
|
|
|
@@ -1284,7 +1388,7 @@ type TeamMemberRow = { teamId: string; userId: string; login: string; viewDepend
|
|
|
1284
1388
|
// workspace has.
|
|
1285
1389
|
function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql) {
|
|
1286
1390
|
const {
|
|
1287
|
-
insertTeamStmt, selectTeamByNameStmt, renameTeamStmt, deleteTeamStmt, selectTeamStmt,
|
|
1391
|
+
insertTeamStmt, selectTeamByNameStmt, renameTeamStmt, setTeamHiddenStmt, deleteTeamStmt, selectTeamStmt,
|
|
1288
1392
|
selectUserRepoScopesStmt, selectTeamsStmt, selectTeamsForUserStmt, selectUserTeamReportsStmt, selectUserTeamBundlesStmt, selectReportReadableStmt,
|
|
1289
1393
|
selectReportPermsStmt, selectUserOptionsStmt, selectTeamReposStmt, selectTeamMembersStmt,
|
|
1290
1394
|
upsertTeamRepoStmt, deleteTeamRepoStmt, deleteTeamRepoPathStmt, upsertTeamMemberStmt, deleteTeamMemberStmt,
|
|
@@ -1304,9 +1408,12 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1304
1408
|
async deleteTeam(id: string): Promise<boolean> {
|
|
1305
1409
|
return Number((await deleteTeamStmt.run(id)).changes) > 0
|
|
1306
1410
|
},
|
|
1307
|
-
async
|
|
1411
|
+
async setTeamHidden(id: string, hidden: boolean, now: number): Promise<boolean> {
|
|
1412
|
+
return Number((await setTeamHiddenStmt.run(hidden ? 1 : 0, now, id)).changes) > 0
|
|
1413
|
+
},
|
|
1414
|
+
async getTeam(id: string): Promise<{ id: string; slug: string; name: string; hidden: boolean } | null> {
|
|
1308
1415
|
const row = (await selectTeamStmt.get(id)) as TeamRow | undefined
|
|
1309
|
-
return row == null ? null : { id: row.id, slug: row.slug, name: row.name }
|
|
1416
|
+
return row == null ? null : { id: row.id, slug: row.slug, name: row.name, hidden: row.hidden === 1 }
|
|
1310
1417
|
},
|
|
1311
1418
|
async listUserOptions(): Promise<UserOption[]> {
|
|
1312
1419
|
return ((await selectUserOptionsStmt.all()) as UserOption[]).map((u) => ({ id: u.id, login: u.login, name: u.name, role: u.role }))
|
|
@@ -1317,30 +1424,31 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1317
1424
|
async listTeamsForUser(userId: string): Promise<UserTeam[]> {
|
|
1318
1425
|
const teams = (await selectTeamsForUserStmt.all(userId)) as { id: string; slug: string; name: string; dependencies: number; security: number }[]
|
|
1319
1426
|
const scopes = await stmts.selectUserTeamScopesStmt.all(userId) as { teamId: string; repoId: number; path: string; fullName: string }[]
|
|
1427
|
+
const links = await linkRevision(db)
|
|
1320
1428
|
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
1321
1429
|
for (const r of (await selectUserTeamReportsStmt.all(userId)) as {
|
|
1322
1430
|
teamId: string; id: string; slug: string; filename: string; analyzer: string | null; sha256: string; bundleId: string | null; repoId: number
|
|
1323
|
-
repoDirectory: string; repoFullName: string | null; dependencies: number; security: number; role: string
|
|
1431
|
+
repoDirectory: string; repoFullName: string | null; visible: number; dependencies: number; security: number; role: string
|
|
1324
1432
|
}[]) {
|
|
1325
1433
|
const list = reportsByTeam.get(r.teamId) ?? []
|
|
1326
1434
|
const cacheKey = createHash('sha256').update(JSON.stringify([
|
|
1327
|
-
r.sha256, r.bundleId, r.filename, r.repoId, r.repoDirectory, r.repoFullName, r.dependencies, r.security, r.role,
|
|
1435
|
+
r.sha256, r.bundleId, r.filename, r.repoId, r.repoDirectory, r.repoFullName, r.dependencies, r.security, r.role, links,
|
|
1328
1436
|
])).digest('base64url')
|
|
1329
|
-
list.push({ id: r.id, slug: r.slug, filename: r.filename, analyzer: r.analyzer, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory, cacheKey })
|
|
1437
|
+
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 })
|
|
1330
1438
|
reportsByTeam.set(r.teamId, list)
|
|
1331
1439
|
}
|
|
1332
1440
|
const bundlesByTeam = new Map<string, UserTeamBundle[]>()
|
|
1333
|
-
for (const b of (await selectUserTeamBundlesStmt.all(userId)) as (UserTeamBundle & { teamId: string })[]) {
|
|
1441
|
+
for (const b of (await selectUserTeamBundlesStmt.all(userId)) as (Omit<UserTeamBundle, 'visible'> & { teamId: string; visible: number })[]) {
|
|
1334
1442
|
const list = bundlesByTeam.get(b.teamId) ?? []
|
|
1335
|
-
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename,
|
|
1336
|
-
kind: b.kind, byteSize: b.byteSize, repoId: b.repoId, repoDirectory: b.repoDirectory, repoFullName: b.repoFullName })
|
|
1443
|
+
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename, visible: b.visible === 1,
|
|
1444
|
+
kind: b.kind, byteSize: b.byteSize, repoId: b.repoId, repoDirectory: b.repoDirectory, repoFullName: b.repoFullName, provenance: b.provenance })
|
|
1337
1445
|
bundlesByTeam.set(b.teamId, list)
|
|
1338
1446
|
}
|
|
1339
1447
|
return teams.map((t) => ({
|
|
1340
1448
|
id: t.id, slug: t.slug, name: t.name,
|
|
1341
1449
|
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
1342
1450
|
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1343
|
-
t.dependencies, t.security, scopes.filter(scope => scope.teamId === t.id),
|
|
1451
|
+
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
1344
1452
|
])).digest('base64url'),
|
|
1345
1453
|
reports: reportsByTeam.get(t.id) ?? [],
|
|
1346
1454
|
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
@@ -1386,6 +1494,7 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1386
1494
|
}
|
|
1387
1495
|
return teams.map((t) => ({
|
|
1388
1496
|
id: t.id, slug: t.slug, name: t.name,
|
|
1497
|
+
hidden: t.hidden === 1,
|
|
1389
1498
|
repos: reposByTeam.get(t.id) ?? [],
|
|
1390
1499
|
members: membersByTeam.get(t.id) ?? [],
|
|
1391
1500
|
}))
|
|
@@ -1431,7 +1540,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1431
1540
|
|
|
1432
1541
|
const methods: Omit<ManagedDb, keyof ManagementStore | keyof ManagementCatalogStore> = {
|
|
1433
1542
|
...storageMethods(db, key),
|
|
1543
|
+
...linkReportMethods(db, key),
|
|
1544
|
+
...repositoryAliasMethods(db),
|
|
1434
1545
|
...bundleBuildLeaseMethods(db),
|
|
1546
|
+
...upstreamCacheMethods(db),
|
|
1435
1547
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
1436
1548
|
return !!await db.prepare(`INSERT INTO managed_maintenance_lease (id, owner, expires_at) VALUES (?, ?, ?)
|
|
1437
1549
|
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
@@ -1503,7 +1615,14 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1503
1615
|
...activity,
|
|
1504
1616
|
...comments,
|
|
1505
1617
|
async getAnnotations(ids) {
|
|
1506
|
-
return { triage: await triage.listTriage(ids), comments: await comments.listComments(ids) }
|
|
1618
|
+
return { triage: await triage.listTriage(ids), comments: await comments.listComments(ids), issues: await methods.listManagedIssues(ids) }
|
|
1619
|
+
},
|
|
1620
|
+
async applyManagedIssueFixes(sessionId, snapshot, updates) {
|
|
1621
|
+
// Recheck access inside the same write transaction as the derived field.
|
|
1622
|
+
const current = await methods.getTeamReportAccessSnapshot(sessionId, Date.now(), snapshot.teamId!, snapshot.reportId)
|
|
1623
|
+
if (!current?.teamId || JSON.stringify(current) !== JSON.stringify(snapshot)) return false
|
|
1624
|
+
for (const update of updates) await methods.setManagedIssueAutoFix(update.findingId, update.issueUrl, update.previous, update.next, update.checkedAt)
|
|
1625
|
+
return true
|
|
1507
1626
|
},
|
|
1508
1627
|
...workspaceShareMethods(db),
|
|
1509
1628
|
...reports,
|