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 API | 1 | //! 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 | ||
| 9 | use serde::{Deserialize, Serialize}; | |
| 10 | ||
| 11 | use crate::User; | |
| 12 | use crate::repos::RepoPath; | |
| 13 | ||
| 14 | /// Where a secret stands. | |
| 15 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 16 | #[serde(rename_all = "lowercase")] | |
| 17 | pub 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 | ||
| 28 | impl 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 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 49 | /// Why a person dismissed an alert. The first four are for secrets, the |
| 50 | /// rest for vulnerable dependencies; see [`DismissReason::for_secrets`]. | |
| 51 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 52 | #[serde(rename_all = "snake_case")] | |
| 53 | pub enum DismissReason { | |
| 54 | /// Not a secret at all. | |
| 55 | FalsePositive, | |
| 56 | /// A value made for tests or examples. | |
| 57 | UsedInTests, | |
| 58 | /// It was real, and has been revoked or rotated: the alert is fixed. | |
| 59 | Revoked, | |
| 60 | /// Real, and accepted as it is. | |
| 61 | WontFix, | |
| 62 | /// Someone is already upgrading it. | |
| 63 | FixStarted, | |
| 64 | /// Nobody can get to it now. | |
| 65 | NoBandwidth, | |
| 66 | /// The vulnerability does not matter for how this project uses it. | |
| 67 | TolerableRisk, | |
| 68 | /// The advisory is wrong about this package or version. | |
| 69 | Inaccurate, | |
| 70 | /// The vulnerable code is never called. | |
| 71 | NotUsed, | |
| 72 | } | |
| 73 | ||
| 74 | impl DismissReason { | |
| 75 | pub const ALL: [DismissReason; 9] = [ | |
| 76 | DismissReason::FalsePositive, | |
| 77 | DismissReason::UsedInTests, | |
| 78 | DismissReason::Revoked, | |
| 79 | DismissReason::WontFix, | |
| 80 | DismissReason::FixStarted, | |
| 81 | DismissReason::NoBandwidth, | |
| 82 | DismissReason::TolerableRisk, | |
| 83 | DismissReason::Inaccurate, | |
| 84 | DismissReason::NotUsed, | |
| 85 | ]; | |
| 86 | ||
| 87 | pub fn as_str(self) -> &'static str { | |
| 88 | match self { | |
| 89 | DismissReason::FalsePositive => "false_positive", | |
| 90 | DismissReason::UsedInTests => "used_in_tests", | |
| 91 | DismissReason::Revoked => "revoked", | |
| 92 | DismissReason::WontFix => "wont_fix", | |
| 93 | DismissReason::FixStarted => "fix_started", | |
| 94 | DismissReason::NoBandwidth => "no_bandwidth", | |
| 95 | DismissReason::TolerableRisk => "tolerable_risk", | |
| 96 | DismissReason::Inaccurate => "inaccurate", | |
| 97 | DismissReason::NotUsed => "not_used", | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | pub fn parse(text: &str) -> Option<DismissReason> { | |
| 102 | DismissReason::ALL.into_iter().find(|reason| reason.as_str() == text) | |
| 103 | } | |
| 104 | ||
| 105 | /// For people. | |
| 106 | pub fn label(self) -> &'static str { | |
| 107 | match self { | |
| 108 | DismissReason::FalsePositive => "False positive", | |
| 109 | DismissReason::UsedInTests => "Used in tests", | |
| 110 | DismissReason::Revoked => "Revoked", | |
| 111 | DismissReason::WontFix => "Won't fix", | |
| 112 | DismissReason::FixStarted => "A fix has already been started", | |
| 113 | DismissReason::NoBandwidth => "No bandwidth to fix this", | |
| 114 | DismissReason::TolerableRisk => "Risk is tolerable to this project", | |
| 115 | DismissReason::Inaccurate => "This alert is inaccurate or incorrect", | |
| 116 | DismissReason::NotUsed => "Vulnerable code is not actually used", | |
| 117 | } | |
| 118 | } | |
| 119 | ||
| 120 | /// Whether it closes a secret alert (the rest close dependency alerts). | |
| 121 | pub fn for_secrets(self) -> bool { | |
| 122 | matches!( | |
| 123 | self, | |
| 124 | DismissReason::FalsePositive | DismissReason::UsedInTests | DismissReason::Revoked | DismissReason::WontFix | |
| 125 | ) | |
| 126 | } | |
| 127 | ||
| 128 | /// The status a secret takes: revoked means fixed (`resolved`); the | |
| 129 | /// others say it is no danger, so pushes carrying it go through | |
| 130 | /// (`allowed`). | |
| 131 | pub fn secret_status(self) -> SecretStatus { | |
| 132 | if self == DismissReason::Revoked { SecretStatus::Resolved } else { SecretStatus::Allowed } | |
| 133 | } | |
| 134 | } | |
| 135 | ||
| 136 | /// Where an alert stands, as the Security page's filters and the API put it. | |
| 137 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 138 | #[serde(rename_all = "lowercase")] | |
| 139 | pub enum AlertState { | |
| 140 | /// Needs someone: a secret open or blocked, a dependency still vulnerable. | |
| 141 | Open, | |
| 142 | /// Someone said why it can stay. | |
| 143 | Dismissed, | |
| 144 | /// A secret revoked, or a dependency no longer vulnerable. | |
| 145 | Fixed, | |
| 146 | } | |
| 147 | ||
| 148 | impl AlertState { | |
| 149 | pub fn as_str(self) -> &'static str { | |
| 150 | match self { | |
| 151 | AlertState::Open => "open", | |
| 152 | AlertState::Dismissed => "dismissed", | |
| 153 | AlertState::Fixed => "fixed", | |
| 154 | } | |
| 155 | } | |
| 156 | ||
| 157 | pub fn parse(text: &str) -> Option<AlertState> { | |
| 158 | Some(match text { | |
| 159 | "open" => AlertState::Open, | |
| 160 | "dismissed" => AlertState::Dismissed, | |
| 161 | "fixed" => AlertState::Fixed, | |
| 162 | _ => return None, | |
| 163 | }) | |
| 164 | } | |
| 165 | } | |
| 166 | ||
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 167 | /// A secret found in a repository. The secret itself is never kept: only |
| 168 | /// a fingerprint, to know it again, and a preview a person recognises. | |
| 169 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 170 | #[serde(rename_all = "camelCase")] | |
| 171 | pub struct SecretFinding { | |
| 172 | pub id: String, | |
| 173 | pub repo_id: String, | |
| 174 | /// `aws_access_key`, `github_token`, … | |
| 175 | pub kind: String, | |
| 176 | /// "an AWS access key". | |
| 177 | pub label: String, | |
| 178 | pub path: String, | |
| 179 | pub line: u32, | |
| 180 | pub commit: String, | |
| 181 | pub preview: String, | |
| 182 | pub status: SecretStatus, | |
| 183 | /// `push` or `history`. | |
| 184 | pub source: String, | |
| 185 | /// Who pushed it, for a push. | |
| 186 | pub found_by: Option<String>, | |
| 187 | /// RFC 3339. | |
| 188 | pub found_at: String, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 189 | /// 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 API | 190 | pub decided_by: Option<String>, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 191 | /// 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 API | 192 | pub reason: Option<String>, |
| 193 | pub decided_at: Option<String>, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 194 | /// Why it was dismissed. Absent on open alerts, and on alerts decided |
| 195 | /// before reasons were recorded. | |
| 196 | #[serde(default)] | |
| 197 | pub dismissed_reason: Option<DismissReason>, | |
| 198 | /// Why the value looks made for tests or documentation (a documented | |
| 199 | /// example key, a counting or repeating value), when it does. Such an | |
| 200 | /// alert never stops a push and is never counted as critical. | |
| 201 | #[serde(default)] | |
| 202 | pub test_value: Option<String>, | |
| 203 | /// Open, dismissed or fixed. | |
| 204 | pub state: AlertState, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 205 | /// What its issuer said when last asked: `active`, `inactive`, |
| 206 | /// `unknown` or `unsupported`; absent when never asked. | |
| 207 | #[serde(default)] | |
| 208 | pub validity: Option<String>, | |
| 209 | #[serde(default)] | |
| 210 | pub validity_checked_at: Option<String>, | |
| 211 | /// How it got past push protection, when someone bypassed it. | |
| 212 | #[serde(default)] | |
| 213 | pub bypass: Option<crate::security_suite::Bypass>, | |
| 214 | /// The custom pattern that found it, for a `custom_pattern` finding. | |
| 215 | #[serde(default)] | |
| 216 | pub pattern_id: Option<String>, | |
| 217 | #[serde(default)] | |
| 218 | pub pattern_name: Option<String>, | |
| 219 | /// How many places it was found in. | |
| 220 | #[serde(default)] | |
| 221 | pub locations: u32, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 222 | } |
| 223 | ||
| 224 | impl SecretStatus { | |
| 225 | /// The alert state a secret in this status is in. | |
| 226 | pub fn state(self) -> AlertState { | |
| 227 | match self { | |
| 228 | SecretStatus::Open | SecretStatus::Blocked => AlertState::Open, | |
| 229 | SecretStatus::Allowed => AlertState::Dismissed, | |
| 230 | SecretStatus::Resolved => AlertState::Fixed, | |
| 231 | } | |
| 232 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 233 | } |
| 234 | ||
| 235 | /// A secret as the repos service finds it, before it is stored. | |
| 236 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 237 | #[serde(rename_all = "camelCase")] | |
| 238 | pub struct NewSecret { | |
| 239 | pub fingerprint: String, | |
| 240 | pub kind: String, | |
| 241 | pub path: String, | |
| 242 | pub line: u32, | |
| 243 | pub commit: String, | |
| 244 | pub preview: String, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 245 | /// Why it looks like a test value, from `g1t_scan::secrets::test_value`. |
| 246 | /// Such a secret is recorded but never stops a push. | |
| 247 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 248 | pub test_value: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 249 | /// For a custom pattern's finding (`kind` `custom_pattern`): which |
| 250 | /// pattern, and its name. | |
| 251 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 252 | pub pattern_id: Option<String>, | |
| 253 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 254 | pub pattern_name: Option<String>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 255 | } |
| 256 | ||
| 257 | /// `push_blocked`: the secrets a push would add. Those allowed before are | |
| 258 | /// returned; the rest are recorded as blocked. Called by the repos service. | |
| 259 | /// Returns `PushVerdict`. | |
| 260 | #[derive(Debug, Serialize, Deserialize)] | |
| 261 | #[serde(rename_all = "camelCase")] | |
| 262 | pub struct PushBlockedArgs { | |
| 263 | /// The repository the findings belong to: for a pull request's fork, | |
| 264 | /// the repository it was made from. | |
| 265 | pub repo_id: String, | |
| 266 | pub path: RepoPath, | |
| 267 | pub pusher: Option<String>, | |
| 268 | pub secrets: Vec<NewSecret>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 269 | /// Whether the repository is private, as repos read it. |
| 270 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 271 | pub private: Option<bool>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 272 | } |
| 273 | ||
| 274 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 275 | #[serde(rename_all = "camelCase")] | |
| 276 | pub struct PushVerdict { | |
| 277 | /// Fingerprints that were allowed and so do not stop the push. | |
| 278 | pub allowed: Vec<String>, | |
| 279 | /// The finding recorded for each fingerprint that stops it. | |
| 280 | pub ids: Vec<(String, String)>, | |
| 281 | } | |
| 282 | ||
| 283 | /// `scan_history` (repos): looks for secrets in a page of the default | |
| 284 | /// branch's history, newest first, each commit against its first parent. | |
| 285 | /// Returns `HistoryPage`. | |
| 286 | #[derive(Debug, Serialize, Deserialize)] | |
| 287 | #[serde(rename_all = "camelCase")] | |
| 288 | pub struct ScanHistoryArgs { | |
| 289 | pub repo_id: String, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 290 | /// Where the last page stopped; `from`, or the head of the default |
| 291 | /// branch, when absent. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 292 | #[serde(default)] |
| 293 | pub after: Option<String>, | |
| 294 | pub limit: u32, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 295 | /// For a pushed range: its newest commit, on whichever branch. |
| 296 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 297 | pub from: Option<String>, | |
| 298 | /// For a pushed range: where the branch was before, which is not | |
| 299 | /// scanned. The page ends there, with no next. | |
| 300 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 301 | pub until: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 302 | /// Custom patterns to look for too. |
| 303 | #[serde(default, skip_serializing_if = "Vec::is_empty")] | |
| 304 | pub patterns: Vec<crate::security_suite::PatternSpec>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 305 | } |
| 306 | ||
| 307 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 308 | #[serde(rename_all = "camelCase")] | |
| 309 | pub struct HistoryPage { | |
| 310 | pub secrets: Vec<NewSecret>, | |
| 311 | pub commits: u32, | |
| 312 | /// Where the next page starts; none when the history is done. | |
| 313 | pub next: Option<String>, | |
| 314 | /// How many objects were read from the store: what the scan cost. | |
| 315 | pub reads: u32, | |
| 316 | } | |
| 317 | ||
| 318 | /// `find_lockfiles` (repos): the lockfiles on the default branch. | |
| 319 | /// Returns `Lockfiles`. | |
| 320 | #[derive(Debug, Serialize, Deserialize)] | |
| 321 | #[serde(rename_all = "camelCase")] | |
| 322 | pub struct FindLockfilesArgs { | |
| 323 | pub repo_id: String, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 324 | /// A commit, branch or tag to read them at; the default branch when |
| 325 | /// absent. | |
| 326 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 327 | pub git_ref: Option<String>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 328 | } |
| 329 | ||
| 330 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 331 | #[serde(rename_all = "camelCase")] | |
| 332 | pub struct Lockfiles { | |
| 333 | /// The commit they were read at; none for an empty repository. | |
| 334 | pub commit: Option<String>, | |
| 335 | pub files: Vec<LockfileText>, | |
| 336 | } | |
| 337 | ||
| 338 | #[derive(Debug, Serialize, Deserialize)] | |
| 339 | pub struct LockfileText { | |
| 340 | pub path: String, | |
| 341 | pub text: String, | |
| 342 | } | |
| 343 | ||
| 344 | /// Where a vulnerable dependency stands. | |
| 345 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 346 | #[serde(rename_all = "lowercase")] | |
| 347 | pub enum VulnStatus { | |
| 348 | Open, | |
| 349 | /// The version in use is no longer affected. | |
| 350 | Fixed, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 351 | /// Someone said why it can stay. Found again, it stays dismissed until |
| 352 | /// someone reopens it. | |
| 353 | Dismissed, | |
| 354 | } | |
| 355 | ||
| 356 | impl VulnStatus { | |
| 357 | pub fn as_str(self) -> &'static str { | |
| 358 | match self { | |
| 359 | VulnStatus::Open => "open", | |
| 360 | VulnStatus::Fixed => "fixed", | |
| 361 | VulnStatus::Dismissed => "dismissed", | |
| 362 | } | |
| 363 | } | |
| 364 | ||
| 365 | pub fn parse(text: &str) -> Option<VulnStatus> { | |
| 366 | Some(match text { | |
| 367 | "open" => VulnStatus::Open, | |
| 368 | "fixed" => VulnStatus::Fixed, | |
| 369 | "dismissed" => VulnStatus::Dismissed, | |
| 370 | _ => return None, | |
| 371 | }) | |
| 372 | } | |
| 373 | ||
| 374 | pub fn state(self) -> AlertState { | |
| 375 | match self { | |
| 376 | VulnStatus::Open => AlertState::Open, | |
| 377 | VulnStatus::Fixed => AlertState::Fixed, | |
| 378 | VulnStatus::Dismissed => AlertState::Dismissed, | |
| 379 | } | |
| 380 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 381 | } |
| 382 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 383 | /// Where the security update for a vulnerable package stands: the pull |
| 384 | /// request g1t opens itself to upgrade it, on the branch | |
| 385 | /// `g1t/security/<package>-<version>`. | |
| 386 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 387 | #[serde(rename_all = "snake_case")] | |
| 388 | pub enum UpdateState { | |
| 389 | /// A sandbox is making the change. | |
| 390 | Requested, | |
| 391 | /// Its pull request is open, going through the required checks. | |
| 392 | Open, | |
| 393 | Merged, | |
| 394 | /// Closed without merging, by a person. | |
| 395 | Closed, | |
| 396 | /// A newer security update replaced it, or the package is no longer | |
| 397 | /// vulnerable; g1t closed it. | |
| 398 | Superseded, | |
| 399 | /// The version could not be raised without changing code: an issue | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 400 | /// was opened for g1t instead. |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 401 | NeedsCode, |
| 402 | /// It could not be made; why is in `error`. | |
| 403 | Failed, | |
| 404 | } | |
| 405 | ||
| 406 | impl UpdateState { | |
| 407 | pub const ALL: [UpdateState; 7] = [ | |
| 408 | UpdateState::Requested, | |
| 409 | UpdateState::Open, | |
| 410 | UpdateState::Merged, | |
| 411 | UpdateState::Closed, | |
| 412 | UpdateState::Superseded, | |
| 413 | UpdateState::NeedsCode, | |
| 414 | UpdateState::Failed, | |
| 415 | ]; | |
| 416 | ||
| 417 | pub fn as_str(self) -> &'static str { | |
| 418 | match self { | |
| 419 | UpdateState::Requested => "requested", | |
| 420 | UpdateState::Open => "open", | |
| 421 | UpdateState::Merged => "merged", | |
| 422 | UpdateState::Closed => "closed", | |
| 423 | UpdateState::Superseded => "superseded", | |
| 424 | UpdateState::NeedsCode => "needs_code", | |
| 425 | UpdateState::Failed => "failed", | |
| 426 | } | |
| 427 | } | |
| 428 | ||
| 429 | pub fn parse(text: &str) -> Option<UpdateState> { | |
| 430 | UpdateState::ALL.into_iter().find(|state| state.as_str() == text) | |
| 431 | } | |
| 432 | ||
| 433 | /// Whether g1t is still working on it. | |
| 434 | pub fn in_progress(self) -> bool { | |
| 435 | matches!(self, UpdateState::Requested | UpdateState::Open | UpdateState::NeedsCode) | |
| 436 | } | |
| 437 | } | |
| 438 | ||
| 439 | /// The security update for one package. | |
| 440 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 441 | #[serde(rename_all = "camelCase")] | |
| 442 | pub struct SecurityUpdate { | |
| 443 | pub state: UpdateState, | |
| 444 | /// The version it upgrades to. | |
| 445 | pub target: String, | |
| 446 | /// `g1t/security/<package>-<version>`. | |
| 447 | pub branch: Option<String>, | |
| 448 | /// The pull request g1t opened. | |
| 449 | pub pull: Option<u32>, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 450 | /// The issue opened for g1t, when the upgrade needs code changes. |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 451 | pub issue: Option<u32>, |
| 452 | /// Why it failed, when it did. | |
| 453 | pub error: Option<String>, | |
| 454 | /// RFC 3339. | |
| 455 | pub updated_at: String, | |
| 456 | } | |
| 457 | ||
| 458 | /// The prefix every security update's branch starts with. | |
| 459 | pub const UPDATE_BRANCH_PREFIX: &str = "g1t/security/"; | |
| 460 | ||
| 461 | /// The branch a security update is made on: `g1t/security/<package>-<version>`, | |
| 462 | /// with anything a branch name cannot hold (a scope's `@` and `/`) as `-`. | |
| 463 | pub fn update_branch(package: &str, version: &str) -> String { | |
| 464 | let clean = |text: &str| -> String { | |
| 465 | let mut out = String::new(); | |
| 466 | for c in text.chars() { | |
| 467 | let c = if c.is_ascii_alphanumeric() || matches!(c, '.' | '_') { c } else { '-' }; | |
| 468 | if !(c == '-' && out.ends_with('-')) { | |
| 469 | out.push(c); | |
| 470 | } | |
| 471 | } | |
| 472 | out.trim_matches(['-', '.']).to_owned() | |
| 473 | }; | |
| 474 | format!("{UPDATE_BRANCH_PREFIX}{}-{}", clean(package), clean(version)) | |
| 475 | } | |
| 476 | ||
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 477 | /// One advisory against one package at the version a lockfile resolves. |
| 478 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 479 | #[serde(rename_all = "camelCase")] | |
| 480 | pub struct Vulnerability { | |
| 481 | pub id: String, | |
| 482 | pub repo_id: String, | |
| 483 | /// `npm`, `crates.io`, `Go`, `PyPI`. | |
| 484 | pub ecosystem: String, | |
| 485 | pub package: String, | |
| 486 | pub version: String, | |
| 487 | /// The lockfile that resolves it. | |
| 488 | pub manifest: String, | |
| 489 | /// The id people know it by (its GHSA id when it has one). | |
| 490 | pub advisory: String, | |
| 491 | pub osv_id: String, | |
| 492 | pub summary: String, | |
| 493 | /// `critical`, `high`, `medium`, `low` or `unknown`. | |
| 494 | pub severity: String, | |
| 495 | pub fixed_version: Option<String>, | |
| 496 | pub status: VulnStatus, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 497 | /// The issue opened to upgrade the package, when g1t was put on |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 498 | /// it: the upgrade needs code changes, or predates security updates. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 499 | pub issue: Option<u32>, |
| 500 | /// RFC 3339. | |
| 501 | pub found_at: String, | |
| 502 | pub fixed_at: Option<String>, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 503 | /// Open, dismissed or fixed. |
| 504 | pub state: AlertState, | |
| 505 | /// Who dismissed it, why, with what comment, and when. | |
| 506 | #[serde(default)] | |
| 507 | pub dismissed_by: Option<String>, | |
| 508 | #[serde(default)] | |
| 509 | pub dismissed_reason: Option<DismissReason>, | |
| 510 | #[serde(default)] | |
| 511 | pub dismissed_comment: Option<String>, | |
| 512 | #[serde(default)] | |
| 513 | pub dismissed_at: Option<String>, | |
| 514 | /// The security update for its package, if g1t has started one. | |
| 515 | #[serde(default)] | |
| 516 | pub update: Option<SecurityUpdate>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 517 | } |
| 518 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 519 | /// Open alerts by severity: vulnerabilities open and not dismissed, and |
| 520 | /// secrets in the history that look real, as critical. A blocked secret | |
| 521 | /// never landed and a likely test value is no danger, so neither counts. | |
| 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. | 522 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 523 | pub struct SeverityCounts { |
| 524 | pub critical: u32, | |
| 525 | pub high: u32, | |
| 526 | pub medium: u32, | |
| 527 | pub low: u32, | |
| 528 | pub unknown: u32, | |
| 529 | } | |
| 530 | ||
| 531 | /// How a repository's history scan stands. | |
| 532 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 533 | #[serde(rename_all = "camelCase")] | |
| 534 | pub struct ScanState { | |
| 535 | /// `pending`, `running`, `done` or `stopped` (over the workspace's limit). | |
| 536 | pub history: String, | |
| 537 | pub commits_scanned: u32, | |
| 538 | /// RFC 3339. | |
| 539 | pub history_finished_at: Option<String>, | |
| 540 | pub dependencies_scanned_at: Option<String>, | |
| 541 | /// Why the last dependency scan failed, if it did. | |
| 542 | pub dependencies_error: Option<String>, | |
| 543 | pub lockfiles: Vec<String>, | |
| 544 | } | |
| 545 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 546 | /// Secret alerts by where they stand. |
| 547 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 548 | #[serde(rename_all = "camelCase")] | |
| 549 | pub struct SecretCounts { | |
| 550 | /// In the history and looking real: rotate these. | |
| 551 | pub open: u32, | |
| 552 | /// Stopped at a push, so never landed, and looking real. | |
| 553 | pub blocked: u32, | |
| 554 | /// Open or blocked, but likely test values. | |
| 555 | pub test_values: u32, | |
| 556 | pub dismissed: u32, | |
| 557 | pub fixed: u32, | |
| 558 | } | |
| 559 | ||
| 560 | /// One thing that happened to an alert, for its activity log. | |
| 561 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 562 | #[serde(rename_all = "camelCase")] | |
| 563 | pub struct AlertActivity { | |
| 564 | pub id: String, | |
| 565 | /// The secret's or vulnerability's id. | |
| 566 | pub alert_id: String, | |
| 567 | /// `dismissed`, `reopened`, `update_requested`, `update_opened`, | |
| 568 | /// `update_merged`, `update_closed`, `update_superseded`, | |
| 569 | /// `update_needs_code` or `update_failed`. | |
| 570 | pub action: String, | |
| 571 | /// Who did it: a person's username, or `g1t`. | |
| 572 | pub actor: Option<String>, | |
| 573 | pub reason: Option<DismissReason>, | |
| 574 | pub comment: Option<String>, | |
| 575 | /// The pull request or issue it concerns. | |
| 576 | pub number: Option<u32>, | |
| 577 | /// RFC 3339. | |
| 578 | pub at: String, | |
| 579 | } | |
| 580 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 581 | /// Version updates, and what the dependency update file asks of security |
| 582 | /// updates: see [`crate::updates`]. | |
| 583 | pub use crate::updates::{ | |
| 584 | UpdateAllow, UpdateGroup, UpdateIgnore, VersionUpdateEntry, VersionUpdatesState, | |
| 585 | }; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 586 | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 587 | /// Everything the Security page shows for one repository. |
| 588 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 589 | #[serde(rename_all = "camelCase")] | |
| 590 | pub struct SecurityOverview { | |
| 591 | pub repo_id: String, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 592 | /// Open alerts by severity; see [`SeverityCounts`]. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 593 | pub counts: SeverityCounts, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 594 | pub secret_counts: SecretCounts, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 595 | pub secrets: Vec<SecretFinding>, |
| 596 | pub vulnerabilities: Vec<Vulnerability>, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 597 | /// What happened to the alerts, newest first. |
| 598 | pub activity: Vec<AlertActivity>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 599 | pub scan: ScanState, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 600 | /// Security updates: whether g1t opens a pull request to upgrade each |
| 601 | /// vulnerable dependency that has a fix. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 602 | pub upkeep: bool, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 603 | pub version_updates: VersionUpdatesState, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 604 | } |
| 605 | ||
| 606 | /// `overview`: members of the workspace only. Returns | |
| 607 | /// `Outcome<SecurityOverview>`. | |
| 608 | #[derive(Debug, Serialize, Deserialize)] | |
| 609 | pub struct OverviewArgs { | |
| 610 | pub repo: RepoPath, | |
| 611 | pub viewer: Option<User>, | |
| 612 | } | |
| 613 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 614 | /// `dismiss`: closes an alert with a reason and an optional comment. A |
| 615 | /// secret takes Admin on the repository (a dismissed secret is let through | |
| 616 | /// push protection, unless it was revoked); a vulnerable dependency takes | |
| 617 | /// Write. Returns `Outcome<AlertChange>`. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 618 | #[derive(Debug, Serialize, Deserialize)] |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 619 | pub struct DismissArgs { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 620 | pub actor: User, |
| 621 | pub repo: RepoPath, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 622 | /// A secret's id (`sec_…`) or a vulnerability's (`vul_…`). |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 623 | pub id: String, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 624 | pub reason: DismissReason, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 625 | #[serde(default)] |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 626 | pub comment: String, |
| 627 | } | |
| 628 | ||
| 629 | /// `reopen`: opens a dismissed alert again, with the same roles as | |
| 630 | /// `dismiss`. Returns `Outcome<AlertChange>`. | |
| 631 | #[derive(Debug, Serialize, Deserialize)] | |
| 632 | pub struct ReopenArgs { | |
| 633 | pub actor: User, | |
| 634 | pub repo: RepoPath, | |
| 635 | pub id: String, | |
| 636 | } | |
| 637 | ||
| 638 | /// The alert `dismiss` or `reopen` changed, as it is now: one of the two. | |
| 639 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 640 | #[serde(rename_all = "camelCase")] | |
| 641 | pub struct AlertChange { | |
| 642 | pub secret: Option<SecretFinding>, | |
| 643 | pub vulnerability: Option<Vulnerability>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 644 | } |
| 645 | ||
| 646 | /// `rescan`: scans the dependencies again now, and the history from the | |
| 647 | /// start. Members only. Returns `Outcome<ScanState>`. | |
| 648 | #[derive(Debug, Serialize, Deserialize)] | |
| 649 | pub struct RescanArgs { | |
| 650 | pub actor: User, | |
| 651 | pub repo: RepoPath, | |
| 652 | } | |
| 653 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 654 | /// `set_upkeep`: security updates on or off: whether g1t opens a pull |
| 655 | /// request to upgrade each vulnerable dependency that has a fix. Maintain | |
| 656 | /// and up. Returns `Outcome<bool>`. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 657 | #[derive(Debug, Serialize, Deserialize)] |
| 658 | pub struct SetUpkeepArgs { | |
| 659 | pub actor: User, | |
| 660 | pub repo: RepoPath, | |
| 661 | pub enabled: bool, | |
| 662 | } | |
| 663 | ||
| 664 | /// `workspace`: every repository of a workspace that has findings, for | |
| 665 | /// its members. Returns `Outcome<Vec<RepoSecurity>>`. | |
| 666 | #[derive(Debug, Serialize, Deserialize)] | |
| 667 | pub struct WorkspaceArgs { | |
| 668 | pub workspace: String, | |
| 669 | pub viewer: Option<User>, | |
| 670 | } | |
| 671 | ||
| 672 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 673 | #[serde(rename_all = "camelCase")] | |
| 674 | pub struct RepoSecurity { | |
| 675 | pub repo_id: String, | |
| 676 | pub name: String, | |
| 677 | pub counts: SeverityCounts, | |
| 678 | pub secrets: u32, | |
| 679 | pub vulnerabilities: u32, | |
| 680 | pub upkeep: bool, | |
| 681 | pub dependencies_scanned_at: Option<String>, | |
| 682 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 683 | |
| 684 | /// `bump` (runner): makes a security update in a sandbox. It clones the | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 685 | /// default branch (or `base`), raises `package` to `version` in each lockfile with the |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 686 | /// ecosystem's own tool (`npm`, `cargo`, `go`, `pip`), commits that as g1t |
| 687 | /// (`g1t <g1t@users.noreply.g1t.sh>`) and pushes it to `branch`, which must | |
| 688 | /// start with [`UPDATE_BRANCH_PREFIX`]. The push is what tells the security | |
| 689 | /// service to open the pull request. Returns `Outcome<bool>`: whether the | |
| 690 | /// sandbox started. | |
| 691 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 692 | #[serde(rename_all = "camelCase")] | |
| 693 | pub struct BumpArgs { | |
| 694 | pub repo: RepoPath, | |
| 695 | /// OSV's name for the ecosystem: `npm`, `crates.io`, `Go` or `PyPI`. | |
| 696 | pub ecosystem: String, | |
| 697 | pub package: String, | |
| 698 | pub version: String, | |
| 699 | /// The lockfiles that resolve a vulnerable version, from the root. | |
| 700 | pub lockfiles: Vec<String>, | |
| 701 | pub branch: String, | |
| 702 | /// The commit's message. | |
| 703 | pub message: String, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 704 | /// `version` for a version update, whose branch is named by the |
| 705 | /// dependency update file (any branch but the default one); a security | |
| 706 | /// update when absent. | |
| 707 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 708 | pub kind: Option<String>, | |
| 709 | /// Several packages raised in one commit, for a grouped update. When | |
| 710 | /// given, `package` and `version` are its first. | |
| 711 | #[serde(default, skip_serializing_if = "Vec::is_empty")] | |
| 712 | pub packages: Vec<crate::updates::BumpPackage>, | |
| 713 | /// How a manifest's requirement changes (`versioning-strategy`): | |
| 714 | /// `increase` (the default), `increase-if-necessary`, `widen` or | |
| 715 | /// `lockfile-only`. | |
| 716 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 717 | pub strategy: Option<String>, | |
| 718 | /// Replace the branch if it is there already: a rebase or a recreate. | |
| 719 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 720 | pub force: bool, | |
| 721 | /// Private registries the tools may read, with their credentials. | |
| 722 | #[serde(default, skip_serializing_if = "Vec::is_empty")] | |
| 723 | pub registries: Vec<crate::updates::BumpRegistry>, | |
| 724 | /// The branch to start from, which its pull request merges into | |
| 725 | /// (`target-branch`): the default branch when absent. | |
| 726 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 727 | pub base: Option<String>, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 728 | } |