Skip to main content

mir_extractor/rules/
crypto.rs

1//! Cryptographic security rules.
2//!
3//! Rules detecting weak or insecure cryptographic practices:
4//! - Weak hash algorithms (MD5, SHA-1, RIPEMD, CRC32)
5//! - Hardcoded cryptographic keys
6//! - Weak cipher algorithms (DES, 3DES, RC4)
7//! - Predictable randomness
8//! - Timing attack vulnerabilities
9//! - TLS verification disabled
10
11use crate::{
12    Confidence, Exploitability, Finding, MirPackage, Rule, RuleMetadata, RuleOrigin, Severity,
13};
14use std::ffi::OsStr;
15use std::fs;
16use std::path::Path;
17use walkdir::WalkDir;
18
19// ============================================================================
20// Helper Functions
21// ============================================================================
22
23pub(crate) fn line_contains_md5_usage(line: &str) -> bool {
24    let lower = line.to_lowercase();
25    let mut search_start = 0;
26
27    while let Some(relative_idx) = lower[search_start..].find("md5") {
28        let idx = search_start + relative_idx;
29
30        let mut before_chars = lower[..idx].chars().rev().skip_while(|c| c.is_whitespace());
31        let mut after_chars = lower[idx + 3..].chars().skip_while(|c| c.is_whitespace());
32
33        let after_matches = matches!(
34            (after_chars.next(), after_chars.next()),
35            (Some(':'), Some(':'))
36        );
37
38        let before_first = before_chars.next();
39        let before_second = before_chars.next();
40        let before_matches = matches!((before_first, before_second), (Some(':'), Some(':')));
41
42        if before_matches || after_matches {
43            return true;
44        }
45
46        search_start = idx + 3;
47    }
48
49    false
50}
51
52pub(crate) fn line_contains_sha1_usage(line: &str) -> bool {
53    let lower = line.to_lowercase();
54    lower.contains("sha1::") || lower.contains("::sha1")
55}
56
57pub(crate) fn line_contains_weak_hash_extended(line: &str) -> bool {
58    let lower = line.to_lowercase();
59
60    // Skip const string assignments and hex dumps entirely
61    if lower.contains("= [const \"") || lower.contains("const \"") {
62        return false;
63    }
64    if lower.starts_with("0x") || (lower.contains("0x") && lower.contains("│")) {
65        return false;
66    }
67
68    // RIPEMD family (all variants are deprecated)
69    if lower.contains("ripemd") {
70        if lower.contains("ripemd::")
71            || lower.contains("::ripemd")
72            || lower.contains("ripemd128")
73            || lower.contains("ripemd160")
74            || lower.contains("ripemd256")
75            || lower.contains("ripemd320")
76        {
77            return true;
78        }
79    }
80
81    // CRC family (non-cryptographic checksums)
82    if lower.contains("crc") {
83        if lower.contains("crc::")
84            || lower.contains("::crc")
85            || lower.contains("crc32")
86            || lower.contains("crc_32")
87            || lower.contains("crc16")
88            || lower.contains("crc_16")
89            || lower.contains("crc64")
90            || lower.contains("crc_64")
91        {
92            return true;
93        }
94    }
95
96    // Adler32 (non-cryptographic checksum)
97    if lower.contains("adler")
98        && (lower.contains("adler::") || lower.contains("::adler") || lower.contains("adler32"))
99    {
100        return true;
101    }
102
103    false
104}
105
106fn filter_entry(entry: &walkdir::DirEntry) -> bool {
107    let name = entry.file_name().to_string_lossy();
108    !name.starts_with('.') && name != "target"
109}
110
111// ============================================================================
112// RUSTCOLA004: Insecure MD5 Hash
113// ============================================================================
114
115pub struct InsecureMd5Rule {
116    metadata: RuleMetadata,
117}
118
119impl InsecureMd5Rule {
120    pub fn new() -> Self {
121        Self {
122            metadata: RuleMetadata {
123                id: "RUSTCOLA004".to_string(),
124                name: "insecure-hash-md5".to_string(),
125                short_description: "Usage of MD5 hashing".to_string(),
126                full_description: "Detects calls into md5 hashing APIs which are considered cryptographically broken.".to_string(),
127                help_uri: None,
128                default_severity: Severity::High,
129                origin: RuleOrigin::BuiltIn,
130                cwe_ids: Vec::new(),
131                fix_suggestion: None,
132                exploitability: Exploitability::default(),
133            },
134        }
135    }
136}
137
138impl Rule for InsecureMd5Rule {
139    fn metadata(&self) -> &RuleMetadata {
140        &self.metadata
141    }
142
143    fn evaluate(
144        &self,
145        package: &MirPackage,
146        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
147    ) -> Vec<Finding> {
148        let mut findings = Vec::new();
149
150        for function in &package.functions {
151            let evidence: Vec<String> = function
152                .body
153                .iter()
154                .filter(|line| line_contains_md5_usage(line))
155                .map(|line| line.trim().to_string())
156                .collect();
157            if evidence.is_empty() {
158                continue;
159            }
160
161            findings.push(Finding {
162                rule_id: self.metadata.id.clone(),
163                rule_name: self.metadata.name.clone(),
164                severity: self.metadata.default_severity,
165                message: format!("Insecure MD5 hashing detected in `{}`", function.name),
166                function: function.name.clone(),
167                function_signature: function.signature.clone(),
168                evidence,
169                span: function.span.clone(),
170                confidence: Confidence::Medium,
171                cwe_ids: Vec::new(),
172                fix_suggestion: None,
173                code_snippet: None,
174                exploitability: Exploitability::default(),
175                exploitability_score: Exploitability::default().score(),
176                ..Default::default()
177            });
178        }
179
180        findings
181    }
182}
183
184// ============================================================================
185// RUSTCOLA005: Insecure SHA-1 Hash
186// ============================================================================
187
188pub struct InsecureSha1Rule {
189    metadata: RuleMetadata,
190}
191
192impl InsecureSha1Rule {
193    pub fn new() -> Self {
194        Self {
195            metadata: RuleMetadata {
196                id: "RUSTCOLA005".to_string(),
197                name: "insecure-hash-sha1".to_string(),
198                short_description: "Usage of SHA-1 hashing".to_string(),
199                full_description: "Detects SHA-1 hashing APIs which are deprecated for security-sensitive contexts.".to_string(),
200                help_uri: None,
201                default_severity: Severity::High,
202                origin: RuleOrigin::BuiltIn,
203                cwe_ids: Vec::new(),
204                fix_suggestion: None,
205                exploitability: Exploitability::default(),
206            },
207        }
208    }
209}
210
211impl Rule for InsecureSha1Rule {
212    fn metadata(&self) -> &RuleMetadata {
213        &self.metadata
214    }
215
216    fn evaluate(
217        &self,
218        package: &MirPackage,
219        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
220    ) -> Vec<Finding> {
221        let mut findings = Vec::new();
222
223        for function in &package.functions {
224            if function.name.contains("line_contains_sha1_usage") {
225                continue;
226            }
227
228            let evidence: Vec<String> = function
229                .body
230                .iter()
231                .filter(|line| line_contains_sha1_usage(line))
232                .map(|line| line.trim().to_string())
233                .collect();
234            if evidence.is_empty() {
235                continue;
236            }
237
238            findings.push(Finding {
239                rule_id: self.metadata.id.clone(),
240                rule_name: self.metadata.name.clone(),
241                severity: self.metadata.default_severity,
242                message: format!("Insecure SHA-1 hashing detected in `{}`", function.name),
243                function: function.name.clone(),
244                function_signature: function.signature.clone(),
245                evidence,
246                span: function.span.clone(),
247                confidence: Confidence::Medium,
248                cwe_ids: Vec::new(),
249                fix_suggestion: None,
250                code_snippet: None,
251                exploitability: Exploitability::default(),
252                exploitability_score: Exploitability::default().score(),
253                ..Default::default()
254            });
255        }
256
257        findings
258    }
259}
260
261// ============================================================================
262// RUSTCOLA062: Weak Hashing Extended (RIPEMD, CRC, Adler)
263// ============================================================================
264
265pub struct WeakHashingExtendedRule {
266    metadata: RuleMetadata,
267}
268
269impl WeakHashingExtendedRule {
270    pub fn new() -> Self {
271        Self {
272            metadata: RuleMetadata {
273                id: "RUSTCOLA062".to_string(),
274                name: "weak-hashing-extended".to_string(),
275                short_description: "Usage of weak cryptographic hash algorithms".to_string(),
276                full_description: "Detects usage of weak or deprecated cryptographic hash algorithms beyond MD5/SHA-1, including RIPEMD (all variants), CRC32, CRC32Fast, and Adler32. These algorithms should not be used for security-sensitive operations like password hashing, authentication tokens, or cryptographic signatures. Use SHA-256, SHA-3, BLAKE2, or BLAKE3 instead.".to_string(),
277                help_uri: None,
278                default_severity: Severity::High,
279                origin: RuleOrigin::BuiltIn,
280                cwe_ids: Vec::new(),
281                fix_suggestion: None,
282                exploitability: Exploitability::default(),
283            },
284        }
285    }
286}
287
288impl Rule for WeakHashingExtendedRule {
289    fn metadata(&self) -> &RuleMetadata {
290        &self.metadata
291    }
292
293    fn evaluate(
294        &self,
295        package: &MirPackage,
296        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
297    ) -> Vec<Finding> {
298        if package.crate_name == "mir-extractor" {
299            return Vec::new();
300        }
301
302        let mut findings = Vec::new();
303
304        for function in &package.functions {
305            if function.name.contains("WeakHashingExtendedRule")
306                || function.name.contains("line_contains_weak_hash_extended")
307            {
308                continue;
309            }
310
311            let evidence: Vec<String> = function
312                .body
313                .iter()
314                .filter(|line| line_contains_weak_hash_extended(line))
315                .map(|line| line.trim().to_string())
316                .collect();
317
318            if evidence.is_empty() {
319                continue;
320            }
321
322            findings.push(Finding {
323                rule_id: self.metadata.id.clone(),
324                rule_name: self.metadata.name.clone(),
325                severity: self.metadata.default_severity,
326                message: format!(
327                    "Weak cryptographic hash algorithm detected in `{}`",
328                    function.name
329                ),
330                function: function.name.clone(),
331                function_signature: function.signature.clone(),
332                evidence,
333                span: function.span.clone(),
334                confidence: Confidence::Medium,
335                cwe_ids: Vec::new(),
336                fix_suggestion: None,
337                code_snippet: None,
338                exploitability: Exploitability::default(),
339                exploitability_score: Exploitability::default().score(),
340                ..Default::default()
341            });
342        }
343
344        findings
345    }
346}
347
348// ============================================================================
349// RUSTCOLA039: Hardcoded Crypto Key
350// ============================================================================
351
352pub struct HardcodedCryptoKeyRule {
353    metadata: RuleMetadata,
354}
355
356impl HardcodedCryptoKeyRule {
357    pub fn new() -> Self {
358        Self {
359            metadata: RuleMetadata {
360                id: "RUSTCOLA039".to_string(),
361                name: "hardcoded-crypto-key".to_string(),
362                short_description: "Hard-coded cryptographic key or IV".to_string(),
363                full_description: "Detects hard-coded cryptographic keys, initialization vectors, or secrets in source code. Embedded secrets cannot be rotated without code changes, enable credential theft if the binary is reverse-engineered, and violate security best practices. Use environment variables, configuration files, or secret management services instead.".to_string(),
364                help_uri: Some("https://cwe.mitre.org/data/definitions/798.html".to_string()),
365                default_severity: Severity::High,
366                origin: RuleOrigin::BuiltIn,
367                cwe_ids: Vec::new(),
368                fix_suggestion: None,
369                exploitability: Exploitability::default(),
370            },
371        }
372    }
373
374    fn crypto_key_patterns() -> &'static [&'static str] {
375        &[
376            "Aes128::new",
377            "Aes192::new",
378            "Aes256::new",
379            "ChaCha20::new",
380            "ChaCha20Poly1305::new",
381            "Hmac::new_from_slice",
382            "GenericArray::from_slice",
383            "Key::from_slice",
384            "Cipher::new",
385        ]
386    }
387
388    fn suspicious_var_names() -> &'static [&'static str] {
389        &["key", "secret", "password", "token", "iv", "nonce", "salt"]
390    }
391
392    fn is_suspicious_assignment(line: &str, pattern: &str) -> bool {
393        let lower_line = line.to_lowercase();
394        let lower_pattern = pattern.to_lowercase();
395
396        if !lower_line.contains(&lower_pattern) {
397            return false;
398        }
399
400        if !line.contains('=') {
401            return false;
402        }
403
404        let parts: Vec<&str> = line.splitn(2, '=').collect();
405        if parts.len() != 2 {
406            return false;
407        }
408
409        let left_side = parts[0].trim().to_lowercase();
410        let right_side = parts[1].trim();
411
412        let has_pattern_in_identifier = Self::has_word_boundary_match(&left_side, &lower_pattern);
413
414        if !has_pattern_in_identifier {
415            return false;
416        }
417
418        // Check if right side contains a literal value
419        if right_side.contains("b\"") || right_side.contains("b'") {
420            return true;
421        }
422        if right_side.contains("&[") || right_side.contains("[0x") || right_side.contains("[0u8") {
423            return true;
424        }
425        if right_side.starts_with('"') && right_side.len() > 30 {
426            // Filter out URL paths - these are not secrets
427            if Self::is_likely_url_path(right_side) {
428                return false;
429            }
430            return true;
431        }
432        if right_side.starts_with('"')
433            && right_side.chars().filter(|c| c.is_ascii_hexdigit()).count() > 20
434        {
435            return true;
436        }
437
438        false
439    }
440
441    /// Check if a string value looks like a URL path rather than a secret
442    fn is_likely_url_path(value: &str) -> bool {
443        let lower = value.to_lowercase();
444        // URL paths start with "/" or contain path patterns
445        lower.contains("\"/") ||           // Starts with /
446        lower.contains("http://") ||
447        lower.contains("https://") ||
448        lower.contains("/api/") ||
449        lower.contains("/v1/") ||
450        lower.contains("/v2/") ||
451        lower.contains("/v3/") ||
452        lower.contains("/v4/") ||
453        lower.contains("/auth/") ||
454        lower.contains("/oauth/") ||
455        lower.contains("/token/") ||       // Token endpoint path
456        lower.contains("/configure/") ||
457        lower.contains("/admin/")
458    }
459
460    fn has_word_boundary_match(text: &str, pattern: &str) -> bool {
461        if let Some(pos) = text.find(pattern) {
462            let before_ok = if pos == 0 {
463                true
464            } else {
465                let char_before = text.chars().nth(pos - 1).unwrap_or(' ');
466                !char_before.is_alphanumeric() || char_before == '_'
467            };
468
469            let after_pos = pos + pattern.len();
470            let after_ok = if after_pos >= text.len() {
471                true
472            } else {
473                let char_after = text.chars().nth(after_pos).unwrap_or(' ');
474                !char_after.is_alphanumeric()
475            };
476
477            before_ok && after_ok
478        } else {
479            false
480        }
481    }
482}
483
484impl Rule for HardcodedCryptoKeyRule {
485    fn metadata(&self) -> &RuleMetadata {
486        &self.metadata
487    }
488
489    fn evaluate(
490        &self,
491        package: &MirPackage,
492        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
493    ) -> Vec<Finding> {
494        if package.crate_name == "mir-extractor" {
495            return Vec::new();
496        }
497
498        let mut findings = Vec::new();
499        let crate_root = Path::new(&package.crate_root);
500
501        if !crate_root.exists() {
502            return findings;
503        }
504
505        for entry in WalkDir::new(crate_root)
506            .into_iter()
507            .filter_entry(|e| filter_entry(e))
508        {
509            let entry = match entry {
510                Ok(e) => e,
511                Err(_) => continue,
512            };
513
514            if !entry.file_type().is_file() {
515                continue;
516            }
517
518            let path = entry.path();
519            if path.extension() != Some(OsStr::new("rs")) {
520                continue;
521            }
522
523            let rel_path = path
524                .strip_prefix(crate_root)
525                .unwrap_or(path)
526                .to_string_lossy()
527                .replace('\\', "/");
528
529            let content = match fs::read_to_string(path) {
530                Ok(c) => c,
531                Err(_) => continue,
532            };
533
534            let lines: Vec<&str> = content.lines().collect();
535
536            for (idx, line) in lines.iter().enumerate() {
537                let trimmed = line.trim();
538
539                if trimmed.starts_with("//") || trimmed.starts_with("/*") {
540                    continue;
541                }
542
543                for pattern in Self::crypto_key_patterns() {
544                    if trimmed.contains(pattern) {
545                        if trimmed.contains("b\"")
546                            || trimmed.contains("&[")
547                            || trimmed.contains("[0x")
548                        {
549                            let location = format!("{}:{}", rel_path, idx + 1);
550
551                            findings.push(Finding::new(
552                                self.metadata.id.clone(),
553                                self.metadata.name.clone(),
554                                self.metadata.default_severity,
555                                "Hard-coded cryptographic key or IV detected in source code",
556                                location,
557                                pattern.to_string(),
558                                vec![trimmed.to_string()],
559                                None,
560                            ));
561                        }
562                    }
563                }
564
565                for var_pattern in Self::suspicious_var_names() {
566                    if Self::is_suspicious_assignment(trimmed, var_pattern) {
567                        let location = format!("{}:{}", rel_path, idx + 1);
568
569                        findings.push(Finding::new(
570                            self.metadata.id.clone(),
571                            self.metadata.name.clone(),
572                            self.metadata.default_severity,
573                            format!(
574                                "Potential hard-coded secret in variable containing '{}'",
575                                var_pattern
576                            ),
577                            location,
578                            var_pattern.to_string(),
579                            vec![trimmed.to_string()],
580                            None,
581                        ));
582                    }
583                }
584            }
585        }
586
587        findings
588    }
589}
590
591// ============================================================================
592// RUSTCOLA044: Timing Attack (Non-constant-time secret comparison)
593// ============================================================================
594
595pub struct TimingAttackRule {
596    metadata: RuleMetadata,
597}
598
599impl TimingAttackRule {
600    pub fn new() -> Self {
601        Self {
602            metadata: RuleMetadata {
603                id: "RUSTCOLA044".to_string(),
604                name: "timing-attack-secret-comparison".to_string(),
605                short_description: "Non-constant-time secret comparison".to_string(),
606                full_description: "Detects comparisons of secrets (passwords, tokens, HMACs) using non-constant-time operations like == or .starts_with(). These can leak information through timing side-channels. Use constant_time_eq or subtle::ConstantTimeEq instead.".to_string(),
607                help_uri: Some("https://codahale.com/a-lesson-in-timing-attacks/".to_string()),
608                default_severity: Severity::High,
609                origin: RuleOrigin::BuiltIn,
610                cwe_ids: Vec::new(),
611                fix_suggestion: None,
612                exploitability: Exploitability::default(),
613            },
614        }
615    }
616
617    fn looks_like_secret(var_name: &str) -> bool {
618        let lowered = var_name.to_lowercase();
619        let secret_markers = [
620            "password",
621            "passwd",
622            "pwd",
623            "token",
624            "secret",
625            "key",
626            "hmac",
627            "signature",
628            "auth",
629            "credential",
630            "api_key",
631            "apikey",
632        ];
633        secret_markers.iter().any(|marker| lowered.contains(marker))
634    }
635
636    fn is_non_constant_time_comparison(line: &str) -> bool {
637        let lowered = line.to_lowercase();
638
639        if lowered.contains("constant_time_eq")
640            || lowered.contains("constanttimeeq")
641            || lowered.contains("subtle::")
642        {
643            return false;
644        }
645
646        if lowered.contains(" == ")
647            || lowered.contains(" != ")
648            || lowered.contains(".eq(")
649            || lowered.contains(".ne(")
650            || lowered.contains(".starts_with(")
651            || lowered.contains(".ends_with(")
652        {
653            let words: Vec<&str> = line
654                .split(&[' ', '(', ')', ',', ';', '=', '!'][..])
655                .filter(|w| !w.is_empty())
656                .collect();
657
658            return words.iter().any(|w| Self::looks_like_secret(w));
659        }
660
661        false
662    }
663}
664
665impl Rule for TimingAttackRule {
666    fn metadata(&self) -> &RuleMetadata {
667        &self.metadata
668    }
669
670    fn evaluate(
671        &self,
672        package: &MirPackage,
673        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
674    ) -> Vec<Finding> {
675        if package.crate_name == "mir-extractor" {
676            return Vec::new();
677        }
678
679        let mut findings = Vec::new();
680        let crate_root = Path::new(&package.crate_root);
681
682        if !crate_root.exists() {
683            return findings;
684        }
685
686        for entry in WalkDir::new(crate_root)
687            .into_iter()
688            .filter_entry(|e| filter_entry(e))
689        {
690            let entry = match entry {
691                Ok(e) => e,
692                Err(_) => continue,
693            };
694
695            if !entry.file_type().is_file() {
696                continue;
697            }
698
699            let path = entry.path();
700            if path.extension() != Some(OsStr::new("rs")) {
701                continue;
702            }
703
704            let rel_path = path
705                .strip_prefix(crate_root)
706                .unwrap_or(path)
707                .to_string_lossy()
708                .replace('\\', "/");
709
710            let content = match fs::read_to_string(path) {
711                Ok(c) => c,
712                Err(_) => continue,
713            };
714
715            for (idx, line) in content.lines().enumerate() {
716                if line.trim().starts_with("//") {
717                    continue;
718                }
719
720                if Self::is_non_constant_time_comparison(line) {
721                    let location = format!("{}:{}", rel_path, idx + 1);
722
723                    findings.push(Finding::new(
724                        self.metadata.id.clone(),
725                        self.metadata.name.clone(),
726                        self.metadata.default_severity,
727                        "Secret comparison using non-constant-time operation; vulnerable to timing attacks",
728                        location,
729                        String::new(),
730                        vec![line.trim().to_string()],
731                        None,
732                    ));
733                }
734            }
735        }
736
737        findings
738    }
739}
740
741// ============================================================================
742// RUSTCOLA045: Weak Cipher Usage
743// ============================================================================
744
745pub struct WeakCipherRule {
746    metadata: RuleMetadata,
747}
748
749impl WeakCipherRule {
750    pub fn new() -> Self {
751        Self {
752            metadata: RuleMetadata {
753                id: "RUSTCOLA045".to_string(),
754                name: "weak-cipher-usage".to_string(),
755                short_description: "Weak or deprecated cipher algorithm".to_string(),
756                full_description: "Detects use of cryptographically broken or deprecated ciphers including DES, 3DES, RC4, RC2, and Blowfish. These algorithms have known vulnerabilities and should not be used for security-sensitive operations. Use modern algorithms like AES-256-GCM or ChaCha20-Poly1305 instead.".to_string(),
757                help_uri: Some("https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/09-Testing_for_Weak_Cryptography/04-Testing_for_Weak_Encryption".to_string()),
758                default_severity: Severity::High,
759                origin: RuleOrigin::BuiltIn,
760                cwe_ids: Vec::new(),
761                fix_suggestion: None,
762                exploitability: Exploitability::default(),
763            },
764        }
765    }
766
767    fn contains_weak_cipher(line: &str) -> bool {
768        let lowered = line.to_lowercase();
769
770        if lowered.trim_start().starts_with("//") {
771            return false;
772        }
773
774        let weak_patterns = [
775            "::des::",
776            "::des<",
777            " des::",
778            "<des>",
779            "cipher::des",
780            "block_modes::des",
781            "des_ede3",
782            "tripledes",
783            "::tdes::",
784            "::tdes<",
785            "tdesede",
786            "::rc4::",
787            "::rc4<",
788            " rc4::",
789            "<rc4>",
790            "cipher::rc4",
791            "stream_cipher::rc4",
792            "::rc2::",
793            "::rc2<",
794            " rc2::",
795            "<rc2>",
796            "cipher::rc2",
797            "::blowfish::",
798            "::blowfish<",
799            " blowfish::",
800            "<blowfish>",
801            "cipher::blowfish",
802            "block_modes::blowfish",
803            "::arcfour::",
804            " arcfour::",
805            "::cast5::",
806            " cast5::",
807        ];
808
809        for pattern in weak_patterns {
810            if lowered.contains(pattern) {
811                if lowered.contains("alloc") && (lowered.contains("0x") || lowered.contains("│"))
812                {
813                    continue;
814                }
815                return true;
816            }
817        }
818
819        false
820    }
821}
822
823impl Rule for WeakCipherRule {
824    fn metadata(&self) -> &RuleMetadata {
825        &self.metadata
826    }
827
828    fn evaluate(
829        &self,
830        package: &MirPackage,
831        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
832    ) -> Vec<Finding> {
833        let mut findings = Vec::new();
834
835        for function in &package.functions {
836            if function.name.contains("WeakCipherRule")
837                || function.name.contains("contains_weak_cipher")
838            {
839                continue;
840            }
841
842            for line in &function.body {
843                if Self::contains_weak_cipher(line) {
844                    findings.push(Finding {
845                        rule_id: self.metadata.id.clone(),
846                        rule_name: self.metadata.name.clone(),
847                        severity: self.metadata.default_severity,
848                        message: format!(
849                            "Weak or deprecated cipher algorithm detected in `{}`",
850                            function.name
851                        ),
852                        function: function.name.clone(),
853                        function_signature: function.signature.clone(),
854                        evidence: vec![line.trim().to_string()],
855                        span: function.span.clone(),
856                        confidence: Confidence::Medium,
857                        cwe_ids: Vec::new(),
858                        fix_suggestion: None,
859                        code_snippet: None,
860                        exploitability: Exploitability::default(),
861                        exploitability_score: Exploitability::default().score(),
862                        ..Default::default()
863                    });
864                }
865            }
866        }
867
868        findings
869    }
870}
871
872// ============================================================================
873// RUSTCOLA046: Predictable Randomness
874// ============================================================================
875
876pub struct PredictableRandomnessRule {
877    metadata: RuleMetadata,
878}
879
880impl PredictableRandomnessRule {
881    pub fn new() -> Self {
882        Self {
883            metadata: RuleMetadata {
884                id: "RUSTCOLA046".to_string(),
885                name: "predictable-randomness".to_string(),
886                short_description: "Predictable random number generation".to_string(),
887                full_description: "Detects RNG initialization using constant or hardcoded seeds. Predictable randomness is a critical security flaw in cryptographic operations, session token generation, and nonce creation. Use cryptographically secure random sources like OsRng, ThreadRng, or properly seeded RNGs from entropy sources.".to_string(),
888                help_uri: Some("https://owasp.org/www-community/vulnerabilities/Insecure_Randomness".to_string()),
889                default_severity: Severity::High,
890                origin: RuleOrigin::BuiltIn,
891                cwe_ids: Vec::new(),
892                fix_suggestion: None,
893                exploitability: Exploitability::default(),
894            },
895        }
896    }
897
898    fn is_predictable_seed(line: &str) -> bool {
899        let lowered = line.to_lowercase();
900
901        if lowered.trim_start().starts_with("//") {
902            return false;
903        }
904        if lowered.contains("alloc") && (lowered.contains("0x") || lowered.contains("│")) {
905            return false;
906        }
907
908        if lowered.contains("seed_from_u64") {
909            if lowered.contains("const") && (lowered.contains("_u64") || lowered.contains("_i64")) {
910                return true;
911            }
912        }
913
914        if lowered.contains("from_seed") {
915            if lowered.contains("const") && lowered.contains("[") {
916                return true;
917            }
918        }
919
920        let seedable_rngs = [
921            "stdrng::seed_from_u64",
922            "chacharng::seed_from_u64",
923            "chacha8rng::seed_from_u64",
924            "chacha12rng::seed_from_u64",
925            "chacha20rng::seed_from_u64",
926            "isaacrng::seed_from_u64",
927            "isaac64rng::seed_from_u64",
928            "smallrng::seed_from_u64",
929        ];
930
931        for rng in seedable_rngs {
932            if lowered.contains(rng) && lowered.contains("const") {
933                return true;
934            }
935        }
936
937        false
938    }
939}
940
941impl Rule for PredictableRandomnessRule {
942    fn metadata(&self) -> &RuleMetadata {
943        &self.metadata
944    }
945
946    fn evaluate(
947        &self,
948        package: &MirPackage,
949        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
950    ) -> Vec<Finding> {
951        let mut findings = Vec::new();
952
953        for function in &package.functions {
954            if function.name.contains("PredictableRandomnessRule")
955                || function.name.contains("is_predictable_seed")
956            {
957                continue;
958            }
959
960            for line in &function.body {
961                if Self::is_predictable_seed(line) {
962                    findings.push(Finding {
963                        rule_id: self.metadata.id.clone(),
964                        rule_name: self.metadata.name.clone(),
965                        severity: self.metadata.default_severity,
966                        message: format!("Predictable RNG seed detected in `{}`", function.name),
967                        function: function.name.clone(),
968                        function_signature: function.signature.clone(),
969                        evidence: vec![line.trim().to_string()],
970                        span: function.span.clone(),
971                        confidence: Confidence::Medium,
972                        cwe_ids: Vec::new(),
973                        fix_suggestion: None,
974                        code_snippet: None,
975                        exploitability: Exploitability::default(),
976                        exploitability_score: Exploitability::default().score(),
977                        ..Default::default()
978                    });
979                }
980            }
981        }
982
983        findings
984    }
985}
986
987// ============================================================================
988// RUSTCOLA011: Modulo Bias in Random
989// ============================================================================
990
991pub struct ModuloBiasRandomRule {
992    metadata: RuleMetadata,
993}
994
995impl ModuloBiasRandomRule {
996    pub fn new() -> Self {
997        Self {
998            metadata: RuleMetadata {
999                id: "RUSTCOLA011".to_string(),
1000                name: "modulo-bias-random".to_string(),
1001                short_description: "Modulo bias in random number generation".to_string(),
1002                full_description: "Detects patterns where random numbers are reduced using modulo (%), which can introduce statistical bias. For cryptographic or security contexts, use proper bounded range generation methods like gen_range().".to_string(),
1003                help_uri: None,
1004                default_severity: Severity::Medium,
1005                origin: RuleOrigin::BuiltIn,
1006                cwe_ids: Vec::new(),
1007                fix_suggestion: None,
1008                exploitability: Exploitability::default(),
1009            },
1010        }
1011    }
1012}
1013
1014impl Rule for ModuloBiasRandomRule {
1015    fn metadata(&self) -> &RuleMetadata {
1016        &self.metadata
1017    }
1018
1019    fn evaluate(
1020        &self,
1021        package: &MirPackage,
1022        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1023    ) -> Vec<Finding> {
1024        let mut findings = Vec::new();
1025
1026        for function in &package.functions {
1027            // Skip self-references
1028            if function.name.contains("ModuloBiasRandomRule") {
1029                continue;
1030            }
1031
1032            // Look for random generation followed by modulo
1033            let has_random = function.body.iter().any(|line| {
1034                let lower = line.to_lowercase();
1035                lower.contains("::random()")
1036                    || lower.contains(".next_u32()")
1037                    || lower.contains(".next_u64()")
1038                    || lower.contains(".gen::<")
1039                    || lower.contains("getrandom")
1040            });
1041
1042            if !has_random {
1043                continue;
1044            }
1045
1046            // Look for modulo operation in MIR (Rem operator)
1047            let modulo_evidence: Vec<String> = function
1048                .body
1049                .iter()
1050                .filter(|line| line.contains("Rem(") || line.contains(" % "))
1051                .map(|line| line.trim().to_string())
1052                .collect();
1053
1054            if !modulo_evidence.is_empty() {
1055                findings.push(Finding {
1056                    rule_id: self.metadata.id.clone(),
1057                    rule_name: self.metadata.name.clone(),
1058                    severity: self.metadata.default_severity,
1059                    message: format!(
1060                        "Potential modulo bias in random number generation in `{}`",
1061                        function.name
1062                    ),
1063                    function: function.name.clone(),
1064                    function_signature: function.signature.clone(),
1065                    evidence: modulo_evidence,
1066                    span: function.span.clone(),
1067                    confidence: Confidence::Medium,
1068                    cwe_ids: Vec::new(),
1069                    fix_suggestion: None,
1070                    code_snippet: None,
1071                    exploitability: Exploitability::default(),
1072                    exploitability_score: Exploitability::default().score(),
1073                    ..Default::default()
1074                });
1075            }
1076        }
1077
1078        findings
1079    }
1080}
1081
1082/// Register all crypto rules with the rule engine.
1083pub fn register_crypto_rules(engine: &mut crate::RuleEngine) {
1084    engine.register_rule(Box::new(InsecureMd5Rule::new()));
1085    engine.register_rule(Box::new(InsecureSha1Rule::new()));
1086    engine.register_rule(Box::new(WeakHashingExtendedRule::new()));
1087    engine.register_rule(Box::new(HardcodedCryptoKeyRule::new()));
1088    engine.register_rule(Box::new(TimingAttackRule::new()));
1089    engine.register_rule(Box::new(WeakCipherRule::new()));
1090    engine.register_rule(Box::new(PredictableRandomnessRule::new()));
1091    engine.register_rule(Box::new(ModuloBiasRandomRule::new()));
1092}