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