pr_01m47d15m3e54sn21z27rpy5n9/crates/contracts/src/security.rs

292 lines9,208 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 API1//! The security service: secrets found in what is pushed and in history,
2//! vulnerable dependencies, and the upgrades g1t opens for them.
3//!
4//! The repos service reads git for it (`scan_history`, `find_lockfiles`)
5//! and asks it, during a push, which secrets have been allowed
6//! (`push_blocked`). Members of a workspace see and act on its findings;
7//! nobody else does, whether or not the repository is public.
8
9use serde::{Deserialize, Serialize};
10
11use crate::User;
12use crate::repos::RepoPath;
13
14/// Where a secret stands.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum SecretStatus {
18 /// In the repository's history: it has to be rotated, then resolved.
19 Open,
20 /// A push carrying it was refused, so it never landed.
21 Blocked,
22 /// Someone said it is not a real secret; pushes carrying it go through.
23 Allowed,
24 /// Someone rotated or removed it.
25 Resolved,
26}
27
28impl SecretStatus {
29 pub fn as_str(self) -> &'static str {
30 match self {
31 SecretStatus::Open => "open",
32 SecretStatus::Blocked => "blocked",
33 SecretStatus::Allowed => "allowed",
34 SecretStatus::Resolved => "resolved",
35 }
36 }
37
38 pub fn parse(text: &str) -> Option<SecretStatus> {
39 Some(match text {
40 "open" => SecretStatus::Open,
41 "blocked" => SecretStatus::Blocked,
42 "allowed" => SecretStatus::Allowed,
43 "resolved" => SecretStatus::Resolved,
44 _ => return None,
45 })
46 }
47}
48
49/// A secret found in a repository. The secret itself is never kept: only
50/// a fingerprint, to know it again, and a preview a person recognises.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct SecretFinding {
54 pub id: String,
55 pub repo_id: String,
56 /// `aws_access_key`, `github_token`, …
57 pub kind: String,
58 /// "an AWS access key".
59 pub label: String,
60 pub path: String,
61 pub line: u32,
62 pub commit: String,
63 pub preview: String,
64 pub status: SecretStatus,
65 /// `push` or `history`.
66 pub source: String,
67 /// Who pushed it, for a push.
68 pub found_by: Option<String>,
69 /// RFC 3339.
70 pub found_at: String,
71 /// Who allowed or resolved it, and why.
72 pub decided_by: Option<String>,
73 pub reason: Option<String>,
74 pub decided_at: Option<String>,
75}
76
77/// A secret as the repos service finds it, before it is stored.
78#[derive(Clone, Debug, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct NewSecret {
81 pub fingerprint: String,
82 pub kind: String,
83 pub path: String,
84 pub line: u32,
85 pub commit: String,
86 pub preview: String,
87}
88
89/// `push_blocked`: the secrets a push would add. Those allowed before are
90/// returned; the rest are recorded as blocked. Called by the repos service.
91/// Returns `PushVerdict`.
92#[derive(Debug, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct PushBlockedArgs {
95 /// The repository the findings belong to: for a pull request's fork,
96 /// the repository it was made from.
97 pub repo_id: String,
98 pub path: RepoPath,
99 pub pusher: Option<String>,
100 pub secrets: Vec<NewSecret>,
101}
102
103#[derive(Debug, Default, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct PushVerdict {
106 /// Fingerprints that were allowed and so do not stop the push.
107 pub allowed: Vec<String>,
108 /// The finding recorded for each fingerprint that stops it.
109 pub ids: Vec<(String, String)>,
110}
111
112/// `scan_history` (repos): looks for secrets in a page of the default
113/// branch's history, newest first, each commit against its first parent.
114/// Returns `HistoryPage`.
115#[derive(Debug, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct ScanHistoryArgs {
118 pub repo_id: String,
119 /// Where the last page stopped; the head of the default branch when absent.
120 #[serde(default)]
121 pub after: Option<String>,
122 pub limit: u32,
123}
124
125#[derive(Debug, Default, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct HistoryPage {
128 pub secrets: Vec<NewSecret>,
129 pub commits: u32,
130 /// Where the next page starts; none when the history is done.
131 pub next: Option<String>,
132 /// How many objects were read from the store: what the scan cost.
133 pub reads: u32,
134}
135
136/// `find_lockfiles` (repos): the lockfiles on the default branch.
137/// Returns `Lockfiles`.
138#[derive(Debug, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct FindLockfilesArgs {
141 pub repo_id: String,
142}
143
144#[derive(Debug, Default, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct Lockfiles {
147 /// The commit they were read at; none for an empty repository.
148 pub commit: Option<String>,
149 pub files: Vec<LockfileText>,
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153pub struct LockfileText {
154 pub path: String,
155 pub text: String,
156}
157
158/// Where a vulnerable dependency stands.
159#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "lowercase")]
161pub enum VulnStatus {
162 Open,
163 /// The version in use is no longer affected.
164 Fixed,
165}
166
167/// One advisory against one package at the version a lockfile resolves.
168#[derive(Clone, Debug, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct Vulnerability {
171 pub id: String,
172 pub repo_id: String,
173 /// `npm`, `crates.io`, `Go`, `PyPI`.
174 pub ecosystem: String,
175 pub package: String,
176 pub version: String,
177 /// The lockfile that resolves it.
178 pub manifest: String,
179 /// The id people know it by (its GHSA id when it has one).
180 pub advisory: String,
181 pub osv_id: String,
182 pub summary: String,
183 /// `critical`, `high`, `medium`, `low` or `unknown`.
184 pub severity: String,
185 pub fixed_version: Option<String>,
186 pub status: VulnStatus,
187 /// The issue opened to upgrade the package.
188 pub issue: Option<u32>,
189 /// RFC 3339.
190 pub found_at: String,
191 pub fixed_at: Option<String>,
192}
193
194/// Open findings by severity.
195#[derive(Clone, Debug, Default, Serialize, Deserialize)]
196pub struct SeverityCounts {
197 pub critical: u32,
198 pub high: u32,
199 pub medium: u32,
200 pub low: u32,
201 pub unknown: u32,
202}
203
204/// How a repository's history scan stands.
205#[derive(Clone, Debug, Default, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct ScanState {
208 /// `pending`, `running`, `done` or `stopped` (over the workspace's limit).
209 pub history: String,
210 pub commits_scanned: u32,
211 /// RFC 3339.
212 pub history_finished_at: Option<String>,
213 pub dependencies_scanned_at: Option<String>,
214 /// Why the last dependency scan failed, if it did.
215 pub dependencies_error: Option<String>,
216 pub lockfiles: Vec<String>,
217}
218
219/// Everything the Security page shows for one repository.
220#[derive(Clone, Debug, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct SecurityOverview {
223 pub repo_id: String,
224 /// Open vulnerabilities by severity. Open and blocked secrets are
225 /// counted as critical: a leaked key is the worst thing a repository
226 /// can hold.
227 pub counts: SeverityCounts,
228 pub secrets: Vec<SecretFinding>,
229 pub vulnerabilities: Vec<Vulnerability>,
230 pub scan: ScanState,
231 /// Whether g1t opens upgrade issues and puts its agent on them.
232 pub upkeep: bool,
233}
234
235/// `overview`: members of the workspace only. Returns
236/// `Outcome<SecurityOverview>`.
237#[derive(Debug, Serialize, Deserialize)]
238pub struct OverviewArgs {
239 pub repo: RepoPath,
240 pub viewer: Option<User>,
241}
242
243/// `decide_secret`: allows a secret (not a real one, or accepted), marks it
244/// resolved (rotated or removed), or opens it again. Members only; a reason
245/// is required to allow or resolve. Returns `Outcome<SecretFinding>`.
246#[derive(Debug, Serialize, Deserialize)]
247pub struct DecideSecretArgs {
248 pub actor: User,
249 pub repo: RepoPath,
250 pub id: String,
251 /// `allow`, `resolve` or `reopen`.
252 pub decision: String,
253 #[serde(default)]
254 pub reason: String,
255}
256
257/// `rescan`: scans the dependencies again now, and the history from the
258/// start. Members only. Returns `Outcome<ScanState>`.
259#[derive(Debug, Serialize, Deserialize)]
260pub struct RescanArgs {
261 pub actor: User,
262 pub repo: RepoPath,
263}
264
265/// `set_upkeep`: whether g1t opens upgrade issues for this repository and
266/// puts its agent on them. Members only. Returns `Outcome<bool>`.
267#[derive(Debug, Serialize, Deserialize)]
268pub struct SetUpkeepArgs {
269 pub actor: User,
270 pub repo: RepoPath,
271 pub enabled: bool,
272}
273
274/// `workspace`: every repository of a workspace that has findings, for
275/// its members. Returns `Outcome<Vec<RepoSecurity>>`.
276#[derive(Debug, Serialize, Deserialize)]
277pub struct WorkspaceArgs {
278 pub workspace: String,
279 pub viewer: Option<User>,
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct RepoSecurity {
285 pub repo_id: String,
286 pub name: String,
287 pub counts: SeverityCounts,
288 pub secrets: u32,
289 pub vulnerabilities: u32,
290 pub upkeep: bool,
291 pub dependencies_scanned_at: Option<String>,
292}