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