Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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