Skip to content
161 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
104/** Whether a repository takes private reports, and how many advisories it published: for anyone who can see it. */
105export type ReportSettings = { privateVulnerabilityReporting: boolean; published: number };
106
107export interface AdvisoriesApi {
108 /** Published ones for anyone who can see the repository; every state for those who see findings. */
109 list(repo: RepoPath, viewer: Viewer, state?: AdvisoryState | null): Promise<Result<Advisory[]>>;
110 /** By `adv_…` or `G1TSA-…` id. */
111 get(repo: RepoPath, id: string, viewer: Viewer): Promise<Result<Advisory>>;
112 /** A draft. Admin, or a security manager of the workspace. */
113 create(actor: User, repo: RepoPath, advisory: AdvisoryInput): Promise<Result<Advisory>>;
114 update(actor: User, repo: RepoPath, id: string, advisory: AdvisoryInput): Promise<Result<Advisory>>;
115 /** `published` publishes a draft or brings a withdrawn one back; `withdrawn` withdraws. */
116 setState(actor: User, repo: RepoPath, id: string, state: "published" | "withdrawn"): Promise<Result<Advisory>>;
117 /** Tells the maintainers privately. Someone signed out gives an `email`. */
118 report(
119 viewer: Viewer,
120 repo: RepoPath,
121 report: { title: string; description: string; severity?: AdvisorySeverity | null; email?: string | null },
122 ): Promise<Result<VulnerabilityReport>>;
123 /** Whether the repository takes private reports, and how many advisories it published. Anyone who can see it. */
124 reportSettings(repo: RepoPath, viewer: Viewer): Promise<Result<ReportSettings>>;
125 /** The repository's reports, newest first. Those who see findings. */
126 reports(repo: RepoPath, viewer: Viewer, state?: ReportState | null): Promise<Result<VulnerabilityReport[]>>;
127 /** `accept` makes a draft advisory from the report; `decline` closes it. */
128 review(actor: User, repo: RepoPath, id: string, decision: "accept" | "decline", note: string): Promise<Result<ReviewedReport>>;
129}
130
131/** Advisories and reports: methods of the security service. */
132export function advisoriesClient(service: ServiceBinding): AdvisoriesApi {
133 const call = async <T>(method: string, args: object): Promise<T> => {
134 const response = await service.fetch(`https://service/rpc/${method}`, {
135 method: "POST",
136 headers: { "content-type": "application/json" },
137 body: JSON.stringify(args),
138 });
139 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
140 return (await response.json()) as T;
141 };
142 return {
143 list: (repo, viewer, state = null) => call("list_advisories", { repo, viewer, state }),
144 get: (repo, id, viewer) => call("get_advisory", { repo, id, viewer }),
145 create: (actor, repo, advisory) => call("create_advisory", { actor, repo, advisory }),
146 update: (actor, repo, id, advisory) => call("update_advisory", { actor, repo, id, advisory }),
147 setState: (actor, repo, id, state) => call("set_advisory_state", { actor, repo, id, state }),
148 report: (viewer, repo, report) =>
149 call("report_vulnerability", {
150 viewer,
151 repo,
152 title: report.title,
153 description: report.description,
154 severity: report.severity ?? null,
155 email: report.email ?? null,
156 }),
157 reportSettings: (repo, viewer) => call("report_settings", { repo, viewer }),
158 reports: (repo, viewer, state = null) => call("list_vulnerability_reports", { repo, viewer, state }),
159 review: (actor, repo, id, decision, note) => call("review_vulnerability_report", { actor, repo, id, decision, note }),
160 };
161}