@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2WDZPXVL.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-UK7RGJT6.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +362 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +21 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +73 -0
- package/server-managed/comparison.ts +41 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/locked-packages.ts +129 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +215 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +355 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +165 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +156 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +150 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +307 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
2
|
+
import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
3
|
+
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
|
+
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
5
|
+
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
6
|
+
import { GITHUB_LINK_SCHEMA } from './github-links.ts'
|
|
7
|
+
|
|
8
|
+
// Created after the column migration, which predates it on older databases.
|
|
9
|
+
export const VIEW_SESSION_INDEX = 'CREATE INDEX IF NOT EXISTS managed_session_viewer_idx ON managed_session(viewer_session);'
|
|
10
|
+
// What a team's scopes join reports and bundles on: their repository, and the
|
|
11
|
+
// directory in it. Created after the column migrations, as above.
|
|
12
|
+
export const REPO_UPLOAD_INDEXES = `CREATE INDEX IF NOT EXISTS managed_report_repo_idx ON managed_report(repo_id, repo_directory);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS managed_bundle_repo_idx ON managed_bundle(repo_id, repo_directory);`
|
|
14
|
+
|
|
15
|
+
export const MANAGED_SCHEMA = `
|
|
16
|
+
CREATE TABLE IF NOT EXISTS managed_user (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
github_user_id INTEGER NOT NULL UNIQUE,
|
|
19
|
+
login TEXT NOT NULL,
|
|
20
|
+
name TEXT,
|
|
21
|
+
avatar_url TEXT,
|
|
22
|
+
role TEXT NOT NULL DEFAULT 'none',
|
|
23
|
+
-- GitHub user-to-server token, persisted so the repositories page can list
|
|
24
|
+
-- the user's repos (GET /user/repos) on demand. Refresh token + expiry are
|
|
25
|
+
-- null for non-expiring tokens (GitHub App with expiring tokens disabled).
|
|
26
|
+
gh_access_token TEXT,
|
|
27
|
+
gh_refresh_token TEXT,
|
|
28
|
+
gh_token_expires_at INTEGER,
|
|
29
|
+
created_at INTEGER NOT NULL,
|
|
30
|
+
updated_at INTEGER NOT NULL,
|
|
31
|
+
-- Presence is independent of admin edits and background token refreshes.
|
|
32
|
+
-- NULL for legacy accounts until a known session authenticates them.
|
|
33
|
+
last_seen_at INTEGER
|
|
34
|
+
) STRICT;
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS managed_session (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
|
|
39
|
+
csrf_token TEXT NOT NULL,
|
|
40
|
+
created_at INTEGER NOT NULL,
|
|
41
|
+
expires_at INTEGER NOT NULL,
|
|
42
|
+
-- Private X25519 JWK that this session's uploads are sealed to (see
|
|
43
|
+
-- common/managed/upload-seal.ts). Created on first use.
|
|
44
|
+
upload_key TEXT,
|
|
45
|
+
-- An administrator's read-only view as user_id: the admin's own session,
|
|
46
|
+
-- which must stay valid. Ending that session ends the view.
|
|
47
|
+
viewer_session TEXT REFERENCES managed_session(id) ON DELETE CASCADE
|
|
48
|
+
) STRICT;
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS managed_session_user_idx ON managed_session(user_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS managed_session_expires_idx ON managed_session(expires_at);
|
|
52
|
+
|
|
53
|
+
-- Repositories selected to operate on. Keyed by GitHub's numeric repo id
|
|
54
|
+
-- (stable across renames). The row carries everything needed to read the repo's
|
|
55
|
+
-- contents server-side later: installation_id mints an App installation token
|
|
56
|
+
-- (Contents: Read) for PRIVATE repos — NULL means a PUBLIC repo readable without
|
|
57
|
+
-- the App — and full_name + default_branch locate the contents. added_by is the
|
|
58
|
+
-- selector, nulled (not cascaded) if that user is removed so the selection
|
|
59
|
+
-- survives.
|
|
60
|
+
CREATE TABLE IF NOT EXISTS managed_selected_repo (
|
|
61
|
+
repo_id INTEGER PRIMARY KEY,
|
|
62
|
+
full_name TEXT NOT NULL,
|
|
63
|
+
is_private INTEGER NOT NULL,
|
|
64
|
+
installation_id INTEGER,
|
|
65
|
+
default_branch TEXT NOT NULL,
|
|
66
|
+
-- Live browser hint; independent of the default discovered at selection.
|
|
67
|
+
cached_default_branch TEXT,
|
|
68
|
+
html_url TEXT NOT NULL,
|
|
69
|
+
added_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
70
|
+
added_at INTEGER NOT NULL,
|
|
71
|
+
updated_at INTEGER NOT NULL,
|
|
72
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
73
|
+
-- Its pull requests' dependencies reviewed (repository-dependency-reviews.ts):
|
|
74
|
+
-- as many published reviews required of each version, and the commit at
|
|
75
|
+
-- whose lockfiles' versions none is (null for none).
|
|
76
|
+
dependency_review INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
dependency_reviews_required INTEGER NOT NULL DEFAULT 1,
|
|
78
|
+
dependency_review_since TEXT
|
|
79
|
+
) STRICT;
|
|
80
|
+
|
|
81
|
+
CREATE INDEX IF NOT EXISTS selected_repo_full_name_idx ON managed_selected_repo(full_name);
|
|
82
|
+
|
|
83
|
+
-- Bundles uploaded to the server (the "Manage bundles" page). Like reports, the
|
|
84
|
+
-- bytes are stored in the clear (blob-store, keyed by this opaque uuid id) for
|
|
85
|
+
-- the server to operate on. integrity is the content hash (sha512-<base64>,
|
|
86
|
+
-- byte-identical to the client's) so re-uploading the same bytes dedupes (UNIQUE);
|
|
87
|
+
-- it is also the key a report's bundleHashes match to auto-link.
|
|
88
|
+
-- uploaded_by / repo_id are the (nullable) user + repo links, both nulled (not
|
|
89
|
+
-- cascaded) when the referenced user / selected repo goes away.
|
|
90
|
+
CREATE TABLE IF NOT EXISTS managed_bundle (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
slug TEXT NOT NULL,
|
|
93
|
+
integrity TEXT NOT NULL UNIQUE,
|
|
94
|
+
filename TEXT NOT NULL,
|
|
95
|
+
kind TEXT,
|
|
96
|
+
byte_size INTEGER NOT NULL,
|
|
97
|
+
uploaded_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
98
|
+
-- Durable snapshot of the uploader's login at upload time, so "who uploaded
|
|
99
|
+
-- this" survives the uploader being removed (when uploaded_by nulls out).
|
|
100
|
+
uploaded_by_login TEXT,
|
|
101
|
+
repo_id INTEGER REFERENCES managed_selected_repo(repo_id) ON DELETE SET NULL,
|
|
102
|
+
repo_directory TEXT NOT NULL DEFAULT '',
|
|
103
|
+
visible INTEGER NOT NULL DEFAULT 1,
|
|
104
|
+
uploaded_at INTEGER NOT NULL,
|
|
105
|
+
-- 'upload' or 'build' (generated on this server). A server build of bytes
|
|
106
|
+
-- already stored promotes the row to 'build'; NULL predates the column.
|
|
107
|
+
provenance TEXT,
|
|
108
|
+
-- JSON {preset, conditions, platforms} of a JS/TS server build; NULL otherwise.
|
|
109
|
+
build_conditions TEXT
|
|
110
|
+
) STRICT;
|
|
111
|
+
|
|
112
|
+
CREATE INDEX IF NOT EXISTS managed_bundle_uploaded_at_idx ON managed_bundle(uploaded_at);
|
|
113
|
+
|
|
114
|
+
-- Reports uploaded to the server (the "Manage reports" page). A managed server
|
|
115
|
+
-- is TRUSTED, so the bytes are stored in the clear (blob-store, keyed by this
|
|
116
|
+
-- opaque uuid id) for the server to operate on later; this row carries the
|
|
117
|
+
-- metadata + attribution. uploaded_by / repo_id are the (nullable) user + repo
|
|
118
|
+
-- links, nulled (not cascaded) when the user / selected repo goes away. bundle_id
|
|
119
|
+
-- is the (nullable) auto-resolved link to a stored bundle; bundle_integrity is
|
|
120
|
+
-- the report's declared primary bundle (from its bundleHashes), kept so a later
|
|
121
|
+
-- bundle upload of that integrity can re-link.
|
|
122
|
+
CREATE TABLE IF NOT EXISTS managed_report (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
slug TEXT NOT NULL,
|
|
125
|
+
filename TEXT NOT NULL,
|
|
126
|
+
content_type TEXT NOT NULL,
|
|
127
|
+
byte_size INTEGER NOT NULL,
|
|
128
|
+
sha256 TEXT NOT NULL,
|
|
129
|
+
uploaded_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
130
|
+
-- Durable snapshot of the uploader's login (see managed_bundle).
|
|
131
|
+
uploaded_by_login TEXT,
|
|
132
|
+
repo_id INTEGER REFERENCES managed_selected_repo(repo_id) ON DELETE SET NULL,
|
|
133
|
+
repo_directory TEXT NOT NULL DEFAULT '',
|
|
134
|
+
repo_embedded INTEGER NOT NULL DEFAULT 0,
|
|
135
|
+
analyzer TEXT,
|
|
136
|
+
visible INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
bundle_id TEXT REFERENCES managed_bundle(id) ON DELETE SET NULL,
|
|
138
|
+
bundle_integrity TEXT,
|
|
139
|
+
uploaded_at INTEGER NOT NULL
|
|
140
|
+
) STRICT;
|
|
141
|
+
|
|
142
|
+
CREATE INDEX IF NOT EXISTS managed_report_uploaded_at_idx ON managed_report(uploaded_at);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS managed_report_hash_idx ON managed_report(sha256, uploaded_at, id);
|
|
144
|
+
CREATE INDEX IF NOT EXISTS managed_report_bundle_integrity_idx ON managed_report(bundle_integrity);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS managed_report_bundle_hash_idx ON managed_report(bundle_id, sha256);
|
|
146
|
+
|
|
147
|
+
-- Per-finding triage annotations — the managed (trusted-plaintext) counterpart
|
|
148
|
+
-- of the client's localStorage triage map, keyed the same way: by finding id
|
|
149
|
+
-- alone, not by report. Reports mostly repeat one another (a re-scan of the
|
|
150
|
+
-- same code carries the same finding ids) and a finding's triage is shared by
|
|
151
|
+
-- every report that carries it; which ids a viewer may read or write is
|
|
152
|
+
-- decided per report at the endpoint, not here. The wire shape lives in
|
|
153
|
+
-- common/managed/triage.ts. Whole-entry replace, NULL columns are unset;
|
|
154
|
+
-- flagged is tri-state: NULL unset / 1 flagged / 0 an explicit un-flag
|
|
155
|
+
-- tombstone. A cleared entry keeps its row with every field NULL — a
|
|
156
|
+
-- tombstone a reader adopts as "cleared", where a missing row means "never
|
|
157
|
+
-- annotated" (so a stale client copy can't resurrect a teammate's clear).
|
|
158
|
+
-- updated_by is the last writer (nulled when that user is removed);
|
|
159
|
+
-- updated_by_login is the durable login snapshot (see managed_bundle).
|
|
160
|
+
CREATE TABLE IF NOT EXISTS managed_finding_triage (
|
|
161
|
+
finding_id TEXT PRIMARY KEY,
|
|
162
|
+
color TEXT,
|
|
163
|
+
triage TEXT,
|
|
164
|
+
comment TEXT,
|
|
165
|
+
fix TEXT,
|
|
166
|
+
flagged INTEGER,
|
|
167
|
+
updated_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
168
|
+
updated_by_login TEXT,
|
|
169
|
+
updated_at INTEGER NOT NULL
|
|
170
|
+
) STRICT;
|
|
171
|
+
|
|
172
|
+
-- The trail behind managed_finding_triage: one row per write that CHANGED an entry,
|
|
173
|
+
-- holding the entry as written (every field NULL = a clear), who wrote it and
|
|
174
|
+
-- when. managed_finding_triage stays the current-state projection reads hit; this is
|
|
175
|
+
-- walked only for a finding's history. The wire is whole-entry replace, so a
|
|
176
|
+
-- snapshot per write is exactly what arrived and "what changed" is the diff
|
|
177
|
+
-- against the previous row for the same id, computed on read. batch_id groups
|
|
178
|
+
-- the rows of one request. seq is the rowid: a total order that breaks ties
|
|
179
|
+
-- on at. actor_id / actor_login follow the uploaded_by / uploaded_by_login
|
|
180
|
+
-- convention (live account, durable login snapshot). Everything is kept by
|
|
181
|
+
-- default — the trail is the record; an operator may bound it per finding
|
|
182
|
+
-- (TRIAGE_HISTORY_LIMIT), in which case a finding's older events are trimmed
|
|
183
|
+
-- as new ones land. An id no report carries any more is the tombstone GC's
|
|
184
|
+
-- concern.
|
|
185
|
+
CREATE TABLE IF NOT EXISTS managed_finding_triage_event (
|
|
186
|
+
seq INTEGER PRIMARY KEY,
|
|
187
|
+
finding_id TEXT NOT NULL,
|
|
188
|
+
batch_id TEXT NOT NULL,
|
|
189
|
+
color TEXT,
|
|
190
|
+
triage TEXT,
|
|
191
|
+
comment TEXT,
|
|
192
|
+
fix TEXT,
|
|
193
|
+
flagged INTEGER,
|
|
194
|
+
actor_id TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
195
|
+
actor_login TEXT,
|
|
196
|
+
at INTEGER NOT NULL
|
|
197
|
+
) STRICT;
|
|
198
|
+
CREATE INDEX IF NOT EXISTS finding_triage_event_finding_idx ON managed_finding_triage_event(finding_id, seq);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS finding_triage_event_actor_at_idx ON managed_finding_triage_event(actor_id, at);
|
|
200
|
+
|
|
201
|
+
-- Teams group users + repos for access scoping. Hidden teams grant no access;
|
|
202
|
+
-- the two link tables below carry the many-many relations.
|
|
203
|
+
CREATE TABLE IF NOT EXISTS managed_team (
|
|
204
|
+
id TEXT PRIMARY KEY,
|
|
205
|
+
slug TEXT NOT NULL,
|
|
206
|
+
name TEXT NOT NULL UNIQUE,
|
|
207
|
+
hidden INTEGER NOT NULL DEFAULT 0,
|
|
208
|
+
dependency_review INTEGER NOT NULL DEFAULT 0,
|
|
209
|
+
created_at INTEGER NOT NULL,
|
|
210
|
+
updated_at INTEGER NOT NULL
|
|
211
|
+
) STRICT;
|
|
212
|
+
|
|
213
|
+
-- Team <-> repo, many-many, with an OPTIONAL path (a subpath of the repo the
|
|
214
|
+
-- team is scoped to; empty path = the whole repo). Distinct paths can coexist;
|
|
215
|
+
-- adding the whole repo replaces them. CASCADE removes links with either side. repo_id
|
|
216
|
+
-- references the selected (operate-on) repos.
|
|
217
|
+
CREATE TABLE IF NOT EXISTS managed_team_repo (
|
|
218
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
219
|
+
repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
|
|
220
|
+
path TEXT NOT NULL DEFAULT '',
|
|
221
|
+
PRIMARY KEY (team_id, repo_id, path)
|
|
222
|
+
) STRICT;
|
|
223
|
+
|
|
224
|
+
CREATE INDEX IF NOT EXISTS team_repo_repo_idx ON managed_team_repo(repo_id);
|
|
225
|
+
|
|
226
|
+
-- Team <-> user, many-many, with per-membership visibility permissions (see
|
|
227
|
+
-- common/managed/permissions.ts) — view_dependencies / view_security, both
|
|
228
|
+
-- default 0 (off). CASCADE so the membership dies with either side.
|
|
229
|
+
CREATE TABLE IF NOT EXISTS managed_team_user (
|
|
230
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
231
|
+
user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
|
|
232
|
+
view_dependencies INTEGER NOT NULL DEFAULT 0,
|
|
233
|
+
view_security INTEGER NOT NULL DEFAULT 0,
|
|
234
|
+
PRIMARY KEY (team_id, user_id)
|
|
235
|
+
) STRICT;
|
|
236
|
+
|
|
237
|
+
CREATE INDEX IF NOT EXISTS team_user_user_idx ON managed_team_user(user_id);
|
|
238
|
+
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA + GITHUB_LINK_SCHEMA
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Table identifiers are code constants. Renames retain rows and foreign keys;
|
|
2
|
+
// an ambiguous old/new pair must be resolved before the server can start.
|
|
3
|
+
const MANAGED_TABLE_RENAMES = [
|
|
4
|
+
['selected_repo', 'managed_selected_repo'],
|
|
5
|
+
['team_repo', 'managed_team_repo'],
|
|
6
|
+
['team_user', 'managed_team_user'],
|
|
7
|
+
['finding_triage', 'managed_finding_triage'],
|
|
8
|
+
['finding_triage_event', 'managed_finding_triage_event'],
|
|
9
|
+
['finding_comment', 'managed_finding_comment'],
|
|
10
|
+
['finding_comment_event', 'managed_finding_comment_event'],
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
export function managedTableRenames(tables: Set<string>) {
|
|
14
|
+
// A fresh installation must not claim similarly named unrelated tables in
|
|
15
|
+
// a shared database. managed_user anchors an existing managed installation.
|
|
16
|
+
if (!tables.has('managed_user')) return []
|
|
17
|
+
const renames = MANAGED_TABLE_RENAMES.filter(([from]) => tables.has(from))
|
|
18
|
+
for (const [from, to] of renames) {
|
|
19
|
+
if (tables.has(to)) throw new Error(`Cannot migrate managed tables: both ${from} and ${to} exist`)
|
|
20
|
+
}
|
|
21
|
+
return renames
|
|
22
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SQLite bootstrap and legacy upgrades. Runtime methods are shared with Neon.
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
import { migrateSlugs } from './slugs.ts'
|
|
6
|
+
import { migrateReportLocations } from './report-migration.ts'
|
|
7
|
+
import { initCommentMethods } from './comments.ts'
|
|
8
|
+
import { initActivityMethods } from './activity.ts'
|
|
9
|
+
import { revisionSchema } from './revisions.ts'
|
|
10
|
+
import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
|
+
import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
12
|
+
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
13
|
+
import { DEPENDENCY_REVIEW_COLUMNS } from './dependency-reviews.ts'
|
|
14
|
+
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
15
|
+
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
16
|
+
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
17
|
+
import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
|
|
18
|
+
import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
|
|
19
|
+
import { managedTableRenames } from './db-table-names.ts'
|
|
20
|
+
import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
|
|
21
|
+
import { createSqliteDriver, scopeManagedMethods } from './sql.ts'
|
|
22
|
+
export type * from './db-methods.ts'
|
|
23
|
+
|
|
24
|
+
function migrateManagedTableNames(db: DatabaseSync): void {
|
|
25
|
+
db.exec('BEGIN IMMEDIATE')
|
|
26
|
+
try {
|
|
27
|
+
db.exec('CREATE TABLE IF NOT EXISTS managed_schema_version (version INTEGER PRIMARY KEY) STRICT;')
|
|
28
|
+
if (!db.prepare('SELECT version FROM managed_schema_version WHERE version = 3').get()) {
|
|
29
|
+
const rows = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all() as { name: string }[]
|
|
30
|
+
for (const [from, to] of managedTableRenames(new Set(rows.map(row => row.name)))) {
|
|
31
|
+
db.exec(`ALTER TABLE ${from} RENAME TO ${to}`)
|
|
32
|
+
}
|
|
33
|
+
db.exec('INSERT INTO managed_schema_version VALUES (3)')
|
|
34
|
+
}
|
|
35
|
+
db.exec('COMMIT')
|
|
36
|
+
} catch (err) { db.exec('ROLLBACK'); throw err }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Preserve the legacy single-path links while adding path to their identity.
|
|
40
|
+
function migrateTeamRepoPaths(db: DatabaseSync): void {
|
|
41
|
+
const columns = db.prepare('PRAGMA table_info(managed_team_repo)').all() as { name: string; pk: number }[]
|
|
42
|
+
if (columns.some(column => column.name === 'path' && column.pk === 3)) return
|
|
43
|
+
db.exec('BEGIN IMMEDIATE')
|
|
44
|
+
try {
|
|
45
|
+
db.exec(`CREATE TABLE managed_team_repo_paths (
|
|
46
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
47
|
+
repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
|
|
48
|
+
path TEXT NOT NULL DEFAULT '', PRIMARY KEY (team_id, repo_id, path)
|
|
49
|
+
) STRICT;
|
|
50
|
+
INSERT INTO managed_team_repo_paths SELECT team_id, repo_id, COALESCE(path, '') FROM managed_team_repo;
|
|
51
|
+
DROP TABLE managed_team_repo;
|
|
52
|
+
ALTER TABLE managed_team_repo_paths RENAME TO managed_team_repo;
|
|
53
|
+
CREATE INDEX team_repo_repo_idx ON managed_team_repo(repo_id);`)
|
|
54
|
+
db.exec('COMMIT')
|
|
55
|
+
} catch (err) { db.exec('ROLLBACK'); throw err }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add `column` to `table` if it's missing (a lightweight migration for DBs that
|
|
59
|
+
// predate the column; `table`/`column` are code constants, never user input).
|
|
60
|
+
function ensureColumn(db: DatabaseSync, table: string, column: string, type: string): boolean {
|
|
61
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[]
|
|
62
|
+
if (cols.some((c) => c.name === column)) return false
|
|
63
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`)
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}): ManagedDb {
|
|
68
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
69
|
+
const db = new DatabaseSync(path)
|
|
70
|
+
try {
|
|
71
|
+
db.exec('PRAGMA journal_mode = WAL;')
|
|
72
|
+
db.exec('PRAGMA synchronous = FULL;')
|
|
73
|
+
db.exec('PRAGMA foreign_keys = ON;')
|
|
74
|
+
migrateManagedTableNames(db)
|
|
75
|
+
db.exec(MANAGED_SCHEMA + GITHUB_METADATA_SCHEMA + GITHUB_COMMIT_SCHEMA + MANAGED_ISSUE_SCHEMA + STORAGE_SCHEMA + BUNDLE_BUILD_LEASE_SCHEMA + UPSTREAM_CACHE_SCHEMA)
|
|
76
|
+
ensureColumn(db, 'managed_selected_repo', 'cached_default_branch', 'TEXT')
|
|
77
|
+
ensureColumn(db, 'managed_session', 'upload_key', 'TEXT')
|
|
78
|
+
ensureColumn(db, 'managed_session', 'viewer_session', 'TEXT REFERENCES managed_session(id) ON DELETE CASCADE')
|
|
79
|
+
db.exec(VIEW_SESSION_INDEX)
|
|
80
|
+
for (const table of ['managed_report', 'managed_bundle']) {
|
|
81
|
+
ensureColumn(db, table, 'data_key', 'TEXT')
|
|
82
|
+
ensureColumn(db, table, 'storage_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
83
|
+
}
|
|
84
|
+
ensureColumn(db, 'managed_user', 'gh_tokens_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
85
|
+
// Existing names are the sign-in accounts' own: none was chosen yet.
|
|
86
|
+
for (const [column, type] of GITHUB_LINK_USER_COLUMNS) {
|
|
87
|
+
if (ensureColumn(db, 'managed_user', column, type) && column === 'github_name') db.exec('UPDATE managed_user SET github_name = name')
|
|
88
|
+
}
|
|
89
|
+
ensureColumn(db, 'managed_github_metadata', 'state_reason', GITHUB_STATE_REASON_COLUMN)
|
|
90
|
+
ensureColumn(db, 'managed_github_metadata', 'attempted_at', 'INTEGER')
|
|
91
|
+
// Existing public links must opt in too; never preserve an implicit grant.
|
|
92
|
+
ensureColumn(db, 'managed_workspace_share', 'dependencies', 'INTEGER NOT NULL DEFAULT 0')
|
|
93
|
+
ensureColumn(db, 'managed_workspace_share', 'security', 'INTEGER NOT NULL DEFAULT 0')
|
|
94
|
+
migrateTeamRepoPaths(db)
|
|
95
|
+
migrateSlugs(db)
|
|
96
|
+
// Migrate DBs created before a column existed (CREATE TABLE IF NOT EXISTS
|
|
97
|
+
// never alters an already-present table). Idempotent — skipped on fresh DBs.
|
|
98
|
+
const addedLastSeen = ensureColumn(db, 'managed_user', 'last_seen_at', 'INTEGER')
|
|
99
|
+
if (addedLastSeen) {
|
|
100
|
+
// Session creation is known authentication activity. updated_at is not:
|
|
101
|
+
// role changes and token refreshes also wrote it. Keep unknowns NULL.
|
|
102
|
+
db.exec(`UPDATE managed_user SET last_seen_at = (
|
|
103
|
+
SELECT MAX(s.created_at) FROM managed_session s WHERE s.user_id = managed_user.id
|
|
104
|
+
)`)
|
|
105
|
+
}
|
|
106
|
+
ensureColumn(db, 'managed_report', 'uploaded_by_login', 'TEXT')
|
|
107
|
+
ensureColumn(db, 'managed_report', 'repo_embedded', 'INTEGER NOT NULL DEFAULT 0')
|
|
108
|
+
migrateReportLocations(db, join(dirname(path), 'reports'))
|
|
109
|
+
ensureColumn(db, 'managed_report', 'analyzer', 'TEXT')
|
|
110
|
+
// Existing rows predate publication controls and were already visible.
|
|
111
|
+
// Backfill only when adding the column; newly inserted rows still use the
|
|
112
|
+
// hidden default and must be explicitly published.
|
|
113
|
+
const addedVisible = ensureColumn(db, 'managed_report', 'visible', 'INTEGER NOT NULL DEFAULT 0')
|
|
114
|
+
if (addedVisible) db.prepare('UPDATE managed_report SET visible = 1 WHERE visible = 0').run()
|
|
115
|
+
ensureColumn(db, 'managed_bundle', 'uploaded_by_login', 'TEXT')
|
|
116
|
+
ensureColumn(db, 'managed_bundle', 'repo_directory', "TEXT NOT NULL DEFAULT ''")
|
|
117
|
+
db.exec(REPO_UPLOAD_INDEXES)
|
|
118
|
+
ensureColumn(db, 'managed_bundle', 'visible', 'INTEGER NOT NULL DEFAULT 1')
|
|
119
|
+
// Existing rows stay NULL (unknown): some may have been built on the server.
|
|
120
|
+
ensureColumn(db, 'managed_bundle', 'provenance', 'TEXT')
|
|
121
|
+
ensureColumn(db, 'managed_bundle', 'build_conditions', 'TEXT')
|
|
122
|
+
ensureColumn(db, 'managed_team', 'hidden', 'INTEGER NOT NULL DEFAULT 0')
|
|
123
|
+
ensureColumn(db, 'managed_team', 'dependency_review', 'INTEGER NOT NULL DEFAULT 0')
|
|
124
|
+
ensureColumn(db, 'managed_finding_issue', 'auto_fix_url', 'TEXT')
|
|
125
|
+
ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
|
|
126
|
+
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|
|
127
|
+
for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
|
|
128
|
+
for (const [column, type] of DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_dependency_review', column, type)
|
|
129
|
+
ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
|
|
130
|
+
ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
|
|
131
|
+
ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
|
|
132
|
+
initCommentMethods(db)
|
|
133
|
+
initActivityMethods(db)
|
|
134
|
+
db.exec(revisionSchema())
|
|
135
|
+
} catch (err) {
|
|
136
|
+
try { db.close() } catch {}
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const driver = createSqliteDriver(db)
|
|
141
|
+
return scopeManagedMethods(createManagedMethods(driver, options), driver)
|
|
142
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// A repository's Dependabot alerts for a repository risk scan, read with the
|
|
2
|
+
// App's installation token where the admin's own access lets them see them on
|
|
3
|
+
// GitHub, as doc/bundles.md's repository risk scan section tells.
|
|
4
|
+
import type { ManagedConfig } from './config.ts'
|
|
5
|
+
import type { SelectedRepo } from './db.ts'
|
|
6
|
+
import { type DependabotAlert, dependabotAlerts } from './dependabot-alerts.ts'
|
|
7
|
+
import { GithubApiError, type GithubIdentity, githubRepoPermission, repoAccessToken, repositoryInstallation } from './github-app.ts'
|
|
8
|
+
|
|
9
|
+
// The levels GitHub lets see a repository's Dependabot alerts by default
|
|
10
|
+
// (maintain's reads `write`).
|
|
11
|
+
const SEES_ALERTS: ReadonlySet<string> = new Set(['admin', 'write'])
|
|
12
|
+
// Failures of verifying the sign-in that are the sign-in's own: no token, or
|
|
13
|
+
// one of another account (repository-browser.ts getIdentity), or one GitHub no
|
|
14
|
+
// longer takes. Any other, as GitHub failing to answer, is told as it is.
|
|
15
|
+
const UNVERIFIED: ReadonlySet<string> = new Set(['no-repository', 'github-unauthorized'])
|
|
16
|
+
|
|
17
|
+
// What of a repository's reader (repository-browser.ts) the alerts are asked
|
|
18
|
+
// with: the user's verified identity, and their permission level where its
|
|
19
|
+
// access gate asked GitHub of it already.
|
|
20
|
+
export interface AlertsReader {
|
|
21
|
+
identity(): Promise<GithubIdentity>
|
|
22
|
+
permission(): string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Alerts read, and their `recheck`: the user's access to them checked again,
|
|
26
|
+
// asked of GitHub afresh, just before they are told (repository-risk.ts), as
|
|
27
|
+
// access to the repository is, as it may be withdrawn meanwhile.
|
|
28
|
+
export interface AlertsRead { alerts: DependabotAlert[]; recheck(): Promise<void> }
|
|
29
|
+
|
|
30
|
+
// `repo`'s Dependabot alerts (dependabot-alerts.ts), read with the token of
|
|
31
|
+
// the App's installation GitHub tells covers it now, its token checked to read
|
|
32
|
+
// it (github-app.ts repositoryInstallation), never the one its connection
|
|
33
|
+
// keeps, which may no longer cover it, once the user's own permission on it
|
|
34
|
+
// lets them see them; else refused, the alerts not asked:
|
|
35
|
+
// `dependabot-not-installed`, `dependabot-unverified`, or
|
|
36
|
+
// `dependabot-forbidden`, naming their permission. The token is asked each
|
|
37
|
+
// time of repoAccessToken, which renews it before it expires, as the recheck
|
|
38
|
+
// may come long after the alerts are read.
|
|
39
|
+
export async function appDependabotAlerts(config: ManagedConfig, repo: Pick<SelectedRepo, 'repoId' | 'fullName'>, reader: AlertsReader,
|
|
40
|
+
fetchImpl: typeof fetch = globalThis.fetch): Promise<AlertsRead> {
|
|
41
|
+
const id = await repositoryInstallation(config, repo.repoId, repo.fullName, fetchImpl)
|
|
42
|
+
const tokenNow = async () => {
|
|
43
|
+
const token = await repoAccessToken(config, id, fetchImpl)
|
|
44
|
+
if (token == null) throw new GithubApiError(403, 'dependabot-not-installed')
|
|
45
|
+
return token
|
|
46
|
+
}
|
|
47
|
+
const token = await tokenNow()
|
|
48
|
+
let who
|
|
49
|
+
try { who = await reader.identity() } catch (error) {
|
|
50
|
+
throw error instanceof GithubApiError && UNVERIFIED.has(error.message) ? new GithubApiError(403, 'dependabot-unverified') : error
|
|
51
|
+
}
|
|
52
|
+
const seen = (permission: string) => {
|
|
53
|
+
if (!SEES_ALERTS.has(permission)) {
|
|
54
|
+
throw new GithubApiError(403, 'dependabot-forbidden', null, `@${who.login}'s access to this repository (${permission}) doesn't let them see its Dependabot alerts on GitHub`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
seen(reader.permission() ?? await githubRepoPermission(repo.fullName, token, who, fetchImpl))
|
|
58
|
+
const alerts = await dependabotAlerts(repo.fullName, token, fetchImpl)
|
|
59
|
+
return { alerts, recheck: async () => { seen(await githubRepoPermission(repo.fullName, await tokenNow(), who, fetchImpl)) } }
|
|
60
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// A repository's Dependabot alerts of npm's ecosystem, open or dismissed, as a
|
|
2
|
+
// repository risk scan blends them into npm's advisories (repository-risk.ts),
|
|
3
|
+
// a dismissed one with who dismissed it, when and why: GitHub's
|
|
4
|
+
// `GET /repos/:repo/dependabot/alerts`, a page of 100 at a time, each found by
|
|
5
|
+
// the cursor the one before's Link header names, MAX_PAGES at most. Asked with
|
|
6
|
+
// the App's installation token, once the admin's own access to the repository
|
|
7
|
+
// is found to let them see the alerts on GitHub (dependabot-access.ts): where
|
|
8
|
+
// the installation hasn't Dependabot alerts: read, or the repository has them
|
|
9
|
+
// off, that is thrown as its code, never GitHub's text, for the scan to tell
|
|
10
|
+
// and go on.
|
|
11
|
+
import { isExactVersion, satisfies, validRange } from '@preventive/upstream/semver.js'
|
|
12
|
+
import { isNpmPackageName } from '../common/managed/npm-packages.js'
|
|
13
|
+
import { GithubApiError, githubUnreachable, throwGithubResponseError } from './github-errors.ts'
|
|
14
|
+
import type { LockedPackage } from './locked-packages.ts'
|
|
15
|
+
|
|
16
|
+
// An alert, as GitHub has it: its `number` in its repository and its page
|
|
17
|
+
// there (`url`, built, never fetched), the manifest or lockfile it is of
|
|
18
|
+
// (`manifest`, from the repository's root), its package and advisory, the
|
|
19
|
+
// versions it affects as GitHub writes them (`range`, `>= 1.0.0, < 1.2.6`)
|
|
20
|
+
// and the first that isn't (`patched`), `dev` where GitHub tells the
|
|
21
|
+
// dependency is only for development, and `dismissed` where it is: by whom,
|
|
22
|
+
// when, why and what they said, or by one of the repository's rules (`auto`).
|
|
23
|
+
export interface DependabotAlert {
|
|
24
|
+
number: number
|
|
25
|
+
url: string
|
|
26
|
+
manifest: string
|
|
27
|
+
name: string
|
|
28
|
+
ghsa: string
|
|
29
|
+
// The advisory's other ids, as its CVE.
|
|
30
|
+
aliases: string[]
|
|
31
|
+
title?: string
|
|
32
|
+
// `critical`, `high`, `moderate`, `low`: GitHub's `medium` reads `moderate`.
|
|
33
|
+
severity?: string
|
|
34
|
+
range?: string
|
|
35
|
+
patched?: string
|
|
36
|
+
cwe: string[]
|
|
37
|
+
dev?: true
|
|
38
|
+
dismissed?: AlertDismissal
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GitHub's `dismissed_reason`: `fix_started`, `inaccurate`, `no_bandwidth`,
|
|
42
|
+
// `not_used` or `tolerable_risk`; `at` as GitHub writes it (ISO 8601).
|
|
43
|
+
export interface AlertDismissal { auto?: true; reason?: string; at?: string; by?: string; comment?: string }
|
|
44
|
+
|
|
45
|
+
const API = 'https://api.github.com'
|
|
46
|
+
const API_VERSION = '2022-11-28'
|
|
47
|
+
const USER_AGENT = 'deepview-triage'
|
|
48
|
+
const PER_PAGE = 100
|
|
49
|
+
// Up to how many pages a listing asks: 2,000 alerts of npm's in a repository,
|
|
50
|
+
// past which the rest are left out; where open and dismissed ones are more,
|
|
51
|
+
// the open ones are listed again on their own (dependabotAlerts).
|
|
52
|
+
const MAX_PAGES = 20
|
|
53
|
+
// A page holds each advisory's whole text: given longer than githubJson's.
|
|
54
|
+
const PAGE_TIMEOUT_MS = 20_000
|
|
55
|
+
const GHSA = /^GHSA(?:-[23456789cfghjmpqrvwx]{4}){3}$/u
|
|
56
|
+
const SEVERITIES = new Map([['critical', 'critical'], ['high', 'high'], ['medium', 'moderate'], ['moderate', 'moderate'], ['low', 'low']])
|
|
57
|
+
// A path from a repository's root, as git takes one: no empty, `.` or `..`
|
|
58
|
+
// component, nor a control character.
|
|
59
|
+
const isRepoPath = (path: unknown): path is string => typeof path === 'string' && path.length <= 1024 && !/\p{Cc}/u.test(path)
|
|
60
|
+
&& path.split('/').every(part => part !== '' && part !== '.' && part !== '..')
|
|
61
|
+
|
|
62
|
+
// The states asked: a fixed alert's dependency is no longer what it was.
|
|
63
|
+
const STATES = ['open', 'dismissed', 'auto_dismissed']
|
|
64
|
+
const REASONS = new Set(['fix_started', 'inaccurate', 'no_bandwidth', 'not_used', 'tolerable_risk'])
|
|
65
|
+
const LOGIN = /^[\dA-Za-z](?:[\dA-Za-z-]{0,38})(?:\[bot\])?$/u
|
|
66
|
+
|
|
67
|
+
const text = (value: unknown, most = 500) => typeof value === 'string' && value.trim() ? value.trim().slice(0, most) : undefined
|
|
68
|
+
|
|
69
|
+
// How a dismissed alert was dismissed, as far as GitHub tells it soundly.
|
|
70
|
+
function dismissalOf(state: string, raw: Record<string, any>): AlertDismissal {
|
|
71
|
+
const at = state === 'auto_dismissed' ? raw['auto_dismissed_at'] : raw['dismissed_at']
|
|
72
|
+
const when = typeof at === 'string' && at.length <= 40 && !Number.isNaN(Date.parse(at)) ? { at } : {}
|
|
73
|
+
if (state === 'auto_dismissed') return { auto: true, ...when }
|
|
74
|
+
const by = raw['dismissed_by']?.login, comment = text(raw['dismissed_comment'], 280), reason = raw['dismissed_reason']
|
|
75
|
+
return { ...REASONS.has(reason) ? { reason } : {}, ...when, ...typeof by === 'string' && LOGIN.test(by) ? { by } : {}, ...comment === undefined ? {} : { comment } }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// An alert as GitHub answers it, or null for one that isn't an open or
|
|
79
|
+
// dismissed one of npm's, of a package and GHSA npm and GitHub would name.
|
|
80
|
+
function alertOf(fullName: string, raw: unknown): DependabotAlert | null {
|
|
81
|
+
if (raw == null || typeof raw !== 'object') return null
|
|
82
|
+
const { number, state, dependency, security_advisory: advisory, security_vulnerability: vulnerability } = raw as Record<string, any>
|
|
83
|
+
if (!Number.isSafeInteger(number) || number <= 0 || !STATES.includes(state)) return null
|
|
84
|
+
const ghsa = advisory?.ghsa_id, manifest = dependency?.manifest_path, name = dependency?.package?.name
|
|
85
|
+
if (dependency?.package?.ecosystem !== 'npm' || !isNpmPackageName(name) || !isRepoPath(manifest) || typeof ghsa !== 'string' || !GHSA.test(ghsa)) return null
|
|
86
|
+
const severity = SEVERITIES.get(vulnerability?.severity ?? advisory?.severity)
|
|
87
|
+
const patched = text(vulnerability?.first_patched_version?.identifier, 100), range = text(vulnerability?.vulnerable_version_range, 200), title = text(advisory?.summary)
|
|
88
|
+
const ids = Array.isArray(advisory?.identifiers) ? advisory.identifiers as { value?: unknown }[] : []
|
|
89
|
+
const cwes = Array.isArray(advisory?.cwes) ? advisory.cwes as { cwe_id?: unknown }[] : []
|
|
90
|
+
return {
|
|
91
|
+
number, url: `https://github.com/${fullName.split('/').map(encodeURIComponent).join('/')}/security/dependabot/${number}`, manifest, name, ghsa,
|
|
92
|
+
aliases: [...new Set(ids.flatMap(id => typeof id?.value === 'string' && /^[\w.:-]{1,64}$/u.test(id.value) && id.value !== ghsa ? [id.value] : []))],
|
|
93
|
+
...title === undefined ? {} : { title }, ...severity === undefined ? {} : { severity },
|
|
94
|
+
...range === undefined ? {} : { range }, ...patched === undefined ? {} : { patched },
|
|
95
|
+
cwe: [...new Set(cwes.flatMap(cwe => typeof cwe?.cwe_id === 'string' && /^CWE-\d{1,6}$/u.test(cwe.cwe_id) ? [cwe.cwe_id] : []))],
|
|
96
|
+
...dependency.scope === 'development' ? { dev: true as const } : {},
|
|
97
|
+
...state === 'open' ? {} : { dismissed: dismissalOf(state, raw as Record<string, any>) },
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The next page's cursor, as the Link header's `next` names it: its `after`,
|
|
102
|
+
// or its `page`, all that is taken of it, the rest of its URL left as asked.
|
|
103
|
+
function nextPage(link: string | null): [string, string] | null {
|
|
104
|
+
const next = link?.split(',').map(part => /^\s*<([^>]*)>\s*;\s*rel="next"\s*$/u.exec(part)?.[1]).find(Boolean)
|
|
105
|
+
if (!next) return null
|
|
106
|
+
let url
|
|
107
|
+
try { url = new URL(next) } catch { return null }
|
|
108
|
+
if (url.origin !== API) return null
|
|
109
|
+
const after = url.searchParams.get('after'), page = url.searchParams.get('page')
|
|
110
|
+
return after ? ['after', after] : page && /^\d{1,4}$/u.test(page) ? ['page', page] : null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A page asked of GitHub: its alerts and the headers they came with. A 403
|
|
114
|
+
// that isn't a rate limit is told as the alerts being off
|
|
115
|
+
// (`dependabot-disabled`) or not to be read by the App's installation
|
|
116
|
+
// (`dependabot-app-permission`), as a 404 is, the repository found just
|
|
117
|
+
// before; the rest as githubJson tells them.
|
|
118
|
+
async function askPage(url: string, token: string, fetchImpl: typeof fetch): Promise<{ alerts: unknown[]; link: string | null }> {
|
|
119
|
+
const started = Date.now()
|
|
120
|
+
let res: Response
|
|
121
|
+
try {
|
|
122
|
+
res = await fetchImpl(url, {
|
|
123
|
+
signal: AbortSignal.timeout(PAGE_TIMEOUT_MS), redirect: 'error',
|
|
124
|
+
headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json', 'user-agent': USER_AGENT, 'x-github-api-version': API_VERSION },
|
|
125
|
+
})
|
|
126
|
+
} catch (error) { throw githubUnreachable(error, started) }
|
|
127
|
+
if (res.status === 403) {
|
|
128
|
+
const message = (await res.clone().json().catch(() => null) as { message?: unknown } | null)?.message
|
|
129
|
+
const limited = res.headers.get('x-ratelimit-remaining') === '0' || res.headers.has('retry-after')
|
|
130
|
+
|| (typeof message === 'string' && /rate limit|abuse detection/iu.test(message))
|
|
131
|
+
if (!limited) throw new GithubApiError(403, typeof message === 'string' && /disabled/iu.test(message) ? 'dependabot-disabled' : 'dependabot-app-permission')
|
|
132
|
+
}
|
|
133
|
+
if (res.status === 404) throw new GithubApiError(403, 'dependabot-app-permission')
|
|
134
|
+
if (!res.ok) await throwGithubResponseError(res)
|
|
135
|
+
let body
|
|
136
|
+
try { body = await res.json() } catch (error) {
|
|
137
|
+
throw error instanceof SyntaxError ? new GithubApiError(502, 'github-malformed') : githubUnreachable(error, started)
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(body)) throw new GithubApiError(502, 'github-malformed')
|
|
140
|
+
return { alerts: body, link: res.headers.get('link') }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// `fullName`'s alerts in `states`, MAX_PAGES at most, in GitHub's order (the
|
|
144
|
+
// newest first), and whether that is all of them.
|
|
145
|
+
async function listAlerts(fullName: string, states: readonly string[], token: string, fetchImpl: typeof fetch) {
|
|
146
|
+
const base = `${API}/repos/${fullName.split('/').map(encodeURIComponent).join('/')}/dependabot/alerts`
|
|
147
|
+
const alerts: DependabotAlert[] = []
|
|
148
|
+
const seen = new Set<number>()
|
|
149
|
+
let cursor: [string, string] | null = null
|
|
150
|
+
for (let pages = 0; pages < MAX_PAGES; pages++) {
|
|
151
|
+
const query = new URLSearchParams({ state: states.join(','), ecosystem: 'npm', per_page: String(PER_PAGE), ...cursor ? { [cursor[0]]: cursor[1] } : {} })
|
|
152
|
+
const answer = await askPage(`${base}?${query}`, token, fetchImpl)
|
|
153
|
+
for (const raw of answer.alerts) {
|
|
154
|
+
const alert = alertOf(fullName, raw)
|
|
155
|
+
if (alert && !seen.has(alert.number)) { seen.add(alert.number); alerts.push(alert) }
|
|
156
|
+
}
|
|
157
|
+
cursor = answer.alerts.length === 0 ? null : nextPage(answer.link)
|
|
158
|
+
if (!cursor) return { alerts, whole: true }
|
|
159
|
+
}
|
|
160
|
+
return { alerts, whole: false }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// `fullName`'s Dependabot alerts of npm's ecosystem, open or dismissed, as
|
|
164
|
+
// `token`, the App's installation's (dependabot-access.ts), may see them, in
|
|
165
|
+
// GitHub's order. Asked at once, but where they are more than a listing holds,
|
|
166
|
+
// as newer dismissed ones would leave older open ones out, the open ones are
|
|
167
|
+
// listed again on their own, and the dismissed ones had kept after them.
|
|
168
|
+
export async function dependabotAlerts(fullName: string, token: string, fetchImpl: typeof fetch = globalThis.fetch): Promise<DependabotAlert[]> {
|
|
169
|
+
const all = await listAlerts(fullName, STATES, token, fetchImpl)
|
|
170
|
+
if (all.whole) return all.alerts
|
|
171
|
+
const open = await listAlerts(fullName, ['open'], token, fetchImpl)
|
|
172
|
+
const told = new Set(open.alerts.map(alert => alert.number))
|
|
173
|
+
return [...open.alerts, ...all.alerts.filter(alert => alert.dismissed && !told.has(alert.number))]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The versions of an alert's package that a lockfile it is of locks
|
|
177
|
+
// (`packages`) and its range covers, GitHub's read as npm's semver reads its
|
|
178
|
+
// own (`>= 1.0.0, < 1.2.6` as `>= 1.0.0 < 1.2.6`); none where it is of no
|
|
179
|
+
// lockfile read, or its range is none semver reads.
|
|
180
|
+
export function alertVersions(alert: Pick<DependabotAlert, 'name' | 'range'>, packages: readonly LockedPackage[] | undefined): string[] {
|
|
181
|
+
const range = alert.range === undefined ? null : validRange(alert.range.replaceAll(',', ' '))
|
|
182
|
+
if (!packages || range === null) return []
|
|
183
|
+
return [...new Set(packages.flatMap(({ name, version, source }) => name === alert.name && (source === 'registry' || source === 'mirror')
|
|
184
|
+
&& isExactVersion(version) && satisfies(version, range) ? [version] : []))]
|
|
185
|
+
}
|