1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub enum ComplianceFramework {
12 PCIDSS,
13 CISBenchmark,
14 NIST80053,
15 HIPAA,
16 SOC2,
17}
18
19impl ComplianceFramework {
20 pub fn name(&self) -> &'static str {
22 match self {
23 ComplianceFramework::PCIDSS => "PCI-DSS v4.0",
24 ComplianceFramework::CISBenchmark => "CIS Controls v8",
25 ComplianceFramework::NIST80053 => "NIST SP 800-53",
26 ComplianceFramework::HIPAA => "HIPAA Security Rule",
27 ComplianceFramework::SOC2 => "SOC 2 Type II",
28 }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub enum ComplianceStatus {
35 Pass,
36 Fail,
37 Warning,
38 NotApplicable,
39 NotTested,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ComplianceCheck {
45 pub id: String,
46 pub framework: ComplianceFramework,
47 pub requirement: String,
48 pub description: String,
49 pub status: ComplianceStatus,
50 pub evidence: String,
51 pub remediation: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ComplianceResult {
57 pub target: String,
58 pub framework: ComplianceFramework,
59 pub scan_time: DateTime<Utc>,
60 pub checks: Vec<ComplianceCheck>,
61 pub passed: usize,
62 pub failed: usize,
63 pub warnings: usize,
64 pub compliance_score: f32,
65}
66
67impl ComplianceResult {
68 pub fn new(target: &str, framework: ComplianceFramework) -> Self {
70 Self {
71 target: target.to_string(),
72 framework,
73 scan_time: Utc::now(),
74 checks: Vec::new(),
75 passed: 0,
76 failed: 0,
77 warnings: 0,
78 compliance_score: 0.0,
79 }
80 }
81
82 pub fn add_check(&mut self, check: ComplianceCheck) {
84 match check.status {
85 ComplianceStatus::Pass => self.passed += 1,
86 ComplianceStatus::Fail => self.failed += 1,
87 ComplianceStatus::Warning => self.warnings += 1,
88 _ => {}
89 }
90 self.checks.push(check);
91 self.calculate_score();
92 }
93
94 fn calculate_score(&mut self) {
96 let total = self.passed + self.failed;
97 if total > 0 {
98 self.compliance_score = (self.passed as f32 / total as f32) * 100.0;
99 }
100 }
101
102 pub fn is_compliant(&self) -> bool {
104 self.failed == 0
105 }
106
107 pub fn summary(&self) -> String {
109 format!(
110 "{} Compliance: {:.1}% | Passed: {} | Failed: {} | Warnings: {}",
111 self.framework.name(),
112 self.compliance_score,
113 self.passed,
114 self.failed,
115 self.warnings
116 )
117 }
118
119 pub fn to_json(&self) -> Result<String, serde_json::Error> {
121 serde_json::to_string_pretty(self)
122 }
123
124 pub fn get_failed_checks(&self) -> Vec<&ComplianceCheck> {
126 self.checks
127 .iter()
128 .filter(|c| c.status == ComplianceStatus::Fail)
129 .collect()
130 }
131}
132
133#[derive(Debug, Clone)]
135pub struct PortSecurityConfig {
136 pub prohibited_ports: Vec<u16>,
138 pub encryption_required_ports: Vec<u16>,
140 pub allowed_ports: Vec<u16>,
142}
143
144impl Default for PortSecurityConfig {
145 fn default() -> Self {
146 Self {
147 prohibited_ports: vec![23, 21, 69, 512, 513, 514, 137, 138, 139],
149 encryption_required_ports: vec![80, 8080, 25, 110, 143],
151 allowed_ports: vec![22, 443, 8443, 993, 995, 465, 587],
153 }
154 }
155}
156
157pub struct ComplianceScanner {
159 port_config: PortSecurityConfig,
160}
161
162impl ComplianceScanner {
163 pub fn new() -> Self {
165 Self {
166 port_config: PortSecurityConfig::default(),
167 }
168 }
169
170 pub fn scan_pci_dss(&self, target: &str, open_ports: &[u16], services: &HashMap<u16, String>) -> ComplianceResult {
172 let mut result = ComplianceResult::new(target, ComplianceFramework::PCIDSS);
173
174 self.check_prohibited_ports(&mut result, open_ports);
176
177 self.check_encryption_required(&mut result, open_ports, services);
179
180 self.check_secure_admin(&mut result, open_ports, services);
182
183 self.check_data_encryption(&mut result, open_ports);
185
186 self.check_insecure_protocols(&mut result, services);
188
189 self.check_vulnerability_scanning(&mut result, open_ports);
191
192 result
193 }
194
195 pub fn scan_cis_benchmark(&self, target: &str, open_ports: &[u16], services: &HashMap<u16, String>) -> ComplianceResult {
197 let mut result = ComplianceResult::new(target, ComplianceFramework::CISBenchmark);
198
199 self.check_secure_configuration(&mut result, open_ports);
201
202 self.check_unnecessary_services(&mut result, open_ports, services);
204
205 self.check_open_port_count(&mut result, open_ports);
207
208 self.check_data_encryption(&mut result, open_ports);
210
211 result
212 }
213
214 fn check_prohibited_ports(&self, result: &mut ComplianceResult, open_ports: &[u16]) {
216 let prohibited_open: Vec<u16> = open_ports
217 .iter()
218 .filter(|p| self.port_config.prohibited_ports.contains(p))
219 .copied()
220 .collect();
221
222 let status = if prohibited_open.is_empty() {
223 ComplianceStatus::Pass
224 } else {
225 ComplianceStatus::Fail
226 };
227
228 let evidence = if prohibited_open.is_empty() {
229 "No prohibited ports found open".to_string()
230 } else {
231 format!("Prohibited ports open: {:?}", prohibited_open)
232 };
233
234 result.add_check(ComplianceCheck {
235 id: "PCI-DSS-1.3.1".to_string(),
236 framework: ComplianceFramework::PCIDSS,
237 requirement: "Restrict inbound traffic to necessary ports".to_string(),
238 description: "No insecure or unnecessary ports should be accessible".to_string(),
239 status,
240 evidence,
241 remediation: Some("Close or firewall prohibited ports: Telnet (23), FTP (21), TFTP (69)".to_string()),
242 });
243 }
244
245 fn check_encryption_required(&self, result: &mut ComplianceResult, open_ports: &[u16], services: &HashMap<u16, String>) {
247 let unencrypted: Vec<u16> = open_ports
248 .iter()
249 .filter(|p| self.port_config.encryption_required_ports.contains(p))
250 .copied()
251 .collect();
252
253 let status = if unencrypted.is_empty() {
254 ComplianceStatus::Pass
255 } else {
256 ComplianceStatus::Fail
257 };
258
259 let evidence = if unencrypted.is_empty() {
260 "All services use encrypted protocols".to_string()
261 } else {
262 format!("Unencrypted services on ports: {:?}", unencrypted)
263 };
264
265 result.add_check(ComplianceCheck {
266 id: "PCI-DSS-2.2.7".to_string(),
267 framework: ComplianceFramework::PCIDSS,
268 requirement: "Use strong cryptography for non-console administrative access".to_string(),
269 description: "All administrative access must be encrypted".to_string(),
270 status,
271 evidence,
272 remediation: Some("Replace HTTP with HTTPS, use IMAPS/POP3S instead of IMAP/POP3".to_string()),
273 });
274 }
275
276 fn check_secure_admin(&self, result: &mut ComplianceResult, open_ports: &[u16], _services: &HashMap<u16, String>) {
278 let has_telnet = open_ports.contains(&23);
280 let has_ssh = open_ports.contains(&22);
281
282 let status = if has_telnet {
283 ComplianceStatus::Fail
284 } else if has_ssh {
285 ComplianceStatus::Pass
286 } else {
287 ComplianceStatus::NotApplicable
288 };
289
290 let evidence = match (has_telnet, has_ssh) {
291 (true, _) => "Telnet (insecure) is enabled".to_string(),
292 (false, true) => "SSH (secure) is used for remote access".to_string(),
293 (false, false) => "No remote administration ports detected".to_string(),
294 };
295
296 result.add_check(ComplianceCheck {
297 id: "PCI-DSS-2.3".to_string(),
298 framework: ComplianceFramework::PCIDSS,
299 requirement: "Encrypt all non-console administrative access".to_string(),
300 description: "Use SSH instead of Telnet for remote administration".to_string(),
301 status,
302 evidence,
303 remediation: Some("Disable Telnet and use SSH with key-based authentication".to_string()),
304 });
305 }
306
307 fn check_data_encryption(&self, result: &mut ComplianceResult, open_ports: &[u16]) {
309 let has_https = open_ports.contains(&443) || open_ports.contains(&8443);
310 let has_http_only = open_ports.contains(&80) && !has_https;
311
312 let status = if has_http_only {
313 ComplianceStatus::Fail
314 } else if has_https {
315 ComplianceStatus::Pass
316 } else {
317 ComplianceStatus::NotApplicable
318 };
319
320 let evidence = if has_https {
321 "HTTPS is available for secure data transmission".to_string()
322 } else if has_http_only {
323 "Only HTTP (unencrypted) is available".to_string()
324 } else {
325 "No web services detected".to_string()
326 };
327
328 result.add_check(ComplianceCheck {
329 id: "PCI-DSS-4.1".to_string(),
330 framework: ComplianceFramework::PCIDSS,
331 requirement: "Use strong cryptography to protect cardholder data during transmission".to_string(),
332 description: "All data transmission must be encrypted with TLS 1.2+".to_string(),
333 status,
334 evidence,
335 remediation: Some("Enable HTTPS with TLS 1.2 or higher, disable HTTP".to_string()),
336 });
337 }
338
339 fn check_insecure_protocols(&self, result: &mut ComplianceResult, services: &HashMap<u16, String>) {
341 let insecure_services: Vec<String> = services
342 .values()
343 .filter(|s| {
344 let s_lower = s.to_lowercase();
345 s_lower.contains("telnet")
346 || s_lower.contains("ftp")
347 || (s_lower.contains("ssl") && s_lower.contains("2"))
348 || s_lower.contains("sslv3")
349 })
350 .cloned()
351 .collect();
352
353 let status = if insecure_services.is_empty() {
354 ComplianceStatus::Pass
355 } else {
356 ComplianceStatus::Fail
357 };
358
359 let evidence = if insecure_services.is_empty() {
360 "No insecure protocols detected".to_string()
361 } else {
362 format!("Insecure protocols found: {:?}", insecure_services)
363 };
364
365 result.add_check(ComplianceCheck {
366 id: "PCI-DSS-6.5.4".to_string(),
367 framework: ComplianceFramework::PCIDSS,
368 requirement: "Do not use insecure protocols".to_string(),
369 description: "Telnet, FTP, SSL 2.0/3.0, and early TLS must be disabled".to_string(),
370 status,
371 evidence,
372 remediation: Some("Disable Telnet, FTP, SSL 2.0/3.0, TLS 1.0/1.1".to_string()),
373 });
374 }
375
376 fn check_vulnerability_scanning(&self, result: &mut ComplianceResult, _open_ports: &[u16]) {
378 result.add_check(ComplianceCheck {
380 id: "PCI-DSS-11.2".to_string(),
381 framework: ComplianceFramework::PCIDSS,
382 requirement: "Run internal and external network vulnerability scans".to_string(),
383 description: "Quarterly vulnerability scans required".to_string(),
384 status: ComplianceStatus::Pass, evidence: "Vulnerability scan is being performed".to_string(),
386 remediation: None,
387 });
388 }
389
390 fn check_secure_configuration(&self, result: &mut ComplianceResult, open_ports: &[u16]) {
392 let high_risk_ports: Vec<u16> = open_ports
393 .iter()
394 .filter(|p| [23, 21, 25, 110, 143, 445, 3389].contains(p))
395 .copied()
396 .collect();
397
398 let status = if high_risk_ports.is_empty() {
399 ComplianceStatus::Pass
400 } else if high_risk_ports.len() <= 2 {
401 ComplianceStatus::Warning
402 } else {
403 ComplianceStatus::Fail
404 };
405
406 result.add_check(ComplianceCheck {
407 id: "CIS-4.1".to_string(),
408 framework: ComplianceFramework::CISBenchmark,
409 requirement: "Establish secure configurations".to_string(),
410 description: "Minimize attack surface by closing unnecessary ports".to_string(),
411 status,
412 evidence: format!("High-risk ports open: {:?}", high_risk_ports),
413 remediation: Some("Close or restrict high-risk ports, use encrypted alternatives".to_string()),
414 });
415 }
416
417 fn check_unnecessary_services(&self, result: &mut ComplianceResult, open_ports: &[u16], _services: &HashMap<u16, String>) {
419 let common_unnecessary: Vec<u16> = open_ports
420 .iter()
421 .filter(|p| [7, 9, 13, 17, 19, 37, 79].contains(p))
422 .copied()
423 .collect();
424
425 let status = if common_unnecessary.is_empty() {
426 ComplianceStatus::Pass
427 } else {
428 ComplianceStatus::Fail
429 };
430
431 result.add_check(ComplianceCheck {
432 id: "CIS-4.8".to_string(),
433 framework: ComplianceFramework::CISBenchmark,
434 requirement: "Uninstall or disable unnecessary services".to_string(),
435 description: "Legacy and unnecessary services should be disabled".to_string(),
436 status,
437 evidence: format!("Unnecessary service ports: {:?}", common_unnecessary),
438 remediation: Some("Disable echo, discard, daytime, chargen, finger services".to_string()),
439 });
440 }
441
442 fn check_open_port_count(&self, result: &mut ComplianceResult, open_ports: &[u16]) {
444 let port_count = open_ports.len();
445
446 let status = if port_count <= 5 {
447 ComplianceStatus::Pass
448 } else if port_count <= 10 {
449 ComplianceStatus::Warning
450 } else {
451 ComplianceStatus::Fail
452 };
453
454 result.add_check(ComplianceCheck {
455 id: "CIS-9.2".to_string(),
456 framework: ComplianceFramework::CISBenchmark,
457 requirement: "Ensure only approved ports are open".to_string(),
458 description: "Limit network exposure to minimum necessary ports".to_string(),
459 status,
460 evidence: format!("{} ports open: {:?}", port_count, open_ports),
461 remediation: Some("Review and close unnecessary ports, implement firewall rules".to_string()),
462 });
463 }
464}
465
466impl Default for ComplianceScanner {
467 fn default() -> Self {
468 Self::new()
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn test_pci_dss_scan_pass() {
478 let scanner = ComplianceScanner::new();
479 let open_ports = vec![22, 443];
480 let services = HashMap::new();
481
482 let result = scanner.scan_pci_dss("192.168.1.1", &open_ports, &services);
483
484 assert!(result.compliance_score > 50.0);
485 assert!(result.failed < result.passed);
486 }
487
488 #[test]
489 fn test_pci_dss_scan_fail() {
490 let scanner = ComplianceScanner::new();
491 let open_ports = vec![23, 21, 80]; let services = HashMap::new();
493
494 let result = scanner.scan_pci_dss("192.168.1.1", &open_ports, &services);
495
496 assert!(result.failed > 0);
497 }
498
499 #[test]
500 fn test_cis_benchmark_scan() {
501 let scanner = ComplianceScanner::new();
502 let open_ports = vec![22, 443, 8443];
503 let services = HashMap::new();
504
505 let result = scanner.scan_cis_benchmark("192.168.1.1", &open_ports, &services);
506
507 assert!(!result.checks.is_empty());
508 }
509
510 #[test]
511 fn test_compliance_result_summary() {
512 let mut result = ComplianceResult::new("test", ComplianceFramework::PCIDSS);
513
514 result.add_check(ComplianceCheck {
515 id: "TEST-1".to_string(),
516 framework: ComplianceFramework::PCIDSS,
517 requirement: "Test".to_string(),
518 description: "Test check".to_string(),
519 status: ComplianceStatus::Pass,
520 evidence: "Passed".to_string(),
521 remediation: None,
522 });
523
524 assert_eq!(result.passed, 1);
525 assert!(result.summary().contains("100.0%"));
526 }
527
528 #[test]
529 fn test_failed_checks() {
530 let mut result = ComplianceResult::new("test", ComplianceFramework::PCIDSS);
531
532 result.add_check(ComplianceCheck {
533 id: "TEST-1".to_string(),
534 framework: ComplianceFramework::PCIDSS,
535 requirement: "Test".to_string(),
536 description: "Test".to_string(),
537 status: ComplianceStatus::Pass,
538 evidence: "OK".to_string(),
539 remediation: None,
540 });
541
542 result.add_check(ComplianceCheck {
543 id: "TEST-2".to_string(),
544 framework: ComplianceFramework::PCIDSS,
545 requirement: "Test".to_string(),
546 description: "Test".to_string(),
547 status: ComplianceStatus::Fail,
548 evidence: "Failed".to_string(),
549 remediation: Some("Fix it".to_string()),
550 });
551
552 let failed = result.get_failed_checks();
553 assert_eq!(failed.len(), 1);
554 assert_eq!(failed[0].id, "TEST-2");
555 }
556}