g1t/services/security/src/history.rs

74 lines3,378 bytesCodeBlame
1//! Scanning a repository's history for secrets once, in the background, a
2//! page of commits at a time, metered to its workspace.
3
4use g1t_contracts::billing::{CheckLimitArgs, Limit, LimitState, NotePendingArgs};
5use g1t_contracts::security::{HistoryPage, ScanHistoryArgs, SecretStatus};
6use g1t_contracts::Outcome;
7use worker::Result;
8
9use crate::Security;
10use crate::store::RepoRow;
11
12/// Commits read per call to the repos service.
13const PAGE: u32 = 25;
14/// What one read of a git object is taken to cost g1t, in millionths of a
15/// dollar: a store call and the Worker time around it, rounded up.
16pub const MICROS_PER_READ: i64 = 1;
17
18impl Security {
19 /// Whether the workspace's usage has reached its limit, which stops
20 /// background work. Unknown counts as not.
21 async fn over_limit(&self, workspace: &str) -> bool {
22 let limit: Result<Outcome<Limit>> =
23 g1t_kit::call(&self.billing, "check_limit", &CheckLimitArgs { workspace: workspace.to_owned() }).await;
24 matches!(limit, Ok(Outcome::Ok(limit)) if limit.state == LimitState::Stopped)
25 }
26
27 /// Records what scanning cost, and tells billing the month's total so
28 /// the workspace's limit counts it.
29 pub async fn meter(&self, workspace: &str, reads: u32, commits: u32, osv_calls: u32, cost_micros: i64) -> Result<()> {
30 let total = self.store.meter(workspace, reads, commits, osv_calls, cost_micros).await?;
31 let noted: Result<bool> = g1t_kit::call(
32 &self.billing,
33 "note_pending",
34 &NotePendingArgs { workspace: workspace.to_owned(), source: "security".to_owned(), cost_micros: total },
35 )
36 .await;
37 if let Err(error) = noted {
38 worker::console_error!("security: usage for {workspace} not noted: {error}");
39 }
40 Ok(())
41 }
42
43 /// Scans up to `pages` pages of a repository's history from where the
44 /// last scan stopped.
45 pub async fn advance_history(&self, repo: &RepoRow, pages: u32) -> Result<()> {
46 if repo.history == "done" {
47 return Ok(());
48 }
49 if self.over_limit(&repo.namespace).await {
50 return self.store.set_history(&repo.repo_id, "stopped", repo.history_cursor.as_deref(), 0).await;
51 }
52 let mut cursor = repo.history_cursor.clone();
53 for _ in 0..pages {
54 let page: HistoryPage = g1t_kit::call(
55 &self.repos,
56 "scan_history",
57 &ScanHistoryArgs { repo_id: repo.repo_id.clone(), after: cursor.clone(), limit: PAGE },
58 )
59 .await?;
60 let fingerprints: Vec<String> = page.secrets.iter().map(|secret| secret.fingerprint.clone()).collect();
61 self.store.landed(&repo.repo_id, &fingerprints).await?;
62 self.store.add_secrets(&repo.repo_id, &page.secrets, SecretStatus::Open, "history", None).await?;
63 self.meter(&repo.namespace, page.reads, page.commits, 0, i64::from(page.reads) * MICROS_PER_READ).await?;
64 match page.next {
65 Some(next) => {
66 self.store.set_history(&repo.repo_id, "running", Some(&next), page.commits).await?;
67 cursor = Some(next);
68 }
69 None => return self.store.set_history(&repo.repo_id, "done", None, page.commits).await,
70 }
71 }
72 Ok(())
73 }
74}