nabla_cli/enterprise/secure/
crypto_analysis.rs

1#![allow(dead_code)]
2use crate::binary::BinaryAnalysis;
3use crate::enterprise::types::{CodeLocation, SeverityLevel};
4use chrono::Utc;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CryptoAnalysisResult {
11    pub analysis_id: Uuid,
12    pub file_path: String,
13    pub key_issues: Vec<KeyIssue>,
14    pub algorithm_issues: Vec<AlgorithmIssue>,
15    pub implementation_issues: Vec<CryptoImplementationIssue>,
16    pub analysis_duration_ms: u64,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct KeyIssue {
21    pub issue_type: KeyIssueType,
22    pub location: CodeLocation,
23    pub severity: SeverityLevel,
24    pub key_material: Option<String>, // Redacted/hashed for security
25    pub recommendation: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum KeyIssueType {
30    HardcodedKey,
31    WeakKey,
32    KeyReuse,
33    InsecureKeyGeneration,
34    KeyInPlaintext,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AlgorithmIssue {
39    pub algorithm_name: String,
40    pub issue_type: AlgorithmIssueType,
41    pub location: CodeLocation,
42    pub severity: SeverityLevel,
43    pub replacement_suggestion: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub enum AlgorithmIssueType {
48    Deprecated,
49    Weak,
50    Broken,
51    Misconfigured,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct CryptoImplementationIssue {
56    pub issue_description: String,
57    pub location: CodeLocation,
58    pub severity: SeverityLevel,
59    pub cwe_id: Option<String>,
60}
61
62pub fn analyze_crypto_security(analysis: &BinaryAnalysis) -> CryptoAnalysisResult {
63    let start_time = Utc::now();
64
65    let mut result = CryptoAnalysisResult {
66        analysis_id: Uuid::new_v4(),
67        file_path: analysis.file_name.clone(),
68        key_issues: Vec::new(),
69        algorithm_issues: Vec::new(),
70        implementation_issues: Vec::new(),
71        analysis_duration_ms: 0,
72    };
73
74    // Analyze key management issues
75    result.key_issues = analyze_key_issues(analysis);
76
77    // Analyze cryptographic algorithm usage
78    result.algorithm_issues = analyze_algorithm_issues(analysis);
79
80    // Analyze implementation-specific crypto issues
81    result.implementation_issues = analyze_implementation_issues(analysis);
82
83    let end_time = Utc::now();
84    result.analysis_duration_ms = (end_time - start_time).num_milliseconds() as u64;
85
86    result
87}
88
89fn analyze_key_issues(analysis: &BinaryAnalysis) -> Vec<KeyIssue> {
90    let mut issues = Vec::new();
91
92    // Check for hardcoded keys in embedded strings
93    for string in &analysis.embedded_strings {
94        if let Some(key_issue) = detect_hardcoded_key(string, &analysis.file_name) {
95            issues.push(key_issue);
96        }
97    }
98
99    // Check for weak key generation functions
100    let weak_key_gen_functions = [
101        (
102            "rand",
103            "Use cryptographically secure random number generators like /dev/urandom or CryptGenRandom",
104        ),
105        (
106            "random",
107            "Use cryptographically secure random number generators",
108        ),
109        ("srand", "Predictable seed - use secure random generators"),
110        (
111            "time",
112            "Time-based seeds are predictable - use secure entropy sources",
113        ),
114    ];
115
116    for (func, recommendation) in &weak_key_gen_functions {
117        if analysis.imports.contains(&func.to_string())
118            || analysis.detected_symbols.contains(&func.to_string())
119        {
120            issues.push(KeyIssue {
121                issue_type: KeyIssueType::InsecureKeyGeneration,
122                location: CodeLocation {
123                    file_path: analysis.file_name.clone(),
124                    line_number: None,
125                    column_number: None,
126                    function_name: Some(func.to_string()),
127                    binary_offset: None,
128                },
129                severity: SeverityLevel::High,
130                key_material: None,
131                recommendation: recommendation.to_string(),
132            });
133        }
134    }
135
136    // Check for key storage issues
137    let key_storage_functions = ["fopen", "fwrite", "fprintf"];
138    let has_file_ops = key_storage_functions.iter().any(|&func| {
139        analysis.imports.contains(&func.to_string())
140            || analysis.detected_symbols.contains(&func.to_string())
141    });
142
143    if has_file_ops && has_crypto_context(analysis) {
144        issues.push(KeyIssue {
145            issue_type: KeyIssueType::KeyInPlaintext,
146            location: CodeLocation {
147                file_path: analysis.file_name.clone(),
148                line_number: None,
149                column_number: None,
150                function_name: None,
151                binary_offset: None,
152            },
153            severity: SeverityLevel::Medium,
154            key_material: None,
155            recommendation: "Ensure cryptographic keys are encrypted before storage".to_string(),
156        });
157    }
158
159    issues
160}
161
162fn detect_hardcoded_key(string: &str, file_path: &str) -> Option<KeyIssue> {
163    // Check for various key patterns
164
165    // RSA private key headers
166    if string.contains("-----BEGIN RSA PRIVATE KEY-----")
167        || string.contains("-----BEGIN PRIVATE KEY-----")
168    {
169        return Some(KeyIssue {
170            issue_type: KeyIssueType::HardcodedKey,
171            location: CodeLocation {
172                file_path: file_path.to_string(),
173                line_number: None,
174                column_number: None,
175                function_name: None,
176                binary_offset: None,
177            },
178            severity: SeverityLevel::Critical,
179            key_material: Some(format!(
180                "RSA_KEY_{}...",
181                &string.chars().take(8).collect::<String>()
182            )),
183            recommendation: "Remove hardcoded private keys - use secure key management".to_string(),
184        });
185    }
186
187    // Certificate patterns
188    if string.contains("-----BEGIN CERTIFICATE-----") {
189        return Some(KeyIssue {
190            issue_type: KeyIssueType::HardcodedKey,
191            location: CodeLocation {
192                file_path: file_path.to_string(),
193                line_number: None,
194                column_number: None,
195                function_name: None,
196                binary_offset: None,
197            },
198            severity: SeverityLevel::Medium,
199            key_material: Some("CERTIFICATE_DATA".to_string()),
200            recommendation: "Certificates should be loaded from secure storage".to_string(),
201        });
202    }
203
204    // Base64 encoded keys (common pattern)
205    if is_likely_encoded_key(string) {
206        return Some(KeyIssue {
207            issue_type: KeyIssueType::HardcodedKey,
208            location: CodeLocation {
209                file_path: file_path.to_string(),
210                line_number: None,
211                column_number: None,
212                function_name: None,
213                binary_offset: None,
214            },
215            severity: SeverityLevel::High,
216            key_material: Some(format!(
217                "ENCODED_KEY_{}...",
218                &string.chars().take(8).collect::<String>()
219            )),
220            recommendation: "Suspected encoded key material should not be hardcoded".to_string(),
221        });
222    }
223
224    // Hexadecimal keys (32+ hex chars could be a key)
225    if is_hex_key_pattern(string) {
226        return Some(KeyIssue {
227            issue_type: KeyIssueType::HardcodedKey,
228            location: CodeLocation {
229                file_path: file_path.to_string(),
230                line_number: None,
231                column_number: None,
232                function_name: None,
233                binary_offset: None,
234            },
235            severity: SeverityLevel::High,
236            key_material: Some(format!(
237                "HEX_KEY_{}...",
238                &string.chars().take(8).collect::<String>()
239            )),
240            recommendation: "Suspected hexadecimal key should not be hardcoded".to_string(),
241        });
242    }
243
244    // API keys and tokens patterns
245    if is_api_key_pattern(string) {
246        return Some(KeyIssue {
247            issue_type: KeyIssueType::HardcodedKey,
248            location: CodeLocation {
249                file_path: file_path.to_string(),
250                line_number: None,
251                column_number: None,
252                function_name: None,
253                binary_offset: None,
254            },
255            severity: SeverityLevel::High,
256            key_material: Some("API_KEY_[REDACTED]".to_string()),
257            recommendation:
258                "API keys should be loaded from environment variables or secure configuration"
259                    .to_string(),
260        });
261    }
262
263    None
264}
265
266fn analyze_algorithm_issues(analysis: &BinaryAnalysis) -> Vec<AlgorithmIssue> {
267    let mut issues = Vec::new();
268
269    // Define deprecated/weak algorithms
270    let weak_algorithms: HashMap<&str, (AlgorithmIssueType, SeverityLevel, Option<&str>)> =
271        HashMap::from([
272            (
273                "md5",
274                (
275                    AlgorithmIssueType::Broken,
276                    SeverityLevel::High,
277                    Some("SHA-256 or SHA-3"),
278                ),
279            ),
280            (
281                "sha1",
282                (
283                    AlgorithmIssueType::Weak,
284                    SeverityLevel::Medium,
285                    Some("SHA-256 or SHA-3"),
286                ),
287            ),
288            (
289                "des",
290                (
291                    AlgorithmIssueType::Broken,
292                    SeverityLevel::Critical,
293                    Some("AES"),
294                ),
295            ),
296            (
297                "3des",
298                (
299                    AlgorithmIssueType::Deprecated,
300                    SeverityLevel::Medium,
301                    Some("AES"),
302                ),
303            ),
304            (
305                "rc4",
306                (
307                    AlgorithmIssueType::Broken,
308                    SeverityLevel::Critical,
309                    Some("AES or ChaCha20"),
310                ),
311            ),
312            (
313                "rc2",
314                (AlgorithmIssueType::Broken, SeverityLevel::High, Some("AES")),
315            ),
316            (
317                "blowfish",
318                (AlgorithmIssueType::Weak, SeverityLevel::Medium, Some("AES")),
319            ),
320            (
321                "md4",
322                (
323                    AlgorithmIssueType::Broken,
324                    SeverityLevel::Critical,
325                    Some("SHA-256 or SHA-3"),
326                ),
327            ),
328            (
329                "md2",
330                (
331                    AlgorithmIssueType::Broken,
332                    SeverityLevel::Critical,
333                    Some("SHA-256 or SHA-3"),
334                ),
335            ),
336        ]);
337
338    // Check for weak algorithms in strings and function names
339    for string in analysis
340        .embedded_strings
341        .iter()
342        .chain(analysis.imports.iter())
343        .chain(analysis.detected_symbols.iter())
344    {
345        let lower = string.to_lowercase();
346
347        for (algorithm, (issue_type, severity, replacement)) in &weak_algorithms {
348            if lower.contains(algorithm) || lower.contains(&algorithm.to_uppercase()) {
349                issues.push(AlgorithmIssue {
350                    algorithm_name: algorithm.to_uppercase(),
351                    issue_type: issue_type.clone(),
352                    location: CodeLocation {
353                        file_path: analysis.file_name.clone(),
354                        line_number: None,
355                        column_number: None,
356                        function_name: if analysis.imports.contains(string)
357                            || analysis.detected_symbols.contains(string)
358                        {
359                            Some(string.clone())
360                        } else {
361                            None
362                        },
363                        binary_offset: None,
364                    },
365                    severity: severity.clone(),
366                    replacement_suggestion: replacement.map(|s| s.to_string()),
367                });
368            }
369        }
370    }
371
372    // Check for crypto libraries and their algorithm usage
373    for lib in &analysis.linked_libraries {
374        let lower = lib.to_lowercase();
375
376        // OpenSSL version analysis
377        if lower.contains("openssl") || lower.contains("libssl") || lower.contains("libcrypto") {
378            // Check for old OpenSSL versions (very basic detection)
379            if lower.contains("0.9") || lower.contains("1.0.0") {
380                issues.push(AlgorithmIssue {
381                    algorithm_name: "OpenSSL".to_string(),
382                    issue_type: AlgorithmIssueType::Deprecated,
383                    location: CodeLocation {
384                        file_path: analysis.file_name.clone(),
385                        line_number: None,
386                        column_number: None,
387                        function_name: None,
388                        binary_offset: None,
389                    },
390                    severity: SeverityLevel::High,
391                    replacement_suggestion: Some("Update to OpenSSL 1.1.1+ or 3.x".to_string()),
392                });
393            }
394        }
395    }
396
397    // Check for RSA key size issues (basic heuristic)
398    if analysis
399        .embedded_strings
400        .iter()
401        .any(|s| s.contains("1024") && s.to_lowercase().contains("rsa"))
402    {
403        issues.push(AlgorithmIssue {
404            algorithm_name: "RSA-1024".to_string(),
405            issue_type: AlgorithmIssueType::Weak,
406            location: CodeLocation {
407                file_path: analysis.file_name.clone(),
408                line_number: None,
409                column_number: None,
410                function_name: None,
411                binary_offset: None,
412            },
413            severity: SeverityLevel::Medium,
414            replacement_suggestion: Some("Use RSA-2048 or higher, or consider ECDSA".to_string()),
415        });
416    }
417
418    issues
419}
420
421fn analyze_implementation_issues(analysis: &BinaryAnalysis) -> Vec<CryptoImplementationIssue> {
422    let mut issues = Vec::new();
423
424    // Check for ECB mode usage (Electronic Codebook - insecure)
425    if analysis
426        .embedded_strings
427        .iter()
428        .any(|s| s.to_lowercase().contains("ecb"))
429    {
430        issues.push(CryptoImplementationIssue {
431            issue_description:
432                "ECB (Electronic Codebook) mode detected - insecure block cipher mode".to_string(),
433            location: CodeLocation {
434                file_path: analysis.file_name.clone(),
435                line_number: None,
436                column_number: None,
437                function_name: None,
438                binary_offset: None,
439            },
440            severity: SeverityLevel::High,
441            cwe_id: Some("CWE-327".to_string()),
442        });
443    }
444
445    // Check for hardcoded IV/salt patterns
446    let iv_salt_indicators = ["iv", "salt", "nonce", "vector"];
447    for indicator in &iv_salt_indicators {
448        if analysis.embedded_strings.iter().any(|s| {
449            let lower = s.to_lowercase();
450            lower.contains(indicator) && (s.len() > 16 && s.chars().all(|c| c.is_ascii_hexdigit()))
451        }) {
452            issues.push(CryptoImplementationIssue {
453                issue_description: format!("Potential hardcoded {} detected", indicator)
454                    .to_string(),
455                location: CodeLocation {
456                    file_path: analysis.file_name.clone(),
457                    line_number: None,
458                    column_number: None,
459                    function_name: None,
460                    binary_offset: None,
461                },
462                severity: SeverityLevel::Medium,
463                cwe_id: Some("CWE-330".to_string()),
464            });
465        }
466    }
467
468    // Check for insecure random number generation in crypto context
469    if has_crypto_context(analysis) {
470        let insecure_rng = ["rand", "random", "srand"];
471        for rng_func in &insecure_rng {
472            if analysis.imports.contains(&rng_func.to_string())
473                || analysis.detected_symbols.contains(&rng_func.to_string())
474            {
475                issues.push(CryptoImplementationIssue {
476                    issue_description: format!(
477                        "Insecure RNG ({}) used in cryptographic context",
478                        rng_func
479                    ),
480                    location: CodeLocation {
481                        file_path: analysis.file_name.clone(),
482                        line_number: None,
483                        column_number: None,
484                        function_name: Some(rng_func.to_string()),
485                        binary_offset: None,
486                    },
487                    severity: SeverityLevel::High,
488                    cwe_id: Some("CWE-338".to_string()),
489                });
490            }
491        }
492    }
493
494    // Check for timing attack vulnerabilities
495    let timing_vuln_functions = ["strcmp", "memcmp"];
496    let has_crypto_functions = analysis
497        .imports
498        .iter()
499        .chain(analysis.detected_symbols.iter())
500        .any(|s| {
501            s.to_lowercase().contains("crypto")
502                || s.to_lowercase().contains("hash")
503                || s.to_lowercase().contains("cipher")
504        });
505
506    if has_crypto_functions {
507        for func in &timing_vuln_functions {
508            if analysis.imports.contains(&func.to_string())
509                || analysis.detected_symbols.contains(&func.to_string())
510            {
511                issues.push(CryptoImplementationIssue {
512                    issue_description: format!(
513                        "Timing attack vulnerability: {} used in crypto context",
514                        func
515                    ),
516                    location: CodeLocation {
517                        file_path: analysis.file_name.clone(),
518                        line_number: None,
519                        column_number: None,
520                        function_name: Some(func.to_string()),
521                        binary_offset: None,
522                    },
523                    severity: SeverityLevel::Medium,
524                    cwe_id: Some("CWE-208".to_string()),
525                });
526            }
527        }
528    }
529
530    // Check for SSL/TLS implementation issues
531    let ssl_functions = ["SSL_CTX_new", "SSL_new", "TLS_method"];
532    let has_ssl = ssl_functions.iter().any(|&func| {
533        analysis.imports.contains(&func.to_string())
534            || analysis.detected_symbols.contains(&func.to_string())
535    });
536
537    if has_ssl {
538        // Check for certificate verification bypass
539        let dangerous_ssl_funcs = ["SSL_CTX_set_verify_mode", "SSL_set_verify"];
540        for func in &dangerous_ssl_funcs {
541            if analysis.imports.contains(&func.to_string())
542                || analysis.detected_symbols.contains(&func.to_string())
543            {
544                issues.push(CryptoImplementationIssue {
545                    issue_description: "SSL/TLS certificate verification may be disabled"
546                        .to_string(),
547                    location: CodeLocation {
548                        file_path: analysis.file_name.clone(),
549                        line_number: None,
550                        column_number: None,
551                        function_name: Some(func.to_string()),
552                        binary_offset: None,
553                    },
554                    severity: SeverityLevel::High,
555                    cwe_id: Some("CWE-295".to_string()),
556                });
557            }
558        }
559
560        // Check for insecure TLS versions
561        if analysis
562            .embedded_strings
563            .iter()
564            .any(|s| s.contains("SSLv3") || s.contains("TLSv1.0") || s.contains("TLSv1.1"))
565        {
566            issues.push(CryptoImplementationIssue {
567                issue_description: "Insecure SSL/TLS version detected".to_string(),
568                location: CodeLocation {
569                    file_path: analysis.file_name.clone(),
570                    line_number: None,
571                    column_number: None,
572                    function_name: None,
573                    binary_offset: None,
574                },
575                severity: SeverityLevel::High,
576                cwe_id: Some("CWE-326".to_string()),
577            });
578        }
579    }
580
581    issues
582}
583
584// Helper functions
585
586fn has_crypto_context(analysis: &BinaryAnalysis) -> bool {
587    let crypto_indicators = [
588        "crypto", "cipher", "encrypt", "decrypt", "hash", "hmac", "aes", "rsa", "ecdsa", "ssl",
589        "tls", "key", "openssl",
590    ];
591
592    for item in analysis
593        .imports
594        .iter()
595        .chain(analysis.detected_symbols.iter())
596        .chain(analysis.embedded_strings.iter())
597        .chain(analysis.linked_libraries.iter())
598    {
599        let lower = item.to_lowercase();
600        if crypto_indicators
601            .iter()
602            .any(|&indicator| lower.contains(indicator))
603        {
604            return true;
605        }
606    }
607    false
608}
609
610fn is_likely_encoded_key(string: &str) -> bool {
611    // Base64 pattern: 32+ chars, base64 alphabet, proper padding
612    if string.len() >= 32 && string.len() % 4 == 0 {
613        let base64_chars = string
614            .chars()
615            .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=');
616        let padding_count = string.chars().filter(|&c| c == '=').count();
617
618        if base64_chars && padding_count <= 2 {
619            // Additional heuristic: high entropy
620            let entropy = calculate_entropy_simple(string);
621            return entropy > 4.5;
622        }
623    }
624    false
625}
626
627fn is_hex_key_pattern(string: &str) -> bool {
628    // 32+ hex characters (16+ bytes, suitable for keys)
629    string.len() >= 32 && 
630    string.len() <= 128 && // Reasonable upper bound
631    string.chars().all(|c| c.is_ascii_hexdigit())
632}
633
634fn is_api_key_pattern(string: &str) -> bool {
635    let lower = string.to_lowercase();
636
637    // Common API key prefixes
638    let api_prefixes = [
639        "api_key",
640        "apikey",
641        "api-key",
642        "secret_key",
643        "secretkey",
644        "secret-key",
645        "access_token",
646        "accesstoken",
647        "access-token",
648        "bearer",
649        "jwt",
650        "aws_access_key",
651        "aws_secret",
652        "gcp_key",
653        "azure_key",
654    ];
655
656    for prefix in &api_prefixes {
657        if lower.contains(prefix) && string.len() > 20 {
658            return true;
659        }
660    }
661
662    // Pattern-based detection for common API key formats
663    if string.len() >= 20 {
664        // AWS-style keys
665        if string.starts_with("AKIA") || string.starts_with("ASIA") {
666            return true;
667        }
668
669        // Google API keys
670        if string.starts_with("AIza") && string.len() == 39 {
671            return true;
672        }
673
674        // GitHub tokens
675        if string.starts_with("ghp_") || string.starts_with("gho_") || string.starts_with("ghu_") {
676            return true;
677        }
678    }
679
680    false
681}
682
683fn calculate_entropy_simple(string: &str) -> f32 {
684    let mut char_counts = HashMap::new();
685    for c in string.chars() {
686        *char_counts.entry(c).or_insert(0) += 1;
687    }
688
689    let len = string.len() as f32;
690    let mut entropy = 0.0;
691
692    for count in char_counts.values() {
693        let p = *count as f32 / len;
694        entropy -= p * p.log2();
695    }
696
697    entropy
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703    use chrono::Utc;
704
705    fn create_test_analysis() -> BinaryAnalysis {
706        BinaryAnalysis {
707            id: Uuid::new_v4(),
708            file_name: "test.bin".to_string(),
709            format: "elf".to_string(),
710            architecture: "x86_64".to_string(),
711            languages: vec!["C".to_string()],
712            detected_symbols: vec!["md5".to_string(), "rand".to_string()],
713            embedded_strings: vec![
714                "-----BEGIN RSA PRIVATE KEY-----".to_string(),
715                "des_encrypt".to_string(),
716                "ecb_mode".to_string(),
717            ],
718            suspected_secrets: vec![],
719            imports: vec!["SSL_CTX_new".to_string(), "strcmp".to_string()],
720            exports: vec![],
721            hash_sha256: "test".to_string(),
722            hash_blake3: None,
723            size_bytes: 1024,
724            linked_libraries: vec!["libssl.so.1.0.0".to_string()],
725            static_linked: false,
726            version_info: None,
727            license_info: None,
728            metadata: serde_json::json!({}),
729            created_at: Utc::now(),
730            sbom: None,
731            binary_data: Some(vec![0x7f, 0x45, 0x4c, 0x46]),
732            entry_point: Some("0x401000".to_string()),
733            code_sections: vec![],
734        }
735    }
736
737    #[test]
738    fn test_analyze_crypto_security() {
739        let analysis = create_test_analysis();
740        let result = analyze_crypto_security(&analysis);
741
742        assert_eq!(result.file_path, "test.bin");
743        assert!(!result.key_issues.is_empty());
744        assert!(!result.algorithm_issues.is_empty());
745        assert!(!result.implementation_issues.is_empty());
746    }
747
748    #[test]
749    fn test_detect_hardcoded_key() {
750        let key_string = "-----BEGIN RSA PRIVATE KEY-----";
751        let issue = detect_hardcoded_key(key_string, "test.bin");
752        assert!(issue.is_some());
753        assert!(matches!(
754            issue.unwrap().issue_type,
755            KeyIssueType::HardcodedKey
756        ));
757    }
758
759    #[test]
760    fn test_is_hex_key_pattern() {
761        assert!(is_hex_key_pattern("0123456789abcdef0123456789abcdef"));
762        assert!(!is_hex_key_pattern("short"));
763        assert!(!is_hex_key_pattern("not_hex_at_all"));
764    }
765
766    #[test]
767    fn test_is_api_key_pattern() {
768        assert!(is_api_key_pattern("api_key_abcdef123456789"));
769        assert!(is_api_key_pattern("AKIA1234567890123456"));
770        assert!(!is_api_key_pattern("normal_string"));
771    }
772}