1use crate::{LogEntry, ThreatAlert, ThreatCategory, ThreatSeverity};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
13pub enum IOCType {
14 IPAddress,
15 Domain,
16 FileHash,
17 URL,
18 Email,
19 UserAgent,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct IOC {
25 pub ioc_type: IOCType,
26 pub value: String,
27 pub severity: ThreatSeverity,
28 pub description: String,
29 pub source: String, pub first_seen: DateTime<Utc>,
31 pub last_seen: DateTime<Utc>,
32 pub confidence: f64, }
34
35impl IOC {
36 pub fn new(
37 ioc_type: IOCType,
38 value: String,
39 severity: ThreatSeverity,
40 description: String,
41 source: String,
42 ) -> Self {
43 Self {
44 ioc_type,
45 value,
46 severity,
47 description,
48 source,
49 first_seen: Utc::now(),
50 last_seen: Utc::now(),
51 confidence: 0.8, }
53 }
54}
55
56pub struct ThreatIntelligence {
58 iocs: HashMap<IOCType, HashMap<String, IOC>>,
59 malicious_ips: HashSet<String>,
60 malicious_domains: HashSet<String>,
61 malicious_hashes: HashSet<String>,
62 threat_actors: HashMap<String, ThreatActor>,
63 matches_count: usize,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ThreatActor {
69 pub name: String,
70 pub aliases: Vec<String>,
71 pub first_seen: DateTime<Utc>,
72 pub techniques: Vec<String>, pub targeted_sectors: Vec<String>,
74 pub associated_iocs: Vec<String>,
75}
76
77impl ThreatIntelligence {
78 pub fn new() -> Self {
80 let mut intel = Self {
81 iocs: HashMap::new(),
82 malicious_ips: HashSet::new(),
83 malicious_domains: HashSet::new(),
84 malicious_hashes: HashSet::new(),
85 threat_actors: HashMap::new(),
86 matches_count: 0,
87 };
88
89 intel.iocs.insert(IOCType::IPAddress, HashMap::new());
91 intel.iocs.insert(IOCType::Domain, HashMap::new());
92 intel.iocs.insert(IOCType::FileHash, HashMap::new());
93 intel.iocs.insert(IOCType::URL, HashMap::new());
94 intel.iocs.insert(IOCType::Email, HashMap::new());
95 intel.iocs.insert(IOCType::UserAgent, HashMap::new());
96
97 intel.load_default_iocs();
99
100 intel
101 }
102
103 fn load_default_iocs(&mut self) {
105 self.add_ioc(IOC::new(
107 IOCType::IPAddress,
108 "185.220.101.1".to_string(),
109 ThreatSeverity::High,
110 "Tor exit node - potential anonymization".to_string(),
111 "TorProject".to_string(),
112 ));
113
114 self.add_ioc(IOC::new(
115 IOCType::IPAddress,
116 "45.142.214.0".to_string(),
117 ThreatSeverity::Critical,
118 "Known C2 server IP".to_string(),
119 "ThreatFeed".to_string(),
120 ));
121
122 self.add_ioc(IOC::new(
124 IOCType::Domain,
125 "malicious-example.com".to_string(),
126 ThreatSeverity::Critical,
127 "Known phishing domain".to_string(),
128 "PhishTank".to_string(),
129 ));
130
131 self.add_ioc(IOC::new(
133 IOCType::FileHash,
134 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
135 ThreatSeverity::Critical,
136 "Known malware hash".to_string(),
137 "VirusTotal".to_string(),
138 ));
139
140 self.add_ioc(IOC::new(
142 IOCType::UserAgent,
143 "sqlmap".to_string(),
144 ThreatSeverity::High,
145 "Automated SQL injection tool".to_string(),
146 "SecurityTools".to_string(),
147 ));
148
149 self.add_ioc(IOC::new(
150 IOCType::UserAgent,
151 "nikto".to_string(),
152 ThreatSeverity::High,
153 "Automated vulnerability scanner".to_string(),
154 "SecurityTools".to_string(),
155 ));
156 }
157
158 pub fn add_ioc(&mut self, ioc: IOC) {
160 let value = ioc.value.clone();
161
162 match ioc.ioc_type {
164 IOCType::IPAddress => {
165 self.malicious_ips.insert(value.clone());
166 }
167 IOCType::Domain => {
168 self.malicious_domains.insert(value.clone());
169 }
170 IOCType::FileHash => {
171 self.malicious_hashes.insert(value.clone());
172 }
173 _ => {}
174 }
175
176 if let Some(type_map) = self.iocs.get_mut(&ioc.ioc_type) {
178 type_map.insert(value, ioc);
179 }
180 }
181
182 pub fn check_log(&mut self, log: &LogEntry) -> Vec<ThreatAlert> {
184 let mut alerts = Vec::new();
185
186 if let Some(ref ip) = log.source_ip {
188 if let Some(alert) = self.check_ioc(IOCType::IPAddress, ip, log) {
189 alerts.push(alert);
190 }
191 }
192
193 for domain in self.extract_domains(&log.message) {
195 if let Some(alert) = self.check_ioc(IOCType::Domain, &domain, log) {
196 alerts.push(alert);
197 }
198 }
199
200 if let Some(hash) = log.metadata.get("file_hash") {
202 if let Some(alert) = self.check_ioc(IOCType::FileHash, hash, log) {
203 alerts.push(alert);
204 }
205 }
206
207 if let Some(url) = log.metadata.get("url") {
209 if let Some(alert) = self.check_ioc(IOCType::URL, url, log) {
210 alerts.push(alert);
211 }
212 }
213
214 if let Some(user_agent) = log.metadata.get("user_agent") {
216 for ioc_map in self.iocs.get(&IOCType::UserAgent).iter() {
217 for (pattern, ioc) in ioc_map.iter() {
218 if user_agent.to_lowercase().contains(&pattern.to_lowercase()) {
219 self.matches_count += 1;
220 alerts.push(self.create_alert(ioc, log));
221 }
222 }
223 }
224 }
225
226 alerts
227 }
228
229 fn check_ioc(&mut self, ioc_type: IOCType, value: &str, log: &LogEntry) -> Option<ThreatAlert> {
231 if let Some(type_map) = self.iocs.get(&ioc_type) {
232 if let Some(ioc) = type_map.get(value) {
233 self.matches_count += 1;
234 return Some(self.create_alert(ioc, log));
235 }
236 }
237 None
238 }
239
240 fn create_alert(&self, ioc: &IOC, log: &LogEntry) -> ThreatAlert {
242 ThreatAlert {
243 alert_id: format!("IOC-{}", self.matches_count),
244 timestamp: Utc::now(),
245 severity: ioc.severity,
246 category: ThreatCategory::SystemCompromise,
247 description: format!(
248 "Threat Intelligence Match: {} ({:?})",
249 ioc.description, ioc.ioc_type
250 ),
251 source_log: format!("{} - {}", log.timestamp, log.message),
252 indicators: vec![
253 format!("{:?}: {}", ioc.ioc_type, ioc.value),
254 format!("Source: {}", ioc.source),
255 format!("Confidence: {:.0}%", ioc.confidence * 100.0),
256 ],
257 recommended_action: format!(
258 "Block {} {}, investigate affected systems, review network traffic",
259 format!("{:?}", ioc.ioc_type).to_lowercase(),
260 ioc.value
261 ),
262 threat_score: self.calculate_threat_score(ioc),
263 correlated_alerts: vec![],
264 }
265 }
266
267 fn calculate_threat_score(&self, ioc: &IOC) -> u32 {
269 let base_score = match ioc.severity {
270 ThreatSeverity::Info => 10,
271 ThreatSeverity::Low => 25,
272 ThreatSeverity::Medium => 50,
273 ThreatSeverity::High => 75,
274 ThreatSeverity::Critical => 95,
275 };
276
277 let confidence_adjustment = (ioc.confidence * 10.0) as u32;
278 (base_score + confidence_adjustment).min(100)
279 }
280
281 fn extract_domains(&self, text: &str) -> Vec<String> {
283 let mut domains = Vec::new();
284 let words: Vec<&str> = text.split_whitespace().collect();
285
286 for word in words {
287 if word.contains('.') && !word.starts_with("http") {
288 if let Some(domain) = word.split('/').next() {
290 if domain.contains('.') {
291 domains.push(domain.to_string());
292 }
293 }
294 }
295 }
296
297 domains
298 }
299
300 pub fn get_ioc(&self, ioc_type: IOCType, value: &str) -> Option<&IOC> {
302 self.iocs.get(&ioc_type)?.get(value)
303 }
304
305 pub fn get_iocs_by_type(&self, ioc_type: IOCType) -> Vec<&IOC> {
307 self.iocs
308 .get(&ioc_type)
309 .map(|map| map.values().collect())
310 .unwrap_or_default()
311 }
312
313 pub fn get_stats(&self) -> HashMap<String, usize> {
315 let mut stats = HashMap::new();
316 stats.insert("total_matches".to_string(), self.matches_count);
317 stats.insert("malicious_ips".to_string(), self.malicious_ips.len());
318 stats.insert(
319 "malicious_domains".to_string(),
320 self.malicious_domains.len(),
321 );
322 stats.insert("malicious_hashes".to_string(), self.malicious_hashes.len());
323 stats.insert("threat_actors".to_string(), self.threat_actors.len());
324
325 let total_iocs: usize = self.iocs.values().map(|m| m.len()).sum();
326 stats.insert("total_iocs".to_string(), total_iocs);
327
328 stats
329 }
330
331 pub fn import_iocs_json(&mut self, json: &str) -> Result<usize, serde_json::Error> {
333 let iocs: Vec<IOC> = serde_json::from_str(json)?;
334 let count = iocs.len();
335 for ioc in iocs {
336 self.add_ioc(ioc);
337 }
338 Ok(count)
339 }
340
341 pub fn export_iocs_json(&self) -> Result<String, serde_json::Error> {
343 let all_iocs: Vec<&IOC> = self.iocs.values().flat_map(|m| m.values()).collect();
344 serde_json::to_string_pretty(&all_iocs)
345 }
346
347 pub fn clear_old_iocs(&mut self, before: DateTime<Utc>) {
349 for type_map in self.iocs.values_mut() {
350 type_map.retain(|_, ioc| ioc.last_seen >= before);
351 }
352 self.rebuild_lookup_sets();
354 }
355
356 fn rebuild_lookup_sets(&mut self) {
358 self.malicious_ips.clear();
359 self.malicious_domains.clear();
360 self.malicious_hashes.clear();
361
362 if let Some(ip_map) = self.iocs.get(&IOCType::IPAddress) {
363 self.malicious_ips.extend(ip_map.keys().cloned());
364 }
365 if let Some(domain_map) = self.iocs.get(&IOCType::Domain) {
366 self.malicious_domains.extend(domain_map.keys().cloned());
367 }
368 if let Some(hash_map) = self.iocs.get(&IOCType::FileHash) {
369 self.malicious_hashes.extend(hash_map.keys().cloned());
370 }
371 }
372}
373
374impl Default for ThreatIntelligence {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use std::collections::HashMap;
384
385 fn create_log_with_ip(ip: &str) -> LogEntry {
386 LogEntry {
387 timestamp: Utc::now(),
388 source_ip: Some(ip.to_string()),
389 user: Some("test_user".to_string()),
390 event_type: "connection".to_string(),
391 message: "Network connection established".to_string(),
392 metadata: HashMap::new(),
393 }
394 }
395
396 #[test]
397 fn test_ioc_creation() {
398 let ioc = IOC::new(
399 IOCType::IPAddress,
400 "1.2.3.4".to_string(),
401 ThreatSeverity::High,
402 "Test IOC".to_string(),
403 "TestFeed".to_string(),
404 );
405
406 assert_eq!(ioc.value, "1.2.3.4");
407 assert_eq!(ioc.severity, ThreatSeverity::High);
408 }
409
410 #[test]
411 fn test_add_ioc() {
412 let mut intel = ThreatIntelligence::new();
413 let initial_count = intel.malicious_ips.len();
414
415 intel.add_ioc(IOC::new(
416 IOCType::IPAddress,
417 "10.0.0.1".to_string(),
418 ThreatSeverity::Medium,
419 "Test IP".to_string(),
420 "Test".to_string(),
421 ));
422
423 assert_eq!(intel.malicious_ips.len(), initial_count + 1);
424 assert!(intel.malicious_ips.contains("10.0.0.1"));
425 }
426
427 #[test]
428 fn test_check_malicious_ip() {
429 let mut intel = ThreatIntelligence::new();
430
431 intel.add_ioc(IOC::new(
433 IOCType::IPAddress,
434 "99.99.99.99".to_string(),
435 ThreatSeverity::Critical,
436 "Malicious server".to_string(),
437 "ThreatFeed".to_string(),
438 ));
439
440 let log = create_log_with_ip("99.99.99.99");
441 let alerts = intel.check_log(&log);
442
443 assert!(!alerts.is_empty());
444 assert_eq!(alerts[0].severity, ThreatSeverity::Critical);
445 assert!(alerts[0].description.contains("Threat Intelligence Match"));
446 }
447
448 #[test]
449 fn test_check_clean_ip() {
450 let mut intel = ThreatIntelligence::new();
451 let log = create_log_with_ip("192.168.1.1");
452 let alerts = intel.check_log(&log);
453
454 assert!(alerts
456 .iter()
457 .all(|a| a.severity != ThreatSeverity::Critical));
458 }
459
460 #[test]
461 fn test_domain_extraction() {
462 let intel = ThreatIntelligence::new();
463 let text = "User accessed malicious-example.com and another.domain.org";
464 let domains = intel.extract_domains(text);
465
466 assert!(domains.contains(&"malicious-example.com".to_string()));
467 assert!(domains.contains(&"another.domain.org".to_string()));
468 }
469
470 #[test]
471 fn test_get_ioc() {
472 let mut intel = ThreatIntelligence::new();
473
474 intel.add_ioc(IOC::new(
475 IOCType::Domain,
476 "evil.com".to_string(),
477 ThreatSeverity::High,
478 "Malicious domain".to_string(),
479 "Test".to_string(),
480 ));
481
482 let ioc = intel.get_ioc(IOCType::Domain, "evil.com");
483 assert!(ioc.is_some());
484 assert_eq!(ioc.unwrap().value, "evil.com");
485 }
486
487 #[test]
488 fn test_get_iocs_by_type() {
489 let intel = ThreatIntelligence::new();
490 let ip_iocs = intel.get_iocs_by_type(IOCType::IPAddress);
491
492 assert!(!ip_iocs.is_empty()); }
494
495 #[test]
496 fn test_stats() {
497 let intel = ThreatIntelligence::new();
498 let stats = intel.get_stats();
499
500 assert!(stats.contains_key("total_iocs"));
501 assert!(stats.contains_key("malicious_ips"));
502 assert!(stats.get("total_iocs").unwrap() > &0); }
504
505 #[test]
506 fn test_user_agent_detection() {
507 let mut intel = ThreatIntelligence::new();
508 let mut log = LogEntry {
509 timestamp: Utc::now(),
510 source_ip: Some("192.168.1.1".to_string()),
511 user: Some("attacker".to_string()),
512 event_type: "web_request".to_string(),
513 message: "HTTP request".to_string(),
514 metadata: HashMap::new(),
515 };
516
517 log.metadata
518 .insert("user_agent".to_string(), "sqlmap/1.0".to_string());
519
520 let alerts = intel.check_log(&log);
521 assert!(!alerts.is_empty());
522 assert!(
523 alerts[0].description.contains("UserAgent")
524 || alerts[0].description.contains("SQL injection")
525 );
526 }
527
528 #[test]
529 fn test_threat_score_calculation() {
530 let intel = ThreatIntelligence::new();
531
532 let high_confidence_ioc = IOC {
533 ioc_type: IOCType::IPAddress,
534 value: "1.2.3.4".to_string(),
535 severity: ThreatSeverity::Critical,
536 description: "Test".to_string(),
537 source: "Test".to_string(),
538 first_seen: Utc::now(),
539 last_seen: Utc::now(),
540 confidence: 1.0,
541 };
542
543 let score = intel.calculate_threat_score(&high_confidence_ioc);
544 assert!(score >= 95);
545 }
546
547 #[test]
548 fn test_clear_old_iocs() {
549 let mut intel = ThreatIntelligence::new();
550
551 let old_ioc = IOC {
552 ioc_type: IOCType::IPAddress,
553 value: "1.1.1.1".to_string(),
554 severity: ThreatSeverity::Low,
555 description: "Old IOC".to_string(),
556 source: "Test".to_string(),
557 first_seen: Utc::now() - chrono::Duration::days(100),
558 last_seen: Utc::now() - chrono::Duration::days(100),
559 confidence: 0.5,
560 };
561
562 intel.add_ioc(old_ioc);
563
564 let cutoff = Utc::now() - chrono::Duration::days(50);
565 intel.clear_old_iocs(cutoff);
566
567 assert!(intel.get_ioc(IOCType::IPAddress, "1.1.1.1").is_none());
569 }
570}