Skip to content

g1t/packages/contracts/src/security.ts

319 lines12,027 bytesCodeBlame

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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import type { ServiceBinding } from "./clients";
2import type { User, Viewer } from "./identity";
3import type { RepoPath } from "./repos";
4import type { Result } from "./result";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar5import type { BumpPackage, BumpRegistry, VersionUpdatesState } from "./updates";
6
7export * from "./updates";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API8
9/**
10 * The security service: secrets found in pushes and in history, vulnerable
11 * dependencies, and the upgrade issues g1t opens for them. Members of a
12 * workspace see its findings; nobody else does. Mirrors
13 * `g1t_contracts::security`.
14 */
15
16/**
17 * Where a secret stands. `open`: in history, to be rotated. `blocked`: a
18 * push carrying it was refused, so it never landed. `allowed`: someone said
19 * it is not a real secret, and pushes carrying it go through. `resolved`:
20 * rotated or removed.
21 */
22export type SecretStatus = "open" | "blocked" | "allowed" | "resolved";
23
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily24/**
25 * Where an alert stands, as the Security page's filters and the API put it.
26 * `open`: needs someone. `dismissed`: someone said why it can stay.
27 * `fixed`: a secret revoked, or a dependency no longer vulnerable.
28 */
29export type AlertState = "open" | "dismissed" | "fixed";
30
31/** Why a person dismissed an alert. */
32export type DismissReason =
33 | "false_positive"
34 | "used_in_tests"
35 | "revoked"
36 | "wont_fix"
37 | "fix_started"
38 | "no_bandwidth"
39 | "tolerable_risk"
40 | "inaccurate"
41 | "not_used";
42
43/** The reasons a secret alert can be dismissed with, as people read them. */
44export const SECRET_DISMISS_REASONS: { reason: DismissReason; label: string; about: string }[] = [
45 { reason: "false_positive", label: "False positive", about: "It is not a secret." },
46 { reason: "used_in_tests", label: "Used in tests", about: "A value made for tests or examples." },
47 { reason: "revoked", label: "Revoked", about: "It was real and has been revoked or rotated." },
48 { reason: "wont_fix", label: "Won't fix", about: "It is real, and accepted as it is." },
49];
50
51/** The reasons a dependency alert can be dismissed with, as people read them. */
52export const DEPENDENCY_DISMISS_REASONS: { reason: DismissReason; label: string; about: string }[] = [
53 { reason: "fix_started", label: "A fix has already been started", about: "Someone is upgrading it." },
54 { reason: "no_bandwidth", label: "No bandwidth to fix this", about: "Nobody can get to it now." },
55 { reason: "tolerable_risk", label: "Risk is tolerable to this project", about: "It does not matter for how this project uses it." },
56 { reason: "inaccurate", label: "This alert is inaccurate or incorrect", about: "The advisory is wrong about this package or version." },
57 { reason: "not_used", label: "Vulnerable code is not actually used", about: "The vulnerable code is never called." },
58];
59
60/** A reason's label. */
61export function dismissLabel(reason: DismissReason): string {
62 return [...SECRET_DISMISS_REASONS, ...DEPENDENCY_DISMISS_REASONS].find((r) => r.reason === reason)?.label ?? reason;
63}
64
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API65export type SecretFinding = {
66 id: string;
67 repoId: string;
68 /** `aws_access_key`, `github_token`, … */
69 kind: string;
70 /** "an AWS access key". */
71 label: string;
72 path: string;
73 line: number;
74 commit: string;
75 /** Enough of the secret to recognise it; the secret itself is never kept. */
76 preview: string;
77 status: SecretStatus;
78 source: "push" | "history";
79 foundBy: string | null;
80 /** RFC 3339. */
81 foundAt: string;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily82 /** Who dismissed it (allowed or resolved it). */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API83 decidedBy: string | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily84 /** The comment given when it was dismissed. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API85 reason: string | null;
86 decidedAt: string | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily87 /** Why it was dismissed; absent on open alerts and older decisions. */
88 dismissedReason?: DismissReason | null;
89 /**
90 * Why the value looks made for tests or documentation, when it does. Such
91 * an alert never stops a push and is never counted as critical.
92 */
93 testValue?: string | null;
94 state: AlertState;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar95 /** What its issuer said when last asked: active, inactive, unknown or unsupported. */
96 validity?: "active" | "inactive" | "unknown" | "unsupported" | null;
97 validityCheckedAt?: string | null;
98 /** How it got past push protection, when someone bypassed it. */
99 bypass?: {
100 reason: "false_positive" | "used_in_tests" | "will_fix_later";
101 comment: string | null;
102 by: string;
103 at: string;
104 approvedBy: string | null;
105 } | null;
106 /** The custom pattern that found it, for a `custom_pattern` finding. */
107 patternId?: string | null;
108 patternName?: string | null;
109 /** How many places it was found in. */
110 locations?: number;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API111};
112
113export type Severity = "critical" | "high" | "medium" | "low" | "unknown";
114
115export const SEVERITIES: Severity[] = ["critical", "high", "medium", "low", "unknown"];
116
117export type Vulnerability = {
118 id: string;
119 repoId: string;
120 /** `npm`, `crates.io`, `Go`, `PyPI`. */
121 ecosystem: string;
122 package: string;
123 version: string;
124 /** The lockfile that resolves it. */
125 manifest: string;
126 /** Its GHSA id when it has one. */
127 advisory: string;
128 osvId: string;
129 summary: string;
130 severity: Severity;
131 fixedVersion: string | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily132 status: "open" | "fixed" | "dismissed";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent133 /** The issue opened for g1t, when upgrading needs code changes. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API134 issue: number | null;
135 foundAt: string;
136 fixedAt: string | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily137 state: AlertState;
138 dismissedBy?: string | null;
139 dismissedReason?: DismissReason | null;
140 dismissedComment?: string | null;
141 dismissedAt?: string | null;
142 /** The security update for its package, if g1t has started one. */
143 update?: SecurityUpdate | null;
144};
145
146/**
147 * Where a security update stands. `requested`: a sandbox is making the
148 * change. `open`: its pull request is going through the required checks.
149 * `superseded`: a newer update replaced it, or the package is no longer
150 * vulnerable. `needs_code`: the version could not be raised without code
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent151 * changes, so g1t has an issue for it. `failed`: see `error`.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily152 */
153export type UpdateState = "requested" | "open" | "merged" | "closed" | "superseded" | "needs_code" | "failed";
154
155/** The pull request g1t opens itself to upgrade one vulnerable package. */
156export type SecurityUpdate = {
157 state: UpdateState;
158 /** The version it upgrades to. */
159 target: string;
160 /** `g1t/security/<package>-<version>`. */
161 branch: string | null;
162 pull: number | null;
163 issue: number | null;
164 error: string | null;
165 updatedAt: string;
166};
167
168/** Secret alerts by where they stand. */
169export type SecretCounts = {
170 /** In the history and looking real: rotate these. */
171 open: number;
172 /** Stopped at a push, so never landed, and looking real. */
173 blocked: number;
174 /** Open or blocked, but likely test values. */
175 testValues: number;
176 dismissed: number;
177 fixed: number;
178};
179
180/** One thing that happened to an alert. */
181export type AlertActivity = {
182 id: string;
183 alertId: string;
184 /**
185 * `dismissed`, `reopened`, `update_requested`, `update_opened`,
186 * `update_merged`, `update_closed`, `update_superseded`,
187 * `update_needs_code` or `update_failed`.
188 */
189 action: string;
190 /** A person's username, or `g1t`. */
191 actor: string | null;
192 reason: DismissReason | null;
193 comment: string | null;
194 /** The pull request or issue it concerns. */
195 number: number | null;
196 at: string;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API197};
198
199export type SeverityCounts = Record<Severity, number>;
200
201export type ScanState = {
202 /** `pending`, `running`, `done`, or `stopped` at the workspace's limit. */
203 history: "pending" | "running" | "done" | "stopped";
204 commitsScanned: number;
205 historyFinishedAt: string | null;
206 dependenciesScannedAt: string | null;
207 dependenciesError: string | null;
208 lockfiles: string[];
209};
210
211export type SecurityOverview = {
212 repoId: string;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily213 /**
214 * Open alerts by severity: vulnerabilities open and not dismissed, and
215 * secrets in the history that look real, as critical. Blocked secrets
216 * and likely test values are not counted.
217 */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API218 counts: SeverityCounts;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily219 secretCounts: SecretCounts;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API220 secrets: SecretFinding[];
221 vulnerabilities: Vulnerability[];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily222 /** What happened to the alerts, newest first. */
223 activity: AlertActivity[];
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API224 scan: ScanState;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily225 /** Security updates: whether g1t opens a pull request for each vulnerable dependency with a fix. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API226 upkeep: boolean;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily227 versionUpdates: VersionUpdatesState;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API228};
229
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily230/** The alert `dismiss` or `reopen` changed, as it is now. */
231export type AlertChange = {
232 secret: SecretFinding | null;
233 vulnerability: Vulnerability | null;
234};
235
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API236export type RepoSecurity = {
237 repoId: string;
238 name: string;
239 counts: SeverityCounts;
240 secrets: number;
241 vulnerabilities: number;
242 upkeep: boolean;
243 dependenciesScannedAt: string | null;
244};
245
246export interface SecurityApi {
247 overview(repo: RepoPath, viewer: Viewer): Promise<Result<SecurityOverview>>;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily248 /**
249 * Dismisses an alert (a secret takes Admin, a dependency Write) with a
250 * reason and an optional comment.
251 */
252 dismiss(actor: User, repo: RepoPath, id: string, reason: DismissReason, comment: string): Promise<Result<AlertChange>>;
253 /** Opens a dismissed alert again. */
254 reopen(actor: User, repo: RepoPath, id: string): Promise<Result<AlertChange>>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API255 rescan(actor: User, repo: RepoPath): Promise<Result<ScanState>>;
256 setUpkeep(actor: User, repo: RepoPath, enabled: boolean): Promise<Result<boolean>>;
257 workspace(workspace: string, viewer: Viewer): Promise<Result<RepoSecurity[]>>;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar258 /** Checks one `updates` entry of the dependency update file for new versions now. Write and up. */
259 checkUpdates(actor: User, repo: RepoPath, entry: string): Promise<Result<VersionUpdatesState>>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API260}
261
262export function securityClient(service: ServiceBinding): SecurityApi {
263 const call = async <T>(method: string, args: object): Promise<T> => {
264 const response = await service.fetch(`https://service/rpc/${method}`, {
265 method: "POST",
266 headers: { "content-type": "application/json" },
267 body: JSON.stringify(args),
268 });
269 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
270 return (await response.json()) as T;
271 };
272 return {
273 overview: (repo, viewer) => call("overview", { repo, viewer }),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily274 dismiss: (actor, repo, id, reason, comment) => call("dismiss", { actor, repo, id, reason, comment }),
275 reopen: (actor, repo, id) => call("reopen", { actor, repo, id }),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API276 rescan: (actor, repo) => call("rescan", { actor, repo }),
277 setUpkeep: (actor, repo, enabled) => call("set_upkeep", { actor, repo, enabled }),
278 workspace: (workspace, viewer) => call("workspace", { workspace, viewer }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar279 checkUpdates: (actor, repo, entry) => call("check_updates", { actor, repo, entry }),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API280 };
281}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily282
283/**
284 * The runner's `bump`: makes a security update in a sandbox. It clones the
285 * default branch, raises `package` to `version` in each lockfile with the
286 * ecosystem's own tool, commits that as g1t and pushes it to `branch`
287 * (`g1t/security/…`). The push tells the security service to open the pull
288 * request. Mirrors `g1t_contracts::security::BumpArgs`.
289 */
290export type BumpArgs = {
291 repo: RepoPath;
292 /** OSV's name for the ecosystem: `npm`, `crates.io`, `Go` or `PyPI`. */
293 ecosystem: string;
294 package: string;
295 version: string;
296 /** The lockfiles that resolve a vulnerable version, from the root. */
297 lockfiles: string[];
298 branch: string;
299 /** The commit's message. */
300 message: string;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar301 /** `version` for a version update, whose branch is any but the default one. */
302 kind?: "version";
303 /** Several packages raised in one commit, for a grouped update. */
304 packages?: BumpPackage[];
305 /** `increase` (default), `increase-if-necessary`, `widen` or `lockfile-only`. */
306 strategy?: string;
307 /** Replace the branch if it is there already: a rebase or a recreate. */
308 force?: boolean;
309 /** Private registries the tools may read, with their credentials. */
310 registries?: BumpRegistry[];
311 /**
312 * The branch to start from, which its pull request merges into
313 * (`target-branch`): the default branch when absent.
314 */
315 base?: string;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily316};
317
318/** The prefix every security update's branch starts with. */
319export const UPDATE_BRANCH_PREFIX = "g1t/security/";