OXYGENHUB
# 0ee5c3fb32eb oxygenhub / crates / hub / src / db / push_protection.rs
4.5 KiB · Rust · last changed Push protection: refuse a push that adds what looks like a secret 16 days ago source blame history raw
1
//! Push protection's records (migration 0085, oxygenhub#268): fingerprints
2
//! a maintainer allowed, and the findings of recent refusals.
3
4
use super::{now_ms, Pool};
5
use sqlx::FromRow;
6
7
/// Findings kept per repository: the newest, for maintainers to judge.
8
const KEPT_FINDINGS: i64 = 200;
9
10
#[derive(Clone, Debug, FromRow)]
11
pub struct Bypass {
12
    pub id: i64,
13
    pub fingerprint: String,
14
    pub rule: String,
15
    pub reason: String,
16
    pub by_name: String,
17
    pub created_ms: i64,
18
}
19
20
#[derive(Clone, Debug, FromRow)]
21
pub struct FindingRow {
22
    pub id: i64,
23
    pub at_ms: i64,
24
    pub pusher: String,
25
    pub via: String,
26
    pub rule: String,
27
    pub description: String,
28
    pub path: String,
29
    pub line: i64,
30
    pub fingerprint: String,
31
    pub excerpt: String,
32
}
33
34
/// A finding to record.
35
pub struct NewFinding<'a> {
36
    pub rule: &'a str,
37
    pub description: &'a str,
38
    pub path: &'a str,
39
    pub line: i64,
40
    pub fingerprint: &'a str,
41
    pub excerpt: &'a str,
42
}
43
44
/// The fingerprints allowed in a repository.
45
pub async fn allowed(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
46
    sqlx::query_scalar("SELECT fingerprint FROM secret_bypasses WHERE repo_id = $1").bind(repo_id).fetch_all(pool).await
47
}
48
49
pub async fn bypasses(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<Bypass>> {
50
    sqlx::query_as("SELECT id, fingerprint, rule, reason, by_name, created_ms FROM secret_bypasses WHERE repo_id = $1 ORDER BY id DESC")
51
        .bind(repo_id)
52
        .fetch_all(pool)
53
        .await
54
}
55
56
/// Allow `fingerprint` with `reason`; allowing it again replaces the reason.
57
pub async fn allow(pool: &Pool, repo_id: i64, fingerprint: &str, rule: &str, reason: &str, by: Option<(i64, &str)>) -> sqlx::Result<()> {
58
    sqlx::query(
59
        "INSERT INTO secret_bypasses (repo_id, fingerprint, rule, reason, by_account, by_name, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7) \
60
         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",
61
    )
62
    .bind(repo_id)
63
    .bind(fingerprint)
64
    .bind(rule)
65
    .bind(reason)
66
    .bind(by.map(|b| b.0))
67
    .bind(by.map(|b| b.1).unwrap_or(""))
68
    .bind(now_ms())
69
    .execute(pool)
70
    .await?;
71
    Ok(())
72
}
73
74
/// Record a refused push's findings, keeping the newest few hundred.
75
pub async fn record(pool: &Pool, repo_id: i64, pusher: &str, via: &str, findings: &[NewFinding<'_>]) -> sqlx::Result<()> {
76
    let now = now_ms();
77
    let mut tx = super::begin_write(pool).await?;
78
    for f in findings {
79
        sqlx::query(
80
            "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)",
81
        )
82
        .bind(repo_id)
83
        .bind(now)
84
        .bind(pusher)
85
        .bind(via)
86
        .bind(f.rule)
87
        .bind(f.description)
88
        .bind(f.path)
89
        .bind(f.line)
90
        .bind(f.fingerprint)
91
        .bind(f.excerpt)
92
        .execute(&mut *tx)
93
        .await?;
94
    }
95
    sqlx::query(
96
        "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)",
97
    )
98
    .bind(repo_id)
99
    .bind(repo_id)
100
    .bind(KEPT_FINDINGS)
101
    .execute(&mut *tx)
102
    .await?;
103
    tx.commit().await
104
}
105
106
/// The newest findings of refused pushes, those not since allowed.
107
pub async fn recent(pool: &Pool, repo_id: i64, limit: i64) -> sqlx::Result<Vec<FindingRow>> {
108
    sqlx::query_as(
109
        "SELECT id, at_ms, pusher, via, rule, description, path, line, fingerprint, excerpt FROM secret_findings f \
110
         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) \
111
         ORDER BY id DESC LIMIT $2",
112
    )
113
    .bind(repo_id)
114
    .bind(limit)
115
    .fetch_all(pool)
116
    .await
117
}
118
119
/// The rule a fingerprint was found under in this repository, if it was.
120
pub async fn rule_of(pool: &Pool, repo_id: i64, fingerprint: &str) -> sqlx::Result<Option<String>> {
121
    sqlx::query_scalar("SELECT rule FROM secret_findings WHERE repo_id = $1 AND fingerprint = $2 ORDER BY id DESC LIMIT 1")
122
        .bind(repo_id)
123
        .bind(fingerprint)
124
        .fetch_optional(pool)
125
        .await
126
}
127
128
/// Whether pushes adding possible secrets are refused here.
129
pub async fn set_enabled(pool: &Pool, repo_id: i64, on: bool) -> sqlx::Result<()> {
130
    sqlx::query("UPDATE repositories SET push_protection = $1 WHERE id = $2").bind(on).bind(repo_id).execute(pool).await?;
131
    Ok(())
132
}