1use std::path::Path;
5
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum ScanScheduleKind {
16 Webhook,
17 Poll,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum ScanScheduleProvider {
22 #[serde(rename = "github")]
23 GitHub,
24 #[serde(rename = "gitlab")]
25 GitLab,
26 #[serde(rename = "bitbucket")]
27 Bitbucket,
28 #[serde(rename = "any")]
29 Any,
30}
31
32impl ScanScheduleProvider {
33 pub fn display_name(&self) -> &'static str {
34 match self {
35 Self::GitHub => "GitHub",
36 Self::GitLab => "GitLab",
37 Self::Bitbucket => "Bitbucket",
38 Self::Any => "Any / Poll",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ScanSchedule {
47 pub id: Uuid,
48 pub label: String,
49 pub repo_url: String,
50 pub branch: String,
51 pub kind: ScanScheduleKind,
52 pub provider: ScanScheduleProvider,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub webhook_secret: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub interval_secs: Option<u64>,
57 pub last_scan_sha: Option<String>,
58 pub last_scan_at: Option<DateTime<Utc>>,
59 pub last_run_id: Option<String>,
60 pub enabled: bool,
61}
62
63impl ScanSchedule {
64 pub fn new_webhook(
65 repo_url: String,
66 branch: String,
67 provider: ScanScheduleProvider,
68 label: String,
69 ) -> Self {
70 Self {
71 id: Uuid::new_v4(),
72 label,
73 repo_url,
74 branch,
75 kind: ScanScheduleKind::Webhook,
76 provider,
77 webhook_secret: Some(generate_secret()),
78 interval_secs: None,
79 last_scan_sha: None,
80 last_scan_at: None,
81 last_run_id: None,
82 enabled: true,
83 }
84 }
85
86 pub fn new_poll(repo_url: String, branch: String, interval_secs: u64, label: String) -> Self {
87 Self {
88 id: Uuid::new_v4(),
89 label,
90 repo_url,
91 branch,
92 kind: ScanScheduleKind::Poll,
93 provider: ScanScheduleProvider::Any,
94 webhook_secret: None,
95 interval_secs: Some(interval_secs),
96 last_scan_sha: None,
97 last_scan_at: None,
98 last_run_id: None,
99 enabled: true,
100 }
101 }
102}
103
104fn generate_secret() -> String {
105 format!("{}-{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
106}
107
108#[derive(Debug, Default, Serialize, Deserialize)]
111pub struct ScheduleStore {
112 pub schedules: Vec<ScanSchedule>,
113}
114
115impl ScheduleStore {
116 pub fn load(path: &Path) -> Self {
117 std::fs::read_to_string(path)
118 .ok()
119 .and_then(|s| serde_json::from_str(&s).ok())
120 .unwrap_or_default()
121 }
122
123 pub fn save(&self, path: &Path) -> Result<()> {
124 let json = serde_json::to_string_pretty(self)?;
125 std::fs::write(path, json)?;
126 Ok(())
127 }
128
129 pub fn find_matching<'a>(&'a self, repo_url: &str, branch: &str) -> Vec<&'a ScanSchedule> {
130 self.schedules
131 .iter()
132 .filter(|s| s.enabled && urls_match(&s.repo_url, repo_url) && s.branch == branch)
133 .collect()
134 }
135
136 pub fn by_id_mut(&mut self, id: Uuid) -> Option<&mut ScanSchedule> {
137 self.schedules.iter_mut().find(|s| s.id == id)
138 }
139
140 pub fn remove(&mut self, id: Uuid) {
141 self.schedules.retain(|s| s.id != id);
142 }
143}
144
145fn urls_match(a: &str, b: &str) -> bool {
146 normalize_url(a) == normalize_url(b)
147}
148
149fn normalize_url(url: &str) -> String {
150 url.trim_end_matches('/')
151 .trim_end_matches(".git")
152 .to_lowercase()
153}