Skip to content
387 linesCodeBlameRaw
1import type { ServiceBinding } from "./clients";
2import type { User, Viewer } from "./identity";
3import type { RepoPath } from "./repos";
4import type { Result } from "./result";
5import type { AlertActivity, AlertState, DismissReason, SecretFinding, Severity, SeverityCounts, Vulnerability } from "./security";
6
7/**
8 * The security service's paid suite: custom secret patterns, push
9 * protection bypasses and their review, validity checks, code scanning
10 * from SARIF, the dependency graph with its SBOM and dependency review,
11 * and the workspace's overview. Mirrors `g1t_contracts::security_suite`.
12 *
13 * Free everywhere: secret scanning, push protection, vulnerability alerts
14 * and security updates. Free on public repositories: everything here. On a
15 * private repository the features in `PaidFeature` come with the g1t plan
16 * (billing's `security` feature, with no price of its own); a refusal is a
17 * `payment_required` failure saying how to start the plan.
18 */
19
20export type PaidFeature =
21 | "custom_patterns"
22 | "validity_checks"
23 | "delegated_bypass"
24 | "code_scanning"
25 | "dependency_review"
26 | "security_overview";
27
28export type AlertType = "secret_scanning" | "code_scanning" | "vulnerability";
29
30/** When a pull request's `Code scanning` check fails. */
31export type CodeScanningGate = "none" | "errors" | "critical" | "high" | "medium" | "any";
32
33export const CODE_SCANNING_GATES: { gate: CodeScanningGate; label: string }[] = [
34 { gate: "none", label: "Never" },
35 { gate: "errors", label: "Errors only" },
36 { gate: "critical", label: "Critical, and errors" },
37 { gate: "high", label: "High or higher, and errors" },
38 { gate: "medium", label: "Medium or higher, and errors" },
39 { gate: "any", label: "Any security result, and errors" },
40];
41
42export type ReviewFailOn = "critical" | "high" | "medium" | "low" | "none";
43
44/** The checks the suite reports on pull requests. */
45export const CODE_SCANNING_CHECK = "Code scanning";
46export const DEPENDENCY_REVIEW_CHECK = "Dependency review";
47/** The starter workflow "Set up code scanning" adds. */
48export const STARTER_WORKFLOW_PATH = ".g1t/workflows/code-scanning.yml";
49
50export type RepoSecuritySettings = {
51 codeScanningGate: CodeScanningGate;
52 dependencyReview: boolean;
53 reviewFailOn: ReviewFailOn;
54 reviewDenyLicenses: string[];
55 reviewComment: boolean;
56 /**
57 * Whether secrets found in the history (and in pushes too large to scan
58 * first) open alerts and tell people. Push protection is never off.
59 */
60 historyAlerts: boolean;
61 /** Whether anyone who can see the repository may report a vulnerability privately. */
62 privateVulnerabilityReporting: boolean;
63};
64
65export type WorkspaceSecuritySettings = {
66 delegatedBypass: boolean;
67 validityChecks: boolean;
68};
69
70export type SecuritySettingsView = {
71 settings: RepoSecuritySettings;
72 workspace: WorkspaceSecuritySettings;
73 private: boolean;
74 /** Public, or the workspace has the activation. */
75 entitled: boolean;
76 upkeep: boolean;
77};
78
79export type WorkspaceSecurityView = { settings: WorkspaceSecuritySettings; activated: boolean };
80
81export type CustomPattern = {
82 id: string;
83 scope: "repository" | "workspace";
84 workspace: string;
85 repo: string | null;
86 name: string;
87 pattern: string;
88 before: string | null;
89 after: string | null;
90 testStrings: string[];
91 state: "draft" | "published";
92 createdBy: string;
93 createdAt: string;
94 updatedBy: string;
95 updatedAt: string;
96 openAlerts: number;
97};
98
99export type PatternList = { patterns: CustomPattern[]; entitled: boolean };
100export type SavedPattern = { pattern: CustomPattern; tests: ([number, number] | null)[] };
101
102export type PatternMatch = { path: string; line: number; preview: string };
103export type DryRunRepo = { name: string; filesScanned: number; matches: PatternMatch[]; truncated: boolean; commit: string | null };
104export type DryRun = { repos: DryRunRepo[] };
105
106export type PatternInput = {
107 id?: string;
108 name: string;
109 pattern: string;
110 before?: string | null;
111 after?: string | null;
112 testStrings?: string[];
113 publish?: boolean;
114};
115
116export type BypassReason = "false_positive" | "used_in_tests" | "will_fix_later";
117
118export const BYPASS_REASONS: { reason: BypassReason; label: string; about: string }[] = [
119 { reason: "false_positive", label: "It's a false positive", about: "It is not a secret. The alert is closed as a false positive." },
120 { reason: "used_in_tests", label: "It's used in tests", about: "A value made for tests. The alert is closed as used in tests." },
121 { reason: "will_fix_later", label: "I'll fix it later", about: "It is real. The alert stays open until it is rotated." },
122];
123
124export type SecretLocation = { path: string; line: number; commit: string; source: "push" | "history"; foundAt: string };
125
126export type BypassRequest = {
127 id: string;
128 repoId: string;
129 workspace: string;
130 repo: string;
131 secretId: string;
132 label: string;
133 path: string;
134 line: number;
135 preview: string;
136 requester: string;
137 reason: BypassReason;
138 comment: string | null;
139 state: "pending" | "approved" | "denied" | "cancelled";
140 reviewer: string | null;
141 reviewComment: string | null;
142 createdAt: string;
143 reviewedAt: string | null;
144};
145
146export type SecretAlertDetail = {
147 secret: SecretFinding;
148 locations: SecretLocation[];
149 activity: AlertActivity[];
150 requests: BypassRequest[];
151 checkable: boolean;
152 canBypass: boolean;
153 canRequestBypass: boolean;
154};
155
156export type BypassResult = { secret: SecretFinding; request: BypassRequest | null };
157
158export type CodeAlert = {
159 id: string;
160 number: number;
161 repoId: string;
162 tool: string;
163 category: string;
164 ruleId: string;
165 ruleName: string | null;
166 ruleDescription: string | null;
167 help: string | null;
168 helpUri: string | null;
169 tags: string[];
170 level: "error" | "warning" | "note" | "none";
171 securitySeverity: Severity | null;
172 severity: Severity;
173 message: string;
174 path: string | null;
175 startLine: number | null;
176 endLine: number | null;
177 startColumn: number | null;
178 endColumn: number | null;
179 state: AlertState;
180 fingerprint: string;
181 firstCommit: string;
182 lastCommit: string;
183 createdAt: string;
184 updatedAt: string;
185 fixedAt: string | null;
186 dismissedBy: string | null;
187 dismissedReason: DismissReason | null;
188 dismissedComment: string | null;
189 dismissedAt: string | null;
190 issue: number | null;
191};
192
193export const CODE_DISMISS_REASONS: { reason: DismissReason; label: string; about: string }[] = [
194 { reason: "false_positive", label: "False positive", about: "The tool is wrong about this code." },
195 { reason: "wont_fix", label: "Won't fix", about: "It is real, and accepted as it is." },
196 { reason: "used_in_tests", label: "Used in tests", about: "The code is only in tests." },
197];
198
199export type Analysis = {
200 id: string;
201 repoId: string;
202 sarifId: string;
203 tool: string;
204 toolVersion: string | null;
205 category: string;
206 commitSha: string;
207 gitRef: string;
208 pull: number | null;
209 results: number;
210 newAlerts: number;
211 fixedAlerts: number;
212 dropped: number;
213 createdAt: string;
214};
215
216export type CodeScanning = { alerts: CodeAlert[]; analyses: Analysis[]; entitled: boolean; private: boolean; configured: boolean };
217export type CodeAlertDetail = { alert: CodeAlert; activity: AlertActivity[]; analyses: Analysis[] };
218
219export type PullResult = {
220 tool: string;
221 ruleId: string;
222 level: string;
223 severity: Severity;
224 securitySeverity: Severity | null;
225 message: string;
226 path: string | null;
227 line: number | null;
228 new: boolean;
229 onChangedLine: boolean;
230 failing: boolean;
231};
232
233export type ReviewVulnerability = { advisory: string; osvId: string; summary: string; severity: Severity; fixedVersion: string | null; url: string };
234
235export type ReviewChange = {
236 changeType: "added" | "removed";
237 manifest: string;
238 ecosystem: string;
239 name: string;
240 version: string;
241 relationship: "direct" | "transitive" | "unknown";
242 development: boolean;
243 license: string | null;
244 purl: string;
245 vulnerabilities: ReviewVulnerability[];
246 deniedLicense: boolean;
247 failing: boolean;
248};
249
250export type DependencyReview = {
251 base: string;
252 head: string;
253 changes: ReviewChange[];
254 passed: boolean;
255 headline: string;
256 failOn: string;
257 denyLicenses: string[];
258};
259
260export type PullScanning = {
261 commit: string | null;
262 results: PullResult[];
263 review: DependencyReview | null;
264 codeStatus: string | null;
265 codeDescription: string | null;
266};
267
268export type AlertFix = { issue: number; started: boolean; message: string | null };
269
270export type GraphDependency = {
271 ecosystem: string;
272 name: string;
273 version: string;
274 manifest: string;
275 relationship: "direct" | "transitive" | "unknown";
276 development: boolean;
277 license: string | null;
278 purl: string;
279 vulnerabilities: number;
280};
281
282export type GraphManifest = { path: string; ecosystem: string; dependencies: number; direct: number };
283export type DependencyGraph = { commit: string | null; manifests: GraphManifest[]; dependencies: GraphDependency[] };
284
285export type TypeTotals = { alertType: AlertType; open: SeverityCounts; opened: number; closed: number };
286export type TrendPoint = { day: string; secretScanning: number; codeScanning: number; vulnerability: number };
287export type RepoCoverage = {
288 repoId: string;
289 name: string;
290 private: boolean;
291 customPatterns: number;
292 validityChecks: boolean;
293 codeScanningAt: string | null;
294 dependencyReview: boolean;
295 securityUpdates: boolean;
296 lockfiles: number;
297 secrets: SeverityCounts;
298 code: SeverityCounts;
299 vulnerabilities: SeverityCounts;
300};
301export type WorkspaceOverview = {
302 activated: boolean;
303 privateHidden: number;
304 totals: TypeTotals[];
305 trend: TrendPoint[];
306 repos: RepoCoverage[];
307};
308
309export type WorkspaceAlert = { repo: string; secret?: SecretFinding | null; code?: CodeAlert | null; vulnerability?: Vulnerability | null };
310
311export interface SecuritySuiteApi {
312 settings(repo: RepoPath, viewer: Viewer): Promise<Result<SecuritySettingsView>>;
313 setSettings(actor: User, repo: RepoPath, settings: RepoSecuritySettings): Promise<Result<SecuritySettingsView>>;
314 workspaceSettings(workspace: string, viewer: Viewer): Promise<Result<WorkspaceSecurityView>>;
315 setWorkspaceSettings(actor: User, workspace: string, settings: WorkspaceSecuritySettings): Promise<Result<WorkspaceSecurityView>>;
316 patterns(workspace: string, repo: RepoPath | null, viewer: Viewer): Promise<Result<PatternList>>;
317 savePattern(actor: User, workspace: string, repo: RepoPath | null, pattern: PatternInput): Promise<Result<SavedPattern>>;
318 deletePattern(actor: User, workspace: string, repo: RepoPath | null, id: string): Promise<Result<boolean>>;
319 dryRun(actor: User, workspace: string, repo: RepoPath | null, pattern: { pattern: string; before?: string | null; after?: string | null }, repos?: string[]): Promise<Result<DryRun>>;
320 secretAlert(repo: RepoPath, id: string, viewer: Viewer): Promise<Result<SecretAlertDetail>>;
321 bypass(actor: User, repo: RepoPath, id: string, reason: BypassReason, comment: string): Promise<Result<BypassResult>>;
322 bypassRequests(workspace: string, viewer: Viewer, state?: string | null): Promise<Result<BypassRequest[]>>;
323 reviewBypass(actor: User, workspace: string, id: string, decision: "approve" | "deny" | "cancel", comment: string): Promise<Result<BypassRequest>>;
324 checkValidity(actor: User, repo: RepoPath, id: string): Promise<Result<SecretFinding>>;
325 codeScanning(repo: RepoPath, viewer: Viewer): Promise<Result<CodeScanning>>;
326 codeAlert(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<CodeAlertDetail>>;
327 setCodeAlertState(actor: User, repo: RepoPath, number: number, state: "open" | "dismissed", reason: DismissReason | null, comment: string): Promise<Result<CodeAlert>>;
328 pullScanning(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<PullScanning>>;
329 fixAlert(actor: User, repo: RepoPath, id: string): Promise<Result<AlertFix>>;
330 dependencyGraph(repo: RepoPath, viewer: Viewer): Promise<Result<DependencyGraph>>;
331 sbom(repo: RepoPath, viewer: Viewer): Promise<Result<unknown>>;
332 overview(workspace: string, viewer: Viewer, days?: number): Promise<Result<WorkspaceOverview>>;
333}
334
335export function securitySuiteClient(service: ServiceBinding): SecuritySuiteApi {
336 const call = async <T>(method: string, args: object): Promise<T> => {
337 const response = await service.fetch(`https://service/rpc/${method}`, {
338 method: "POST",
339 headers: { "content-type": "application/json" },
340 body: JSON.stringify(args),
341 });
342 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
343 return (await response.json()) as T;
344 };
345 return {
346 settings: (repo, viewer) => call("security_settings", { repo, viewer }),
347 setSettings: (actor, repo, settings) => call("set_security_settings", { actor, repo, settings }),
348 workspaceSettings: (workspace, viewer) => call("workspace_security_settings", { workspace, viewer }),
349 setWorkspaceSettings: (actor, workspace, settings) => call("set_workspace_security_settings", { actor, workspace, settings }),
350 patterns: (workspace, repo, viewer) => call("custom_patterns", { workspace, repo, viewer }),
351 savePattern: (actor, workspace, repo, pattern) =>
352 call("save_custom_pattern", {
353 actor,
354 workspace,
355 repo,
356 id: pattern.id ?? null,
357 name: pattern.name,
358 pattern: pattern.pattern,
359 before: pattern.before ?? null,
360 after: pattern.after ?? null,
361 testStrings: pattern.testStrings ?? [],
362 publish: pattern.publish ?? false,
363 }),
364 deletePattern: (actor, workspace, repo, id) => call("delete_custom_pattern", { actor, workspace, repo, id }),
365 dryRun: (actor, workspace, repo, pattern, repos = []) =>
366 call("dry_run_pattern", { actor, workspace, repo, repos, pattern: pattern.pattern, before: pattern.before ?? null, after: pattern.after ?? null }),
367 secretAlert: (repo, id, viewer) => call("secret_alert", { repo, id, viewer }),
368 bypass: (actor, repo, id, reason, comment) => call("bypass", { actor, repo, id, reason, comment }),
369 bypassRequests: (workspace, viewer, state = null) => call("bypass_requests", { workspace, viewer, state }),
370 reviewBypass: (actor, workspace, id, decision, comment) => call("review_bypass", { actor, workspace, id, decision, comment }),
371 checkValidity: (actor, repo, id) => call("check_validity", { actor, repo, id }),
372 codeScanning: (repo, viewer) => call("code_scanning", { repo, viewer }),
373 codeAlert: (repo, number, viewer) => call("code_alert", { repo, number, viewer }),
374 setCodeAlertState: (actor, repo, number, state, reason, comment) =>
375 call("set_code_alert_state", { actor, repo, number, state, reason, comment }),
376 pullScanning: (repo, number, viewer) => call("pull_code_scanning", { repo, number, viewer }),
377 fixAlert: (actor, repo, id) => call("fix_alert", { actor, repo, id }),
378 dependencyGraph: (repo, viewer) => call("dependency_graph", { repo, viewer }),
379 sbom: (repo, viewer) => call("sbom", { repo, viewer }),
380 overview: (workspace, viewer, days) => call("security_overview", { workspace, viewer, days: days ?? null }),
381 };
382}
383
384/** Whether a result failed for want of the Security and quality activation. */
385export function needsActivation(result: Result<unknown>): boolean {
386 return !result.ok && result.error.code === "payment_required";
387}