nabla_cli/enterprise/secure/
behavioral_analysis.rs

1#![allow(dead_code)]
2use crate::binary::BinaryAnalysis;
3use crate::enterprise::secure::control_flow::ControlFlowGraph;
4use crate::enterprise::types::{CodeLocation, ConfidenceLevel, SeverityLevel};
5use chrono::Utc;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BehavioralAnalysisResult {
11    pub analysis_id: Uuid,
12    pub file_path: String,
13    pub control_flow_anomalies: Vec<ControlFlowAnomaly>,
14    pub network_patterns: Vec<NetworkPattern>,
15    pub data_flow_issues: Vec<DataFlowIssue>,
16    pub analysis_duration_ms: u64,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ControlFlowAnomaly {
21    pub anomaly_type: ControlFlowAnomalyType,
22    pub location: CodeLocation,
23    pub confidence: ConfidenceLevel,
24    pub description: String,
25    pub call_graph_fragment: Option<Vec<String>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum ControlFlowAnomalyType {
30    UnexpectedJump,
31    SuspiciousLoop,
32    DeadCode,
33    HiddenBranch,
34    AntiDebugPattern,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct NetworkPattern {
39    pub pattern_type: NetworkPatternType,
40    pub endpoints: Vec<String>,
41    pub frequency: Option<u32>,
42    pub suspicious_indicators: Vec<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum NetworkPatternType {
47    Beaconing,
48    DataExfiltration,
49    CommandAndControl,
50    DNSTunneling,
51    SuspiciousPort,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DataFlowIssue {
56    pub issue_type: DataFlowIssueType,
57    pub source: CodeLocation,
58    pub sink: CodeLocation,
59    pub severity: SeverityLevel,
60    pub data_path: Vec<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum DataFlowIssueType {
65    UncontrolledInput,
66    DataLeakage,
67    PrivilegeEscalation,
68    UnsanitizedOutput,
69}
70
71pub fn analyze_behavioral_security(analysis: &BinaryAnalysis) -> BehavioralAnalysisResult {
72    let start_time = Utc::now();
73
74    let mut result = BehavioralAnalysisResult {
75        analysis_id: Uuid::new_v4(),
76        file_path: analysis.file_name.clone(),
77        control_flow_anomalies: Vec::new(),
78        network_patterns: Vec::new(),
79        data_flow_issues: Vec::new(),
80        analysis_duration_ms: 0,
81    };
82
83    // Build control flow graph for analysis
84    let cfg = match ControlFlowGraph::build_cfg(analysis) {
85        Ok(cfg) => cfg,
86        Err(e) => {
87            // Fallback to simplified analysis if CFG construction fails
88            tracing::warn!("CFG construction failed: {}, using simplified analysis", e);
89            return BehavioralAnalysisResult {
90                analysis_id: Uuid::new_v4(),
91                file_path: analysis.file_name.clone(),
92                control_flow_anomalies: analyze_control_flow_anomalies_simple(analysis),
93                network_patterns: analyze_network_patterns(analysis),
94                data_flow_issues: analyze_data_flow_issues(analysis),
95                analysis_duration_ms: (Utc::now() - start_time).num_milliseconds() as u64,
96            };
97        }
98    };
99
100    // Analyze control flow anomalies
101    result.control_flow_anomalies = analyze_control_flow_anomalies(analysis, &cfg);
102
103    // Analyze network communication patterns
104    result.network_patterns = analyze_network_patterns(analysis);
105
106    // Analyze data flow security issues
107    result.data_flow_issues = analyze_data_flow_issues(analysis);
108
109    let end_time = Utc::now();
110    result.analysis_duration_ms = (end_time - start_time).num_milliseconds() as u64;
111
112    result
113}
114
115fn analyze_control_flow_anomalies(
116    analysis: &BinaryAnalysis,
117    cfg: &ControlFlowGraph,
118) -> Vec<ControlFlowAnomaly> {
119    let mut anomalies = Vec::new();
120
121    // Analyze loops for suspicious patterns
122    for loop_info in &cfg.loops {
123        match loop_info.loop_type {
124            crate::enterprise::secure::control_flow::LoopType::Infinite => {
125                anomalies.push(ControlFlowAnomaly {
126                    anomaly_type: ControlFlowAnomalyType::SuspiciousLoop,
127                    location: CodeLocation {
128                        file_path: analysis.file_name.clone(),
129                        line_number: None,
130                        column_number: None,
131                        function_name: None,
132                        binary_offset: None,
133                    },
134                    confidence: ConfidenceLevel::High,
135                    description: format!(
136                        "Infinite loop detected at address 0x{:x}",
137                        loop_info.header
138                    ),
139                    call_graph_fragment: Some(vec![format!("0x{:x}", loop_info.header)]),
140                });
141            }
142            crate::enterprise::secure::control_flow::LoopType::Irreducible => {
143                anomalies.push(ControlFlowAnomaly {
144                    anomaly_type: ControlFlowAnomalyType::HiddenBranch,
145                    location: CodeLocation {
146                        file_path: analysis.file_name.clone(),
147                        line_number: None,
148                        column_number: None,
149                        function_name: None,
150                        binary_offset: None,
151                    },
152                    confidence: ConfidenceLevel::Medium,
153                    description: "Irreducible control flow detected - possible obfuscation"
154                        .to_string(),
155                    call_graph_fragment: None,
156                });
157            }
158            _ => {}
159        }
160    }
161
162    // Analyze call graph for anomalies
163    let call_graph = cfg.analyze_call_graph(analysis);
164
165    // Check for suspicious recursion patterns
166    for recursive_func in &call_graph.recursive_functions {
167        anomalies.push(ControlFlowAnomaly {
168            anomaly_type: ControlFlowAnomalyType::SuspiciousLoop,
169            location: CodeLocation {
170                file_path: analysis.file_name.clone(),
171                line_number: None,
172                column_number: None,
173                function_name: Some(recursive_func.clone()),
174                binary_offset: None,
175            },
176            confidence: ConfidenceLevel::Medium,
177            description: format!("Recursive function detected: {}", recursive_func),
178            call_graph_fragment: Some(vec![recursive_func.clone()]),
179        });
180    }
181
182    // Check for high cyclomatic complexity (possible obfuscation)
183    for (func_name, summary) in &call_graph.function_summaries {
184        if summary.cyclomatic_complexity > 20 {
185            anomalies.push(ControlFlowAnomaly {
186                anomaly_type: ControlFlowAnomalyType::HiddenBranch,
187                location: CodeLocation {
188                    file_path: analysis.file_name.clone(),
189                    line_number: None,
190                    column_number: None,
191                    function_name: Some(func_name.clone()),
192                    binary_offset: None,
193                },
194                confidence: ConfidenceLevel::Medium,
195                description: format!(
196                    "High cyclomatic complexity ({}) in function {}",
197                    summary.cyclomatic_complexity, func_name
198                ),
199                call_graph_fragment: Some(vec![func_name.clone()]),
200            });
201        }
202    }
203
204    // Add traditional pattern-based detection
205    anomalies.extend(analyze_control_flow_anomalies_simple(analysis));
206
207    anomalies
208}
209
210fn analyze_control_flow_anomalies_simple(analysis: &BinaryAnalysis) -> Vec<ControlFlowAnomaly> {
211    let mut anomalies = Vec::new();
212
213    // Detect anti-debugging patterns
214    let anti_debug_functions = [
215        "ptrace",
216        "IsDebuggerPresent",
217        "CheckRemoteDebuggerPresent",
218        "NtQueryInformationProcess",
219        "OutputDebugString",
220        "GetTickCount",
221        "timeGetTime",
222        "rdtsc",
223        "cpuid",
224    ];
225
226    for func in &anti_debug_functions {
227        if analysis.imports.contains(&func.to_string())
228            || analysis.detected_symbols.contains(&func.to_string())
229        {
230            anomalies.push(ControlFlowAnomaly {
231                anomaly_type: ControlFlowAnomalyType::AntiDebugPattern,
232                location: CodeLocation {
233                    file_path: analysis.file_name.clone(),
234                    line_number: None,
235                    column_number: None,
236                    function_name: Some(func.to_string()),
237                    binary_offset: None,
238                },
239                confidence: ConfidenceLevel::High,
240                description: format!("Anti-debugging function {} detected", func),
241                call_graph_fragment: Some(vec![func.to_string()]),
242            });
243        }
244    }
245
246    // Detect suspicious control flow patterns in embedded strings
247    for string in &analysis.embedded_strings {
248        let lower = string.to_lowercase();
249
250        // Look for obfuscation or packing indicators
251        if lower.contains("upx") || lower.contains("packer") || lower.contains("packed") {
252            anomalies.push(ControlFlowAnomaly {
253                anomaly_type: ControlFlowAnomalyType::HiddenBranch,
254                location: CodeLocation {
255                    file_path: analysis.file_name.clone(),
256                    line_number: None,
257                    column_number: None,
258                    function_name: None,
259                    binary_offset: None,
260                },
261                confidence: ConfidenceLevel::Medium,
262                description: "Potential code packing or obfuscation detected".to_string(),
263                call_graph_fragment: None,
264            });
265        }
266
267        // Look for VM detection strings
268        if lower.contains("vmware")
269            || lower.contains("virtualbox")
270            || lower.contains("sandboxie")
271            || lower.contains("wine")
272        {
273            anomalies.push(ControlFlowAnomaly {
274                anomaly_type: ControlFlowAnomalyType::AntiDebugPattern,
275                location: CodeLocation {
276                    file_path: analysis.file_name.clone(),
277                    line_number: None,
278                    column_number: None,
279                    function_name: None,
280                    binary_offset: None,
281                },
282                confidence: ConfidenceLevel::Medium,
283                description: "Virtual machine detection strings found".to_string(),
284                call_graph_fragment: None,
285            });
286        }
287    }
288
289    // Detect unusual function call patterns
290    let suspicious_patterns = detect_suspicious_call_patterns(analysis);
291    anomalies.extend(suspicious_patterns);
292
293    // Detect potential dead code indicators
294    if analysis.detected_symbols.len() > analysis.imports.len() * 5 {
295        anomalies.push(ControlFlowAnomaly {
296            anomaly_type: ControlFlowAnomalyType::DeadCode,
297            location: CodeLocation {
298                file_path: analysis.file_name.clone(),
299                line_number: None,
300                column_number: None,
301                function_name: None,
302                binary_offset: None,
303            },
304            confidence: ConfidenceLevel::Low,
305            description: "Large number of symbols relative to imports - potential dead code"
306                .to_string(),
307            call_graph_fragment: None,
308        });
309    }
310
311    anomalies
312}
313
314fn detect_suspicious_call_patterns(analysis: &BinaryAnalysis) -> Vec<ControlFlowAnomaly> {
315    let mut anomalies = Vec::new();
316
317    // Pattern: Dynamic loading with execution
318    let has_dlopen = analysis.imports.contains(&"dlopen".to_string());
319    let has_dlsym = analysis.imports.contains(&"dlsym".to_string());
320    let has_exec = analysis.imports.iter().any(|s| s.starts_with("exec"));
321
322    if has_dlopen && has_dlsym && has_exec {
323        anomalies.push(ControlFlowAnomaly {
324            anomaly_type: ControlFlowAnomalyType::SuspiciousLoop,
325            location: CodeLocation {
326                file_path: analysis.file_name.clone(),
327                line_number: None,
328                column_number: None,
329                function_name: None,
330                binary_offset: None,
331            },
332            confidence: ConfidenceLevel::Medium,
333            description: "Dynamic loading combined with execution - potential code injection"
334                .to_string(),
335            call_graph_fragment: Some(vec![
336                "dlopen".to_string(),
337                "dlsym".to_string(),
338                "exec*".to_string(),
339            ]),
340        });
341    }
342
343    // Pattern: Memory manipulation with network functions
344    let has_mmap = analysis.imports.contains(&"mmap".to_string());
345    let has_mprotect = analysis.imports.contains(&"mprotect".to_string());
346    let has_network = analysis.imports.iter().any(|s| {
347        s.contains("socket") || s.contains("connect") || s.contains("recv") || s.contains("send")
348    });
349
350    if (has_mmap || has_mprotect) && has_network {
351        anomalies.push(ControlFlowAnomaly {
352            anomaly_type: ControlFlowAnomalyType::UnexpectedJump,
353            location: CodeLocation {
354                file_path: analysis.file_name.clone(),
355                line_number: None,
356                column_number: None,
357                function_name: None,
358                binary_offset: None,
359            },
360            confidence: ConfidenceLevel::Medium,
361            description: "Memory manipulation combined with network functions - potential remote code execution".to_string(),
362            call_graph_fragment: Some(vec!["mmap/mprotect".to_string(), "network_functions".to_string()]),
363        });
364    }
365
366    anomalies
367}
368
369fn analyze_network_patterns(analysis: &BinaryAnalysis) -> Vec<NetworkPattern> {
370    let mut patterns = Vec::new();
371
372    // Detect network functions and suspicious patterns
373    let network_functions = [
374        "socket",
375        "connect",
376        "bind",
377        "listen",
378        "accept",
379        "send",
380        "recv",
381        "sendto",
382        "recvfrom",
383        "getsockopt",
384        "setsockopt",
385        "select",
386        "poll",
387    ];
388
389    let has_network = network_functions.iter().any(|&func| {
390        analysis.imports.contains(&func.to_string())
391            || analysis.detected_symbols.contains(&func.to_string())
392    });
393
394    if has_network {
395        // Look for suspicious network patterns in strings
396        for string in &analysis.embedded_strings {
397            let mut endpoints = Vec::new();
398            let mut suspicious_indicators = Vec::new();
399
400            // Check for IP addresses or domains
401            if is_ip_address(string) || is_domain_name(string) {
402                endpoints.push(string.clone());
403            }
404
405            // Check for suspicious ports
406            if let Some(port) = extract_port_number(string) {
407                if is_suspicious_port(port) {
408                    patterns.push(NetworkPattern {
409                        pattern_type: NetworkPatternType::SuspiciousPort,
410                        endpoints: vec![string.clone()],
411                        frequency: None,
412                        suspicious_indicators: vec![format!("Suspicious port: {}", port)],
413                    });
414                }
415            }
416
417            // Check for DNS tunneling indicators
418            if string.len() > 50
419                && string.contains('.')
420                && string.chars().filter(|&c| c == '.').count() > 5
421            {
422                patterns.push(NetworkPattern {
423                    pattern_type: NetworkPatternType::DNSTunneling,
424                    endpoints: vec![string.clone()],
425                    frequency: None,
426                    suspicious_indicators: vec!["Unusually long domain name".to_string()],
427                });
428            }
429
430            // Check for base64 in network strings (potential C2)
431            if is_likely_base64(string) && string.len() > 20 {
432                suspicious_indicators.push("Base64-encoded data".to_string());
433                patterns.push(NetworkPattern {
434                    pattern_type: NetworkPatternType::CommandAndControl,
435                    endpoints: vec![string.clone()],
436                    frequency: None,
437                    suspicious_indicators,
438                });
439            }
440        }
441
442        // General network usage pattern
443        if !endpoints_found(&patterns) {
444            patterns.push(NetworkPattern {
445                pattern_type: NetworkPatternType::CommandAndControl,
446                endpoints: vec!["Network functions detected".to_string()],
447                frequency: None,
448                suspicious_indicators: vec!["Generic network capability".to_string()],
449            });
450        }
451    }
452
453    // Check for potential beaconing patterns
454    let has_timer = analysis
455        .imports
456        .iter()
457        .any(|s| s.contains("sleep") || s.contains("timer") || s.contains("delay"));
458
459    if has_network && has_timer {
460        patterns.push(NetworkPattern {
461            pattern_type: NetworkPatternType::Beaconing,
462            endpoints: vec!["Timer + Network functions".to_string()],
463            frequency: None,
464            suspicious_indicators: vec!["Periodic network communication pattern".to_string()],
465        });
466    }
467
468    patterns
469}
470
471fn analyze_data_flow_issues(analysis: &BinaryAnalysis) -> Vec<DataFlowIssue> {
472    let mut issues = Vec::new();
473
474    // Analyze input validation issues
475    let input_functions = ["scanf", "gets", "fgets", "getchar", "recv", "recvfrom"];
476    let output_functions = ["printf", "fprintf", "sprintf", "send", "sendto"];
477
478    let has_input = input_functions.iter().any(|&func| {
479        analysis.imports.contains(&func.to_string())
480            || analysis.detected_symbols.contains(&func.to_string())
481    });
482
483    let has_output = output_functions.iter().any(|&func| {
484        analysis.imports.contains(&func.to_string())
485            || analysis.detected_symbols.contains(&func.to_string())
486    });
487
488    // Uncontrolled input without validation
489    if has_input {
490        for func in &input_functions {
491            if analysis.imports.contains(&func.to_string())
492                || analysis.detected_symbols.contains(&func.to_string())
493            {
494                let severity = match *func {
495                    "gets" => SeverityLevel::Critical,
496                    "scanf" => SeverityLevel::High,
497                    _ => SeverityLevel::Medium,
498                };
499
500                issues.push(DataFlowIssue {
501                    issue_type: DataFlowIssueType::UncontrolledInput,
502                    source: CodeLocation {
503                        file_path: analysis.file_name.clone(),
504                        line_number: None,
505                        column_number: None,
506                        function_name: Some(func.to_string()),
507                        binary_offset: None,
508                    },
509                    sink: CodeLocation {
510                        file_path: analysis.file_name.clone(),
511                        line_number: None,
512                        column_number: None,
513                        function_name: Some("unknown".to_string()),
514                        binary_offset: None,
515                    },
516                    severity,
517                    data_path: vec![func.to_string()],
518                });
519            }
520        }
521    }
522
523    // Potential data leakage through output functions
524    if has_input && has_output {
525        issues.push(DataFlowIssue {
526            issue_type: DataFlowIssueType::DataLeakage,
527            source: CodeLocation {
528                file_path: analysis.file_name.clone(),
529                line_number: None,
530                column_number: None,
531                function_name: Some("input_functions".to_string()),
532                binary_offset: None,
533            },
534            sink: CodeLocation {
535                file_path: analysis.file_name.clone(),
536                line_number: None,
537                column_number: None,
538                function_name: Some("output_functions".to_string()),
539                binary_offset: None,
540            },
541            severity: SeverityLevel::Medium,
542            data_path: vec![
543                "input".to_string(),
544                "processing".to_string(),
545                "output".to_string(),
546            ],
547        });
548    }
549
550    // Privilege escalation risks
551    let privilege_functions = ["setuid", "setgid", "seteuid", "setegid", "sudo", "su"];
552    for func in &privilege_functions {
553        if analysis.imports.contains(&func.to_string())
554            || analysis.detected_symbols.contains(&func.to_string())
555        {
556            issues.push(DataFlowIssue {
557                issue_type: DataFlowIssueType::PrivilegeEscalation,
558                source: CodeLocation {
559                    file_path: analysis.file_name.clone(),
560                    line_number: None,
561                    column_number: None,
562                    function_name: Some("user_input".to_string()),
563                    binary_offset: None,
564                },
565                sink: CodeLocation {
566                    file_path: analysis.file_name.clone(),
567                    line_number: None,
568                    column_number: None,
569                    function_name: Some(func.to_string()),
570                    binary_offset: None,
571                },
572                severity: SeverityLevel::High,
573                data_path: vec!["user_input".to_string(), func.to_string()],
574            });
575        }
576    }
577
578    // Unsanitized output to system functions
579    let system_functions = ["system", "exec", "popen"];
580    if has_input {
581        for func in &system_functions {
582            if analysis.imports.contains(&func.to_string())
583                || analysis.detected_symbols.contains(&func.to_string())
584            {
585                issues.push(DataFlowIssue {
586                    issue_type: DataFlowIssueType::UnsanitizedOutput,
587                    source: CodeLocation {
588                        file_path: analysis.file_name.clone(),
589                        line_number: None,
590                        column_number: None,
591                        function_name: Some("user_input".to_string()),
592                        binary_offset: None,
593                    },
594                    sink: CodeLocation {
595                        file_path: analysis.file_name.clone(),
596                        line_number: None,
597                        column_number: None,
598                        function_name: Some(func.to_string()),
599                        binary_offset: None,
600                    },
601                    severity: SeverityLevel::Critical,
602                    data_path: vec!["user_input".to_string(), func.to_string()],
603                });
604            }
605        }
606    }
607
608    issues
609}
610
611// Helper functions for pattern detection
612
613fn is_ip_address(s: &str) -> bool {
614    let parts: Vec<&str> = s.split('.').collect();
615    if parts.len() != 4 {
616        return false;
617    }
618
619    for part in parts {
620        if part.parse::<u8>().is_err() {
621            return false;
622        }
623    }
624    true
625}
626
627fn is_domain_name(s: &str) -> bool {
628    s.contains('.')
629        && s.len() > 4
630        && s.len() < 255
631        && !s.contains(' ')
632        && s.chars()
633            .all(|c| c.is_alphanumeric() || c == '.' || c == '-')
634}
635
636fn extract_port_number(s: &str) -> Option<u16> {
637    // Look for :port pattern
638    if let Some(colon_pos) = s.rfind(':') {
639        if let Ok(port) = s[colon_pos + 1..].parse::<u16>() {
640            return Some(port);
641        }
642    }
643
644    // Check if the entire string is a port number
645    if let Ok(port) = s.parse::<u16>() {
646        if port > 0 {
647            return Some(port);
648        }
649    }
650
651    None
652}
653
654fn is_suspicious_port(port: u16) -> bool {
655    // Common malware/hacking ports
656    matches!(
657        port,
658        31337 | 12345 | 54321 | 9999 | // Common backdoor ports
659        4444 | 5555 | 7777 | 8888 |    // Common reverse shell ports
660        6666 | 666 |                   // Suspicious numbers
661        1234 | 4321 |                  // Simple patterns
662        31338 | 31339 // Elite variations
663    )
664}
665
666fn is_likely_base64(s: &str) -> bool {
667    if s.len() < 4 || s.len() % 4 != 0 {
668        return false;
669    }
670
671    s.chars()
672        .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
673        && s.chars().filter(|&c| c == '=').count() <= 2
674}
675
676fn endpoints_found(patterns: &[NetworkPattern]) -> bool {
677    patterns
678        .iter()
679        .any(|p| !p.endpoints.is_empty() && !p.endpoints.iter().all(|e| e.contains("functions")))
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685    use chrono::Utc;
686
687    fn create_test_analysis() -> BinaryAnalysis {
688        BinaryAnalysis {
689            id: Uuid::new_v4(),
690            file_name: "test.bin".to_string(),
691            format: "elf".to_string(),
692            architecture: "x86_64".to_string(),
693            languages: vec!["C".to_string()],
694            detected_symbols: vec!["socket".to_string(), "ptrace".to_string()],
695            embedded_strings: vec!["192.168.1.1".to_string(), "vmware".to_string()],
696            suspected_secrets: vec![],
697            imports: vec![
698                "connect".to_string(),
699                "recv".to_string(),
700                "gets".to_string(),
701            ],
702            exports: vec![],
703            hash_sha256: "test".to_string(),
704            hash_blake3: None,
705            size_bytes: 1024,
706            linked_libraries: vec!["libc.so.6".to_string()],
707            static_linked: false,
708            version_info: None,
709            license_info: None,
710            metadata: serde_json::json!({}),
711            created_at: Utc::now(),
712            sbom: None,
713            binary_data: Some(vec![0x7f, 0x45, 0x4c, 0x46]),
714            entry_point: Some("0x401000".to_string()),
715            code_sections: vec![],
716        }
717    }
718
719    #[test]
720    fn test_analyze_behavioral_security() {
721        let analysis = create_test_analysis();
722        let result = analyze_behavioral_security(&analysis);
723
724        assert_eq!(result.file_path, "test.bin");
725        assert!(!result.control_flow_anomalies.is_empty());
726        assert!(!result.network_patterns.is_empty());
727        assert!(!result.data_flow_issues.is_empty());
728    }
729
730    #[test]
731    fn test_is_ip_address() {
732        assert!(is_ip_address("192.168.1.1"));
733        assert!(is_ip_address("127.0.0.1"));
734        assert!(!is_ip_address("256.1.1.1"));
735        assert!(!is_ip_address("not.an.ip"));
736    }
737
738    #[test]
739    fn test_is_suspicious_port() {
740        assert!(is_suspicious_port(31337));
741        assert!(is_suspicious_port(12345));
742        assert!(!is_suspicious_port(80));
743        assert!(!is_suspicious_port(443));
744    }
745}