import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { MAX_IN_LIST } from "~/server/db"; import { notFoundError } from "~/server/errors"; import { limitBrowse, limitWrite } from "~/server/limits"; import { assertNotArchived, hasPermission, requireRepoAccess } from "~/server/repos"; import { advisoryCounts, canManageAdvisories } from "~/server/security/advisories"; import { codeAlertCounts, listCodeAlerts, listSarifUploads, loadCodeAlert, pullCodeAlerts, setCodeAlertDismissed, } from "~/server/security/code-scanning"; import { listDependencies } from "~/server/security/dependencies"; import { loadSecurityPolicy } from "~/server/security/policy"; import { bypassPushBlock, loadPushBlock } from "~/server/security/push-protection"; import { enqueueSecurity } from "~/server/security/queue"; import { listSecretAlerts, secretAlertCounts, setSecretAlertState } from "~/server/security/secret-alerts"; import { loadSecuritySettings, saveSecuritySettings } from "~/server/security/settings"; import { loadSignatures } from "~/server/security/signatures"; import { loadPull } from "~/server/pulls"; import { branchSha, listRefs } from "~/server/git/refs"; import { repoRef } from "~/lib/validation"; const page = z.number().int().min(1).max(1000).default(1); const sha = z.string().regex(/^[0-9a-f]{40}$/); const MAX_VERIFY_ON_VIEW = 20; export const getSecurityOverview = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const canWrite = hasPermission(permission, "write"); const [settings, policy, advisories] = await Promise.all([ loadSecuritySettings(repo.id), loadSecurityPolicy(repo), advisoryCounts(repo.id, permission), ]); const [secrets, codeAlerts] = canWrite ? await Promise.all([secretAlertCounts(repo.id), codeAlertCounts(repo.id, `refs/heads/${repo.default_branch}`)]) : [null, null]; return { settings, policy, advisories, secrets, codeAlerts, canWrite, canAdmin: canManageAdvisories(permission), canReport: context.viewer !== null && settings.privateReporting, }; }); export const updateSecuritySettings = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ secretScanning: z.boolean(), pushProtection: z.boolean(), privateReporting: z.boolean(), dependencyGraph: z.boolean(), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); assertNotArchived(repo); await saveSecuritySettings(repo.id, data); if (data.dependencyGraph) { const head = branchSha(await listRefs(repo), repo.default_branch); if (head) await enqueueSecurity([{ kind: "deps.scan", repoId: repo.id, sha: head }]); } return { ok: true }; }); export const getSecretAlerts = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ state: z.enum(["open", "resolved"]).default("open"), page })) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "write"); const [list, counts, settings] = await Promise.all([ listSecretAlerts(repo.id, data.state, data.page), secretAlertCounts(repo.id), loadSecuritySettings(repo.id), ]); return { ...list, counts, settings }; }); export const resolveSecretAlert = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ number: z.number().int().min(1), resolution: z.enum(["revoked", "false_positive", "used_in_tests", "wont_fix"]).nullable(), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); const isAdmin = hasPermission(permission, "admin"); if (!(await setSecretAlertState(repo.id, data.number, data.resolution, viewer.id, isAdmin))) { throw notFoundError("Alert not found"); } return { ok: true }; }); export const getPushBlock = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ id: z.string().min(1).max(64) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); return loadPushBlock(repo.id, data.id); }); export const bypassPushProtection = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ id: z.string().min(1).max(64), reason: z.enum(["false_positive", "used_in_tests", "fix_later"]), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); await bypassPushBlock(repo.id, data.id, viewer.id, data.reason, permission === "admin"); return { ok: true }; }); export const getCodeAlerts = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ ref: z.string().max(255).optional(), state: z.enum(["open", "fixed", "dismissed"]).default("open"), page, }), ) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "write"); const ref = data.ref ?? `refs/heads/${repo.default_branch}`; const [list, uploads] = await Promise.all([ listCodeAlerts(repo.id, { ref, state: data.state, page: data.page }), listSarifUploads(repo.id), ]); return { ...list, ref, uploads }; }); export const getCodeAlert = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ number: z.number().int().min(1) })) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "write"); const alert = await loadCodeAlert(repo.id, data.number); if (!alert) throw notFoundError("Alert not found"); return alert; }); export const dismissCodeAlert = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ number: z.number().int().min(1), reason: z.enum(["false_positive", "wont_fix", "used_in_tests"]).nullable(), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); if (!(await setCodeAlertDismissed(repo.id, data.number, data.reason, viewer.id))) { throw notFoundError("Alert not found, or it is already fixed"); } return { ok: true }; }); export const getPullCodeAlerts = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ number: z.number().int().min(1) })) .handler(async ({ data, context }) => { const { repo, permission } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); if (!hasPermission(permission, "write")) return []; const pull = await loadPull(repo.id, data.number); if (!pull) return []; return pullCodeAlerts(repo.id, data.number, pull.head_sha, pull.head_ref); }); export const getDependencies = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ query: z.string().max(100).default(""), ecosystem: z.enum(["npm", "pip", "go", "cargo", "rubygems", "composer"]).nullable().default(null), page, }), ) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const settings = await loadSecuritySettings(repo.id); if (!settings.dependencyGraph) return { enabled: false, manifests: [], dependencies: [], hasMore: false }; return { enabled: true, ...(await listDependencies(repo.id, data)) }; }); /** Returns recorded verification results and queues bounded verification for commits not yet checked. */ export const getCommitSignatures = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ shas: z.array(sha).max(MAX_IN_LIST) })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const known = await loadSignatures(repo, data.shas); const seen = new Set(known.map((info) => info.sha)); const missing = data.shas.filter((candidate) => !seen.has(candidate)).slice(0, MAX_VERIFY_ON_VIEW); if (missing.length > 0) await enqueueSecurity([{ kind: "signatures.verify", repoId: repo.id, shas: missing }]); return known; });