@preventive/triage 1.0.0-alpha.32 → 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 +417 -201
- package/package.json +23 -17
- package/server-managed/activity.ts +15 -1
- package/server-managed/brotli.ts +3 -3
- 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 +7 -2
- package/server-managed/db-methods.ts +170 -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/object-storage-disk.ts +7 -3
- package/server-managed/object-storage-vercel.ts +6 -2
- package/server-managed/object-storage.ts +3 -2
- package/server-managed/public-workspace.ts +5 -2
- package/server-managed/report-sources.ts +6 -5
- package/server-managed/report-store.ts +47 -0
- 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-encryption.ts +72 -6
- package/server-managed/storage-maintenance.ts +64 -49
- package/server-managed/storage-payload.ts +11 -10
- package/server-managed/storage-stores.ts +3 -1
- 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,18 +337,20 @@ 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
|
|
313
344
|
slug: string
|
|
314
345
|
name: string
|
|
346
|
+
permissions: TeamUserPermissions
|
|
315
347
|
cacheKey?: string
|
|
316
348
|
reports: UserTeamReport[]
|
|
317
349
|
bundles: UserTeamBundle[]
|
|
318
350
|
}
|
|
319
351
|
|
|
320
352
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
321
|
-
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 {
|
|
322
354
|
claimMaintenanceLease(owner: string, now: number, until: number, migration?: boolean): Promise<boolean>
|
|
323
355
|
finishMaintenanceLease(owner: string, until: number): Promise<void>
|
|
324
356
|
getFeedState(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; catalog: number; annotations: number } | null>
|
|
@@ -374,7 +406,7 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
374
406
|
getReport(id: string): Promise<ReportRecord | null>
|
|
375
407
|
// Writer-locked reconciliation waits for an uncertain earlier insert.
|
|
376
408
|
resolveReportUpload(id: string): Promise<ReportRecord | null>
|
|
377
|
-
getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string): Promise<TeamReportAccessSnapshot | null>
|
|
409
|
+
getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string, reportId?: string | null): Promise<TeamReportAccessSnapshot | null>
|
|
378
410
|
getReportAccessSnapshot(sessionId: string, now: number, ids: readonly string[]): Promise<ReportAccessSnapshot | null>
|
|
379
411
|
listReportFilenamesWithBundleHash(bundleId: string, sha256: string): Promise<string[]>
|
|
380
412
|
deleteReport(id: string): Promise<boolean>
|
|
@@ -394,7 +426,8 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
394
426
|
// stamp nor the trail. Every change also appends to managed_finding_triage_event;
|
|
395
427
|
// listTriageHistory walks one finding's trail, newest first.
|
|
396
428
|
listTriage(findingIds: readonly string[]): Promise<TriageRow[]>
|
|
397
|
-
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>
|
|
398
431
|
// A content-free fingerprint of current annotations, read in one snapshot.
|
|
399
432
|
getAnnotationRevision(findingIds: readonly string[]): Promise<string>
|
|
400
433
|
setTriage(findingId: string, entry: TriageEntryPatch | null, updatedBy: string | null, updatedByLogin: string | null, now: number): Promise<void>
|
|
@@ -406,8 +439,14 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
406
439
|
// uploader login + repo; deleteBundle resolves true iff a row was removed
|
|
407
440
|
// (referencing reports' bundle_id null out via the FK). linkReportsToBundle
|
|
408
441
|
// attaches a freshly-stored bundle to the (still-unlinked) reports that
|
|
409
|
-
// 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).
|
|
410
448
|
insertBundle(bundle: BundleInput, now: number, sessionId?: string): Promise<void>
|
|
449
|
+
reuseBundle(id: string, reuse: BundleReuse): Promise<boolean>
|
|
411
450
|
getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
|
|
412
451
|
resolveBundleUpload(integrity: string): Promise<ManagedBundle | null>
|
|
413
452
|
getBundle(id: string): Promise<ManagedBundle | null>
|
|
@@ -426,31 +465,34 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
426
465
|
// Set a bundle's repository and normalized directory (repoId null detaches
|
|
427
466
|
// and clears the directory). The caller validates the location and access.
|
|
428
467
|
setBundleRepo(id: string, repoId: number | null, directory?: string): Promise<boolean>
|
|
468
|
+
setBundleVisible(id: string, visible: boolean): Promise<boolean>
|
|
429
469
|
linkReportsToBundle(integrity: string, bundleId: string, userId?: string): Promise<void>
|
|
430
470
|
// Teams ("Manage teams"). createTeam inserts a team (false iff the name is
|
|
431
471
|
// taken); renameTeam changes a team's name ('name-taken' iff another team
|
|
432
472
|
// already has it, 'not-found' iff no such team, 'ok' otherwise — same name is
|
|
433
473
|
// idempotent); deleteTeam drops it (cascading its links); listTeams returns
|
|
434
|
-
// 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
|
|
435
475
|
// set for the member picker. The set*/remove* pairs maintain the link tables:
|
|
436
476
|
// setTeamRepo adds a path (whole repo replaces paths); setTeamMember upserts a
|
|
437
477
|
// membership + its visibility permissions (each resolves true iff a row was
|
|
438
478
|
// written / removed; the caller validates the team/repo/user exist first).
|
|
439
479
|
createTeam(id: string, name: string, now: number): Promise<boolean>
|
|
440
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>
|
|
441
483
|
deleteTeam(id: string): Promise<boolean>
|
|
442
|
-
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>
|
|
443
485
|
listTeams(): Promise<AdminTeam[]>
|
|
444
486
|
listUserOptions(): Promise<UserOption[]>
|
|
445
487
|
// Current grants for manager content reads, writes, and repository pickers.
|
|
446
488
|
listRepoScopesForUser(userId: string): Promise<{ repoId: number; path: string | null }[]>
|
|
447
|
-
// 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
|
|
448
490
|
// bundles attached to that team's repos — for that user's own sidebar Teams section.
|
|
449
491
|
// Any user; only their own memberships.
|
|
450
492
|
listTeamsForUser(userId: string): Promise<UserTeam[]>
|
|
451
493
|
getUserTeamFeedSnapshot(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; revision: string; teams: UserTeam[]; catalog: number } | null>
|
|
452
494
|
// Whether `userId` may read `reportId`: true iff the report's repo belongs to
|
|
453
|
-
// 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.
|
|
454
496
|
userCanReadReport(userId: string, reportId: string): Promise<boolean>
|
|
455
497
|
// The viewer's effective visibility permissions for a report, OR'd across the
|
|
456
498
|
// teams (holding the report's repo) the user belongs to — drives the content
|
|
@@ -479,6 +521,14 @@ const REPORT_IN_TEAM_PATH_SQL = `(tr.path IS NULL OR tr.path = ''
|
|
|
479
521
|
|
|
480
522
|
const BUNDLE_IN_TEAM_PATH_SQL = REPORT_IN_TEAM_PATH_SQL.replaceAll('r.repo_directory', 'b.repo_directory')
|
|
481
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
|
+
|
|
482
532
|
// Prepare every statement the store uses, returned as a bag the factory
|
|
483
533
|
// destructures — keeps openSqliteManagedDb itself small (one place per query).
|
|
484
534
|
function prepareStatements(db: ManagedSql) {
|
|
@@ -597,6 +647,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
597
647
|
LEFT JOIN managed_bundle b ON b.id = r.bundle_id
|
|
598
648
|
WHERE (? IS NULL OR r.uploaded_by = ? OR EXISTS (
|
|
599
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
|
|
600
651
|
WHERE tr.repo_id = r.repo_id AND tu.user_id = ? AND ${REPORT_IN_TEAM_PATH_SQL}))
|
|
601
652
|
ORDER BY r.uploaded_at DESC, r.filename ASC`,
|
|
602
653
|
),
|
|
@@ -618,6 +669,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
618
669
|
FROM managed_report r JOIN requested q ON q.id = r.id
|
|
619
670
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
620
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
|
|
621
673
|
GROUP BY r.id
|
|
622
674
|
)
|
|
623
675
|
SELECT r.id AS id, r.filename AS filename, r.byte_size AS byteSize, r.sha256 AS sha256,
|
|
@@ -629,21 +681,23 @@ function prepareStatements(db: ManagedSql) {
|
|
|
629
681
|
WHERE ? = 1 OR (? = 1 AND r.uploaded_by = ?)
|
|
630
682
|
OR (g.id IS NOT NULL AND (? = 1 OR r.visible = 1))`,
|
|
631
683
|
),
|
|
632
|
-
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`),
|
|
633
686
|
selectTeamRepositoriesStmt: db.prepare(
|
|
634
687
|
`SELECT tr.repo_id AS repoId, sr.full_name AS github, tr.path AS path
|
|
635
688
|
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
636
689
|
WHERE tr.team_id = ? ORDER BY tr.repo_id, tr.path`,
|
|
637
690
|
),
|
|
638
691
|
selectTeamReportAccessStmt: db.prepare(
|
|
639
|
-
`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,
|
|
640
693
|
r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
641
694
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
642
695
|
FROM managed_team_user tu
|
|
696
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
643
697
|
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
644
698
|
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
645
699
|
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
646
|
-
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 = ?))
|
|
647
701
|
ORDER BY r.id`,
|
|
648
702
|
),
|
|
649
703
|
reportFilenamesWithBundleHashStmt: db.prepare(`SELECT DISTINCT filename FROM managed_report WHERE bundle_id = ? AND sha256 = ?`),
|
|
@@ -706,58 +760,80 @@ function prepareStatements(db: ManagedSql) {
|
|
|
706
760
|
WHERE finding_id IN (SELECT value FROM json_each(?)) ORDER BY id`),
|
|
707
761
|
insertBundleStmt: db.prepare(
|
|
708
762
|
`WITH candidate(id, slug) AS (VALUES (?, ?))
|
|
709
|
-
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)
|
|
710
764
|
SELECT id, CASE WHEN EXISTS (SELECT 1 FROM managed_bundle WHERE slug = candidate.slug) THEN id ELSE slug END,
|
|
711
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? 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))))`,
|
|
712
778
|
),
|
|
713
779
|
selectBundleByIntegrityStmt: db.prepare(
|
|
714
780
|
`SELECT id, slug, integrity, filename, kind, byte_size AS byteSize,
|
|
715
|
-
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
|
|
716
783
|
FROM managed_bundle WHERE integrity = ?`,
|
|
717
784
|
),
|
|
718
785
|
selectBundleStmt: db.prepare(
|
|
719
786
|
`SELECT id, slug, integrity, filename, kind, byte_size AS byteSize,
|
|
720
|
-
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
|
|
721
789
|
FROM managed_bundle WHERE id = ?`,
|
|
722
790
|
),
|
|
723
791
|
selectBundlesStmt: db.prepare(
|
|
724
792
|
`SELECT b.id AS id, b.slug AS slug, b.integrity AS integrity, b.filename AS filename, b.kind AS kind,
|
|
725
793
|
b.byte_size AS byteSize, COALESCE(u.login, b.uploaded_by_login) AS uploadedByLogin,
|
|
726
|
-
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
|
|
727
795
|
FROM managed_bundle b
|
|
728
796
|
LEFT JOIN managed_user u ON u.id = b.uploaded_by
|
|
729
797
|
LEFT JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
730
798
|
WHERE (? IS NULL OR (b.uploaded_by = ? OR EXISTS (
|
|
731
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
|
|
732
801
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?)))
|
|
733
802
|
ORDER BY b.uploaded_at DESC, b.filename ASC`,
|
|
734
803
|
),
|
|
735
804
|
selectBundleReadableStmt: db.prepare(
|
|
736
805
|
`SELECT 1 FROM managed_bundle b WHERE b.id = ? AND (b.uploaded_by = ? OR EXISTS (
|
|
737
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
|
|
738
808
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?))`,
|
|
739
809
|
),
|
|
740
810
|
selectReadableBundleIdsStmt: db.prepare(
|
|
741
811
|
`SELECT b.id FROM managed_bundle b WHERE b.id IN (SELECT value FROM json_each(?)) AND (b.uploaded_by = ? OR EXISTS (
|
|
742
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
|
|
743
814
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?))`,
|
|
744
815
|
),
|
|
745
816
|
selectRepoReadableStmt: db.prepare(
|
|
746
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
|
|
747
819
|
WHERE tr.repo_id = ? AND tu.user_id = ? LIMIT 1`,
|
|
748
820
|
),
|
|
749
821
|
selectRepoPathReadableStmt: db.prepare(
|
|
750
822
|
`WITH r(repo_directory) AS (VALUES (?))
|
|
751
823
|
SELECT 1 FROM r JOIN managed_team_repo tr ON tr.repo_id = ? AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
752
|
-
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`,
|
|
753
827
|
),
|
|
754
828
|
deleteBundleStmt: db.prepare(`DELETE FROM managed_bundle WHERE id = ?`),
|
|
755
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 = ?`),
|
|
756
831
|
// Attach a freshly-stored bundle to the reports that declared its integrity
|
|
757
832
|
// but haven't been linked yet (bundle uploaded after the report).
|
|
758
833
|
linkReportsToBundleStmt: db.prepare(
|
|
759
834
|
`UPDATE managed_report AS r SET bundle_id = ? WHERE bundle_integrity = ? AND bundle_id IS NULL
|
|
760
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
|
|
761
837
|
WHERE tu.user_id = ? AND tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}))`,
|
|
762
838
|
),
|
|
763
839
|
// OR IGNORE: a duplicate name (UNIQUE) is the "taken" signal (0 changes); the
|
|
@@ -767,30 +843,33 @@ function prepareStatements(db: ManagedSql) {
|
|
|
767
843
|
SELECT id, CASE WHEN EXISTS (SELECT 1 FROM managed_team WHERE slug = candidate.slug) THEN id ELSE slug END,
|
|
768
844
|
?, ?, ? FROM candidate`),
|
|
769
845
|
deleteTeamStmt: db.prepare(`DELETE FROM managed_team WHERE id = ?`),
|
|
770
|
-
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 = ?`),
|
|
771
847
|
selectTeamByNameStmt: db.prepare(`SELECT id FROM managed_team WHERE name = ?`),
|
|
772
848
|
renameTeamStmt: db.prepare(`UPDATE managed_team SET name = ?, updated_at = ? WHERE id = ?`),
|
|
773
|
-
|
|
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`),
|
|
774
851
|
selectTeamsForUserStmt: db.prepare(
|
|
775
852
|
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
776
853
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
777
854
|
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
778
|
-
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`,
|
|
779
856
|
),
|
|
780
857
|
selectUserTeamScopesStmt: db.prepare(
|
|
781
858
|
`SELECT tr.team_id AS teamId, tr.repo_id AS repoId, tr.path AS path, sr.full_name AS fullName
|
|
782
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
|
|
783
861
|
JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
784
862
|
WHERE tu.user_id = ? ORDER BY tr.team_id, tr.repo_id, tr.path`,
|
|
785
863
|
),
|
|
786
864
|
// Reports attached to the repos of the user's teams, tagged by team (a report
|
|
787
865
|
// shows under every team whose repo it's attached to). Newest first.
|
|
788
866
|
selectUserTeamReportsStmt: db.prepare(
|
|
789
|
-
`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,
|
|
790
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,
|
|
791
869
|
tu.view_dependencies AS dependencies, tu.view_security AS security,
|
|
792
870
|
(SELECT role FROM managed_user WHERE id = tu.user_id) AS role
|
|
793
871
|
FROM managed_team_user tu
|
|
872
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
794
873
|
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
795
874
|
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
796
875
|
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
@@ -798,33 +877,37 @@ function prepareStatements(db: ManagedSql) {
|
|
|
798
877
|
ORDER BY r.uploaded_at DESC, r.filename ASC, r.id ASC`,
|
|
799
878
|
),
|
|
800
879
|
// Bundles are scoped through the same team -> repository links as reports.
|
|
801
|
-
//
|
|
802
|
-
// team repository scope to appear in that team's sidebar.
|
|
880
|
+
// Hidden bundles remain available to managers within their team scopes.
|
|
803
881
|
selectUserTeamBundlesStmt: db.prepare(
|
|
804
882
|
`SELECT DISTINCT tr.team_id AS teamId, b.id AS id, b.slug AS slug, b.integrity AS integrity,
|
|
805
|
-
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
|
|
806
884
|
FROM managed_team_user tu
|
|
885
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
807
886
|
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
808
887
|
JOIN managed_bundle b ON b.repo_id = tr.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
809
888
|
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
810
|
-
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'))
|
|
811
890
|
ORDER BY b.uploaded_at DESC, b.filename ASC, b.id ASC`,
|
|
812
891
|
),
|
|
813
892
|
selectUserRepoScopesStmt: db.prepare(
|
|
814
893
|
`SELECT DISTINCT tr.repo_id AS repoId, NULLIF(tr.path, '') AS path
|
|
815
|
-
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 = ?`,
|
|
816
897
|
),
|
|
817
898
|
// A report is readable iff one of the user's team scopes contains it.
|
|
818
899
|
selectReportReadableStmt: db.prepare(
|
|
819
900
|
`SELECT 1 FROM managed_report r
|
|
820
901
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
821
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
|
|
822
904
|
WHERE r.id = ? AND tu.user_id = ? LIMIT 1`,
|
|
823
905
|
),
|
|
824
906
|
selectBundleSecurityStmt: db.prepare(
|
|
825
907
|
`SELECT 1 FROM managed_bundle b
|
|
826
908
|
JOIN managed_team_repo tr ON tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
827
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
|
|
828
911
|
WHERE b.id = ? AND tu.user_id = ? AND tu.view_security = 1
|
|
829
912
|
AND (? IS NULL OR tr.team_id = ?) LIMIT 1`,
|
|
830
913
|
),
|
|
@@ -836,6 +919,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
836
919
|
FROM managed_report r
|
|
837
920
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
838
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
|
|
839
923
|
WHERE r.id = ?`,
|
|
840
924
|
),
|
|
841
925
|
selectUserOptionsStmt: db.prepare(`SELECT id, login, name, role FROM managed_user ORDER BY login ASC`),
|
|
@@ -1006,18 +1090,23 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1006
1090
|
async listFindingCatalogReports(after: string, limit: number): Promise<Pick<ReportRecord, 'id' | 'byteSize'>[]> {
|
|
1007
1091
|
return await stmts.findingCatalogReportsStmt.all(after, limit) as Pick<ReportRecord, 'id' | 'byteSize'>[]
|
|
1008
1092
|
},
|
|
1009
|
-
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> {
|
|
1010
1094
|
const session = await stmts.selectSessionStmt.get(sessionId, now) as SessionRow | undefined
|
|
1011
1095
|
if (!session) return null
|
|
1012
1096
|
const user: StoredUser = { id: session.uid, login: session.login, name: session.name, avatarUrl: session.avatar, role: session.role }
|
|
1013
1097
|
if (user.role === 'none' || !await stmts.selectTeamAccessStmt.get(user.id, teamId)) return { user, teamId: null, reports: [], repositories: [] }
|
|
1014
1098
|
const whole = user.role === 'admin' || user.role === 'manage'
|
|
1015
|
-
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 {
|
|
1016
1100
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1017
|
-
repoFullName: string | null; dependencies: number; security: number
|
|
1101
|
+
repoFullName: string | null; dependencies: number; security: number; visible: number
|
|
1018
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
|
|
1019
1108
|
const repositories = await stmts.selectTeamRepositoriesStmt.all(teamId) as TeamReportAccessSnapshot['repositories']
|
|
1020
|
-
return { user, teamId, repositories, reports:
|
|
1109
|
+
return { user, teamId, reportId, hiddenReportId: hidden?.id ?? null, repositories, linkRevision: await linkRevision(db), reports: selected.map(row => ({
|
|
1021
1110
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1022
1111
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1023
1112
|
permissions: { dependencies: whole || row.dependencies === 1, security: whole || row.security === 1 },
|
|
@@ -1034,7 +1123,7 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1034
1123
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1035
1124
|
repoFullName: string | null; dependencies: number; security: number
|
|
1036
1125
|
}[]
|
|
1037
|
-
return { user, reports: rows.map(row => ({
|
|
1126
|
+
return { user, linkRevision: await linkRevision(db), reports: rows.map(row => ({
|
|
1038
1127
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1039
1128
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1040
1129
|
permissions: { dependencies: admin || manage || row.dependencies === 1, security: admin || manage || row.security === 1 },
|
|
@@ -1101,6 +1190,8 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1101
1190
|
return JSON.stringify([
|
|
1102
1191
|
await stmts.annotationTriageRevisionStmt.all(ids),
|
|
1103
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),
|
|
1104
1195
|
])
|
|
1105
1196
|
},
|
|
1106
1197
|
async deleteTriage(findingIds: readonly string[]): Promise<number> {
|
|
@@ -1182,18 +1273,23 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1182
1273
|
}
|
|
1183
1274
|
|
|
1184
1275
|
type BundleRow = {
|
|
1276
|
+
visible: number
|
|
1185
1277
|
id: string; slug: string; integrity: string; filename: string; kind: string | null; byteSize: number
|
|
1186
|
-
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
|
|
1187
1280
|
}
|
|
1188
1281
|
type BundleListRow = {
|
|
1282
|
+
visible: number
|
|
1189
1283
|
id: string; slug: string; integrity: string; filename: string; kind: string | null; byteSize: number
|
|
1190
1284
|
uploadedByLogin: string | null; repoId: number | null; repoFullName: string | null; repoDirectory: string; uploadedAt: number
|
|
1285
|
+
provenance: BundleProvenance | null
|
|
1191
1286
|
}
|
|
1192
1287
|
|
|
1193
1288
|
function mapBundle(r: BundleRow): ManagedBundle {
|
|
1194
1289
|
return {
|
|
1195
1290
|
id: r.id, slug: r.slug, integrity: r.integrity, filename: r.filename, kind: r.kind,
|
|
1196
|
-
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,
|
|
1197
1293
|
}
|
|
1198
1294
|
}
|
|
1199
1295
|
|
|
@@ -1213,9 +1309,14 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1213
1309
|
await validateStorageUpload(db, key, 'bundle', bundle.id, bundle.dataKey)
|
|
1214
1310
|
await insertBundleStmt.run(
|
|
1215
1311
|
bundle.id, preferredSlug(bundle.id), bundle.integrity, bundle.filename, bundle.kind,
|
|
1216
|
-
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),
|
|
1217
1314
|
)
|
|
1218
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
|
+
},
|
|
1219
1320
|
getBundleByIntegrity,
|
|
1220
1321
|
// The distinct method name selects a writer-locked reconciliation scope.
|
|
1221
1322
|
resolveBundleUpload: getBundleByIntegrity,
|
|
@@ -1231,6 +1332,7 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1231
1332
|
const row = await selectBundleStmt.get(id) as BundleRow | undefined
|
|
1232
1333
|
if (!row) return denied
|
|
1233
1334
|
const manager = roleAtLeast(session.role, 'manage')
|
|
1335
|
+
if (!manager && row.visible !== 1) return denied
|
|
1234
1336
|
if (session.role !== 'admin') {
|
|
1235
1337
|
const allowed = manager ? await selectBundleReadableStmt.get(id, session.uid, session.uid)
|
|
1236
1338
|
: row.repoId !== null && await selectRepoPathReadableStmt.get(row.repoDirectory, row.repoId, session.uid)
|
|
@@ -1244,8 +1346,8 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1244
1346
|
const rows = (await selectBundlesStmt.all(userId ?? null, userId ?? null, userId ?? null)) as BundleListRow[]
|
|
1245
1347
|
return rows.map((r) => ({
|
|
1246
1348
|
id: r.id, slug: r.slug, integrity: r.integrity, filename: r.filename, kind: r.kind, byteSize: r.byteSize,
|
|
1247
|
-
uploadedByLogin: r.uploadedByLogin, repoId: r.repoId, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory,
|
|
1248
|
-
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,
|
|
1249
1351
|
}))
|
|
1250
1352
|
},
|
|
1251
1353
|
async userCanReadBundle(userId: string, id: string): Promise<boolean> {
|
|
@@ -1265,6 +1367,9 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1265
1367
|
async deleteBundle(id: string): Promise<boolean> {
|
|
1266
1368
|
return Number((await deleteBundleStmt.run(id)).changes) > 0
|
|
1267
1369
|
},
|
|
1370
|
+
async setBundleVisible(id: string, visible: boolean): Promise<boolean> {
|
|
1371
|
+
return Number((await stmts.setBundleVisibleStmt.run(visible ? 1 : 0, id)).changes) > 0
|
|
1372
|
+
},
|
|
1268
1373
|
async setBundleRepo(id: string, repoId: number | null, directory?: string): Promise<boolean> {
|
|
1269
1374
|
return Number((await setBundleRepoStmt.run(repoId, repoId == null ? '' : directory ?? '', id)).changes) > 0
|
|
1270
1375
|
},
|
|
@@ -1274,7 +1379,7 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1274
1379
|
}
|
|
1275
1380
|
}
|
|
1276
1381
|
|
|
1277
|
-
type TeamRow = { id: string; slug: string; name: string }
|
|
1382
|
+
type TeamRow = { id: string; slug: string; name: string; hidden: number }
|
|
1278
1383
|
type TeamRepoRow = { teamId: string; repoId: number; fullName: string; path: string | null }
|
|
1279
1384
|
type TeamMemberRow = { teamId: string; userId: string; login: string; viewDependencies: number; viewSecurity: number }
|
|
1280
1385
|
|
|
@@ -1283,7 +1388,7 @@ type TeamMemberRow = { teamId: string; userId: string; login: string; viewDepend
|
|
|
1283
1388
|
// workspace has.
|
|
1284
1389
|
function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql) {
|
|
1285
1390
|
const {
|
|
1286
|
-
insertTeamStmt, selectTeamByNameStmt, renameTeamStmt, deleteTeamStmt, selectTeamStmt,
|
|
1391
|
+
insertTeamStmt, selectTeamByNameStmt, renameTeamStmt, setTeamHiddenStmt, deleteTeamStmt, selectTeamStmt,
|
|
1287
1392
|
selectUserRepoScopesStmt, selectTeamsStmt, selectTeamsForUserStmt, selectUserTeamReportsStmt, selectUserTeamBundlesStmt, selectReportReadableStmt,
|
|
1288
1393
|
selectReportPermsStmt, selectUserOptionsStmt, selectTeamReposStmt, selectTeamMembersStmt,
|
|
1289
1394
|
upsertTeamRepoStmt, deleteTeamRepoStmt, deleteTeamRepoPathStmt, upsertTeamMemberStmt, deleteTeamMemberStmt,
|
|
@@ -1303,9 +1408,12 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1303
1408
|
async deleteTeam(id: string): Promise<boolean> {
|
|
1304
1409
|
return Number((await deleteTeamStmt.run(id)).changes) > 0
|
|
1305
1410
|
},
|
|
1306
|
-
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> {
|
|
1307
1415
|
const row = (await selectTeamStmt.get(id)) as TeamRow | undefined
|
|
1308
|
-
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 }
|
|
1309
1417
|
},
|
|
1310
1418
|
async listUserOptions(): Promise<UserOption[]> {
|
|
1311
1419
|
return ((await selectUserOptionsStmt.all()) as UserOption[]).map((u) => ({ id: u.id, login: u.login, name: u.name, role: u.role }))
|
|
@@ -1316,29 +1424,31 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1316
1424
|
async listTeamsForUser(userId: string): Promise<UserTeam[]> {
|
|
1317
1425
|
const teams = (await selectTeamsForUserStmt.all(userId)) as { id: string; slug: string; name: string; dependencies: number; security: number }[]
|
|
1318
1426
|
const scopes = await stmts.selectUserTeamScopesStmt.all(userId) as { teamId: string; repoId: number; path: string; fullName: string }[]
|
|
1427
|
+
const links = await linkRevision(db)
|
|
1319
1428
|
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
1320
1429
|
for (const r of (await selectUserTeamReportsStmt.all(userId)) as {
|
|
1321
1430
|
teamId: string; id: string; slug: string; filename: string; analyzer: string | null; sha256: string; bundleId: string | null; repoId: number
|
|
1322
|
-
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
|
|
1323
1432
|
}[]) {
|
|
1324
1433
|
const list = reportsByTeam.get(r.teamId) ?? []
|
|
1325
1434
|
const cacheKey = createHash('sha256').update(JSON.stringify([
|
|
1326
|
-
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,
|
|
1327
1436
|
])).digest('base64url')
|
|
1328
|
-
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 })
|
|
1329
1438
|
reportsByTeam.set(r.teamId, list)
|
|
1330
1439
|
}
|
|
1331
1440
|
const bundlesByTeam = new Map<string, UserTeamBundle[]>()
|
|
1332
|
-
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 })[]) {
|
|
1333
1442
|
const list = bundlesByTeam.get(b.teamId) ?? []
|
|
1334
|
-
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename,
|
|
1335
|
-
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 })
|
|
1336
1445
|
bundlesByTeam.set(b.teamId, list)
|
|
1337
1446
|
}
|
|
1338
1447
|
return teams.map((t) => ({
|
|
1339
1448
|
id: t.id, slug: t.slug, name: t.name,
|
|
1449
|
+
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
1340
1450
|
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1341
|
-
t.dependencies, t.security, scopes.filter(scope => scope.teamId === t.id),
|
|
1451
|
+
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
1342
1452
|
])).digest('base64url'),
|
|
1343
1453
|
reports: reportsByTeam.get(t.id) ?? [],
|
|
1344
1454
|
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
@@ -1384,6 +1494,7 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1384
1494
|
}
|
|
1385
1495
|
return teams.map((t) => ({
|
|
1386
1496
|
id: t.id, slug: t.slug, name: t.name,
|
|
1497
|
+
hidden: t.hidden === 1,
|
|
1387
1498
|
repos: reposByTeam.get(t.id) ?? [],
|
|
1388
1499
|
members: membersByTeam.get(t.id) ?? [],
|
|
1389
1500
|
}))
|
|
@@ -1429,7 +1540,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1429
1540
|
|
|
1430
1541
|
const methods: Omit<ManagedDb, keyof ManagementStore | keyof ManagementCatalogStore> = {
|
|
1431
1542
|
...storageMethods(db, key),
|
|
1543
|
+
...linkReportMethods(db, key),
|
|
1544
|
+
...repositoryAliasMethods(db),
|
|
1432
1545
|
...bundleBuildLeaseMethods(db),
|
|
1546
|
+
...upstreamCacheMethods(db),
|
|
1433
1547
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
1434
1548
|
return !!await db.prepare(`INSERT INTO managed_maintenance_lease (id, owner, expires_at) VALUES (?, ?, ?)
|
|
1435
1549
|
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
@@ -1501,7 +1615,14 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1501
1615
|
...activity,
|
|
1502
1616
|
...comments,
|
|
1503
1617
|
async getAnnotations(ids) {
|
|
1504
|
-
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
|
|
1505
1626
|
},
|
|
1506
1627
|
...workspaceShareMethods(db),
|
|
1507
1628
|
...reports,
|