Skip to main content

openhawk_core/
pattern_detector.rs

1use rusqlite::{Connection, params};
2use thiserror::Error;
3use uuid::Uuid;
4
5#[derive(Debug, Error)]
6pub enum PatternError {
7    #[error("database error: {0}")]
8    Database(String),
9    #[error("pattern not found: {0}")]
10    NotFound(String),
11    #[error("serialization error: {0}")]
12    Serialization(String),
13}
14
15impl From<rusqlite::Error> for PatternError {
16    fn from(e: rusqlite::Error) -> Self {
17        PatternError::Database(e.to_string())
18    }
19}
20
21impl From<serde_json::Error> for PatternError {
22    fn from(e: serde_json::Error) -> Self {
23        PatternError::Serialization(e.to_string())
24    }
25}
26
27pub struct DetectedPattern {
28    pub id: String,
29    pub action_sequence: Vec<String>,
30    pub occurrence_count: u32,
31}
32
33pub struct PatternRecord {
34    pub id: String,
35    pub action_sequence: Vec<String>,
36    pub occurrence_count: u32,
37    pub last_occurrence: String,
38    pub status: String,
39}
40
41pub struct PatternDetector {
42    db: Connection,
43    retention_days: u32,
44    pub action_log: Vec<String>,
45}
46
47impl PatternDetector {
48    pub fn new(db: Connection, retention_days: u32) -> Self {
49        Self { db, retention_days, action_log: Vec::new() }
50    }
51
52    pub fn record_action(&mut self, action: &str) {
53        self.action_log.push(action.to_string());
54    }
55
56    pub fn detect_patterns(&mut self) -> Vec<DetectedPattern> {
57        let log = &self.action_log;
58        let n = log.len();
59        if n < 3 { return Vec::new(); }
60
61        let mut results: Vec<DetectedPattern> = Vec::new();
62
63        for window_size in 3..=n {
64            let mut counts: std::collections::HashMap<Vec<String>, u32> = std::collections::HashMap::new();
65            for start in 0..=(n - window_size) {
66                let seq: Vec<String> = log[start..start + window_size].to_vec();
67                *counts.entry(seq).or_insert(0) += 1;
68            }
69
70            for (seq, count) in counts {
71                if count >= 5 {
72                    let already = results.iter().any(|p| {
73                        p.occurrence_count >= count && is_subsequence(&seq, &p.action_sequence)
74                    });
75                    if already { continue; }
76
77                    let id = self.upsert_pattern(&seq, count).unwrap_or_else(|_| Uuid::new_v4().to_string());
78                    results.push(DetectedPattern { id, action_sequence: seq, occurrence_count: count });
79                }
80            }
81        }
82
83        results
84    }
85
86    fn upsert_pattern(&self, seq: &[String], count: u32) -> Result<String, PatternError> {
87        let seq_json = serde_json::to_string(seq)?;
88        let now = chrono::Utc::now().to_rfc3339();
89        let expires = (chrono::Utc::now() + chrono::Duration::days(self.retention_days as i64)).to_rfc3339();
90
91        let existing: Option<String> = self.db.query_row(
92            "SELECT id FROM patterns WHERE action_sequence = ?1",
93            params![seq_json],
94            |row| row.get(0),
95        ).ok();
96
97        if let Some(id) = existing {
98            self.db.execute(
99                "UPDATE patterns SET occurrence_count = ?1, last_occurrence = ?2 WHERE id = ?3",
100                params![count, now, id],
101            )?;
102            Ok(id)
103        } else {
104            let id = Uuid::new_v4().to_string();
105            self.db.execute(
106                "INSERT INTO patterns (id, action_sequence, occurrence_count, last_occurrence, status, created_at, expires_at) \
107                 VALUES (?1, ?2, ?3, ?4, 'Detected', ?5, ?6)",
108                params![id, seq_json, count, now, now, expires],
109            )?;
110            Ok(id)
111        }
112    }
113
114    pub fn accept_pattern(&self, pattern_id: &str) -> Result<String, PatternError> {
115        let record = self.get_pattern(pattern_id)?;
116        self.db.execute("UPDATE patterns SET status = 'Accepted' WHERE id = ?1", params![pattern_id])?;
117        Ok(generate_manifest(&record))
118    }
119
120    pub fn decline_pattern(&self, pattern_id: &str) -> Result<(), PatternError> {
121        let rows = self.db.execute("UPDATE patterns SET status = 'Declined' WHERE id = ?1", params![pattern_id])?;
122        if rows == 0 {
123            return Err(PatternError::NotFound(pattern_id.to_string()));
124        }
125        Ok(())
126    }
127
128    pub fn reset_declined(&self) -> Result<u64, PatternError> {
129        let rows = self.db.execute("UPDATE patterns SET status = 'Detected' WHERE status = 'Declined'", [])?;
130        Ok(rows as u64)
131    }
132
133    pub fn list_patterns(&self) -> Result<Vec<PatternRecord>, PatternError> {
134        let mut stmt = self.db.prepare(
135            "SELECT id, action_sequence, occurrence_count, last_occurrence, status \
136             FROM patterns ORDER BY last_occurrence DESC",
137        )?;
138        let rows = stmt.query_map([], |row| {
139            Ok((
140                row.get::<_, String>(0)?,
141                row.get::<_, String>(1)?,
142                row.get::<_, u32>(2)?,
143                row.get::<_, String>(3)?,
144                row.get::<_, String>(4)?,
145            ))
146        })?;
147
148        let mut records = Vec::new();
149        for row in rows {
150            let (id, seq_json, count, last, status) = row?;
151            let action_sequence: Vec<String> = serde_json::from_str(&seq_json)
152                .map_err(|e| PatternError::Serialization(e.to_string()))?;
153            records.push(PatternRecord { id, action_sequence, occurrence_count: count, last_occurrence: last, status });
154        }
155        Ok(records)
156    }
157
158    pub fn cleanup_expired(&self) -> Result<u64, PatternError> {
159        let now = chrono::Utc::now().to_rfc3339();
160        let rows = self.db.execute("DELETE FROM patterns WHERE expires_at < ?1", params![now])?;
161        Ok(rows as u64)
162    }
163
164    fn get_pattern(&self, pattern_id: &str) -> Result<PatternRecord, PatternError> {
165        let (seq_json, count, last, status): (String, u32, String, String) = self.db.query_row(
166            "SELECT action_sequence, occurrence_count, last_occurrence, status FROM patterns WHERE id = ?1",
167            params![pattern_id],
168            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
169        ).map_err(|_| PatternError::NotFound(pattern_id.to_string()))?;
170
171        let action_sequence: Vec<String> = serde_json::from_str(&seq_json)
172            .map_err(|e| PatternError::Serialization(e.to_string()))?;
173
174        Ok(PatternRecord { id: pattern_id.to_string(), action_sequence, occurrence_count: count, last_occurrence: last, status })
175    }
176}
177
178fn is_subsequence(needle: &[String], haystack: &[String]) -> bool {
179    haystack.windows(needle.len()).any(|w| w == needle)
180}
181
182fn generate_manifest(record: &PatternRecord) -> String {
183    let name = format!("pattern-{}", &record.id[..8]);
184    let steps: Vec<String> = record.action_sequence.iter().enumerate()
185        .map(|(i, a)| format!("  # step {}: {}", i + 1, a))
186        .collect();
187    let steps_str = steps.join("\n");
188
189    format!(
190        "[agent]\nname = \"{name}\"\nversion = \"1.0.0\"\ndescription = \"Auto-generated from detected pattern (occurrences: {})\"\nframework = \"hawk-pattern\"\nentry_command = \"hawk pattern-run {}\"\n\n[permissions]\nfilesystem = []\nnetwork = []\ncommands = []\nsecrets = []\n\n[resources]\ncpu_percent = 10\nmemory_mb = 128\nmax_open_fds = 16\n\n[pattern]\nsequence = {sequence}\noccurrence_count = {count}\n\n# Recorded action sequence:\n{steps_str}\n",
191        record.occurrence_count,
192        record.id,
193        sequence = serde_json::to_string(&record.action_sequence).unwrap_or_default(),
194        count = record.occurrence_count,
195        steps_str = steps_str,
196    )
197}