Skip to content

g1t/crates/contracts/src/security.rs

728 lines25,724 bytesCodeBlame
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
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/// 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")]
53pub 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
74impl 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")]
139pub 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
148impl 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")]
171pub 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,
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,
222}
223
224impl 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")]
238pub 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>,
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>,
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")]
262pub 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>,
269 /// Whether the repository is private, as repos read it.
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub private: Option<bool>,
272}
273
274#[derive(Debug, Default, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub 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")]
288pub 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>,
302 /// Custom patterns to look for too.
303 #[serde(default, skip_serializing_if = "Vec::is_empty")]
304 pub patterns: Vec<crate::security_suite::PatternSpec>,
305}
306
307#[derive(Debug, Default, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub 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")]
322pub struct FindLockfilesArgs {
323 pub repo_id: String,
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>,
328}
329
330#[derive(Debug, Default, Serialize, Deserialize)]
331#[serde(rename_all = "camelCase")]
332pub 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)]
339pub 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")]
347pub 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
356impl 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")]
388pub 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
400 /// was opened for g1t instead.
401 NeedsCode,
402 /// It could not be made; why is in `error`.
403 Failed,
404}
405
406impl 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")]
442pub 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>,
450 /// The issue opened for g1t, when the upgrade needs code changes.
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.
459pub 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 `-`.
463pub 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
477/// One advisory against one package at the version a lockfile resolves.
478#[derive(Clone, Debug, Serialize, Deserialize)]
479#[serde(rename_all = "camelCase")]
480pub 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,
497 /// The issue opened to upgrade the package, when g1t was put on
498 /// it: the upgrade needs code changes, or predates security updates.
499 pub issue: Option<u32>,
500 /// RFC 3339.
501 pub found_at: String,
502 pub fixed_at: Option<String>,
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>,
517}
518
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.
522#[derive(Clone, Debug, Default, Serialize, Deserialize)]
523pub 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")]
534pub 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
546/// Secret alerts by where they stand.
547#[derive(Clone, Debug, Default, Serialize, Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub 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")]
563pub 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
581/// Version updates, and what the dependency update file asks of security
582/// updates: see [`crate::updates`].
583pub use crate::updates::{
584 UpdateAllow, UpdateGroup, UpdateIgnore, VersionUpdateEntry, VersionUpdatesState,
585};
586
587/// Everything the Security page shows for one repository.
588#[derive(Clone, Debug, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct SecurityOverview {
591 pub repo_id: String,
592 /// Open alerts by severity; see [`SeverityCounts`].
593 pub counts: SeverityCounts,
594 pub secret_counts: SecretCounts,
595 pub secrets: Vec<SecretFinding>,
596 pub vulnerabilities: Vec<Vulnerability>,
597 /// What happened to the alerts, newest first.
598 pub activity: Vec<AlertActivity>,
599 pub scan: ScanState,
600 /// Security updates: whether g1t opens a pull request to upgrade each
601 /// vulnerable dependency that has a fix.
602 pub upkeep: bool,
603 pub version_updates: VersionUpdatesState,
604}
605
606/// `overview`: members of the workspace only. Returns
607/// `Outcome<SecurityOverview>`.
608#[derive(Debug, Serialize, Deserialize)]
609pub struct OverviewArgs {
610 pub repo: RepoPath,
611 pub viewer: Option<User>,
612}
613
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>`.
618#[derive(Debug, Serialize, Deserialize)]
619pub struct DismissArgs {
620 pub actor: User,
621 pub repo: RepoPath,
622 /// A secret's id (`sec_…`) or a vulnerability's (`vul_…`).
623 pub id: String,
624 pub reason: DismissReason,
625 #[serde(default)]
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)]
632pub 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")]
641pub struct AlertChange {
642 pub secret: Option<SecretFinding>,
643 pub vulnerability: Option<Vulnerability>,
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)]
649pub struct RescanArgs {
650 pub actor: User,
651 pub repo: RepoPath,
652}
653
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>`.
657#[derive(Debug, Serialize, Deserialize)]
658pub 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)]
667pub struct WorkspaceArgs {
668 pub workspace: String,
669 pub viewer: Option<User>,
670}
671
672#[derive(Clone, Debug, Serialize, Deserialize)]
673#[serde(rename_all = "camelCase")]
674pub 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}
683
684/// `bump` (runner): makes a security update in a sandbox. It clones the
685/// default branch (or `base`), raises `package` to `version` in each lockfile with the
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")]
693pub 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,
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>,
728}