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}