Skip to content
387 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import 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
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.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.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar18 */
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;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.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;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar63};
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}