Skip to content
155 linesCodeBlameRaw
1import type { ServiceBinding } from "./clients";
2import type { User, Viewer } from "./identity";
3import type { RepoPath } from "./repos";
4import type { Result } from "./result";
5
6/**
7 * Repository security advisories and private vulnerability reporting, the
8 * Reporting half of a repository's Security page. Mirrors
9 * `crates/contracts/src/advisories.rs`.
10 *
11 * An advisory is a repository's own account of a vulnerability in what it
12 * ships: a draft until its maintainers publish it, visible to anyone who can
13 * see the repository once they do, and withdrawable. A vulnerability report
14 * is how someone outside tells the maintainers privately, when the
15 * repository has private vulnerability reporting on; accepting one makes a
16 * draft advisory from it.
17 */
18
19export type AdvisorySeverity = "critical" | "high" | "medium" | "low";
20
21export const ADVISORY_SEVERITIES: AdvisorySeverity[] = ["critical", "high", "medium", "low"];
22
23export type AdvisoryState = "draft" | "published" | "withdrawn";
24
25export const ADVISORY_STATES: AdvisoryState[] = ["draft", "published", "withdrawn"];
26
27export type ReportState = "new" | "accepted" | "declined";
28
29export const REPORT_STATES: ReportState[] = ["new", "accepted", "declined"];
30
31/** The longest title, description and version range kept. */
32export const MAX_ADVISORY_TITLE = 200;
33export const MAX_ADVISORY_DESCRIPTION = 20_000;
34export const MAX_ADVISORY_RANGE = 200;
35export const MAX_ADVISORY_CREDITS = 20;
36
37export type Advisory = {
38 /** `adv_…`. */
39 id: string;
40 /** `G1TSA-2026-abcd-efgh`: what people quote. */
41 g1tsaId: string;
42 repoId: string;
43 /** `owner/name`. */
44 repo: string;
45 title: string;
46 /** Markdown. */
47 description: string;
48 severity: AdvisorySeverity;
49 cvssVector?: string | null;
50 cvssScore?: number | null;
51 cveId?: string | null;
52 ecosystem?: string | null;
53 package?: string | null;
54 affectedVersions?: string | null;
55 patchedVersions?: string | null;
56 state: AdvisoryState;
57 credits: string[];
58 /** The report it was made from, if any. */
59 reportId?: string | null;
60 createdBy: string;
61 createdAt: string;
62 updatedAt: string;
63 publishedAt?: string | null;
64 withdrawnAt?: string | null;
65};
66
67/** What a maintainer writes when creating or changing an advisory. */
68export type AdvisoryInput = {
69 title: string;
70 description: string;
71 severity: AdvisorySeverity;
72 cvssVector?: string | null;
73 cvssScore?: number | null;
74 cveId?: string | null;
75 ecosystem?: string | null;
76 package?: string | null;
77 affectedVersions?: string | null;
78 patchedVersions?: string | null;
79 credits?: string[];
80};
81
82export type VulnerabilityReport = {
83 /** `vrp_…`. */
84 id: string;
85 repoId: string;
86 repo: string;
87 /** A username when signed in, else the address given. */
88 reporter?: string | null;
89 reporterEmail?: string | null;
90 title: string;
91 description: string;
92 severity?: AdvisorySeverity | null;
93 state: ReportState;
94 /** The draft advisory an accepted report became. */
95 advisoryId?: string | null;
96 createdAt: string;
97 reviewedBy?: string | null;
98 reviewedAt?: string | null;
99 reviewNote?: string | null;
100};
101
102export type ReviewedReport = { report: VulnerabilityReport; advisory?: Advisory | null };
103
104export interface AdvisoriesApi {
105 /** Published ones for anyone who can see the repository; every state for those who see findings. */
106 list(repo: RepoPath, viewer: Viewer, state?: AdvisoryState | null): Promise<Result<Advisory[]>>;
107 /** By `adv_…` or `G1TSA-…` id. */
108 get(repo: RepoPath, id: string, viewer: Viewer): Promise<Result<Advisory>>;
109 /** A draft. Admin, or a security manager of the workspace. */
110 create(actor: User, repo: RepoPath, advisory: AdvisoryInput): Promise<Result<Advisory>>;
111 update(actor: User, repo: RepoPath, id: string, advisory: AdvisoryInput): Promise<Result<Advisory>>;
112 /** `published` publishes a draft or brings a withdrawn one back; `withdrawn` withdraws. */
113 setState(actor: User, repo: RepoPath, id: string, state: "published" | "withdrawn"): Promise<Result<Advisory>>;
114 /** Tells the maintainers privately. Someone signed out gives an `email`. */
115 report(
116 viewer: Viewer,
117 repo: RepoPath,
118 report: { title: string; description: string; severity?: AdvisorySeverity | null; email?: string | null },
119 ): Promise<Result<VulnerabilityReport>>;
120 /** The repository's reports, newest first. Those who see findings. */
121 reports(repo: RepoPath, viewer: Viewer, state?: ReportState | null): Promise<Result<VulnerabilityReport[]>>;
122 /** `accept` makes a draft advisory from the report; `decline` closes it. */
123 review(actor: User, repo: RepoPath, id: string, decision: "accept" | "decline", note: string): Promise<Result<ReviewedReport>>;
124}
125
126/** Advisories and reports: methods of the security service. */
127export function advisoriesClient(service: ServiceBinding): AdvisoriesApi {
128 const call = async <T>(method: string, args: object): Promise<T> => {
129 const response = await service.fetch(`https://service/rpc/${method}`, {
130 method: "POST",
131 headers: { "content-type": "application/json" },
132 body: JSON.stringify(args),
133 });
134 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
135 return (await response.json()) as T;
136 };
137 return {
138 list: (repo, viewer, state = null) => call("list_advisories", { repo, viewer, state }),
139 get: (repo, id, viewer) => call("get_advisory", { repo, id, viewer }),
140 create: (actor, repo, advisory) => call("create_advisory", { actor, repo, advisory }),
141 update: (actor, repo, id, advisory) => call("update_advisory", { actor, repo, id, advisory }),
142 setState: (actor, repo, id, state) => call("set_advisory_state", { actor, repo, id, state }),
143 report: (viewer, repo, report) =>
144 call("report_vulnerability", {
145 viewer,
146 repo,
147 title: report.title,
148 description: report.description,
149 severity: report.severity ?? null,
150 email: report.email ?? null,
151 }),
152 reports: (repo, viewer, state = null) => call("list_vulnerability_reports", { repo, viewer, state }),
153 review: (actor, repo, id, decision, note) => call("review_vulnerability_report", { actor, repo, id, decision, note }),
154 };
155}