Skip to content

g1t/crates/contracts/src/security_suite.rs

1,137 lines37,619 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1//! The security service's paid suite, beside what [`crate::security`]
2//! holds: custom secret patterns, push protection bypasses and their
3//! review, validity checks, code scanning from SARIF uploads, the
4//! dependency graph with its SBOM and pull request review, and the
5//! workspace's security overview.
6//!
7//! What is free and what is paid: secret scanning, push protection,
8//! vulnerability alerts and security updates are free everywhere. On a
9//! public repository everything here is free too. On a private one, the
10//! features in [`PaidFeature`] need the workspace's Security and quality
11//! activation (billing's `Feature::Security`); a refusal is a
12//! `PaymentRequired` failure whose message says how to turn it on.
13
14use serde::{Deserialize, Serialize};
15
16use crate::User;
17use crate::repos::RepoPath;
18use crate::security::{AlertActivity, AlertState, DismissReason, SecretFinding, SeverityCounts};
19
20/// A feature of the suite that a private repository needs the activation
21/// for.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum PaidFeature {
25 CustomPatterns,
26 ValidityChecks,
27 DelegatedBypass,
28 CodeScanning,
29 DependencyReview,
30 SecurityOverview,
31}
32
33impl PaidFeature {
34 pub const ALL: [PaidFeature; 6] = [
35 PaidFeature::CustomPatterns,
36 PaidFeature::ValidityChecks,
37 PaidFeature::DelegatedBypass,
38 PaidFeature::CodeScanning,
39 PaidFeature::DependencyReview,
40 PaidFeature::SecurityOverview,
41 ];
42
43 pub fn as_str(self) -> &'static str {
44 match self {
45 PaidFeature::CustomPatterns => "custom_patterns",
46 PaidFeature::ValidityChecks => "validity_checks",
47 PaidFeature::DelegatedBypass => "delegated_bypass",
48 PaidFeature::CodeScanning => "code_scanning",
49 PaidFeature::DependencyReview => "dependency_review",
50 PaidFeature::SecurityOverview => "security_overview",
51 }
52 }
53
54 /// For a sentence: "Custom patterns".
55 pub fn title(self) -> &'static str {
56 match self {
57 PaidFeature::CustomPatterns => "Custom patterns",
58 PaidFeature::ValidityChecks => "Validity checks",
59 PaidFeature::DelegatedBypass => "Delegated bypass",
60 PaidFeature::CodeScanning => "Code scanning",
61 PaidFeature::DependencyReview => "Dependency review",
62 PaidFeature::SecurityOverview => "The security overview",
63 }
64 }
65}
66
67/// What a refusal for want of the activation says.
68pub fn needs_activation(feature: PaidFeature, workspace: &str) -> String {
69 format!(
70 "{} on private repositories comes with the Security and quality activation, which {workspace} does not have. \
71 An owner can turn it on at /{workspace}/-/billing. Public repositories have it free.",
72 feature.title()
73 )
74}
75
76/// The alert types, as the API and webhooks name them.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum AlertType {
80 SecretScanning,
81 CodeScanning,
82 Vulnerability,
83}
84
85impl AlertType {
86 pub const ALL: [AlertType; 3] = [AlertType::SecretScanning, AlertType::CodeScanning, AlertType::Vulnerability];
87
88 pub fn as_str(self) -> &'static str {
89 match self {
90 AlertType::SecretScanning => "secret_scanning",
91 AlertType::CodeScanning => "code_scanning",
92 AlertType::Vulnerability => "vulnerability",
93 }
94 }
95
96 pub fn parse(text: &str) -> Option<AlertType> {
97 AlertType::ALL.into_iter().find(|kind| kind.as_str() == text)
98 }
99
100 /// Which type an alert id is: `sec_…`, `cod_…` or `vul_…`.
101 pub fn of_id(id: &str) -> Option<AlertType> {
102 match id.split('_').next()? {
103 "sec" => Some(AlertType::SecretScanning),
104 "cod" => Some(AlertType::CodeScanning),
105 "vul" => Some(AlertType::Vulnerability),
106 _ => None,
107 }
108 }
109
110 /// The webhook event prefix: `secret_scanning_alert`.
111 pub fn event_prefix(self) -> &'static str {
112 match self {
113 AlertType::SecretScanning => "secret_scanning_alert",
114 AlertType::CodeScanning => "code_scanning_alert",
115 AlertType::Vulnerability => "vulnerability_alert",
116 }
117 }
118}
119
120// --- Settings ---------------------------------------------------------------
121
122/// A repository's security settings beyond security updates.
123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct RepoSecuritySettings {
126 /// When a pull request's code scanning check fails: `none`, `errors`,
127 /// or new results of `critical`, `high`, `medium` or `any` security
128 /// severity (and errors). The check is `Code scanning`; require it in
129 /// branch protection to block merges.
130 pub code_scanning_gate: String,
131 /// Whether pull requests get the `Dependency review` check.
132 pub dependency_review: bool,
133 /// The lowest severity of a known vulnerability in an added package
134 /// that fails it: `critical`, `high`, `medium`, `low`, or `none`.
135 pub review_fail_on: String,
136 /// SPDX license ids an added package may not have.
137 pub review_deny_licenses: Vec<String>,
138 /// Whether the review also comments its summary on the pull request.
139 pub review_comment: bool,
140}
141
142impl Default for RepoSecuritySettings {
143 fn default() -> Self {
144 RepoSecuritySettings {
145 code_scanning_gate: "high".to_owned(),
146 dependency_review: true,
147 review_fail_on: "high".to_owned(),
148 review_deny_licenses: Vec::new(),
149 review_comment: true,
150 }
151 }
152}
153
154/// A workspace's security settings.
155#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct WorkspaceSecuritySettings {
158 /// Push protection bypasses need an owner's (or the repository's
159 /// admins') approval: a person with Write requests one instead.
160 pub delegated_bypass: bool,
161 /// Ask each secret's issuer whether it still works, where that can be
162 /// done safely.
163 pub validity_checks: bool,
164}
165
166/// `security_settings`: a repository's settings, what its workspace sets,
167/// and whether the paid features are on for it. Write and up.
168#[derive(Debug, Serialize, Deserialize)]
169pub struct SecuritySettingsArgs {
170 pub viewer: Option<User>,
171 pub repo: RepoPath,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct SecuritySettingsView {
177 pub settings: RepoSecuritySettings,
178 pub workspace: WorkspaceSecuritySettings,
179 /// Whether the repository is private.
180 pub private: bool,
181 /// Whether the paid features are on: a public repository, or the
182 /// workspace has the activation (or is comped).
183 pub entitled: bool,
184 /// Security updates.
185 pub upkeep: bool,
186}
187
188/// `set_security_settings`: Admin on the repository.
189#[derive(Debug, Serialize, Deserialize)]
190pub struct SetSecuritySettingsArgs {
191 pub actor: User,
192 pub repo: RepoPath,
193 pub settings: RepoSecuritySettings,
194}
195
196/// `workspace_security_settings`: members only. Returns
197/// `Outcome<WorkspaceSecurityView>`.
198#[derive(Debug, Serialize, Deserialize)]
199pub struct WorkspaceSecuritySettingsArgs {
200 pub viewer: Option<User>,
201 pub workspace: String,
202}
203
204#[derive(Clone, Debug, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct WorkspaceSecurityView {
207 pub settings: WorkspaceSecuritySettings,
208 /// Whether the workspace has the activation (or is comped).
209 pub activated: bool,
210}
211
212/// `set_workspace_security_settings`: owners only.
213#[derive(Debug, Serialize, Deserialize)]
214pub struct SetWorkspaceSecuritySettingsArgs {
215 pub actor: User,
216 pub workspace: String,
217 pub settings: WorkspaceSecuritySettings,
218}
219
220// --- Custom patterns --------------------------------------------------------
221
222/// A pattern as push protection and scans use it: see `g1t_scan::custom`.
223#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct PatternSpec {
226 pub id: String,
227 pub name: String,
228 pub pattern: String,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub before: Option<String>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub after: Option<String>,
233}
234
235/// A custom pattern, as people manage it.
236#[derive(Clone, Debug, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct CustomPattern {
239 /// `pat_…`.
240 pub id: String,
241 /// `repository` or `workspace`.
242 pub scope: String,
243 pub workspace: String,
244 /// The repository's name, for a repository's own pattern.
245 pub repo: Option<String>,
246 pub name: String,
247 pub pattern: String,
248 pub before: Option<String>,
249 pub after: Option<String>,
250 pub test_strings: Vec<String>,
251 /// `draft` (saved, not used) or `published` (push protection and scans
252 /// use it).
253 pub state: String,
254 pub created_by: String,
255 pub created_at: String,
256 pub updated_by: String,
257 pub updated_at: String,
258 /// Open alerts it has found.
259 pub open_alerts: u32,
260}
261
262/// `custom_patterns`: a repository's patterns and those it inherits from
263/// its workspace (with `repo`), or the workspace's own. Write and up for a
264/// repository, members for a workspace. Returns `Outcome<PatternList>`.
265#[derive(Debug, Serialize, Deserialize)]
266pub struct CustomPatternsArgs {
267 pub viewer: Option<User>,
268 pub workspace: String,
269 #[serde(default)]
270 pub repo: Option<RepoPath>,
271}
272
273#[derive(Clone, Debug, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct PatternList {
276 pub patterns: Vec<CustomPattern>,
277 pub entitled: bool,
278}
279
280/// `save_custom_pattern`: creates (no `id`) or changes a pattern. Admin on
281/// the repository, or an owner for a workspace's. Publishing one rescans
282/// the default branch's history with it. Returns `Outcome<SavedPattern>`.
283#[derive(Debug, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct SaveCustomPatternArgs {
286 pub actor: User,
287 pub workspace: String,
288 #[serde(default)]
289 pub repo: Option<RepoPath>,
290 #[serde(default)]
291 pub id: Option<String>,
292 pub name: String,
293 pub pattern: String,
294 #[serde(default)]
295 pub before: Option<String>,
296 #[serde(default)]
297 pub after: Option<String>,
298 #[serde(default)]
299 pub test_strings: Vec<String>,
300 /// Publish it (push protection and scans use it), or keep it a draft.
301 #[serde(default)]
302 pub publish: bool,
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct SavedPattern {
308 pub pattern: CustomPattern,
309 /// For each test string, where the pattern matched it (start and end,
310 /// in characters), or nothing.
311 pub tests: Vec<Option<(u32, u32)>>,
312}
313
314/// `delete_custom_pattern`: with the roles of saving one. Its alerts stay.
315#[derive(Debug, Serialize, Deserialize)]
316pub struct DeleteCustomPatternArgs {
317 pub actor: User,
318 pub workspace: String,
319 #[serde(default)]
320 pub repo: Option<RepoPath>,
321 pub id: String,
322}
323
324/// `dry_run_pattern`: runs a pattern over the default branch of the
325/// repository (or, for a workspace, up to ten of its repositories, or
326/// those named) without saving anything. Returns `Outcome<DryRun>`.
327#[derive(Debug, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct DryRunPatternArgs {
330 pub actor: User,
331 pub workspace: String,
332 #[serde(default)]
333 pub repo: Option<RepoPath>,
334 /// For a workspace: repository names; empty is the first ten.
335 #[serde(default)]
336 pub repos: Vec<String>,
337 pub pattern: String,
338 #[serde(default)]
339 pub before: Option<String>,
340 #[serde(default)]
341 pub after: Option<String>,
342}
343
344#[derive(Clone, Debug, Default, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct DryRun {
347 pub repos: Vec<DryRunRepo>,
348}
349
350#[derive(Clone, Debug, Default, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct DryRunRepo {
353 pub name: String,
354 #[serde(flatten)]
355 pub result: PatternMatches,
356}
357
358/// `match_pattern` (repos): runs `pattern` over the files of the default
359/// branch, up to its limits. Returns `PatternMatches`.
360#[derive(Debug, Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct MatchPatternArgs {
363 pub repo_id: String,
364 pub pattern: PatternSpec,
365 /// Matches returned, at most.
366 pub limit: u32,
367}
368
369#[derive(Clone, Debug, Default, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct PatternMatches {
372 pub files_scanned: u32,
373 pub matches: Vec<PatternMatch>,
374 /// More files or matches than were looked at.
375 pub truncated: bool,
376 /// The commit read.
377 pub commit: Option<String>,
378}
379
380#[derive(Clone, Debug, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct PatternMatch {
383 pub path: String,
384 pub line: u32,
385 /// The line with the match masked but for its first characters.
386 pub preview: String,
387}
388
389/// `patterns_for` (called by repos during a push): the published custom
390/// patterns a repository is scanned with, its own and its workspace's,
391/// when it is entitled to them. Returns `Vec<PatternSpec>`.
392#[derive(Debug, Serialize, Deserialize)]
393#[serde(rename_all = "camelCase")]
394pub struct PatternsForArgs {
395 pub repo_id: String,
396 pub namespace: String,
397 #[serde(default)]
398 pub private: Option<bool>,
399}
400
401// --- Secret alerts: locations, bypasses, validity ---------------------------
402
403/// Why a person pushed past push protection.
404#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(rename_all = "snake_case")]
406pub enum BypassReason {
407 /// It is not a secret: the alert is closed as a false positive.
408 FalsePositive,
409 /// It is a value for tests: the alert is closed as used in tests.
410 UsedInTests,
411 /// It is real: the alert stays open, to be rotated.
412 WillFixLater,
413}
414
415impl BypassReason {
416 pub const ALL: [BypassReason; 3] = [BypassReason::FalsePositive, BypassReason::UsedInTests, BypassReason::WillFixLater];
417
418 pub fn as_str(self) -> &'static str {
419 match self {
420 BypassReason::FalsePositive => "false_positive",
421 BypassReason::UsedInTests => "used_in_tests",
422 BypassReason::WillFixLater => "will_fix_later",
423 }
424 }
425
426 pub fn parse(text: &str) -> Option<BypassReason> {
427 BypassReason::ALL.into_iter().find(|reason| reason.as_str() == text)
428 }
429
430 pub fn label(self) -> &'static str {
431 match self {
432 BypassReason::FalsePositive => "It's a false positive",
433 BypassReason::UsedInTests => "It's used in tests",
434 BypassReason::WillFixLater => "I'll fix it later",
435 }
436 }
437
438 /// The dismissal it amounts to, when it closes the alert.
439 pub fn dismissal(self) -> Option<DismissReason> {
440 match self {
441 BypassReason::FalsePositive => Some(DismissReason::FalsePositive),
442 BypassReason::UsedInTests => Some(DismissReason::UsedInTests),
443 BypassReason::WillFixLater => None,
444 }
445 }
446}
447
448/// A push protection bypass, made or asked for.
449#[derive(Clone, Debug, Serialize, Deserialize)]
450#[serde(rename_all = "camelCase")]
451pub struct Bypass {
452 pub reason: BypassReason,
453 pub comment: Option<String>,
454 /// Who pushed past it.
455 pub by: String,
456 pub at: String,
457 /// Who approved it, when delegated bypass asked for approval.
458 pub approved_by: Option<String>,
459}
460
461/// Where a secret was found: each file, line and commit it is in.
462#[derive(Clone, Debug, Serialize, Deserialize)]
463#[serde(rename_all = "camelCase")]
464pub struct SecretLocation {
465 pub path: String,
466 pub line: u32,
467 pub commit: String,
468 /// `push` or `history`.
469 pub source: String,
470 pub found_at: String,
471}
472
473/// A request to bypass push protection, when delegated bypass is on.
474#[derive(Clone, Debug, Serialize, Deserialize)]
475#[serde(rename_all = "camelCase")]
476pub struct BypassRequest {
477 /// `byp_…`.
478 pub id: String,
479 pub repo_id: String,
480 pub workspace: String,
481 pub repo: String,
482 pub secret_id: String,
483 /// "an AWS access key".
484 pub label: String,
485 pub path: String,
486 pub line: u32,
487 pub preview: String,
488 pub requester: String,
489 pub reason: BypassReason,
490 pub comment: Option<String>,
491 /// `pending`, `approved`, `denied` or `cancelled`.
492 pub state: String,
493 pub reviewer: Option<String>,
494 pub review_comment: Option<String>,
495 pub created_at: String,
496 pub reviewed_at: Option<String>,
497}
498
499/// `secret_alert`: one secret alert with every place it was found, what
500/// happened to it and its bypass requests. Write and up. Returns
501/// `Outcome<SecretAlertDetail>`.
502#[derive(Debug, Serialize, Deserialize)]
503pub struct SecretAlertArgs {
504 pub viewer: Option<User>,
505 pub repo: RepoPath,
506 pub id: String,
507}
508
509#[derive(Clone, Debug, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct SecretAlertDetail {
512 pub secret: SecretFinding,
513 pub locations: Vec<SecretLocation>,
514 pub activity: Vec<AlertActivity>,
515 pub requests: Vec<BypassRequest>,
516 /// Whether this kind of secret can be checked with its issuer.
517 pub checkable: bool,
518 /// Whether the viewer may bypass it directly, may only ask, or neither.
519 pub can_bypass: bool,
520 pub can_request_bypass: bool,
521}
522
523/// `bypass`: lets a blocked secret through push protection with a reason,
524/// recorded on the alert and in the audit log. Write and up; with
525/// delegated bypass on, only those who review requests (owners and the
526/// repository's admins) bypass directly, and anyone else's call makes a
527/// request instead. Returns `Outcome<BypassResult>`.
528#[derive(Debug, Serialize, Deserialize)]
529pub struct BypassArgs {
530 pub actor: User,
531 pub repo: RepoPath,
532 pub id: String,
533 pub reason: BypassReason,
534 #[serde(default)]
535 pub comment: String,
536}
537
538#[derive(Clone, Debug, Serialize, Deserialize)]
539#[serde(rename_all = "camelCase")]
540pub struct BypassResult {
541 pub secret: SecretFinding,
542 /// The request made, when delegated bypass needs approval.
543 pub request: Option<BypassRequest>,
544}
545
546/// `bypass_requests`: a workspace's requests (or one repository's), newest
547/// first. Members see their own; reviewers see all. Returns
548/// `Outcome<Vec<BypassRequest>>`.
549#[derive(Debug, Serialize, Deserialize)]
550pub struct BypassRequestsArgs {
551 pub viewer: Option<User>,
552 pub workspace: String,
553 #[serde(default)]
554 pub repo: Option<RepoPath>,
555 /// `pending`, `approved`, `denied`, `cancelled`; all when absent.
556 #[serde(default)]
557 pub state: Option<String>,
558}
559
560/// `review_bypass`: approves or denies a request (owners and the
561/// repository's admins), or cancels one's own. Returns
562/// `Outcome<BypassRequest>`.
563#[derive(Debug, Serialize, Deserialize)]
564pub struct ReviewBypassArgs {
565 pub actor: User,
566 pub workspace: String,
567 pub id: String,
568 /// `approve`, `deny` or `cancel`.
569 pub decision: String,
570 #[serde(default)]
571 pub comment: String,
572}
573
574/// `check_validity`: asks the issuer whether a secret still works.
575/// Write and up; needs validity checks on for the workspace. Returns
576/// `Outcome<SecretFinding>`.
577#[derive(Debug, Serialize, Deserialize)]
578pub struct CheckValidityArgs {
579 pub actor: User,
580 pub repo: RepoPath,
581 pub id: String,
582}
583
584/// `check_secret` (repos): finds the secret with `fingerprint` at
585/// `commit`:`path` near `line` and asks its issuer about it. The value
586/// never leaves the repos service except to the issuer's own API.
587/// Returns `SecretValidity`.
588#[derive(Debug, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct CheckSecretArgs {
591 pub repo_id: String,
592 pub commit: String,
593 pub path: String,
594 pub line: u32,
595 pub kind: String,
596 pub fingerprint: String,
597}
598
599#[derive(Clone, Debug, Serialize, Deserialize)]
600#[serde(rename_all = "camelCase")]
601pub struct SecretValidity {
602 /// `active`, `inactive`, `unknown` or `unsupported`.
603 pub validity: String,
604 /// Why it is unknown, when it is.
605 pub detail: Option<String>,
606}
607
608// --- Code scanning ----------------------------------------------------------
609
610/// A code scanning alert: one problem a tool reports on the default
611/// branch, the same alert across analyses by its fingerprint.
612#[derive(Clone, Debug, Serialize, Deserialize)]
613#[serde(rename_all = "camelCase")]
614pub struct CodeAlert {
615 /// `cod_…`.
616 pub id: String,
617 /// Per repository, from 1.
618 pub number: u32,
619 pub repo_id: String,
620 pub tool: String,
621 pub category: String,
622 pub rule_id: String,
623 pub rule_name: Option<String>,
624 pub rule_description: Option<String>,
625 pub help: Option<String>,
626 pub help_uri: Option<String>,
627 pub tags: Vec<String>,
628 /// `error`, `warning`, `note` or `none`.
629 pub level: String,
630 /// `critical`, `high`, `medium` or `low`, for security rules.
631 pub security_severity: Option<String>,
632 /// What lists sort by: the security severity, else from the level.
633 pub severity: String,
634 pub message: String,
635 pub path: Option<String>,
636 pub start_line: Option<u32>,
637 pub end_line: Option<u32>,
638 pub start_column: Option<u32>,
639 pub end_column: Option<u32>,
640 pub state: AlertState,
641 pub fingerprint: String,
642 pub first_commit: String,
643 pub last_commit: String,
644 pub created_at: String,
645 pub updated_at: String,
646 pub fixed_at: Option<String>,
647 pub dismissed_by: Option<String>,
648 pub dismissed_reason: Option<DismissReason>,
649 pub dismissed_comment: Option<String>,
650 pub dismissed_at: Option<String>,
651 /// The issue g1t was put on to fix it.
652 pub issue: Option<u32>,
653}
654
655/// One upload's run of one tool on one commit.
656#[derive(Clone, Debug, Serialize, Deserialize)]
657#[serde(rename_all = "camelCase")]
658pub struct Analysis {
659 /// `ana_…`.
660 pub id: String,
661 pub repo_id: String,
662 pub sarif_id: String,
663 pub tool: String,
664 pub tool_version: Option<String>,
665 pub category: String,
666 pub commit_sha: String,
667 /// `refs/heads/main`, `refs/pull/3/head`.
668 pub git_ref: String,
669 /// The pull request it is for, if any.
670 pub pull: Option<u32>,
671 pub results: u32,
672 /// Alerts it opened, and fixed (on the default branch only).
673 pub new_alerts: u32,
674 pub fixed_alerts: u32,
675 /// Results past the limit, not kept.
676 pub dropped: u32,
677 pub created_at: String,
678}
679
680/// `upload_sarif`: reads a SARIF upload into analyses and alerts. Write
681/// and up (a workflow's token can). Returns `Outcome<SarifUpload>`.
682#[derive(Debug, Serialize, Deserialize)]
683#[serde(rename_all = "camelCase")]
684pub struct UploadSarifArgs {
685 pub actor: User,
686 pub repo: RepoPath,
687 pub commit_sha: String,
688 /// `refs/heads/<branch>` or `refs/pull/<number>/head` (or `/merge`).
689 pub git_ref: String,
690 /// The SARIF document, gzipped and base64-encoded.
691 pub sarif: String,
692 #[serde(default)]
693 pub tool_name: Option<String>,
694 #[serde(default)]
695 pub category: Option<String>,
696 /// Where the files were checked out, to make absolute paths relative.
697 #[serde(default)]
698 pub checkout_uri: Option<String>,
699}
700
701#[derive(Clone, Debug, Serialize, Deserialize)]
702#[serde(rename_all = "camelCase")]
703pub struct SarifUpload {
704 /// `sar_…`.
705 pub id: String,
706 /// `complete` or `failed`.
707 pub processing_status: String,
708 pub analyses: Vec<String>,
709 pub errors: Vec<String>,
710 pub commit_sha: String,
711 pub git_ref: String,
712 pub created_at: String,
713}
714
715/// `sarif_status`: one upload. Returns `Outcome<SarifUpload>`.
716#[derive(Debug, Serialize, Deserialize)]
717pub struct SarifStatusArgs {
718 pub viewer: Option<User>,
719 pub repo: RepoPath,
720 pub id: String,
721}
722
723/// `code_scanning`: a repository's alerts (newest 1,000) and recent
724/// analyses. Write and up. Returns `Outcome<CodeScanning>`.
725#[derive(Debug, Serialize, Deserialize)]
726pub struct CodeScanningArgs {
727 pub viewer: Option<User>,
728 pub repo: RepoPath,
729}
730
731#[derive(Clone, Debug, Serialize, Deserialize)]
732#[serde(rename_all = "camelCase")]
733pub struct CodeScanning {
734 pub alerts: Vec<CodeAlert>,
735 pub analyses: Vec<Analysis>,
736 pub entitled: bool,
737 pub private: bool,
738 /// Whether a starter workflow is on the default branch.
739 pub configured: bool,
740}
741
742/// `code_alert`: one alert by number, with its activity and the pull
743/// requests that reported it. Returns `Outcome<CodeAlertDetail>`.
744#[derive(Debug, Serialize, Deserialize)]
745pub struct CodeAlertArgs {
746 pub viewer: Option<User>,
747 pub repo: RepoPath,
748 pub number: u32,
749}
750
751#[derive(Clone, Debug, Serialize, Deserialize)]
752#[serde(rename_all = "camelCase")]
753pub struct CodeAlertDetail {
754 pub alert: CodeAlert,
755 pub activity: Vec<AlertActivity>,
756 /// Analyses that reported it, newest first.
757 pub analyses: Vec<Analysis>,
758}
759
760/// `set_code_alert_state`: dismisses (with `false_positive`, `wont_fix` or
761/// `used_in_tests`) or reopens a code scanning alert. Write and up.
762/// Returns `Outcome<CodeAlert>`.
763#[derive(Debug, Serialize, Deserialize)]
764pub struct SetCodeAlertStateArgs {
765 pub actor: User,
766 pub repo: RepoPath,
767 pub number: u32,
768 /// `dismissed` or `open`.
769 pub state: AlertState,
770 #[serde(default)]
771 pub reason: Option<DismissReason>,
772 #[serde(default)]
773 pub comment: String,
774}
775
776/// `pull_code_scanning`: what code scanning found on a pull request's head,
777/// for its Security results page. Returns `Outcome<PullScanning>`.
778#[derive(Debug, Serialize, Deserialize)]
779pub struct PullScanningArgs {
780 pub viewer: Option<User>,
781 pub repo: RepoPath,
782 pub number: u32,
783}
784
785#[derive(Clone, Debug, Default, Serialize, Deserialize)]
786#[serde(rename_all = "camelCase")]
787pub struct PullScanning {
788 pub commit: Option<String>,
789 pub results: Vec<PullResult>,
790 pub review: Option<DependencyReview>,
791 /// The check's state and line, as the pull request shows it.
792 pub code_status: Option<String>,
793 pub code_description: Option<String>,
794}
795
796/// A result on a pull request's head.
797#[derive(Clone, Debug, Serialize, Deserialize)]
798#[serde(rename_all = "camelCase")]
799pub struct PullResult {
800 pub tool: String,
801 pub rule_id: String,
802 pub level: String,
803 pub severity: String,
804 pub security_severity: Option<String>,
805 pub message: String,
806 pub path: Option<String>,
807 pub line: Option<u32>,
808 /// Not open on the default branch: this pull request brings it.
809 pub new: bool,
810 /// On a line the pull request adds.
811 pub on_changed_line: bool,
812 /// Whether it fails the check.
813 pub failing: bool,
814}
815
816/// `fix_alert`: puts g1t on an issue to fix a code scanning alert, a
817/// vulnerable dependency or a leaked secret (removing it from the code;
818/// rotating it stays with you). Write and up, and agents allowed to run.
819/// The agent's work is billed as agent usage. Returns
820/// `Outcome<AlertFix>`.
821#[derive(Debug, Serialize, Deserialize)]
822pub struct FixAlertArgs {
823 pub actor: User,
824 pub repo: RepoPath,
825 /// `cod_…`, `vul_…` or `sec_…`.
826 pub id: String,
827}
828
829#[derive(Clone, Debug, Serialize, Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct AlertFix {
832 pub issue: u32,
833 /// Whether an agent was started on it.
834 pub started: bool,
835 pub message: Option<String>,
836}
837
838// --- Supply chain -----------------------------------------------------------
839
840/// One package in the dependency graph.
841#[derive(Clone, Debug, Serialize, Deserialize)]
842#[serde(rename_all = "camelCase")]
843pub struct GraphDependency {
844 /// `npm`, `crates.io`, `Go`, `PyPI`.
845 pub ecosystem: String,
846 pub name: String,
847 pub version: String,
848 pub manifest: String,
849 /// `direct`, `transitive` or `unknown`.
850 pub relationship: String,
851 pub development: bool,
852 pub license: Option<String>,
853 pub purl: String,
854 /// Open vulnerability alerts on it.
855 pub vulnerabilities: u32,
856}
857
858/// `dependency_graph`: the packages the lockfiles on the default branch
859/// resolve. Write and up. Returns `Outcome<DependencyGraph>`.
860#[derive(Debug, Serialize, Deserialize)]
861pub struct DependencyGraphArgs {
862 pub viewer: Option<User>,
863 pub repo: RepoPath,
864}
865
866#[derive(Clone, Debug, Default, Serialize, Deserialize)]
867#[serde(rename_all = "camelCase")]
868pub struct DependencyGraph {
869 pub commit: Option<String>,
870 pub manifests: Vec<GraphManifest>,
871 pub dependencies: Vec<GraphDependency>,
872}
873
874#[derive(Clone, Debug, Serialize, Deserialize)]
875#[serde(rename_all = "camelCase")]
876pub struct GraphManifest {
877 pub path: String,
878 pub ecosystem: String,
879 pub dependencies: u32,
880 pub direct: u32,
881}
882
883/// `sbom`: the dependency graph as an SPDX 2.3 JSON document. Returns
884/// `Outcome<serde_json::Value>`.
885#[derive(Debug, Serialize, Deserialize)]
886pub struct SbomArgs {
887 pub viewer: Option<User>,
888 pub repo: RepoPath,
889}
890
891/// `dependency_review`: compares the dependencies at `base` and `head`
892/// (commits, branches or tags). Returns `Outcome<DependencyReview>`.
893#[derive(Debug, Serialize, Deserialize)]
894pub struct DependencyReviewArgs {
895 pub viewer: Option<User>,
896 pub repo: RepoPath,
897 pub base: String,
898 pub head: String,
899}
900
901#[derive(Clone, Debug, Default, Serialize, Deserialize)]
902#[serde(rename_all = "camelCase")]
903pub struct DependencyReview {
904 pub base: String,
905 pub head: String,
906 pub changes: Vec<ReviewChange>,
907 pub passed: bool,
908 pub headline: String,
909 /// The policy it was judged by.
910 pub fail_on: String,
911 pub deny_licenses: Vec<String>,
912}
913
914#[derive(Clone, Debug, Serialize, Deserialize)]
915#[serde(rename_all = "camelCase")]
916pub struct ReviewChange {
917 /// `added` or `removed`.
918 pub change_type: String,
919 pub manifest: String,
920 pub ecosystem: String,
921 pub name: String,
922 pub version: String,
923 pub relationship: String,
924 pub development: bool,
925 pub license: Option<String>,
926 pub purl: String,
927 pub vulnerabilities: Vec<ReviewVulnerability>,
928 pub denied_license: bool,
929 /// Whether it fails the review.
930 pub failing: bool,
931}
932
933#[derive(Clone, Debug, Serialize, Deserialize)]
934#[serde(rename_all = "camelCase")]
935pub struct ReviewVulnerability {
936 pub advisory: String,
937 pub osv_id: String,
938 pub summary: String,
939 pub severity: String,
940 pub fixed_version: Option<String>,
941 pub url: String,
942}
943
944// --- The workspace's overview ------------------------------------------------
945
946/// `security_overview`: totals, trends, coverage and the repositories most
947/// in need, across a workspace. Members only; private repositories count
948/// only with the activation. Returns `Outcome<WorkspaceOverview>`.
949#[derive(Debug, Serialize, Deserialize)]
950pub struct WorkspaceOverviewArgs {
951 pub viewer: Option<User>,
952 pub workspace: String,
953 /// Days of trend, 7 to 90.
954 #[serde(default)]
955 pub days: Option<u32>,
956}
957
958#[derive(Clone, Debug, Default, Serialize, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct WorkspaceOverview {
961 pub activated: bool,
962 /// Private repositories left out for want of the activation.
963 pub private_hidden: u32,
964 pub totals: Vec<TypeTotals>,
965 pub trend: Vec<TrendPoint>,
966 pub repos: Vec<RepoCoverage>,
967}
968
969/// Open alerts of one type, and how many opened and closed lately.
970#[derive(Clone, Debug, Default, Serialize, Deserialize)]
971#[serde(rename_all = "camelCase")]
972pub struct TypeTotals {
973 pub alert_type: String,
974 pub open: SeverityCounts,
975 pub opened: u32,
976 pub closed: u32,
977}
978
979/// Open alerts by type on one day.
980#[derive(Clone, Debug, Default, Serialize, Deserialize)]
981#[serde(rename_all = "camelCase")]
982pub struct TrendPoint {
983 /// `YYYY-MM-DD`.
984 pub day: String,
985 pub secret_scanning: u32,
986 pub code_scanning: u32,
987 pub vulnerability: u32,
988}
989
990/// One repository: what is on, and what is open.
991#[derive(Clone, Debug, Default, Serialize, Deserialize)]
992#[serde(rename_all = "camelCase")]
993pub struct RepoCoverage {
994 pub repo_id: String,
995 pub name: String,
996 pub private: bool,
997 pub custom_patterns: u32,
998 pub validity_checks: bool,
999 /// When code scanning last reported, if ever.
1000 pub code_scanning_at: Option<String>,
1001 pub dependency_review: bool,
1002 pub security_updates: bool,
1003 pub lockfiles: u32,
1004 pub secrets: SeverityCounts,
1005 pub code: SeverityCounts,
1006 pub vulnerabilities: SeverityCounts,
1007}
1008
1009/// `workspace_alerts`: alerts of one type across a workspace, for the API.
1010/// Returns `Outcome<Vec<WorkspaceAlert>>`.
1011#[derive(Debug, Serialize, Deserialize)]
1012#[serde(rename_all = "camelCase")]
1013pub struct WorkspaceAlertsArgs {
1014 pub viewer: Option<User>,
1015 pub workspace: String,
1016 pub alert_type: AlertType,
1017}
1018
1019/// An alert of any type, with its repository.
1020#[derive(Clone, Debug, Serialize, Deserialize)]
1021#[serde(rename_all = "camelCase")]
1022pub struct WorkspaceAlert {
1023 pub repo: String,
1024 #[serde(default)]
1025 pub secret: Option<SecretFinding>,
1026 #[serde(default)]
1027 pub code: Option<CodeAlert>,
1028 #[serde(default)]
1029 pub vulnerability: Option<crate::security::Vulnerability>,
1030}
1031
1032// --- Events ------------------------------------------------------------------
1033
1034/// What every security event carries (`secret_scanning_alert.created`,
1035/// `code_scanning_alert.fixed`, `vulnerability_alert.dismissed`,
1036/// `secret_scanning.bypass_requested`, …), for webhooks and the inbox.
1037#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1038#[serde(rename_all = "camelCase")]
1039pub struct SecurityEvent {
1040 pub repo_id: String,
1041 pub alert_id: String,
1042 /// `secret_scanning`, `code_scanning` or `vulnerability`.
1043 pub alert_type: String,
1044 /// A code scanning alert's number.
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1046 pub alert_number: Option<u32>,
1047 pub severity: String,
1048 /// One line: "An AWS access key in config/prod.env".
1049 pub title: String,
1050 /// The alert's page, from the site's root: `/acme/rocket/security/…`.
1051 pub link: String,
1052 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub path: Option<String>,
1054 #[serde(default, skip_serializing_if = "Option::is_none")]
1055 pub line: Option<u32>,
1056 /// `open`, `dismissed` or `fixed`.
1057 pub state: String,
1058 /// A dismissal's or bypass's reason.
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1060 pub reason: Option<String>,
1061 /// For a blocked push: who pushed it, by username.
1062 #[serde(default, skip_serializing_if = "Option::is_none")]
1063 pub pusher: Option<String>,
1064 /// People to tell besides the repository's security watchers: its
1065 /// workspace's owners, for a new alert or a bypass request, and the
1066 /// requester, for a reviewed one. Usernames.
1067 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1068 pub notify: Vec<String>,
1069 /// A bypass request's id.
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1071 pub request_id: Option<String>,
1072}
1073
1074/// The security event types, as webhooks list them.
1075pub const EVENT_TYPES: [&str; 14] = [
1076 "secret_scanning_alert.created",
1077 "secret_scanning_alert.fixed",
1078 "secret_scanning_alert.dismissed",
1079 "secret_scanning_alert.reopened",
1080 "secret_scanning.bypass_requested",
1081 "secret_scanning.bypass_reviewed",
1082 "code_scanning_alert.created",
1083 "code_scanning_alert.fixed",
1084 "code_scanning_alert.dismissed",
1085 "code_scanning_alert.reopened",
1086 "vulnerability_alert.created",
1087 "vulnerability_alert.fixed",
1088 "vulnerability_alert.dismissed",
1089 "vulnerability_alert.reopened",
1090];
1091
1092/// The checks the suite reports on pull requests, as commit statuses:
1093/// require them in branch protection to gate merges.
1094pub const CODE_SCANNING_CHECK: &str = "Code scanning";
1095pub const DEPENDENCY_REVIEW_CHECK: &str = "Dependency review";
1096
1097/// The starter workflow "Set up code scanning" opens a pull request with.
1098pub const STARTER_WORKFLOW_PATH: &str = ".g1t/workflows/code-scanning.yml";
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 #[test]
1105 fn alert_ids_say_their_type() {
1106 assert_eq!(AlertType::of_id("sec_1"), Some(AlertType::SecretScanning));
1107 assert_eq!(AlertType::of_id("cod_1"), Some(AlertType::CodeScanning));
1108 assert_eq!(AlertType::of_id("vul_1"), Some(AlertType::Vulnerability));
1109 assert_eq!(AlertType::of_id("x"), None);
1110 for kind in AlertType::ALL {
1111 assert_eq!(AlertType::parse(kind.as_str()), Some(kind));
1112 }
1113 }
1114
1115 #[test]
1116 fn every_alert_type_has_its_four_events() {
1117 for kind in AlertType::ALL {
1118 for action in ["created", "fixed", "dismissed", "reopened"] {
1119 assert!(EVENT_TYPES.contains(&format!("{}.{action}", kind.event_prefix()).as_str()));
1120 }
1121 }
1122 }
1123
1124 #[test]
1125 fn a_bypass_for_a_real_secret_leaves_the_alert_open() {
1126 assert_eq!(BypassReason::WillFixLater.dismissal(), None);
1127 assert_eq!(BypassReason::UsedInTests.dismissal(), Some(DismissReason::UsedInTests));
1128 assert_eq!(BypassReason::parse("false_positive"), Some(BypassReason::FalsePositive));
1129 }
1130
1131 #[test]
1132 fn the_refusal_says_how_to_turn_it_on() {
1133 let message = needs_activation(PaidFeature::CodeScanning, "acme");
1134 assert!(message.starts_with("Code scanning on private repositories comes with the Security and quality activation"));
1135 assert!(message.contains("/acme/-/billing") && message.contains("Public repositories have it free"));
1136 }
1137}