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.
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 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 | ||
| 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 | ||
| 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, | |
| 189 | /// Who dismissed it (allowed or resolved it). | |
| 190 | pub decided_by: Option<String>, | |
| 191 | /// The comment given when it was dismissed. | |
| 192 | pub reason: Option<String>, | |
| 193 | pub decided_at: Option<String>, | |
| 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, | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 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 | } | |
| 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, | |
| 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>, | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 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>, | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 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, | |
| 290 | /// Where the last page stopped; `from`, or the head of the default | |
| 291 | /// branch, when absent. | |
| 292 | #[serde(default)] | |
| 293 | pub after: Option<String>, | |
| 294 | pub limit: u32, | |
| 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>, | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 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>, | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 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, | |
| 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 | } | |
| 381 | } | |
| 382 | ||
| 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, | |
| Security and version updates open no issues on their own: an update whose version cannot be raised fails with why on its alert and is tried again a day later, and one whose required checks fail stays open for a person to decide on; an agent takes one only when a person asks with Fix with g1t. That issue now names the lockfiles the package is locked in and the commands that show it is done, which were left empty ("Locked in: .") because the lockfiles were never passed in. | 399 | /// An earlier update g1t opened an issue for, when the version could |
| 400 | /// not be raised without changing code. New updates no longer reach it: | |
| 401 | /// they fail with why, or their pull request stays open. | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 402 | NeedsCode, |
| 403 | /// It could not be made; why is in `error`. | |
| 404 | Failed, | |
| 405 | } | |
| 406 | ||
| 407 | impl UpdateState { | |
| 408 | pub const ALL: [UpdateState; 7] = [ | |
| 409 | UpdateState::Requested, | |
| 410 | UpdateState::Open, | |
| 411 | UpdateState::Merged, | |
| 412 | UpdateState::Closed, | |
| 413 | UpdateState::Superseded, | |
| 414 | UpdateState::NeedsCode, | |
| 415 | UpdateState::Failed, | |
| 416 | ]; | |
| 417 | ||
| 418 | pub fn as_str(self) -> &'static str { | |
| 419 | match self { | |
| 420 | UpdateState::Requested => "requested", | |
| 421 | UpdateState::Open => "open", | |
| 422 | UpdateState::Merged => "merged", | |
| 423 | UpdateState::Closed => "closed", | |
| 424 | UpdateState::Superseded => "superseded", | |
| 425 | UpdateState::NeedsCode => "needs_code", | |
| 426 | UpdateState::Failed => "failed", | |
| 427 | } | |
| 428 | } | |
| 429 | ||
| 430 | pub fn parse(text: &str) -> Option<UpdateState> { | |
| 431 | UpdateState::ALL.into_iter().find(|state| state.as_str() == text) | |
| 432 | } | |
| 433 | ||
| 434 | /// Whether g1t is still working on it. | |
| 435 | pub fn in_progress(self) -> bool { | |
| 436 | matches!(self, UpdateState::Requested | UpdateState::Open | UpdateState::NeedsCode) | |
| 437 | } | |
| 438 | } | |
| 439 | ||
| 440 | /// The security update for one package. | |
| 441 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 442 | #[serde(rename_all = "camelCase")] | |
| 443 | pub struct SecurityUpdate { | |
| 444 | pub state: UpdateState, | |
| 445 | /// The version it upgrades to. | |
| 446 | pub target: String, | |
| 447 | /// `g1t/security/<package>-<version>`. | |
| 448 | pub branch: Option<String>, | |
| 449 | /// The pull request g1t opened. | |
| 450 | 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 | 451 | /// The issue opened for g1t, when the upgrade needs code changes. |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 452 | pub issue: Option<u32>, |
| 453 | /// Why it failed, when it did. | |
| 454 | pub error: Option<String>, | |
| 455 | /// RFC 3339. | |
| 456 | pub updated_at: String, | |
| 457 | } | |
| 458 | ||
| 459 | /// The prefix every security update's branch starts with. | |
| 460 | pub const UPDATE_BRANCH_PREFIX: &str = "g1t/security/"; | |
| 461 | ||
| 462 | /// The branch a security update is made on: `g1t/security/<package>-<version>`, | |
| 463 | /// with anything a branch name cannot hold (a scope's `@` and `/`) as `-`. | |
| 464 | pub fn update_branch(package: &str, version: &str) -> String { | |
| 465 | let clean = |text: &str| -> String { | |
| 466 | let mut out = String::new(); | |
| 467 | for c in text.chars() { | |
| 468 | let c = if c.is_ascii_alphanumeric() || matches!(c, '.' | '_') { c } else { '-' }; | |
| 469 | if !(c == '-' && out.ends_with('-')) { | |
| 470 | out.push(c); | |
| 471 | } | |
| 472 | } | |
| 473 | out.trim_matches(['-', '.']).to_owned() | |
| 474 | }; | |
| 475 | format!("{UPDATE_BRANCH_PREFIX}{}-{}", clean(package), clean(version)) | |
| 476 | } | |
| 477 | ||
| 478 | /// One advisory against one package at the version a lockfile resolves. | |
| 479 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 480 | #[serde(rename_all = "camelCase")] | |
| 481 | pub struct Vulnerability { | |
| 482 | pub id: String, | |
| 483 | pub repo_id: String, | |
| 484 | /// `npm`, `crates.io`, `Go`, `PyPI`. | |
| 485 | pub ecosystem: String, | |
| 486 | pub package: String, | |
| 487 | pub version: String, | |
| 488 | /// The lockfile that resolves it. | |
| 489 | pub manifest: String, | |
| 490 | /// The id people know it by (its GHSA id when it has one). | |
| 491 | pub advisory: String, | |
| 492 | pub osv_id: String, | |
| 493 | pub summary: String, | |
| 494 | /// `critical`, `high`, `medium`, `low` or `unknown`. | |
| 495 | pub severity: String, | |
| 496 | pub fixed_version: Option<String>, | |
| 497 | 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 | 498 | /// The issue opened to upgrade the package, when g1t was put on |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 499 | /// it: the upgrade needs code changes, or predates security updates. |
| 500 | pub issue: Option<u32>, | |
| 501 | /// RFC 3339. | |
| 502 | pub found_at: String, | |
| 503 | pub fixed_at: Option<String>, | |
| 504 | /// Open, dismissed or fixed. | |
| 505 | pub state: AlertState, | |
| 506 | /// Who dismissed it, why, with what comment, and when. | |
| 507 | #[serde(default)] | |
| 508 | pub dismissed_by: Option<String>, | |
| 509 | #[serde(default)] | |
| 510 | pub dismissed_reason: Option<DismissReason>, | |
| 511 | #[serde(default)] | |
| 512 | pub dismissed_comment: Option<String>, | |
| 513 | #[serde(default)] | |
| 514 | pub dismissed_at: Option<String>, | |
| 515 | /// The security update for its package, if g1t has started one. | |
| 516 | #[serde(default)] | |
| 517 | pub update: Option<SecurityUpdate>, | |
| 518 | } | |
| 519 | ||
| 520 | /// Open alerts by severity: vulnerabilities open and not dismissed, and | |
| 521 | /// secrets in the history that look real, as critical. A blocked secret | |
| 522 | /// never landed and a likely test value is no danger, so neither counts. | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 523 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 524 | pub struct SeverityCounts { |
| 525 | pub critical: u32, | |
| 526 | pub high: u32, | |
| 527 | pub medium: u32, | |
| 528 | pub low: u32, | |
| 529 | pub unknown: u32, | |
| 530 | } | |
| 531 | ||
| 532 | /// How a repository's history scan stands. | |
| 533 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 534 | #[serde(rename_all = "camelCase")] | |
| 535 | pub struct ScanState { | |
| 536 | /// `pending`, `running`, `done` or `stopped` (over the workspace's limit). | |
| 537 | pub history: String, | |
| 538 | pub commits_scanned: u32, | |
| 539 | /// RFC 3339. | |
| 540 | pub history_finished_at: Option<String>, | |
| 541 | pub dependencies_scanned_at: Option<String>, | |
| 542 | /// Why the last dependency scan failed, if it did. | |
| 543 | pub dependencies_error: Option<String>, | |
| 544 | pub lockfiles: Vec<String>, | |
| 545 | } | |
| 546 | ||
| 547 | /// Secret alerts by where they stand. | |
| 548 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 549 | #[serde(rename_all = "camelCase")] | |
| 550 | pub struct SecretCounts { | |
| 551 | /// In the history and looking real: rotate these. | |
| 552 | pub open: u32, | |
| 553 | /// Stopped at a push, so never landed, and looking real. | |
| 554 | pub blocked: u32, | |
| 555 | /// Open or blocked, but likely test values. | |
| 556 | pub test_values: u32, | |
| 557 | pub dismissed: u32, | |
| 558 | pub fixed: u32, | |
| 559 | } | |
| 560 | ||
| 561 | /// One thing that happened to an alert, for its activity log. | |
| 562 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 563 | #[serde(rename_all = "camelCase")] | |
| 564 | pub struct AlertActivity { | |
| 565 | pub id: String, | |
| 566 | /// The secret's or vulnerability's id. | |
| 567 | pub alert_id: String, | |
| 568 | /// `dismissed`, `reopened`, `update_requested`, `update_opened`, | |
| 569 | /// `update_merged`, `update_closed`, `update_superseded`, | |
| 570 | /// `update_needs_code` or `update_failed`. | |
| 571 | pub action: String, | |
| 572 | /// Who did it: a person's username, or `g1t`. | |
| 573 | pub actor: Option<String>, | |
| 574 | pub reason: Option<DismissReason>, | |
| 575 | pub comment: Option<String>, | |
| 576 | /// The pull request or issue it concerns. | |
| 577 | pub number: Option<u32>, | |
| 578 | /// RFC 3339. | |
| 579 | pub at: String, | |
| 580 | } | |
| 581 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 582 | /// Version updates, and what the dependency update file asks of security |
| 583 | /// updates: see [`crate::updates`]. | |
| 584 | pub use crate::updates::{ | |
| 585 | UpdateAllow, UpdateGroup, UpdateIgnore, VersionUpdateEntry, VersionUpdatesState, | |
| 586 | }; | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 587 | |
| 588 | /// Everything the Security page shows for one repository. | |
| 589 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 590 | #[serde(rename_all = "camelCase")] | |
| 591 | pub struct SecurityOverview { | |
| 592 | pub repo_id: String, | |
| 593 | /// Open alerts by severity; see [`SeverityCounts`]. | |
| 594 | pub counts: SeverityCounts, | |
| 595 | pub secret_counts: SecretCounts, | |
| 596 | pub secrets: Vec<SecretFinding>, | |
| 597 | pub vulnerabilities: Vec<Vulnerability>, | |
| 598 | /// What happened to the alerts, newest first. | |
| 599 | pub activity: Vec<AlertActivity>, | |
| 600 | pub scan: ScanState, | |
| 601 | /// Security updates: whether g1t opens a pull request to upgrade each | |
| 602 | /// vulnerable dependency that has a fix. | |
| 603 | pub upkeep: bool, | |
| 604 | pub version_updates: VersionUpdatesState, | |
| 605 | } | |
| 606 | ||
| 607 | /// `overview`: members of the workspace only. Returns | |
| 608 | /// `Outcome<SecurityOverview>`. | |
| 609 | #[derive(Debug, Serialize, Deserialize)] | |
| 610 | pub struct OverviewArgs { | |
| 611 | pub repo: RepoPath, | |
| 612 | pub viewer: Option<User>, | |
| 613 | } | |
| 614 | ||
| 615 | /// `dismiss`: closes an alert with a reason and an optional comment. A | |
| 616 | /// secret takes Admin on the repository (a dismissed secret is let through | |
| 617 | /// push protection, unless it was revoked); a vulnerable dependency takes | |
| 618 | /// Write. Returns `Outcome<AlertChange>`. | |
| 619 | #[derive(Debug, Serialize, Deserialize)] | |
| 620 | pub struct DismissArgs { | |
| 621 | pub actor: User, | |
| 622 | pub repo: RepoPath, | |
| 623 | /// A secret's id (`sec_…`) or a vulnerability's (`vul_…`). | |
| 624 | pub id: String, | |
| 625 | pub reason: DismissReason, | |
| 626 | #[serde(default)] | |
| 627 | pub comment: String, | |
| 628 | } | |
| 629 | ||
| 630 | /// `reopen`: opens a dismissed alert again, with the same roles as | |
| 631 | /// `dismiss`. Returns `Outcome<AlertChange>`. | |
| 632 | #[derive(Debug, Serialize, Deserialize)] | |
| 633 | pub struct ReopenArgs { | |
| 634 | pub actor: User, | |
| 635 | pub repo: RepoPath, | |
| 636 | pub id: String, | |
| 637 | } | |
| 638 | ||
| 639 | /// The alert `dismiss` or `reopen` changed, as it is now: one of the two. | |
| 640 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 641 | #[serde(rename_all = "camelCase")] | |
| 642 | pub struct AlertChange { | |
| 643 | pub secret: Option<SecretFinding>, | |
| 644 | pub vulnerability: Option<Vulnerability>, | |
| 645 | } | |
| 646 | ||
| 647 | /// `rescan`: scans the dependencies again now, and the history from the | |
| 648 | /// start. Members only. Returns `Outcome<ScanState>`. | |
| 649 | #[derive(Debug, Serialize, Deserialize)] | |
| 650 | pub struct RescanArgs { | |
| 651 | pub actor: User, | |
| 652 | pub repo: RepoPath, | |
| 653 | } | |
| 654 | ||
| 655 | /// `set_upkeep`: security updates on or off: whether g1t opens a pull | |
| 656 | /// request to upgrade each vulnerable dependency that has a fix. Maintain | |
| 657 | /// and up. Returns `Outcome<bool>`. | |
| 658 | #[derive(Debug, Serialize, Deserialize)] | |
| 659 | pub struct SetUpkeepArgs { | |
| 660 | pub actor: User, | |
| 661 | pub repo: RepoPath, | |
| 662 | pub enabled: bool, | |
| 663 | } | |
| 664 | ||
| 665 | /// `workspace`: every repository of a workspace that has findings, for | |
| 666 | /// its members. Returns `Outcome<Vec<RepoSecurity>>`. | |
| 667 | #[derive(Debug, Serialize, Deserialize)] | |
| 668 | pub struct WorkspaceArgs { | |
| 669 | pub workspace: String, | |
| 670 | pub viewer: Option<User>, | |
| 671 | } | |
| 672 | ||
| 673 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 674 | #[serde(rename_all = "camelCase")] | |
| 675 | pub struct RepoSecurity { | |
| 676 | pub repo_id: String, | |
| 677 | pub name: String, | |
| 678 | pub counts: SeverityCounts, | |
| 679 | pub secrets: u32, | |
| 680 | pub vulnerabilities: u32, | |
| 681 | pub upkeep: bool, | |
| 682 | pub dependencies_scanned_at: Option<String>, | |
| 683 | } | |
| 684 | ||
| 685 | /// `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 | 686 | /// default branch (or `base`), raises `package` to `version` in each lockfile with the |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 687 | /// ecosystem's own tool (`npm`, `cargo`, `go`, `pip`), commits that as g1t |
| 688 | /// (`g1t <g1t@users.noreply.g1t.sh>`) and pushes it to `branch`, which must | |
| 689 | /// start with [`UPDATE_BRANCH_PREFIX`]. The push is what tells the security | |
| 690 | /// service to open the pull request. Returns `Outcome<bool>`: whether the | |
| 691 | /// sandbox started. | |
| 692 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 693 | #[serde(rename_all = "camelCase")] | |
| 694 | pub struct BumpArgs { | |
| 695 | pub repo: RepoPath, | |
| 696 | /// OSV's name for the ecosystem: `npm`, `crates.io`, `Go` or `PyPI`. | |
| 697 | pub ecosystem: String, | |
| 698 | pub package: String, | |
| 699 | pub version: String, | |
| 700 | /// The lockfiles that resolve a vulnerable version, from the root. | |
| 701 | pub lockfiles: Vec<String>, | |
| 702 | pub branch: String, | |
| 703 | /// The commit's message. | |
| 704 | pub message: String, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 705 | /// `version` for a version update, whose branch is named by the |
| 706 | /// dependency update file (any branch but the default one); a security | |
| 707 | /// update when absent. | |
| 708 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 709 | pub kind: Option<String>, | |
| 710 | /// Several packages raised in one commit, for a grouped update. When | |
| 711 | /// given, `package` and `version` are its first. | |
| 712 | #[serde(default, skip_serializing_if = "Vec::is_empty")] | |
| 713 | pub packages: Vec<crate::updates::BumpPackage>, | |
| 714 | /// How a manifest's requirement changes (`versioning-strategy`): | |
| 715 | /// `increase` (the default), `increase-if-necessary`, `widen` or | |
| 716 | /// `lockfile-only`. | |
| 717 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 718 | pub strategy: Option<String>, | |
| 719 | /// Replace the branch if it is there already: a rebase or a recreate. | |
| 720 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 721 | pub force: bool, | |
| 722 | /// Private registries the tools may read, with their credentials. | |
| 723 | #[serde(default, skip_serializing_if = "Vec::is_empty")] | |
| 724 | pub registries: Vec<crate::updates::BumpRegistry>, | |
| 725 | /// The branch to start from, which its pull request merges into | |
| 726 | /// (`target-branch`): the default branch when absent. | |
| 727 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 728 | pub base: Option<String>, | |
| Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays | 729 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.