1use std::path::Path;
4
5use anyhow::Context;
6use rusqlite::{params, Connection};
7use serde::{Deserialize, Serialize};
8use std::sync::Mutex;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct IntelRecord {
13 pub ip: String,
14 pub host: Option<String>,
15 pub port: u16,
16 pub protocol: String,
17 pub banner: Option<String>,
18 #[serde(default)]
19 pub tech_stack: Vec<String>,
20 pub last_seen: Option<String>,
21}
22
23pub struct IntelDb {
25 conn: Mutex<Connection>,
27}
28
29impl IntelDb {
30 #[doc(hidden)]
37 pub fn _test_conn(&self) -> &Mutex<Connection> {
38 &self.conn
39 }
40}
41
42impl IntelDb {
43 pub fn open(path: impl AsRef<Path>) -> anyhow::Result<Self> {
45 let conn = Connection::open(path).context("opening intel database")?;
46
47 conn.execute_batch(
49 "PRAGMA journal_mode = WAL;
50 PRAGMA synchronous = NORMAL;
51 PRAGMA cache_size = -64000; -- 64MB cache
52 PRAGMA temp_store = MEMORY;",
53 )?;
54
55 conn.execute_batch(
61 "CREATE TABLE IF NOT EXISTS intel (
62 id INTEGER PRIMARY KEY AUTOINCREMENT,
63 ip TEXT NOT NULL,
64 host TEXT,
65 port INTEGER NOT NULL,
66 protocol TEXT NOT NULL,
67 banner TEXT,
68 tech_stack TEXT, -- JSON array
69 last_seen TEXT
70 );
71 CREATE UNIQUE INDEX IF NOT EXISTS idx_intel_unique
72 ON intel(ip, COALESCE(host, ''), port, protocol);
73 CREATE INDEX IF NOT EXISTS idx_intel_ip ON intel(ip);
74 CREATE INDEX IF NOT EXISTS idx_intel_host ON intel(host);",
75 )
76 .context("initialising intel schema")?;
77
78 Ok(Self {
79 conn: Mutex::new(conn),
80 })
81 }
82
83 pub fn insert_batch(&self, records: &[IntelRecord]) -> anyhow::Result<()> {
85 let mut conn = self
86 .conn
87 .lock()
88 .map_err(|e| anyhow::anyhow!("mutex poisoned: {e}"))?;
89 let tx = conn.transaction()?;
90 {
91 let mut stmt = tx.prepare(
92 "INSERT OR REPLACE INTO intel (ip, host, port, protocol, banner, tech_stack, last_seen)
93 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
94 )?;
95
96 for r in records {
97 let tech_json = serde_json::to_string(&r.tech_stack)?;
98 stmt.execute(params![
99 r.ip,
100 r.host,
101 r.port,
102 r.protocol,
103 r.banner,
104 tech_json,
105 r.last_seen
106 ])?;
107 }
108 }
109 tx.commit()?;
110 Ok(())
111 }
112
113 pub fn query_by_ip(&self, ip: &str) -> anyhow::Result<Vec<IntelRecord>> {
115 let conn = self
116 .conn
117 .lock()
118 .map_err(|e| anyhow::anyhow!("mutex poisoned: {e}"))?;
119 let mut stmt = conn.prepare(
120 "SELECT ip, host, port, protocol, banner, tech_stack, last_seen
121 FROM intel WHERE ip = ?1",
122 )?;
123
124 let rows = stmt.query_map(params![ip], |row| {
125 let tech_json: String = row.get(5)?;
126 let tech_stack: Vec<String> = serde_json::from_str(&tech_json).unwrap_or_default();
127 let port_i32: i32 = row.get(2)?;
128 let port = u16::try_from(port_i32)
129 .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(2, port_i32.into()))?;
130 Ok(IntelRecord {
131 ip: row.get(0)?,
132 host: row.get(1)?,
133 port,
134 protocol: row.get(3)?,
135 banner: row.get(4)?,
136 tech_stack,
137 last_seen: row.get(6)?,
138 })
139 })?;
140
141 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
142 }
143
144 pub fn query_by_host(&self, host: &str) -> anyhow::Result<Vec<IntelRecord>> {
146 let conn = self
147 .conn
148 .lock()
149 .map_err(|e| anyhow::anyhow!("mutex poisoned: {e}"))?;
150 let mut stmt = conn.prepare(
151 "SELECT ip, host, port, protocol, banner, tech_stack, last_seen
152 FROM intel WHERE host = ?1",
153 )?;
154
155 let rows = stmt.query_map(params![host], |row| {
156 let tech_json: String = row.get(5)?;
157 let tech_stack: Vec<String> = serde_json::from_str(&tech_json).unwrap_or_default();
158 let port_i32: i32 = row.get(2)?;
159 let port = u16::try_from(port_i32)
160 .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(2, port_i32.into()))?;
161 Ok(IntelRecord {
162 ip: row.get(0)?,
163 host: row.get(1)?,
164 port,
165 protocol: row.get(3)?,
166 banner: row.get(4)?,
167 tech_stack,
168 last_seen: row.get(6)?,
169 })
170 })?;
171
172 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
173 }
174}