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 bar | 1 | import type { ServiceBinding } from "./clients"; |
| 2 | import type { User, Viewer } from "./identity"; | |
| 3 | import type { RepoPath } from "./repos"; | |
| 4 | import type { Result } from "./result"; | |
| 5 | import 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 bar | 18 | */ |
| 19 | ||
| 20 | export type PaidFeature = | |
| 21 | | "custom_patterns" | |
| 22 | | "validity_checks" | |
| 23 | | "delegated_bypass" | |
| 24 | | "code_scanning" | |
| 25 | | "dependency_review" | |
| 26 | | "security_overview"; | |
| 27 | ||
| 28 | export type AlertType = "secret_scanning" | "code_scanning" | "vulnerability"; | |
| 29 | ||
| 30 | /** When a pull request's `Code scanning` check fails. */ | |
| 31 | export type CodeScanningGate = "none" | "errors" | "critical" | "high" | "medium" | "any"; | |
| 32 | ||
| 33 | export 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 | ||
| 42 | export type ReviewFailOn = "critical" | "high" | "medium" | "low" | "none"; | |
| 43 | ||
| 44 | /** The checks the suite reports on pull requests. */ | |
| 45 | export const CODE_SCANNING_CHECK = "Code scanning"; | |
| 46 | export const DEPENDENCY_REVIEW_CHECK = "Dependency review"; | |
| 47 | /** The starter workflow "Set up code scanning" adds. */ | |
| 48 | export const STARTER_WORKFLOW_PATH = ".g1t/workflows/code-scanning.yml"; | |
| 49 | ||
| 50 | export 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 bar | 63 | }; |
| 64 | ||
| 65 | export type WorkspaceSecuritySettings = { | |
| 66 | delegatedBypass: boolean; | |
| 67 | validityChecks: boolean; | |
| 68 | }; | |
| 69 | ||
| 70 | export 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 | ||
| 79 | export type WorkspaceSecurityView = { settings: WorkspaceSecuritySettings; activated: boolean }; | |
| 80 | ||
| 81 | export 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 | ||
| 99 | export type PatternList = { patterns: CustomPattern[]; entitled: boolean }; | |
| 100 | export type SavedPattern = { pattern: CustomPattern; tests: ([number, number] | null)[] }; | |
| 101 | ||
| 102 | export type PatternMatch = { path: string; line: number; preview: string }; | |
| 103 | export type DryRunRepo = { name: string; filesScanned: number; matches: PatternMatch[]; truncated: boolean; commit: string | null }; | |
| 104 | export type DryRun = { repos: DryRunRepo[] }; | |
| 105 | ||
| 106 | export 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 | ||
| 116 | export type BypassReason = "false_positive" | "used_in_tests" | "will_fix_later"; | |
| 117 | ||
| 118 | export 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 | ||
| 124 | export type SecretLocation = { path: string; line: number; commit: string; source: "push" | "history"; foundAt: string }; | |
| 125 | ||
| 126 | export 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 | ||
| 146 | export 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 | ||
| 156 | export type BypassResult = { secret: SecretFinding; request: BypassRequest | null }; | |
| 157 | ||
| 158 | export 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 | ||
| 193 | export 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 | ||
| 199 | export 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 | ||
| 216 | export type CodeScanning = { alerts: CodeAlert[]; analyses: Analysis[]; entitled: boolean; private: boolean; configured: boolean }; | |
| 217 | export type CodeAlertDetail = { alert: CodeAlert; activity: AlertActivity[]; analyses: Analysis[] }; | |
| 218 | ||
| 219 | export 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 | ||
| 233 | export type ReviewVulnerability = { advisory: string; osvId: string; summary: string; severity: Severity; fixedVersion: string | null; url: string }; | |
| 234 | ||
| 235 | export 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 | ||
| 250 | export 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 | ||
| 260 | export type PullScanning = { | |
| 261 | commit: string | null; | |
| 262 | results: PullResult[]; | |
| 263 | review: DependencyReview | null; | |
| 264 | codeStatus: string | null; | |
| 265 | codeDescription: string | null; | |
| 266 | }; | |
| 267 | ||
| 268 | export type AlertFix = { issue: number; started: boolean; message: string | null }; | |
| 269 | ||
| 270 | export 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 | ||
| 282 | export type GraphManifest = { path: string; ecosystem: string; dependencies: number; direct: number }; | |
| 283 | export type DependencyGraph = { commit: string | null; manifests: GraphManifest[]; dependencies: GraphDependency[] }; | |
| 284 | ||
| 285 | export type TypeTotals = { alertType: AlertType; open: SeverityCounts; opened: number; closed: number }; | |
| 286 | export type TrendPoint = { day: string; secretScanning: number; codeScanning: number; vulnerability: number }; | |
| 287 | export 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 | }; | |
| 301 | export type WorkspaceOverview = { | |
| 302 | activated: boolean; | |
| 303 | privateHidden: number; | |
| 304 | totals: TypeTotals[]; | |
| 305 | trend: TrendPoint[]; | |
| 306 | repos: RepoCoverage[]; | |
| 307 | }; | |
| 308 | ||
| 309 | export type WorkspaceAlert = { repo: string; secret?: SecretFinding | null; code?: CodeAlert | null; vulnerability?: Vulnerability | null }; | |
| 310 | ||
| 311 | export 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 | ||
| 335 | export 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. */ | |
| 385 | export function needsActivation(result: Result<unknown>): boolean { | |
| 386 | return !result.ok && result.error.code === "payment_required"; | |
| 387 | } |