flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/security/src/store.rs

1,114 lines41,118 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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! The security service's tables in D1.
2
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3use std::collections::HashMap;
4
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API5use g1t_contracts::security::{
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily6 AlertActivity, DismissReason, NewSecret, ScanState, SecretCounts, SecretFinding, SecretStatus, SecurityUpdate,
7 SeverityCounts, UpdateState, VersionUpdatesState, VulnStatus, Vulnerability,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API8};
9use g1t_contracts::time::rfc3339;
10use g1t_contracts::new_id;
11use g1t_kit::now_ms;
12use g1t_scan::secrets::SecretKind;
13use serde::Deserialize;
14use worker::wasm_bindgen::JsValue;
15use worker::{D1Database, Result};
16
17pub fn optional(value: Option<&str>) -> JsValue {
18 value.map_or(JsValue::NULL, JsValue::from)
19}
20
21pub fn now() -> String {
22 rfc3339(now_ms())
23}
24
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25/// A repository purged (`repo.purged`): every row kept for it, `?1` its
26/// id. Advisories are shared by every repository, so they stay.
27pub const PURGED: &[&str] = &[
28 "DELETE FROM secrets WHERE repo_id = ?1",
29 "DELETE FROM vulnerabilities WHERE repo_id = ?1",
30 "DELETE FROM upgrades WHERE repo_id = ?1",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily31 "DELETE FROM updates WHERE repo_id = ?1",
32 "DELETE FROM alert_activity WHERE repo_id = ?1",
33 "DELETE FROM push_scans WHERE repo_id = ?1",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look34 "DELETE FROM repos WHERE repo_id = ?1",
35];
36
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API37#[derive(Clone, Deserialize)]
38pub struct RepoRow {
39 pub repo_id: String,
40 pub namespace: String,
41 pub name: String,
42 pub upkeep: i64,
43 pub history: String,
44 pub history_cursor: Option<String>,
45 pub history_commits: i64,
46 pub history_finished_at: Option<String>,
47 pub deps_scanned_at: Option<String>,
48 pub deps_error: Option<String>,
49 pub lockfiles: String,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily50 #[serde(default)]
51 pub version_updates: Option<String>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API52}
53
54impl RepoRow {
55 pub fn scan_state(&self) -> ScanState {
56 ScanState {
57 history: self.history.clone(),
58 commits_scanned: self.history_commits.max(0) as u32,
59 history_finished_at: self.history_finished_at.clone(),
60 dependencies_scanned_at: self.deps_scanned_at.clone(),
61 dependencies_error: self.deps_error.clone(),
62 lockfiles: serde_json::from_str(&self.lockfiles).unwrap_or_default(),
63 }
64 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily65
66 /// What `.g1t/dependencies.yml` said when it was last read.
67 pub fn version_updates(&self) -> VersionUpdatesState {
68 self.version_updates
69 .as_deref()
70 .and_then(|json| serde_json::from_str(json).ok())
71 .unwrap_or_default()
72 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API73}
74
75#[derive(Deserialize)]
76struct SecretRow {
77 id: String,
78 repo_id: String,
79 kind: String,
80 path: String,
81 line: i64,
82 commit_hash: String,
83 preview: String,
84 status: String,
85 source: String,
86 found_by: Option<String>,
87 found_at: String,
88 decided_by: Option<String>,
89 reason: Option<String>,
90 decided_at: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily91 dismiss_reason: Option<String>,
92 test_value: Option<String>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API93}
94
95impl From<SecretRow> for SecretFinding {
96 fn from(row: SecretRow) -> Self {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily97 let status = SecretStatus::parse(&row.status).unwrap_or(SecretStatus::Open);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API98 SecretFinding {
99 label: SecretKind::parse(&row.kind).map_or("a secret", |kind| kind.label()).to_owned(),
100 id: row.id,
101 repo_id: row.repo_id,
102 kind: row.kind,
103 path: row.path,
104 line: row.line.max(0) as u32,
105 commit: row.commit_hash,
106 preview: row.preview,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily107 status,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API108 source: row.source,
109 found_by: row.found_by,
110 found_at: row.found_at,
111 decided_by: row.decided_by,
112 reason: row.reason,
113 decided_at: row.decided_at,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily114 dismissed_reason: row.dismiss_reason.as_deref().and_then(DismissReason::parse),
115 test_value: row.test_value,
116 state: status.state(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API117 }
118 }
119}
120
121#[derive(Deserialize)]
122pub struct VulnRow {
123 pub id: String,
124 pub repo_id: String,
125 pub ecosystem: String,
126 pub package: String,
127 pub version: String,
128 pub manifest: String,
129 pub osv_id: String,
130 pub advisory: String,
131 pub summary: String,
132 pub severity: String,
133 pub fixed_version: Option<String>,
134 pub status: String,
135 pub found_at: String,
136 pub fixed_at: Option<String>,
137 pub number: Option<i64>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily138 #[serde(default)]
139 pub dismiss_reason: Option<String>,
140 #[serde(default)]
141 pub dismiss_comment: Option<String>,
142 #[serde(default)]
143 pub dismissed_by: Option<String>,
144 #[serde(default)]
145 pub dismissed_at: Option<String>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API146}
147
148impl From<VulnRow> for Vulnerability {
149 fn from(row: VulnRow) -> Self {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily150 let status = VulnStatus::parse(&row.status).unwrap_or(VulnStatus::Open);
151 let dismissed = status == VulnStatus::Dismissed;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API152 Vulnerability {
153 id: row.id,
154 repo_id: row.repo_id,
155 ecosystem: row.ecosystem,
156 package: row.package,
157 version: row.version,
158 manifest: row.manifest,
159 advisory: row.advisory,
160 osv_id: row.osv_id,
161 summary: row.summary,
162 severity: row.severity,
163 fixed_version: row.fixed_version,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily164 status,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API165 issue: row.number.map(|number| number as u32),
166 found_at: row.found_at,
167 fixed_at: row.fixed_at,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily168 state: status.state(),
169 dismissed_by: row.dismissed_by.filter(|_| dismissed),
170 dismissed_reason: row.dismiss_reason.as_deref().and_then(DismissReason::parse).filter(|_| dismissed),
171 dismissed_comment: row.dismiss_comment.filter(|_| dismissed),
172 dismissed_at: row.dismissed_at.filter(|_| dismissed),
173 update: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API174 }
175 }
176}
177
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily178/// A security update, as stored.
179#[derive(Clone, Deserialize)]
180pub struct UpdateRow {
181 pub repo_id: String,
182 pub ecosystem: String,
183 pub package: String,
184 pub target: String,
185 pub state: String,
186 pub branch: Option<String>,
187 pub pull: Option<i64>,
188 pub issue: Option<i64>,
189 pub error: Option<String>,
190 pub updated_at: String,
191}
192
193impl UpdateRow {
194 pub fn state(&self) -> UpdateState {
195 UpdateState::parse(&self.state).unwrap_or(UpdateState::Failed)
196 }
197
198 pub fn pull(&self) -> Option<u32> {
199 self.pull.map(|n| n.max(0) as u32)
200 }
201
202 pub fn issue(&self) -> Option<u32> {
203 self.issue.map(|n| n.max(0) as u32)
204 }
205
206 pub fn to_contract(&self) -> SecurityUpdate {
207 SecurityUpdate {
208 state: self.state(),
209 target: self.target.clone(),
210 branch: self.branch.clone(),
211 pull: self.pull(),
212 issue: self.issue(),
213 error: self.error.clone(),
214 updated_at: self.updated_at.clone(),
215 }
216 }
217}
218
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API219#[derive(Deserialize)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily220struct ActivityRow {
221 id: String,
222 alert_id: String,
223 action: String,
224 actor: Option<String>,
225 reason: Option<String>,
226 comment: Option<String>,
227 number: Option<i64>,
228 at: String,
229}
230
231impl From<ActivityRow> for AlertActivity {
232 fn from(row: ActivityRow) -> Self {
233 AlertActivity {
234 id: row.id,
235 alert_id: row.alert_id,
236 action: row.action,
237 actor: row.actor,
238 reason: row.reason.as_deref().and_then(DismissReason::parse),
239 comment: row.comment,
240 number: row.number.map(|n| n.max(0) as u32),
241 at: row.at,
242 }
243 }
244}
245
246/// A push too large to scan before it was stored, scanned after it landed.
247#[derive(Clone, Deserialize)]
248pub struct PushScanRow {
249 pub id: String,
250 pub repo_id: String,
251 pub git_ref: String,
252 pub head: String,
253 pub base: Option<String>,
254 pub cursor: Option<String>,
255 pub pusher: Option<String>,
256 pub pages: i64,
257}
258
259/// One action on an alert, to record.
260pub struct Activity<'a> {
261 pub alert_id: &'a str,
262 pub action: &'a str,
263 pub actor: Option<&'a str>,
264 pub reason: Option<DismissReason>,
265 pub comment: Option<&'a str>,
266 pub number: Option<u32>,
267}
268
269#[derive(Deserialize)]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API270pub struct UpgradeRow {
271 pub number: i64,
272}
273
274#[derive(Deserialize)]
275struct CountRow {
276 severity: String,
277 n: i64,
278}
279
280#[derive(Deserialize)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily281struct SecretCountRow {
282 status: String,
283 test: i64,
284 n: i64,
285}
286
287#[derive(Deserialize)]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API288struct NumberRow {
289 n: i64,
290}
291
292#[derive(Deserialize)]
293struct FingerprintRow {
294 fingerprint: String,
295 id: String,
296 status: String,
297}
298
299#[derive(Deserialize)]
300struct BodyRow {
301 body: String,
302 fetched_at: String,
303}
304
305pub struct Store {
306 pub db: D1Database,
307}
308
309const VULN_COLUMNS: &str = "v.id, v.repo_id, v.ecosystem, v.package, v.version, v.manifest, v.osv_id, v.advisory,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily310 v.summary, v.severity, v.fixed_version, v.status, v.found_at, v.fixed_at, v.dismiss_reason, v.dismiss_comment,
311 v.dismissed_by, v.dismissed_at, COALESCE(n.issue, u.number) AS number";
312
313/// The issue an upgrade has: the security update's, when it went to
314/// g1t-agent, or one opened before security updates.
315const VULN_JOINS: &str = "LEFT JOIN upgrades u ON u.repo_id = v.repo_id AND u.ecosystem = v.ecosystem AND u.package = v.package
316 LEFT JOIN updates n ON n.repo_id = v.repo_id AND n.ecosystem = v.ecosystem AND n.package = v.package";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API317
318impl Store {
319 pub async fn repo(&self, repo_id: &str) -> Result<Option<RepoRow>> {
320 self.db
321 .prepare("SELECT * FROM repos WHERE repo_id = ?")
322 .bind(&[repo_id.into()])?
323 .first::<RepoRow>(None)
324 .await
325 }
326
327 /// Records a repository the first time it is seen, and keeps its
328 /// address current. Returns its row.
329 pub async fn register(&self, repo_id: &str, namespace: &str, name: &str) -> Result<RepoRow> {
330 self.db
331 .prepare(
332 "INSERT INTO repos (repo_id, namespace, name, created_at) VALUES (?1, ?2, ?3, ?4)
333 ON CONFLICT (repo_id) DO UPDATE SET namespace = ?2, name = ?3",
334 )
335 .bind(&[repo_id.into(), namespace.into(), name.into(), now().into()])?
336 .run()
337 .await?;
338 self.repo(repo_id)
339 .await?
340 .ok_or_else(|| worker::Error::RustError("the repository was not recorded".into()))
341 }
342
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look343 /// A repository's path changed: transferred or renamed.
344 pub async fn moved(&self, repo_id: &str, namespace: &str, name: &str) -> Result<()> {
345 self.db
346 .prepare("UPDATE repos SET namespace = ?, name = ? WHERE repo_id = ?")
347 .bind(&[namespace.into(), name.into(), repo_id.into()])?
348 .run()
349 .await?;
350 Ok(())
351 }
352
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API353 pub async fn rename_namespace(&self, stale: &[String], current: &str) -> Result<()> {
354 for slug in stale {
355 self.db
356 .prepare("UPDATE repos SET namespace = ? WHERE namespace = ?")
357 .bind(&[current.into(), slug.as_str().into()])?
358 .run()
359 .await?;
360 }
361 Ok(())
362 }
363
364 pub async fn in_namespace(&self, namespace: &str) -> Result<Vec<RepoRow>> {
365 self.db
366 .prepare("SELECT * FROM repos WHERE namespace = ? ORDER BY name")
367 .bind(&[namespace.into()])?
368 .all()
369 .await?
370 .results::<RepoRow>()
371 }
372
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look373 /// A repository purged: everything found in it goes. `?1` its id.
374 pub async fn purge(&self, repo_id: &str) -> Result<()> {
375 let mut batch = Vec::with_capacity(PURGED.len());
376 for sql in PURGED {
377 batch.push(self.db.prepare(*sql).bind(&[repo_id.into()])?);
378 }
379 self.db.batch(batch).await?;
380 Ok(())
381 }
382
383 /// Records that a repository's daily dependency read was skipped, and
384 /// why, so the sweep moves on to others until the next day.
385 pub async fn skip_dependencies(&self, repo_id: &str, why: &str) -> Result<()> {
386 self.db
387 .prepare("UPDATE repos SET deps_scanned_at = ?, deps_error = ? WHERE repo_id = ?")
388 .bind(&[now().into(), why.into(), repo_id.into()])?
389 .run()
390 .await?;
391 Ok(())
392 }
393
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API394 /// Repositories whose history still has to be scanned, oldest first.
395 pub async fn unfinished_histories(&self, limit: u32) -> Result<Vec<RepoRow>> {
396 self.db
397 .prepare("SELECT * FROM repos WHERE history IN ('pending', 'running', 'stopped') ORDER BY created_at LIMIT ?")
398 .bind(&[limit.into()])?
399 .all()
400 .await?
401 .results::<RepoRow>()
402 }
403
404 /// Repositories whose dependencies were last read before `before`.
405 pub async fn stale_dependencies(&self, before: &str, limit: u32) -> Result<Vec<RepoRow>> {
406 self.db
407 .prepare(
408 "SELECT * FROM repos WHERE deps_scanned_at IS NULL OR deps_scanned_at < ?
409 ORDER BY deps_scanned_at LIMIT ?",
410 )
411 .bind(&[before.into(), limit.into()])?
412 .all()
413 .await?
414 .results::<RepoRow>()
415 }
416
417 pub async fn set_history(&self, repo_id: &str, state: &str, cursor: Option<&str>, commits: u32) -> Result<()> {
418 let finished = (state == "done").then(now);
419 self.db
420 .prepare(
421 "UPDATE repos SET history = ?, history_cursor = ?, history_commits = history_commits + ?,
422 history_finished_at = COALESCE(?, history_finished_at)
423 WHERE repo_id = ?",
424 )
425 .bind(&[state.into(), optional(cursor), commits.into(), optional(finished.as_deref()), repo_id.into()])?
426 .run()
427 .await?;
428 Ok(())
429 }
430
431 pub async fn restart_history(&self, repo_id: &str) -> Result<()> {
432 self.db
433 .prepare(
434 "UPDATE repos SET history = 'pending', history_cursor = NULL, history_commits = 0,
435 history_finished_at = NULL WHERE repo_id = ?",
436 )
437 .bind(&[repo_id.into()])?
438 .run()
439 .await?;
440 Ok(())
441 }
442
443 pub async fn set_dependencies_scanned(
444 &self,
445 repo_id: &str,
446 commit: Option<&str>,
447 lockfiles: &[String],
448 error: Option<&str>,
449 ) -> Result<()> {
450 self.db
451 .prepare("UPDATE repos SET deps_scanned_at = ?, deps_commit = ?, lockfiles = ?, deps_error = ? WHERE repo_id = ?")
452 .bind(&[now().into(), optional(commit), serde_json::to_string(lockfiles)?.into(), optional(error), repo_id.into()])?
453 .run()
454 .await?;
455 Ok(())
456 }
457
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily458 pub async fn set_version_updates(&self, repo_id: &str, state: &VersionUpdatesState) -> Result<()> {
459 self.db
460 .prepare("UPDATE repos SET version_updates = ? WHERE repo_id = ?")
461 .bind(&[serde_json::to_string(state)?.into(), repo_id.into()])?
462 .run()
463 .await?;
464 Ok(())
465 }
466
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API467 pub async fn set_upkeep(&self, repo_id: &str, enabled: bool, by: &str) -> Result<()> {
468 self.db
469 .prepare("UPDATE repos SET upkeep = ?, upkeep_by = ?, upkeep_at = ? WHERE repo_id = ?")
470 .bind(&[i32::from(enabled).into(), by.into(), now().into(), repo_id.into()])?
471 .run()
472 .await?;
473 Ok(())
474 }
475
476 // --- Secrets ------------------------------------------------------------
477
478 pub async fn secrets(&self, repo_id: &str) -> Result<Vec<SecretFinding>> {
479 let rows = self
480 .db
481 .prepare(
482 "SELECT * FROM secrets WHERE repo_id = ?
483 ORDER BY CASE status WHEN 'open' THEN 0 WHEN 'blocked' THEN 1 WHEN 'allowed' THEN 2 ELSE 3 END,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily484 test_value IS NOT NULL, found_at DESC LIMIT 500",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API485 )
486 .bind(&[repo_id.into()])?
487 .all()
488 .await?
489 .results::<SecretRow>()?;
490 Ok(rows.into_iter().map(SecretFinding::from).collect())
491 }
492
493 pub async fn secret(&self, repo_id: &str, id: &str) -> Result<Option<SecretFinding>> {
494 Ok(self
495 .db
496 .prepare("SELECT * FROM secrets WHERE repo_id = ? AND id = ?")
497 .bind(&[repo_id.into(), id.into()])?
498 .first::<SecretRow>(None)
499 .await?
500 .map(SecretFinding::from))
501 }
502
503 /// The findings already known for these fingerprints: fingerprint, id
504 /// and status.
505 pub async fn known(&self, repo_id: &str, fingerprints: &[String]) -> Result<Vec<(String, String, String)>> {
506 let mut known = Vec::new();
507 for chunk in fingerprints.chunks(90) {
508 let marks = vec!["?"; chunk.len()].join(", ");
509 let mut binds: Vec<JsValue> = vec![repo_id.into()];
510 binds.extend(chunk.iter().map(|fingerprint| JsValue::from(fingerprint.as_str())));
511 let rows = self
512 .db
513 .prepare(format!("SELECT fingerprint, id, status FROM secrets WHERE repo_id = ? AND fingerprint IN ({marks})"))
514 .bind(&binds)?
515 .all()
516 .await?
517 .results::<FingerprintRow>()?;
518 known.extend(rows.into_iter().map(|row| (row.fingerprint, row.id, row.status)));
519 }
520 Ok(known)
521 }
522
523 /// Records secrets not seen before in this repository, with `status`.
524 /// A secret already known keeps its record and its decision.
525 pub async fn add_secrets(
526 &self,
527 repo_id: &str,
528 secrets: &[NewSecret],
529 status: SecretStatus,
530 source: &str,
531 found_by: Option<&str>,
532 ) -> Result<()> {
533 if secrets.is_empty() {
534 return Ok(());
535 }
536 let now_ms = now_ms();
537 let found_at = rfc3339(now_ms);
538 let statements = secrets
539 .iter()
540 .map(|secret| {
541 self.db
542 .prepare(
543 "INSERT INTO secrets (id, repo_id, fingerprint, kind, path, line, commit_hash, preview,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily544 status, source, found_by, found_at, test_value)
545 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
546 ON CONFLICT (repo_id, fingerprint) DO UPDATE SET test_value = excluded.test_value",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API547 )
548 .bind(&[
549 new_id("sec", now_ms).into(),
550 repo_id.into(),
551 secret.fingerprint.as_str().into(),
552 secret.kind.as_str().into(),
553 secret.path.as_str().into(),
554 secret.line.into(),
555 secret.commit.as_str().into(),
556 secret.preview.as_str().into(),
557 status.as_str().into(),
558 source.into(),
559 optional(found_by),
560 found_at.as_str().into(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily561 optional(secret.test_value.as_deref()),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API562 ])
563 })
564 .collect::<Result<Vec<_>>>()?;
565 self.db.batch(statements).await?;
566 Ok(())
567 }
568
569 /// A secret found in history that was only ever blocked has landed
570 /// after all (it was allowed, then pushed): it is open now unless
571 /// someone allowed it.
572 pub async fn landed(&self, repo_id: &str, fingerprints: &[String]) -> Result<()> {
573 for chunk in fingerprints.chunks(90) {
574 let marks = vec!["?"; chunk.len()].join(", ");
575 let mut binds: Vec<JsValue> = vec![repo_id.into()];
576 binds.extend(chunk.iter().map(|fingerprint| JsValue::from(fingerprint.as_str())));
577 self.db
578 .prepare(format!(
579 "UPDATE secrets SET status = 'open', source = 'history'
580 WHERE repo_id = ? AND status = 'blocked' AND fingerprint IN ({marks})"
581 ))
582 .bind(&binds)?
583 .run()
584 .await?;
585 }
586 Ok(())
587 }
588
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily589 /// Dismisses a secret: allowed, or resolved when it was revoked.
590 pub async fn dismiss_secret(&self, repo_id: &str, id: &str, reason: DismissReason, by: &str, comment: Option<&str>) -> Result<()> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API591 self.db
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily592 .prepare(
593 "UPDATE secrets SET status = ?, decided_by = ?, reason = ?, decided_at = ?, dismiss_reason = ?
594 WHERE repo_id = ? AND id = ?",
595 )
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API596 .bind(&[
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily597 reason.secret_status().as_str().into(),
598 by.into(),
599 optional(comment),
600 now().into(),
601 reason.as_str().into(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API602 repo_id.into(),
603 id.into(),
604 ])?
605 .run()
606 .await?;
607 Ok(())
608 }
609
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily610 /// Opens a secret again, as `status` (open, or blocked for one that
611 /// never landed).
612 pub async fn reopen_secret(&self, repo_id: &str, id: &str, status: SecretStatus) -> Result<()> {
613 self.db
614 .prepare(
615 "UPDATE secrets SET status = ?, decided_by = NULL, reason = NULL, decided_at = NULL, dismiss_reason = NULL
616 WHERE repo_id = ? AND id = ?",
617 )
618 .bind(&[status.as_str().into(), repo_id.into(), id.into()])?
619 .run()
620 .await?;
621 Ok(())
622 }
623
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API624 // --- Vulnerabilities ----------------------------------------------------
625
626 pub async fn vulnerabilities(&self, repo_id: &str) -> Result<Vec<Vulnerability>> {
627 let rows = self
628 .db
629 .prepare(format!(
630 "SELECT {VULN_COLUMNS} FROM vulnerabilities v
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily631 {VULN_JOINS}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API632 WHERE v.repo_id = ?
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily633 ORDER BY CASE v.status WHEN 'open' THEN 0 WHEN 'dismissed' THEN 1 ELSE 2 END,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API634 CASE v.severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END,
635 v.package LIMIT 1000"
636 ))
637 .bind(&[repo_id.into()])?
638 .all()
639 .await?
640 .results::<VulnRow>()?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily641 let updates: HashMap<(String, String), UpdateRow> = self
642 .updates(repo_id)
643 .await?
644 .into_iter()
645 .map(|row| ((row.ecosystem.clone(), row.package.clone()), row))
646 .collect();
647 Ok(rows
648 .into_iter()
649 .map(|row| {
650 let update = updates.get(&(row.ecosystem.clone(), row.package.clone())).map(UpdateRow::to_contract);
651 Vulnerability { update, ..Vulnerability::from(row) }
652 })
653 .collect())
654 }
655
656 pub async fn vulnerability(&self, repo_id: &str, id: &str) -> Result<Option<Vulnerability>> {
657 let row = self
658 .db
659 .prepare(format!("SELECT {VULN_COLUMNS} FROM vulnerabilities v {VULN_JOINS} WHERE v.repo_id = ? AND v.id = ?"))
660 .bind(&[repo_id.into(), id.into()])?
661 .first::<VulnRow>(None)
662 .await?;
663 let Some(row) = row else { return Ok(None) };
664 let update = self.update(repo_id, &row.ecosystem, &row.package).await?.map(|row| row.to_contract());
665 Ok(Some(Vulnerability { update, ..Vulnerability::from(row) }))
666 }
667
668 pub async fn dismiss_vulnerability(
669 &self,
670 repo_id: &str,
671 id: &str,
672 reason: DismissReason,
673 by: &str,
674 comment: Option<&str>,
675 ) -> Result<()> {
676 self.db
677 .prepare(
678 "UPDATE vulnerabilities SET status = CASE status WHEN 'fixed' THEN 'fixed' ELSE 'dismissed' END,
679 dismiss_reason = ?, dismiss_comment = ?, dismissed_by = ?, dismissed_at = ?
680 WHERE repo_id = ? AND id = ?",
681 )
682 .bind(&[reason.as_str().into(), optional(comment), by.into(), now().into(), repo_id.into(), id.into()])?
683 .run()
684 .await?;
685 Ok(())
686 }
687
688 pub async fn reopen_vulnerability(&self, repo_id: &str, id: &str) -> Result<()> {
689 self.db
690 .prepare(
691 "UPDATE vulnerabilities SET status = CASE status WHEN 'dismissed' THEN 'open' ELSE status END,
692 dismiss_reason = NULL, dismiss_comment = NULL, dismissed_by = NULL, dismissed_at = NULL
693 WHERE repo_id = ? AND id = ?",
694 )
695 .bind(&[repo_id.into(), id.into()])?
696 .run()
697 .await?;
698 Ok(())
699 }
700
701 /// The ids of a package's open vulnerabilities.
702 pub async fn open_ids(&self, repo_id: &str, ecosystem: &str, package: &str) -> Result<Vec<String>> {
703 #[derive(Deserialize)]
704 struct IdRow {
705 id: String,
706 }
707 Ok(self
708 .db
709 .prepare("SELECT id FROM vulnerabilities WHERE repo_id = ? AND ecosystem = ? AND package = ? AND status = 'open'")
710 .bind(&[repo_id.into(), ecosystem.into(), package.into()])?
711 .all()
712 .await?
713 .results::<IdRow>()?
714 .into_iter()
715 .map(|row| row.id)
716 .collect())
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API717 }
718
719 pub async fn open_vulnerabilities(&self, repo_id: &str) -> Result<Vec<VulnRow>> {
720 self.db
721 .prepare(format!(
722 "SELECT {VULN_COLUMNS} FROM vulnerabilities v
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily723 {VULN_JOINS}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API724 WHERE v.repo_id = ? AND v.status = 'open'"
725 ))
726 .bind(&[repo_id.into()])?
727 .all()
728 .await?
729 .results::<VulnRow>()
730 }
731
732 /// Replaces what is known about a repository's dependencies with what a
733 /// scan found: new findings open, findings no longer true fixed, and
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily734 /// findings that came back open again, or dismissed again when someone
735 /// had dismissed them.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API736 pub async fn replace_vulnerabilities(&self, repo_id: &str, found: &[Vulnerability]) -> Result<()> {
737 let now_ms = now_ms();
738 let now = rfc3339(now_ms);
739 let mut statements = vec![self
740 .db
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily741 .prepare(
742 "UPDATE vulnerabilities SET status = 'fixed', fixed_at = ? WHERE repo_id = ? AND status IN ('open', 'dismissed')",
743 )
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API744 .bind(&[now.as_str().into(), repo_id.into()])?];
745 for vuln in found {
746 statements.push(
747 self.db
748 .prepare(
749 "INSERT INTO vulnerabilities (id, repo_id, ecosystem, package, version, manifest, osv_id, advisory,
750 summary, severity, fixed_version, status, found_at)
751 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?)
752 ON CONFLICT (repo_id, ecosystem, package, version, manifest, osv_id) DO UPDATE SET
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily753 status = CASE WHEN vulnerabilities.dismiss_reason IS NULL THEN 'open' ELSE 'dismissed' END,
754 fixed_at = NULL, advisory = excluded.advisory, summary = excluded.summary,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API755 severity = excluded.severity, fixed_version = excluded.fixed_version",
756 )
757 .bind(&[
758 new_id("vul", now_ms).into(),
759 repo_id.into(),
760 vuln.ecosystem.as_str().into(),
761 vuln.package.as_str().into(),
762 vuln.version.as_str().into(),
763 vuln.manifest.as_str().into(),
764 vuln.osv_id.as_str().into(),
765 vuln.advisory.as_str().into(),
766 vuln.summary.as_str().into(),
767 vuln.severity.as_str().into(),
768 optional(vuln.fixed_version.as_deref()),
769 now.as_str().into(),
770 ])?,
771 );
772 }
773 // D1 runs a batch as one transaction, so readers never see the
774 // moment between marking everything fixed and opening it again.
775 self.db.batch(statements).await?;
776 Ok(())
777 }
778
779 pub async fn counts(&self, repo_id: &str) -> Result<(SeverityCounts, u32, u32)> {
780 let rows = self
781 .db
782 .prepare("SELECT severity, count(*) AS n FROM vulnerabilities WHERE repo_id = ? AND status = 'open' GROUP BY severity")
783 .bind(&[repo_id.into()])?
784 .all()
785 .await?
786 .results::<CountRow>()?;
787 let mut counts = SeverityCounts::default();
788 let mut vulns = 0;
789 for row in rows {
790 let n = row.n.max(0) as u32;
791 vulns += n;
792 match row.severity.as_str() {
793 "critical" => counts.critical += n,
794 "high" => counts.high += n,
795 "medium" => counts.medium += n,
796 "low" => counts.low += n,
797 _ => counts.unknown += n,
798 }
799 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily800 let secrets = self.secret_counts(repo_id).await?;
801 // A secret in the history that looks real is the worst thing a
802 // repository can hold. One stopped at a push never landed, and a
803 // likely test value is no danger: neither is counted.
804 counts.critical += secrets.open;
805 Ok((counts, secrets.open + secrets.blocked, vulns))
806 }
807
808 pub async fn secret_counts(&self, repo_id: &str) -> Result<SecretCounts> {
809 let rows = self
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API810 .db
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily811 .prepare(
812 "SELECT status, (test_value IS NOT NULL) AS test, count(*) AS n FROM secrets WHERE repo_id = ?
813 GROUP BY status, test",
814 )
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API815 .bind(&[repo_id.into()])?
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily816 .all()
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API817 .await?
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily818 .results::<SecretCountRow>()?;
819 Ok(secret_counts(rows.iter().map(|row| (row.status.as_str(), row.test != 0, row.n.max(0) as u32))))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API820 }
821
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily822 // --- Activity -----------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API823
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily824 pub async fn record(&self, repo_id: &str, activity: &[Activity<'_>]) -> Result<()> {
825 if activity.is_empty() {
826 return Ok(());
827 }
828 let now_ms = now_ms();
829 let at = rfc3339(now_ms);
830 let statements = activity
831 .iter()
832 .map(|item| {
833 self.db
834 .prepare(
835 "INSERT INTO alert_activity (id, repo_id, alert_id, action, actor, reason, comment, number, at)
836 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
837 )
838 .bind(&[
839 new_id("act", now_ms).into(),
840 repo_id.into(),
841 item.alert_id.into(),
842 item.action.into(),
843 optional(item.actor),
844 optional(item.reason.map(DismissReason::as_str)),
845 optional(item.comment),
846 item.number.map_or(JsValue::NULL, JsValue::from),
847 at.as_str().into(),
848 ])
849 })
850 .collect::<Result<Vec<_>>>()?;
851 self.db.batch(statements).await?;
852 Ok(())
853 }
854
855 /// What happened to a repository's alerts, newest first.
856 pub async fn activity(&self, repo_id: &str, limit: u32) -> Result<Vec<AlertActivity>> {
857 Ok(self
858 .db
859 .prepare("SELECT * FROM alert_activity WHERE repo_id = ? ORDER BY at DESC, id DESC LIMIT ?")
860 .bind(&[repo_id.into(), limit.into()])?
861 .all()
862 .await?
863 .results::<ActivityRow>()?
864 .into_iter()
865 .map(AlertActivity::from)
866 .collect())
867 }
868
869 // --- Security updates ---------------------------------------------------
870
871 pub async fn updates(&self, repo_id: &str) -> Result<Vec<UpdateRow>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API872 self.db
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily873 .prepare("SELECT * FROM updates WHERE repo_id = ?")
874 .bind(&[repo_id.into()])?
875 .all()
876 .await?
877 .results::<UpdateRow>()
878 }
879
880 pub async fn update(&self, repo_id: &str, ecosystem: &str, package: &str) -> Result<Option<UpdateRow>> {
881 self.db
882 .prepare("SELECT * FROM updates WHERE repo_id = ? AND ecosystem = ? AND package = ?")
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API883 .bind(&[repo_id.into(), ecosystem.into(), package.into()])?
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily884 .first::<UpdateRow>(None)
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API885 .await
886 }
887
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily888 pub async fn update_by_branch(&self, repo_id: &str, branch: &str) -> Result<Option<UpdateRow>> {
889 self.db
890 .prepare("SELECT * FROM updates WHERE repo_id = ? AND branch = ?")
891 .bind(&[repo_id.into(), branch.into()])?
892 .first::<UpdateRow>(None)
893 .await
894 }
895
896 pub async fn update_by_pull(&self, repo_id: &str, pull: u32) -> Result<Option<UpdateRow>> {
897 self.db
898 .prepare("SELECT * FROM updates WHERE repo_id = ? AND pull = ?")
899 .bind(&[repo_id.into(), pull.into()])?
900 .first::<UpdateRow>(None)
901 .await
902 }
903
904 /// Updates asked of a sandbox before `before` that never pushed.
905 pub async fn stalled_updates(&self, before: &str, limit: u32) -> Result<Vec<UpdateRow>> {
906 self.db
907 .prepare("SELECT * FROM updates WHERE state = 'requested' AND updated_at < ? ORDER BY updated_at LIMIT ?")
908 .bind(&[before.into(), limit.into()])?
909 .all()
910 .await?
911 .results::<UpdateRow>()
912 }
913
914 /// Asks for a security update: a sandbox is making the change on
915 /// `branch`. An open pull request for an older version is kept until
916 /// the new one opens, which supersedes it.
917 pub async fn request_update(&self, repo_id: &str, ecosystem: &str, package: &str, target: &str, branch: &str) -> Result<()> {
918 let now = now();
919 self.db
920 .prepare(
921 "INSERT INTO updates (repo_id, ecosystem, package, target, state, branch, requested_at, updated_at)
922 VALUES (?1, ?2, ?3, ?4, 'requested', ?5, ?6, ?6)
923 ON CONFLICT (repo_id, ecosystem, package) DO UPDATE SET
924 target = ?4, state = 'requested', branch = ?5, error = NULL, issue = NULL,
925 pull = CASE WHEN updates.state = 'open' THEN updates.pull ELSE NULL END,
926 requested_at = ?6, updated_at = ?6",
927 )
928 .bind(&[repo_id.into(), ecosystem.into(), package.into(), target.into(), branch.into(), now.into()])?
929 .run()
930 .await?;
931 Ok(())
932 }
933
934 pub async fn set_update(
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API935 &self,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily936 row: &UpdateRow,
937 state: UpdateState,
938 pull: Option<u32>,
939 issue: Option<u32>,
940 error: Option<&str>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API941 ) -> Result<()> {
942 self.db
943 .prepare(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily944 "UPDATE updates SET state = ?, pull = ?, issue = ?, error = ?, updated_at = ?
945 WHERE repo_id = ? AND ecosystem = ? AND package = ?",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API946 )
947 .bind(&[
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily948 state.as_str().into(),
949 pull.map_or(JsValue::NULL, JsValue::from),
950 issue.map_or(JsValue::NULL, JsValue::from),
951 optional(error),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API952 now().into(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily953 row.repo_id.as_str().into(),
954 row.ecosystem.as_str().into(),
955 row.package.as_str().into(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API956 ])?
957 .run()
958 .await?;
959 Ok(())
960 }
961
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily962 // --- Pushes scanned after they landed -----------------------------------
963
964 pub async fn add_push_scan(&self, repo_id: &str, git_ref: &str, head: &str, base: Option<&str>, pusher: Option<&str>) -> Result<String> {
965 let id = new_id("psc", now_ms());
966 self.db
967 .prepare(
968 "INSERT INTO push_scans (id, repo_id, git_ref, head, base, pusher, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
969 )
970 .bind(&[id.as_str().into(), repo_id.into(), git_ref.into(), head.into(), optional(base), optional(pusher), now().into()])?
971 .run()
972 .await?;
973 Ok(id)
974 }
975
976 pub async fn push_scan(&self, id: &str) -> Result<Option<PushScanRow>> {
977 self.db
978 .prepare("SELECT * FROM push_scans WHERE id = ?")
979 .bind(&[id.into()])?
980 .first::<PushScanRow>(None)
981 .await
982 }
983
984 pub async fn pending_push_scans(&self, limit: u32) -> Result<Vec<PushScanRow>> {
985 self.db
986 .prepare("SELECT * FROM push_scans WHERE state = 'pending' ORDER BY created_at LIMIT ?")
987 .bind(&[limit.into()])?
988 .all()
989 .await?
990 .results::<PushScanRow>()
991 }
992
993 /// Where a push's scan stands after a page: its next page, or done.
994 pub async fn advance_push_scan(&self, id: &str, next: Option<&str>, commits: u32, found: u32) -> Result<()> {
995 let done = next.is_none();
996 self.db
997 .prepare(
998 "UPDATE push_scans SET cursor = ?, state = ?, commits = commits + ?, pages = pages + 1, found = found + ?,
999 finished_at = CASE WHEN ? THEN ? ELSE finished_at END
1000 WHERE id = ?",
1001 )
1002 .bind(&[
1003 optional(next),
1004 if done { "done" } else { "pending" }.into(),
1005 commits.into(),
1006 found.into(),
1007 done.into(),
1008 now().into(),
1009 id.into(),
1010 ])?
1011 .run()
1012 .await?;
1013 Ok(())
1014 }
1015
1016 // --- Upgrades -----------------------------------------------------------
1017
1018 /// The issue opened for a package before security updates, if any.
1019 pub async fn upgrade(&self, repo_id: &str, ecosystem: &str, package: &str) -> Result<Option<UpgradeRow>> {
1020 self.db
1021 .prepare("SELECT number FROM upgrades WHERE repo_id = ? AND ecosystem = ? AND package = ?")
1022 .bind(&[repo_id.into(), ecosystem.into(), package.into()])?
1023 .first::<UpgradeRow>(None)
1024 .await
1025 }
1026
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1027 // --- Advisories and usage -----------------------------------------------
1028
1029 /// OSV's record of a vulnerability, if one was fetched after `after`.
1030 pub async fn advisory(&self, osv_id: &str, after: &str) -> Result<Option<serde_json::Value>> {
1031 let row = self
1032 .db
1033 .prepare("SELECT body, fetched_at FROM advisories WHERE osv_id = ?")
1034 .bind(&[osv_id.into()])?
1035 .first::<BodyRow>(None)
1036 .await?;
1037 Ok(row
1038 .filter(|row| row.fetched_at.as_str() >= after)
1039 .and_then(|row| serde_json::from_str(&row.body).ok()))
1040 }
1041
1042 pub async fn keep_advisory(&self, osv_id: &str, body: &serde_json::Value) -> Result<()> {
1043 self.db
1044 .prepare(
1045 "INSERT INTO advisories (osv_id, body, fetched_at) VALUES (?1, ?2, ?3)
1046 ON CONFLICT (osv_id) DO UPDATE SET body = ?2, fetched_at = ?3",
1047 )
1048 .bind(&[osv_id.into(), body.to_string().into(), now().into()])?
1049 .run()
1050 .await?;
1051 Ok(())
1052 }
1053
1054 /// Adds to a workspace's scanning this month; returns what the month
1055 /// has cost so far, in millionths of a dollar.
1056 pub async fn meter(&self, workspace: &str, reads: u32, commits: u32, osv_queries: u32, cost_micros: i64) -> Result<i64> {
1057 let month = now()[..7].to_owned();
1058 self.db
1059 .prepare(
1060 "INSERT INTO usage (workspace, month, reads, commits, osv_queries, cost_micros) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1061 ON CONFLICT (workspace, month) DO UPDATE SET
1062 reads = reads + ?3, commits = commits + ?4, osv_queries = osv_queries + ?5,
1063 cost_micros = cost_micros + ?6",
1064 )
1065 .bind(&[
1066 workspace.into(),
1067 month.as_str().into(),
1068 reads.into(),
1069 commits.into(),
1070 osv_queries.into(),
1071 JsValue::from(cost_micros as f64),
1072 ])?
1073 .run()
1074 .await?;
1075 Ok(self
1076 .db
1077 .prepare("SELECT cost_micros AS n FROM usage WHERE workspace = ? AND month = ?")
1078 .bind(&[workspace.into(), month.into()])?
1079 .first::<NumberRow>(None)
1080 .await?
1081 .map_or(0, |row| row.n))
1082 }
1083}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1084
1085/// Secret alerts counted by where they stand, from (status, looks like a
1086/// test value, how many).
1087pub fn secret_counts<'a>(rows: impl Iterator<Item = (&'a str, bool, u32)>) -> SecretCounts {
1088 let mut counts = SecretCounts::default();
1089 for (status, test, n) in rows {
1090 match (SecretStatus::parse(status), test) {
1091 (Some(SecretStatus::Open | SecretStatus::Blocked), true) => counts.test_values += n,
1092 (Some(SecretStatus::Open), false) => counts.open += n,
1093 (Some(SecretStatus::Blocked), false) => counts.blocked += n,
1094 (Some(SecretStatus::Allowed), _) => counts.dismissed += n,
1095 (Some(SecretStatus::Resolved), _) => counts.fixed += n,
1096 (None, _) => {}
1097 }
1098 }
1099 counts
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104 use super::*;
1105
1106 #[test]
1107 fn only_real_secrets_in_history_are_open() {
1108 let counts = secret_counts(
1109 [("open", false, 2), ("open", true, 3), ("blocked", false, 1), ("blocked", true, 4), ("allowed", true, 5), ("resolved", false, 6)]
1110 .into_iter(),
1111 );
1112 assert_eq!((counts.open, counts.blocked, counts.test_values, counts.dismissed, counts.fixed), (2, 1, 7, 5, 6));
1113 }
1114}