//! Push protection's records (migration 0085, oxygenhub#268): fingerprints //! a maintainer allowed, and the findings of recent refusals. use super::{now_ms, Pool}; use sqlx::FromRow; /// Findings kept per repository: the newest, for maintainers to judge. const KEPT_FINDINGS: i64 = 200; #[derive(Clone, Debug, FromRow)] pub struct Bypass { pub id: i64, pub fingerprint: String, pub rule: String, pub reason: String, pub by_name: String, pub created_ms: i64, } #[derive(Clone, Debug, FromRow)] pub struct FindingRow { pub id: i64, pub at_ms: i64, pub pusher: String, pub via: String, pub rule: String, pub description: String, pub path: String, pub line: i64, pub fingerprint: String, pub excerpt: String, } /// A finding to record. pub struct NewFinding<'a> { pub rule: &'a str, pub description: &'a str, pub path: &'a str, pub line: i64, pub fingerprint: &'a str, pub excerpt: &'a str, } /// The fingerprints allowed in a repository. pub async fn allowed(pool: &Pool, repo_id: i64) -> sqlx::Result> { sqlx::query_scalar("SELECT fingerprint FROM secret_bypasses WHERE repo_id = $1").bind(repo_id).fetch_all(pool).await } pub async fn bypasses(pool: &Pool, repo_id: i64) -> sqlx::Result> { sqlx::query_as("SELECT id, fingerprint, rule, reason, by_name, created_ms FROM secret_bypasses WHERE repo_id = $1 ORDER BY id DESC") .bind(repo_id) .fetch_all(pool) .await } /// Allow `fingerprint` with `reason`; allowing it again replaces the reason. pub async fn allow(pool: &Pool, repo_id: i64, fingerprint: &str, rule: &str, reason: &str, by: Option<(i64, &str)>) -> sqlx::Result<()> { sqlx::query( "INSERT INTO secret_bypasses (repo_id, fingerprint, rule, reason, by_account, by_name, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7) \ ON CONFLICT (repo_id, fingerprint) DO UPDATE SET reason = excluded.reason, rule = excluded.rule, by_account = excluded.by_account, by_name = excluded.by_name, created_ms = excluded.created_ms", ) .bind(repo_id) .bind(fingerprint) .bind(rule) .bind(reason) .bind(by.map(|b| b.0)) .bind(by.map(|b| b.1).unwrap_or("")) .bind(now_ms()) .execute(pool) .await?; Ok(()) } /// Record a refused push's findings, keeping the newest few hundred. pub async fn record(pool: &Pool, repo_id: i64, pusher: &str, via: &str, findings: &[NewFinding<'_>]) -> sqlx::Result<()> { let now = now_ms(); let mut tx = super::begin_write(pool).await?; for f in findings { sqlx::query( "INSERT INTO secret_findings (repo_id, at_ms, pusher, via, rule, description, path, line, fingerprint, excerpt) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", ) .bind(repo_id) .bind(now) .bind(pusher) .bind(via) .bind(f.rule) .bind(f.description) .bind(f.path) .bind(f.line) .bind(f.fingerprint) .bind(f.excerpt) .execute(&mut *tx) .await?; } sqlx::query( "DELETE FROM secret_findings WHERE repo_id = $1 AND id NOT IN (SELECT id FROM secret_findings WHERE repo_id = $2 ORDER BY id DESC LIMIT $3)", ) .bind(repo_id) .bind(repo_id) .bind(KEPT_FINDINGS) .execute(&mut *tx) .await?; tx.commit().await } /// The newest findings of refused pushes, those not since allowed. pub async fn recent(pool: &Pool, repo_id: i64, limit: i64) -> sqlx::Result> { sqlx::query_as( "SELECT id, at_ms, pusher, via, rule, description, path, line, fingerprint, excerpt FROM secret_findings f \ WHERE repo_id = $1 AND NOT EXISTS (SELECT 1 FROM secret_bypasses b WHERE b.repo_id = f.repo_id AND b.fingerprint = f.fingerprint) \ ORDER BY id DESC LIMIT $2", ) .bind(repo_id) .bind(limit) .fetch_all(pool) .await } /// The rule a fingerprint was found under in this repository, if it was. pub async fn rule_of(pool: &Pool, repo_id: i64, fingerprint: &str) -> sqlx::Result> { sqlx::query_scalar("SELECT rule FROM secret_findings WHERE repo_id = $1 AND fingerprint = $2 ORDER BY id DESC LIMIT 1") .bind(repo_id) .bind(fingerprint) .fetch_optional(pool) .await } /// Whether pushes adding possible secrets are refused here. pub async fn set_enabled(pool: &Pool, repo_id: i64, on: bool) -> sqlx::Result<()> { sqlx::query("UPDATE repositories SET push_protection = $1 WHERE id = $2").bind(on).bind(repo_id).execute(pool).await?; Ok(()) }