flagon-io/g1t

public

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

g1t/packages/contracts/src/security.ts

136 lines4,363 bytesCodeBlame
1import type { ServiceBinding } from "./clients";
2import type { User, Viewer } from "./identity";
3import type { RepoPath } from "./repos";
4import type { Result } from "./result";
5
6/**
7 * The security service: secrets found in pushes and in history, vulnerable
8 * dependencies, and the upgrade issues g1t opens for them. Members of a
9 * workspace see its findings; nobody else does. Mirrors
10 * `g1t_contracts::security`.
11 */
12
13/**
14 * Where a secret stands. `open`: in history, to be rotated. `blocked`: a
15 * push carrying it was refused, so it never landed. `allowed`: someone said
16 * it is not a real secret, and pushes carrying it go through. `resolved`:
17 * rotated or removed.
18 */
19export type SecretStatus = "open" | "blocked" | "allowed" | "resolved";
20
21export type SecretFinding = {
22 id: string;
23 repoId: string;
24 /** `aws_access_key`, `github_token`, … */
25 kind: string;
26 /** "an AWS access key". */
27 label: string;
28 path: string;
29 line: number;
30 commit: string;
31 /** Enough of the secret to recognise it; the secret itself is never kept. */
32 preview: string;
33 status: SecretStatus;
34 source: "push" | "history";
35 foundBy: string | null;
36 /** RFC 3339. */
37 foundAt: string;
38 decidedBy: string | null;
39 reason: string | null;
40 decidedAt: string | null;
41};
42
43export type Severity = "critical" | "high" | "medium" | "low" | "unknown";
44
45export const SEVERITIES: Severity[] = ["critical", "high", "medium", "low", "unknown"];
46
47export type Vulnerability = {
48 id: string;
49 repoId: string;
50 /** `npm`, `crates.io`, `Go`, `PyPI`. */
51 ecosystem: string;
52 package: string;
53 version: string;
54 /** The lockfile that resolves it. */
55 manifest: string;
56 /** Its GHSA id when it has one. */
57 advisory: string;
58 osvId: string;
59 summary: string;
60 severity: Severity;
61 fixedVersion: string | null;
62 status: "open" | "fixed";
63 /** The upgrade issue opened for the package. */
64 issue: number | null;
65 foundAt: string;
66 fixedAt: string | null;
67};
68
69export type SeverityCounts = Record<Severity, number>;
70
71export type ScanState = {
72 /** `pending`, `running`, `done`, or `stopped` at the workspace's limit. */
73 history: "pending" | "running" | "done" | "stopped";
74 commitsScanned: number;
75 historyFinishedAt: string | null;
76 dependenciesScannedAt: string | null;
77 dependenciesError: string | null;
78 lockfiles: string[];
79};
80
81export type SecurityOverview = {
82 repoId: string;
83 /** Open vulnerabilities by severity; open and blocked secrets count as critical. */
84 counts: SeverityCounts;
85 secrets: SecretFinding[];
86 vulnerabilities: Vulnerability[];
87 scan: ScanState;
88 /** Whether g1t opens upgrade issues and puts its agent on them. */
89 upkeep: boolean;
90};
91
92export type RepoSecurity = {
93 repoId: string;
94 name: string;
95 counts: SeverityCounts;
96 secrets: number;
97 vulnerabilities: number;
98 upkeep: boolean;
99 dependenciesScannedAt: string | null;
100};
101
102export type SecretDecision = "allow" | "resolve" | "reopen";
103
104export interface SecurityApi {
105 overview(repo: RepoPath, viewer: Viewer): Promise<Result<SecurityOverview>>;
106 decideSecret(
107 actor: User,
108 repo: RepoPath,
109 id: string,
110 decision: SecretDecision,
111 reason: string,
112 ): Promise<Result<SecretFinding>>;
113 rescan(actor: User, repo: RepoPath): Promise<Result<ScanState>>;
114 setUpkeep(actor: User, repo: RepoPath, enabled: boolean): Promise<Result<boolean>>;
115 workspace(workspace: string, viewer: Viewer): Promise<Result<RepoSecurity[]>>;
116}
117
118export function securityClient(service: ServiceBinding): SecurityApi {
119 const call = async <T>(method: string, args: object): Promise<T> => {
120 const response = await service.fetch(`https://service/rpc/${method}`, {
121 method: "POST",
122 headers: { "content-type": "application/json" },
123 body: JSON.stringify(args),
124 });
125 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
126 return (await response.json()) as T;
127 };
128 return {
129 overview: (repo, viewer) => call("overview", { repo, viewer }),
130 decideSecret: (actor, repo, id, decision, reason) =>
131 call("decide_secret", { actor, repo, id, decision, reason }),
132 rescan: (actor, repo) => call("rescan", { actor, repo }),
133 setUpkeep: (actor, repo, enabled) => call("set_upkeep", { actor, repo, enabled }),
134 workspace: (workspace, viewer) => call("workspace", { workspace, viewer }),
135 };
136}