@preventive/triage 1.0.0-alpha.19 → 1.0.0-alpha.20
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/cli.js +6 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +111 -0
- package/common/bundle-metadata.d.ts +10 -0
- package/common/bundle-metadata.js +177 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-sources.d.ts +3 -0
- package/common/bundle-sources.js +284 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +36 -0
- package/common/default-scan-models.ts +30 -0
- package/common/finding-id.js +47 -0
- package/common/github-pr.ts +56 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/permissions.ts +35 -0
- package/common/managed/report-content.ts +42 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +121 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/triage.ts +83 -0
- package/out/index.html +4 -0
- package/out/view.css +1 -1
- package/out/view.js +82 -82
- package/package.json +82 -3
- package/server-e2e/cli.js +1 -1
- package/server-managed/activity.ts +231 -0
- package/server-managed/avatar-store.ts +51 -0
- package/server-managed/blob-store.ts +66 -0
- package/server-managed/blob-vercel.ts +125 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-cache.ts +185 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-store.ts +28 -0
- package/server-managed/bundle-summary-cache.ts +97 -0
- package/server-managed/bundle.ts +39 -0
- package/server-managed/cache-storage.ts +40 -0
- package/server-managed/cli.js +13 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +144 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1330 -0
- package/server-managed/db-neon.ts +171 -0
- package/server-managed/db-schema.ts +203 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +109 -0
- package/server-managed/github-app.ts +332 -0
- package/server-managed/github-metadata.ts +65 -0
- package/server-managed/github-oauth.ts +215 -0
- package/server-managed/github-pulls.ts +115 -0
- package/server-managed/http-response.ts +18 -0
- package/server-managed/http.ts +2043 -0
- package/server-managed/import-triage.ts +48 -0
- package/server-managed/index.ts +135 -0
- package/server-managed/public-workspace.ts +150 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +4 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +154 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +25 -0
- package/server-managed/session.ts +78 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/sql-postgres.ts +30 -0
- package/server-managed/sql.ts +61 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage.ts +34 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +128 -0
- package/server-managed/team-reports.ts +156 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/uploads.ts +47 -0
- package/server-managed/workspace-shares.ts +150 -0
- package/server.ts +50 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Managed-server boot config, parsed once at startup and
|
|
2
|
+
// failing fast on a missing / invalid required value — the same discipline as
|
|
3
|
+
// server-e2e/config.ts. Auth (GitHub identity + sessions) is required. An
|
|
4
|
+
// optional, SEPARATE GitHub App (id + private key + slug) enables PRIVATE repo
|
|
5
|
+
// listing via installation tokens — kept distinct from the login App so its
|
|
6
|
+
// Contents permission never lands on the login consent. The sync protocol is
|
|
7
|
+
// not parsed here yet.
|
|
8
|
+
import { env } from 'node:process'
|
|
9
|
+
import { databaseUrls } from '../server-common/database-config.ts'
|
|
10
|
+
|
|
11
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
|
|
12
|
+
|
|
13
|
+
export interface ManagedConfig {
|
|
14
|
+
port: number
|
|
15
|
+
host: string
|
|
16
|
+
dbPath: string
|
|
17
|
+
neonUrl?: string | null
|
|
18
|
+
blobToken?: string | null
|
|
19
|
+
serverless?: boolean
|
|
20
|
+
debug: boolean
|
|
21
|
+
allowShare: boolean
|
|
22
|
+
trustProxyEnv: string | undefined
|
|
23
|
+
// GitHub App user-authorization (identity) credentials — the App's client id
|
|
24
|
+
// + secret, used by the login flow.
|
|
25
|
+
githubClientId: string
|
|
26
|
+
githubClientSecret: string
|
|
27
|
+
// Preapproved GitHub identity for the first registration in an empty DB.
|
|
28
|
+
// This never changes an existing user's role on later logins.
|
|
29
|
+
initialAdminGithubId: number | null
|
|
30
|
+
// Absolute callback registered with GitHub, e.g.
|
|
31
|
+
// 'https://triage.example.com/api/oauth/github/callback'.
|
|
32
|
+
oauthCallbackUrl: string
|
|
33
|
+
// Whether cookies carry `Secure` (callback is https). Also gates the
|
|
34
|
+
// `__Host-` prefix, so loopback http dev can run with a plain cookie name.
|
|
35
|
+
cookieSecure: boolean
|
|
36
|
+
sessionCookieName: string
|
|
37
|
+
sessionTtlMs: number
|
|
38
|
+
// Optional SEPARATE GitHub App for PRIVATE repository access — distinct from
|
|
39
|
+
// the login App above. It carries the Contents: Read permission (so its
|
|
40
|
+
// consent never touches login) and is read via installation tokens: the id +
|
|
41
|
+
// PEM private key mint them; the slug builds the "Connect a repository"
|
|
42
|
+
// install URL. id/key absent → private repos are off (public-only listing);
|
|
43
|
+
// slug absent → no install link.
|
|
44
|
+
githubAppId: string | null
|
|
45
|
+
githubAppPrivateKey: string | null
|
|
46
|
+
githubAppSlug: string | null
|
|
47
|
+
// Max accepted size (bytes) for an uploaded report on the "Manage reports"
|
|
48
|
+
// page. Reports are findings dumps (JSON / markdown / CSV), small to a few MB.
|
|
49
|
+
maxReportBytes: number
|
|
50
|
+
// Max accepted size (bytes) for an uploaded bundle (sourcemap / stasis
|
|
51
|
+
// archive). Bundles run larger than reports, so a higher cap (default 100 MiB,
|
|
52
|
+
// matching the e2e objstore per-upload cap).
|
|
53
|
+
maxBundleBytes: number
|
|
54
|
+
// How many triage-trail events to keep per finding (managed_finding_triage_event):
|
|
55
|
+
// 0, the default, keeps everything — the trail is the record. An operator
|
|
56
|
+
// who would rather bound the store sets a positive count; older events of a
|
|
57
|
+
// finding are then trimmed as new ones land.
|
|
58
|
+
triageHistoryLimit: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fail(msg: string): never {
|
|
62
|
+
throw new Error(msg)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requireStr(name: string): string {
|
|
66
|
+
const v = env[name]
|
|
67
|
+
if (v == null || v === '') fail(`Missing required env ${name} (managed mode).`)
|
|
68
|
+
return v
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function intEnv(name: string, def: number, min: number, max: number): number {
|
|
72
|
+
const raw = env[name]
|
|
73
|
+
const n = raw == null ? def : Number(raw)
|
|
74
|
+
if (!Number.isSafeInteger(n) || n < min || n > max) {
|
|
75
|
+
fail(`Invalid ${name}: ${raw} — must be an integer in [${min}, ${max}].`)
|
|
76
|
+
}
|
|
77
|
+
return n
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function optionalGithubId(name: string): number | null {
|
|
81
|
+
const raw = env[name]
|
|
82
|
+
if (raw == null || raw === '') return null
|
|
83
|
+
const id = Number(raw)
|
|
84
|
+
if (!/^[1-9]\d*$/u.test(raw) || !Number.isSafeInteger(id)) {
|
|
85
|
+
fail(`Invalid ${name}: must be a single positive numeric GitHub user ID.`)
|
|
86
|
+
}
|
|
87
|
+
return id
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function urlOrFail(name: string, raw: string): URL {
|
|
91
|
+
try { return new URL(raw) } catch { fail(`${name} is not a valid URL: ${raw}`) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function loadManagedConfig({ combined = false } = {}): ManagedConfig {
|
|
95
|
+
const serverless = env['VERCEL'] === '1'
|
|
96
|
+
const neonUrl = databaseUrls({ combined }).managed
|
|
97
|
+
const blobToken = env['BLOB_READ_WRITE_TOKEN'] || null
|
|
98
|
+
if (serverless && !neonUrl) fail('Vercel managed mode requires DATABASE_URL or MANAGED_DATABASE_URL.')
|
|
99
|
+
if (neonUrl && !blobToken) fail('Managed Neon mode requires BLOB_READ_WRITE_TOKEN.')
|
|
100
|
+
const host = env['HOST'] ?? '127.0.0.1'
|
|
101
|
+
const oauthCallbackUrl = requireStr('OAUTH_CALLBACK_URL')
|
|
102
|
+
const callback = urlOrFail('OAUTH_CALLBACK_URL', oauthCallbackUrl)
|
|
103
|
+
const cookieSecure = callback.protocol === 'https:'
|
|
104
|
+
// A `__Host-` cookie mandates `Secure`, so any non-loopback bind must be
|
|
105
|
+
// HTTPS — fail fast (mirrors server-e2e's boot check). Loopback dev over
|
|
106
|
+
// http is allowed with a non-prefixed cookie name.
|
|
107
|
+
if ((serverless || !LOOPBACK_HOSTS.has(host)) && !cookieSecure) {
|
|
108
|
+
fail(`Vercel or non-loopback HOST=${host} requires an https OAUTH_CALLBACK_URL — the session cookie needs Secure.`)
|
|
109
|
+
}
|
|
110
|
+
const sessionCookieName = env['SESSION_COOKIE_NAME'] ?? '__Host-dvsid'
|
|
111
|
+
if (sessionCookieName.startsWith('__Host-') && !cookieSecure) {
|
|
112
|
+
fail(`SESSION_COOKIE_NAME=${sessionCookieName} uses the __Host- prefix but the callback is not https. Use a non-prefixed name for loopback http dev.`)
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
neonUrl, blobToken, serverless,
|
|
116
|
+
port: intEnv('PORT', 8765, 0, 65535),
|
|
117
|
+
host,
|
|
118
|
+
// DB_PATH belongs to e2e in a combined process. Keep the two stores apart.
|
|
119
|
+
dbPath: env['MANAGED_DB_PATH'] ?? (combined ? undefined : env['DB_PATH']) ?? 'server-managed/data/managed.db',
|
|
120
|
+
debug: env['DEBUG'] === '1' || env['DEBUG'] === 'true',
|
|
121
|
+
allowShare: env['DEEPVIEW_ALLOW_SHARE'] === '1',
|
|
122
|
+
trustProxyEnv: env['TRUST_PROXY'] ?? (serverless ? '1' : undefined),
|
|
123
|
+
githubClientId: requireStr('GITHUB_CLIENT_ID'),
|
|
124
|
+
githubClientSecret: requireStr('GITHUB_CLIENT_SECRET'),
|
|
125
|
+
initialAdminGithubId: optionalGithubId('MANAGED_INITIAL_ADMIN_GITHUB_ID'),
|
|
126
|
+
oauthCallbackUrl,
|
|
127
|
+
cookieSecure,
|
|
128
|
+
sessionCookieName,
|
|
129
|
+
sessionTtlMs: intEnv('SESSION_TTL_MS', 1_209_600_000, 60_000, 7_776_000_000),
|
|
130
|
+
githubAppId: env['GITHUB_APP_ID'] ?? null,
|
|
131
|
+
githubAppPrivateKey: normalizePem(env['GITHUB_APP_PRIVATE_KEY']),
|
|
132
|
+
githubAppSlug: env['GITHUB_APP_SLUG'] ?? null,
|
|
133
|
+
maxReportBytes: intEnv('MAX_REPORT_BYTES', 10_485_760, 1, 104_857_600),
|
|
134
|
+
maxBundleBytes: intEnv('MAX_BUNDLE_BYTES', 104_857_600, 1, 1_073_741_824),
|
|
135
|
+
triageHistoryLimit: intEnv('TRIAGE_HISTORY_LIMIT', 0, 0, 1_000_000_000),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// PEM private keys are awkward in env vars; accept a literal multi-line value or
|
|
140
|
+
// one with escaped newlines (`\n`). Empty/absent → null (private repos off).
|
|
141
|
+
function normalizePem(raw: string | undefined): string | null {
|
|
142
|
+
if (raw == null || raw === '') return null
|
|
143
|
+
return raw.includes('\\n') ? raw.replaceAll('\\n', '\n') : raw
|
|
144
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ManagedDb, StoredUser } from './db.ts'
|
|
2
|
+
|
|
3
|
+
export type RepoScope = { repoId: number; path: string | null }
|
|
4
|
+
type Content = { repoId: number | null; repoDirectory?: string }
|
|
5
|
+
|
|
6
|
+
// Read grants for each request: neither the manager role nor a cached page
|
|
7
|
+
// confers repository access. Bundles have repository scope; reports also have
|
|
8
|
+
// a directory scope. Unassigned content is only available to administrators.
|
|
9
|
+
export async function contentAccess(db: ManagedDb, user: StoredUser) {
|
|
10
|
+
const scopes = user.role === 'admin' ? null : await db.listRepoScopesForUser(user.id)
|
|
11
|
+
const bundle = (item: Content) => scopes == null || scopes.some(scope => scope.repoId === item.repoId)
|
|
12
|
+
const report = (item: Content) => scopes == null || scopes.some(scope => scope.repoId === item.repoId
|
|
13
|
+
&& (!scope.path || item.repoDirectory === scope.path || item.repoDirectory?.startsWith(scope.path + '/')))
|
|
14
|
+
return { scopes, bundle, report }
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Token helpers for the managed auth server. The raw session token lives ONLY
|
|
2
|
+
// in the cookie; the DB row id is its SHA-256 (see db.ts / session.ts), so a
|
|
3
|
+
// DB read can't reconstruct a live cookie. No HMAC pepper is needed — the
|
|
4
|
+
// tokens are 256-bit random, not guessable or reversible from their hash.
|
|
5
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
// 256-bit URL-safe random token: the session cookie value and the OAuth CSRF
|
|
8
|
+
// `state` nonce.
|
|
9
|
+
export function randomToken(): string {
|
|
10
|
+
return randomBytes(32).toString('base64url')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// At-rest id for a session cookie token (stored as the managed_session PK).
|
|
14
|
+
export function hashToken(token: string): string {
|
|
15
|
+
return createHash('sha256').update(token, 'utf8').digest('base64url')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Constant-time equality for CSRF / OAuth-state checks. A length mismatch
|
|
19
|
+
// short-circuits (the length of these fixed-width tokens isn't a secret).
|
|
20
|
+
export function safeEqual(a: string, b: string): boolean {
|
|
21
|
+
const ab = Buffer.from(a, 'utf8')
|
|
22
|
+
const bb = Buffer.from(b, 'utf8')
|
|
23
|
+
if (ab.length !== bb.length) return false
|
|
24
|
+
return timingSafeEqual(ab, bb)
|
|
25
|
+
}
|