pr_01m47d24b0e6n91zwymwxg0vpx/packages/contracts/src/security.ts
| 1 | import type { ServiceBinding } from "./clients"; |
| 2 | import type { User, Viewer } from "./identity"; |
| 3 | import type { RepoPath } from "./repos"; |
| 4 | import type { Result } from "./result"; |
| 5 | |
| 6 | /** |
| 7 | * The security service: secrets found in pushes and in history, vulnerable |
| 8 | * dependencies, and the upgrade issues g1t opens for them. Members of a |
| 9 | * workspace see its findings; nobody else does. Mirrors |
| 10 | * `g1t_contracts::security`. |
| 11 | */ |
| 12 | |
| 13 | /** |
| 14 | * Where a secret stands. `open`: in history, to be rotated. `blocked`: a |
| 15 | * push carrying it was refused, so it never landed. `allowed`: someone said |
| 16 | * it is not a real secret, and pushes carrying it go through. `resolved`: |
| 17 | * rotated or removed. |
| 18 | */ |
| 19 | export type SecretStatus = "open" | "blocked" | "allowed" | "resolved"; |
| 20 | |
| 21 | export type SecretFinding = { |
| 22 | id: string; |
| 23 | repoId: string; |
| 24 | /** `aws_access_key`, `github_token`, … */ |
| 25 | kind: string; |
| 26 | /** "an AWS access key". */ |
| 27 | label: string; |
| 28 | path: string; |
| 29 | line: number; |
| 30 | commit: string; |
| 31 | /** Enough of the secret to recognise it; the secret itself is never kept. */ |
| 32 | preview: string; |
| 33 | status: SecretStatus; |
| 34 | source: "push" | "history"; |
| 35 | foundBy: string | null; |
| 36 | /** RFC 3339. */ |
| 37 | foundAt: string; |
| 38 | decidedBy: string | null; |
| 39 | reason: string | null; |
| 40 | decidedAt: string | null; |
| 41 | }; |
| 42 | |
| 43 | export type Severity = "critical" | "high" | "medium" | "low" | "unknown"; |
| 44 | |
| 45 | export const SEVERITIES: Severity[] = ["critical", "high", "medium", "low", "unknown"]; |
| 46 | |
| 47 | export type Vulnerability = { |
| 48 | id: string; |
| 49 | repoId: string; |
| 50 | /** `npm`, `crates.io`, `Go`, `PyPI`. */ |
| 51 | ecosystem: string; |
| 52 | package: string; |
| 53 | version: string; |
| 54 | /** The lockfile that resolves it. */ |
| 55 | manifest: string; |
| 56 | /** Its GHSA id when it has one. */ |
| 57 | advisory: string; |
| 58 | osvId: string; |
| 59 | summary: string; |
| 60 | severity: Severity; |
| 61 | fixedVersion: string | null; |
| 62 | status: "open" | "fixed"; |
| 63 | /** The upgrade issue opened for the package. */ |
| 64 | issue: number | null; |
| 65 | foundAt: string; |
| 66 | fixedAt: string | null; |
| 67 | }; |
| 68 | |
| 69 | export type SeverityCounts = Record<Severity, number>; |
| 70 | |
| 71 | export type ScanState = { |
| 72 | /** `pending`, `running`, `done`, or `stopped` at the workspace's limit. */ |
| 73 | history: "pending" | "running" | "done" | "stopped"; |
| 74 | commitsScanned: number; |
| 75 | historyFinishedAt: string | null; |
| 76 | dependenciesScannedAt: string | null; |
| 77 | dependenciesError: string | null; |
| 78 | lockfiles: string[]; |
| 79 | }; |
| 80 | |
| 81 | export type SecurityOverview = { |
| 82 | repoId: string; |
| 83 | /** Open vulnerabilities by severity; open and blocked secrets count as critical. */ |
| 84 | counts: SeverityCounts; |
| 85 | secrets: SecretFinding[]; |
| 86 | vulnerabilities: Vulnerability[]; |
| 87 | scan: ScanState; |
| 88 | /** Whether g1t opens upgrade issues and puts its agent on them. */ |
| 89 | upkeep: boolean; |
| 90 | }; |
| 91 | |
| 92 | export type RepoSecurity = { |
| 93 | repoId: string; |
| 94 | name: string; |
| 95 | counts: SeverityCounts; |
| 96 | secrets: number; |
| 97 | vulnerabilities: number; |
| 98 | upkeep: boolean; |
| 99 | dependenciesScannedAt: string | null; |
| 100 | }; |
| 101 | |
| 102 | export type SecretDecision = "allow" | "resolve" | "reopen"; |
| 103 | |
| 104 | export interface SecurityApi { |
| 105 | overview(repo: RepoPath, viewer: Viewer): Promise<Result<SecurityOverview>>; |
| 106 | decideSecret( |
| 107 | actor: User, |
| 108 | repo: RepoPath, |
| 109 | id: string, |
| 110 | decision: SecretDecision, |
| 111 | reason: string, |
| 112 | ): Promise<Result<SecretFinding>>; |
| 113 | rescan(actor: User, repo: RepoPath): Promise<Result<ScanState>>; |
| 114 | setUpkeep(actor: User, repo: RepoPath, enabled: boolean): Promise<Result<boolean>>; |
| 115 | workspace(workspace: string, viewer: Viewer): Promise<Result<RepoSecurity[]>>; |
| 116 | } |
| 117 | |
| 118 | export function securityClient(service: ServiceBinding): SecurityApi { |
| 119 | const call = async <T>(method: string, args: object): Promise<T> => { |
| 120 | const response = await service.fetch(`https://service/rpc/${method}`, { |
| 121 | method: "POST", |
| 122 | headers: { "content-type": "application/json" }, |
| 123 | body: JSON.stringify(args), |
| 124 | }); |
| 125 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); |
| 126 | return (await response.json()) as T; |
| 127 | }; |
| 128 | return { |
| 129 | overview: (repo, viewer) => call("overview", { repo, viewer }), |
| 130 | decideSecret: (actor, repo, id, decision, reason) => |
| 131 | call("decide_secret", { actor, repo, id, decision, reason }), |
| 132 | rescan: (actor, repo) => call("rescan", { actor, repo }), |
| 133 | setUpkeep: (actor, repo, enabled) => call("set_upkeep", { actor, repo, enabled }), |
| 134 | workspace: (workspace, viewer) => call("workspace", { workspace, viewer }), |
| 135 | }; |
| 136 | } |