1use crate::scanner::{Location, ScanResult, Severity, Threat, ThreatType};
12use regex::Regex;
13use tracing::{debug, error, trace};
14
15#[derive(Debug, Clone)]
16pub struct CryptoScanner {
17 deprecated_hash_patterns: Vec<Regex>,
19
20 weak_encryption_patterns: Vec<Regex>,
22
23 insecure_rng_patterns: Vec<Regex>,
25
26 weak_key_patterns: Vec<Regex>,
28
29 insecure_mode_patterns: Vec<Regex>,
31
32 bad_kdf_patterns: Vec<Regex>,
34}
35
36impl Default for CryptoScanner {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl CryptoScanner {
43 fn compile_patterns(patterns: &[&str]) -> Vec<Regex> {
45 patterns
46 .iter()
47 .filter_map(|pattern| match Regex::new(pattern) {
48 Ok(regex) => Some(regex),
49 Err(e) => {
50 error!(
51 "Failed to compile crypto scanner regex '{}': {}",
52 pattern, e
53 );
54 None
55 },
56 })
57 .collect()
58 }
59
60 pub fn new() -> Self {
61 let deprecated_hash_patterns = Self::compile_patterns(&[
63 r"(?i)\b(md5|Md5Hash|md5sum|MD5_)\b",
65 r"(?i)use\s+md5(?:::|\s|;)",
66 r"(?i)md5::compute",
67 r"(?i)\b(sha1|Sha1|SHA1_|sha1sum)\b",
69 r"(?i)use\s+sha1(?:::|\s|;)",
70 r"(?i)sha1::Sha1",
71 r"(?i)\b(md4|Md4Hash|MD4_)\b",
73 r"(?i)\b(md2|ripemd|RIPEMD)\b",
75 ]);
76
77 let weak_encryption_patterns = Self::compile_patterns(&[
79 r"(?i)\b(des|DES|DesKey|des_key)\b",
81 r"(?i)use\s+des(?:::|\s|;)",
82 r"(?i)\b(3des|triple_?des|TDES)\b",
84 r"(?i)\b(rc4|RC4|arcfour)\b",
86 r"(?i)\b(rc2|RC2)\b",
88 r"(?i)\b(skipjack|blowfish|CAST5?)\b",
90 ]);
91
92 let insecure_rng_patterns = Self::compile_patterns(&[
94 r"(?i)(rand::random|thread_rng)\s*\(\s*\)",
96 r"(?i)fastrand::",
98 r"(?i)oorandom::",
100 r"(?i)(SystemTime::now|time::now).*seed",
102 r"(?i)seed\s*=\s*(\d+|0x[0-9a-fA-F]+)",
104 r"(?i)\b(SmallRng|StdRng|Xorshift|Pcg32)\b.*(?:key|crypto|secure)",
106 ]);
107
108 let weak_key_patterns = Self::compile_patterns(&[
110 r"(?i)rsa.*(?:512|768|1024)",
112 r"(?i)RsaKeySize::Rsa(?:512|768|1024)",
113 r"(?i)RsaPrivateKey::new.*1024",
114 r"(?i)(?:ecc|ecdsa|ecdh).*(?:112|128|160|192)\s*(?:bit|_bit)",
116 r"(?i)(?:P-?112|P-?128|P-?160|P-?192|secp112|secp128|secp160|secp192)",
117 r"(?i)aes.*(?:64|80|96)\s*(?:bit|_bit)",
119 r"(?i)(?:dh|diffie).*(?:512|768|1024)\s*(?:bit|_bit)",
121 ]);
122
123 let insecure_mode_patterns = Self::compile_patterns(&[
125 r"(?i)\b(ecb|ECB|EcbMode|ecb_mode)\b",
127 r#"(?i)mode\s*=\s*["']?ecb["']?"#,
128 r#"(?i)cipher.*ecb"#,
129 r#"(?i)encrypt_ecb"#,
130 r#"(?i)iv\s*=\s*(\[0(?:,\s*0)*\]|vec!\[0(?:;\s*\d+)?\])"#,
132 r#"(?i)const\s+IV\s*:\s*\[u8"#,
133 r#"(?i)static\s+IV\s*:\s*\[u8"#,
134 r#"(?i)self\.iv|cached_iv|reuse.*iv"#,
136 r#"(?i)cbc.*encrypt.*\(\s*[^,)]+\s*\)"#,
138 ]);
139
140 let bad_kdf_patterns = Self::compile_patterns(&[
142 r#"(?i)(sha256|sha512|md5|sha1)\s*\(\s*password"#,
144 r#"(?i)(Sha256|Sha512|Md5|Sha1)::new\(\)"#,
145 r#"(?i)hasher\.update\(password"#,
146 r#"(?i)pbkdf2.*iterations?\s*[=:]\s*(?:[1-9]\d{0,3}|10000)\b"#,
148 r#"(?i)simple_hash_password\s*\("#,
150 r#"(?i)salt\s*=\s*["'][\w\s]+["']"#,
152 r#"(?i)const\s+SALT\s*:\s*(?:&str|&\[u8\])"#,
153 ]);
154
155 Self {
156 deprecated_hash_patterns,
157 weak_encryption_patterns,
158 insecure_rng_patterns,
159 weak_key_patterns,
160 insecure_mode_patterns,
161 bad_kdf_patterns,
162 }
163 }
164
165 fn check_deprecated_algorithms(&self, text: &str) -> Vec<Threat> {
166 let mut threats = Vec::new();
167 let mut line_start = 0;
168
169 for (_line_num, line) in text.lines().enumerate() {
170 if line.trim_start().starts_with("//") {
172 line_start += line.len() + 1; continue;
174 }
175
176 for pattern in &self.deprecated_hash_patterns {
178 if let Some(m) = pattern.find(line) {
179 let algo = m.as_str();
180 let (name, recommendation) = if algo.to_lowercase().contains("md5") {
181 ("MD5", "Use SHA-256, SHA-3, or BLAKE3 instead")
182 } else if algo.to_lowercase().contains("sha1") {
183 ("SHA-1", "Use SHA-256, SHA-3, or BLAKE3 instead")
184 } else if algo.to_lowercase().contains("md4") {
185 ("MD4", "Use SHA-256, SHA-3, or BLAKE3 instead")
186 } else {
187 ("deprecated hash", "Use SHA-256, SHA-3, or BLAKE3 instead")
188 };
189
190 threats.push(Threat {
191 threat_type: ThreatType::Custom("crypto_deprecated_hash".to_string()),
192 severity: Severity::High,
193 location: Location::Text {
194 offset: line_start + m.start(),
195 length: m.len(),
196 },
197 description: format!(
198 "Deprecated hash algorithm {} detected. {}",
199 name, recommendation
200 ),
201 remediation: Some(recommendation.to_string()),
202 });
203 }
204 }
205
206 for pattern in &self.weak_encryption_patterns {
208 if let Some(m) = pattern.find(line) {
209 let algo = m.as_str();
210 let (name, recommendation) = if algo.to_lowercase().contains("des") {
211 ("DES/3DES", "Use AES-256-GCM or ChaCha20-Poly1305")
212 } else if algo.to_lowercase().contains("rc4") {
213 ("RC4", "Use AES-256-GCM or ChaCha20-Poly1305")
214 } else {
215 ("weak cipher", "Use AES-256-GCM or ChaCha20-Poly1305")
216 };
217
218 threats.push(Threat {
219 threat_type: ThreatType::Custom("crypto_weak_cipher".to_string()),
220 severity: Severity::Critical,
221 location: Location::Text {
222 offset: line_start + m.start(),
223 length: m.len(),
224 },
225 description: format!(
226 "Weak encryption algorithm {} detected. {}",
227 name, recommendation
228 ),
229 remediation: Some(recommendation.to_string()),
230 });
231 }
232 }
233
234 line_start += line.len() + 1; }
236
237 threats
238 }
239
240 fn check_insecure_rng(&self, text: &str) -> Vec<Threat> {
241 let mut threats = Vec::new();
242
243 let is_crypto_context = text.contains("key")
245 || text.contains("Key")
246 || text.contains("encrypt")
247 || text.contains("decrypt")
248 || text.contains("hash")
249 || text.contains("sign")
250 || text.contains("nonce")
251 || text.contains("salt")
252 || text.contains("iv")
253 || text.contains("IV");
254
255 if !is_crypto_context {
256 return threats;
257 }
258
259 let mut line_start = 0;
260
261 for (_line_num, line) in text.lines().enumerate() {
262 if line.trim_start().starts_with("//") {
263 line_start += line.len() + 1;
264 continue;
265 }
266
267 for pattern in &self.insecure_rng_patterns {
268 if let Some(m) = pattern.find(line) {
269 threats.push(Threat {
270 threat_type: ThreatType::Custom("crypto_insecure_rng".to_string()),
271 severity: Severity::Critical,
272 location: Location::Text {
273 offset: line_start + m.start(),
274 length: m.len(),
275 },
276 description: "Insecure random number generation for cryptographic use. Use rand::rngs::OsRng or ring::rand::SystemRandom for cryptographic randomness".to_string(),
277 remediation: Some(
278 "Use rand::rngs::OsRng or ring::rand::SystemRandom for cryptographic randomness".to_string()
279 ),
280 });
281 }
282 }
283
284 line_start += line.len() + 1;
285 }
286
287 threats
288 }
289
290 fn check_weak_key_sizes(&self, text: &str) -> Vec<Threat> {
291 let mut threats = Vec::new();
292 let mut line_start = 0;
293
294 for (_line_num, line) in text.lines().enumerate() {
295 if line.trim_start().starts_with("//") {
296 line_start += line.len() + 1;
297 continue;
298 }
299
300 for pattern in &self.weak_key_patterns {
301 if let Some(m) = pattern.find(line) {
302 threats.push(Threat {
303 threat_type: ThreatType::Custom("crypto_weak_key_size".to_string()),
304 severity: Severity::High,
305 location: Location::Text {
306 offset: line_start + m.start(),
307 length: m.len(),
308 },
309 description: "Weak key size detected - insufficient for 2025 standards. Use RSA-3072+, ECC P-256+, or AES-256 for 2025 compliance".to_string(),
310 remediation: Some(
311 "Use RSA-3072+, ECC P-256+, or AES-256 for 2025 compliance".to_string()
312 ),
313 });
314 }
315 }
316
317 line_start += line.len() + 1;
318 }
319
320 threats
321 }
322
323 fn check_insecure_modes(&self, text: &str) -> Vec<Threat> {
324 let mut threats = Vec::new();
325 let mut line_start = 0;
326
327 for (_line_num, line) in text.lines().enumerate() {
328 if line.trim_start().starts_with("//") {
329 line_start += line.len() + 1;
330 continue;
331 }
332
333 for pattern in &self.insecure_mode_patterns {
334 if let Some(m) = pattern.find(line) {
335 let detail = m.as_str();
336 let (message, recommendation) = if detail.to_lowercase().contains("ecb") {
337 (
338 "ECB mode encryption is insecure - reveals patterns",
339 "Use authenticated encryption: AES-GCM, ChaCha20-Poly1305, or AES-GCM-SIV"
340 )
341 } else if detail.to_lowercase().contains("iv") {
342 (
343 "Static or reused IV detected - breaks semantic security",
344 "Generate a unique random IV for each encryption operation",
345 )
346 } else {
347 (
348 "Insecure encryption mode detected",
349 "Use authenticated encryption modes",
350 )
351 };
352
353 threats.push(Threat {
354 threat_type: ThreatType::Custom("crypto_insecure_mode".to_string()),
355 severity: Severity::Critical,
356 location: Location::Text {
357 offset: line_start + m.start(),
358 length: m.len(),
359 },
360 description: format!("{}. {}", message, recommendation),
361 remediation: Some(recommendation.to_string()),
362 });
363 }
364 }
365
366 line_start += line.len() + 1;
367 }
368
369 threats
370 }
371
372 fn check_bad_kdf(&self, text: &str) -> Vec<Threat> {
373 let mut threats = Vec::new();
374
375 let has_good_kdf = text.contains("argon2")
377 || text.contains("Argon2")
378 || text.contains("scrypt")
379 || text.contains("bcrypt")
380 || text.contains("pbkdf2") && text.contains("100000");
381
382 let mut line_start = 0;
383
384 for (_line_num, line) in text.lines().enumerate() {
385 if line.trim_start().starts_with("//") {
386 line_start += line.len() + 1;
387 continue;
388 }
389
390 for pattern in &self.bad_kdf_patterns {
391 if let Some(m) = pattern.find(line) {
392 if has_good_kdf
394 && (line.contains("argon")
395 || line.contains("scrypt")
396 || line.contains("bcrypt"))
397 {
398 continue;
399 }
400
401 threats.push(Threat {
402 threat_type: ThreatType::Custom("crypto_bad_kdf".to_string()),
403 severity: Severity::High,
404 location: Location::Text {
405 offset: line_start + m.start(),
406 length: m.len(),
407 },
408 description: "Insecure key derivation detected. Use Argon2id, scrypt, or bcrypt with proper parameters for password hashing".to_string(),
409 remediation: Some(
410 "Use Argon2id, scrypt, or bcrypt with proper parameters for password hashing".to_string()
411 ),
412 });
413 }
414 }
415
416 line_start += line.len() + 1;
417 }
418
419 threats
420 }
421
422 fn check_post_quantum_readiness(&self, text: &str) -> Vec<Threat> {
423 let mut threats = Vec::new();
424
425 let has_rsa = text.contains("RSA") || text.contains("rsa");
427 let has_ecc = text.contains("ECC") || text.contains("ECDSA") || text.contains("ECDH");
428 let has_pqc = text.contains("ML-KEM")
429 || text.contains("ML-DSA")
430 || text.contains("SPHINCS")
431 || text.contains("Dilithium")
432 || text.contains("Kyber")
433 || text.contains("post-quantum")
434 || text.contains("pqcrypto");
435
436 if (has_rsa || has_ecc) && !has_pqc {
437 threats.push(Threat {
438 threat_type: ThreatType::Custom("crypto_no_pqc_plan".to_string()),
439 severity: Severity::Medium,
440 location: Location::Text {
441 offset: 0,
442 length: text.len(),
443 },
444 description: "No post-quantum cryptography migration detected. Plan migration to NIST PQC standards (ML-KEM, ML-DSA, SLH-DSA) by 2035".to_string(),
445 remediation: Some(
446 "Plan migration to NIST PQC standards (ML-KEM, ML-DSA, SLH-DSA) by 2035".to_string()
447 ),
448 });
449 }
450
451 threats
452 }
453
454 pub fn scan_text(&self, text: &str) -> ScanResult {
455 trace!("Starting cryptographic security scan");
456
457 let mut all_threats = Vec::new();
458
459 all_threats.extend(self.check_deprecated_algorithms(text));
461 all_threats.extend(self.check_insecure_rng(text));
462 all_threats.extend(self.check_weak_key_sizes(text));
463 all_threats.extend(self.check_insecure_modes(text));
464 all_threats.extend(self.check_bad_kdf(text));
465 all_threats.extend(self.check_post_quantum_readiness(text));
466
467 debug!("Crypto scan found {} threats", all_threats.len());
468
469 Ok(all_threats)
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_detect_md5() {
479 let scanner = CryptoScanner::new();
480 let code = r#"
481use md5;
482
483fn hash_password(password: &str) -> String {
484 let digest = md5::compute(password);
485 format!("{:x}", digest)
486}
487"#;
488
489 let threats = scanner.scan_text(code).unwrap();
490 assert!(!threats.is_empty());
491 assert!(
492 matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_deprecated_hash")
493 );
494 assert!(threats[0].description.contains("MD5"));
495 }
496
497 #[test]
498 fn test_detect_weak_rsa_key() {
499 let scanner = CryptoScanner::new();
500 let code = r#"
501const RSA_KEY_SIZE: usize = 1024;
502
503fn generate_rsa_key() {
504 let key = RsaPrivateKey::new(&mut rng, 1024).unwrap();
505}
506"#;
507
508 let threats = scanner.scan_text(code).unwrap();
509 assert!(!threats.is_empty());
510 assert!(
511 matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_weak_key_size")
512 );
513 }
514
515 #[test]
516 fn test_detect_ecb_mode() {
517 let scanner = CryptoScanner::new();
518 let code = r#"
519use aes::cipher::{BlockEncrypt, KeyInit};
520use aes::Aes256;
521
522fn encrypt_ecb(key: &[u8], data: &[u8]) -> Vec<u8> {
523 let cipher = Aes256::new_from_slice(key).unwrap();
524 // ECB mode encryption
525 let mut output = data.to_vec();
526 cipher.encrypt_block((&mut output).into());
527 output
528}
529"#;
530
531 let threats = scanner.scan_text(code).unwrap();
532 assert!(!threats.is_empty());
533 assert!(threats.iter().any(
534 |t| matches!(t.threat_type, ThreatType::Custom(ref s) if s == "crypto_insecure_mode")
535 ));
536 }
537
538 #[test]
539 fn test_detect_insecure_rng() {
540 let scanner = CryptoScanner::new();
541 let code = r#"
542use rand::prelude::*;
543
544fn generate_key() -> [u8; 32] {
545 let mut key = [0u8; 32];
546 let mut rng = thread_rng();
547 rng.fill_bytes(&mut key);
548 key
549}
550"#;
551
552 let threats = scanner.scan_text(code).unwrap();
553 assert!(!threats.is_empty());
554 assert!(
555 matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_insecure_rng")
556 );
557 }
558
559 #[test]
560 fn test_detect_static_iv() {
561 let scanner = CryptoScanner::new();
562 let code = r#"
563const IV: [u8; 16] = [0; 16];
564
565fn encrypt_data(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
566 let cipher = Aes256Cbc::new_from_slices(key, &IV).unwrap();
567 cipher.encrypt_vec(plaintext)
568}
569"#;
570
571 let threats = scanner.scan_text(code).unwrap();
572 assert!(!threats.is_empty());
573 assert!(
574 matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_insecure_mode")
575 );
576 assert!(threats[0].description.contains("IV"));
577 }
578
579 #[test]
580 fn test_detect_bad_password_hashing() {
581 let scanner = CryptoScanner::new();
582 let code = r#"
583use sha2::{Sha256, Digest};
584
585fn hash_password(password: &str) -> String {
586 let mut hasher = Sha256::new();
587 hasher.update(password);
588 format!("{:x}", hasher.finalize())
589}
590"#;
591
592 let threats = scanner.scan_text(code).unwrap();
593 assert!(!threats.is_empty());
594 assert!(
595 matches!(threats[0].threat_type, ThreatType::Custom(ref s) if s == "crypto_bad_kdf")
596 );
597 }
598
599 #[test]
600 fn test_no_false_positives_for_secure_code() {
601 let scanner = CryptoScanner::new();
602 let code = r#"
603use ring::rand::{SecureRandom, SystemRandom};
604use argon2::{Argon2, PasswordHasher, PasswordHash, PasswordVerifier};
605
606fn generate_secure_key() -> Result<[u8; 32], ring::error::Unspecified> {
607 let rng = SystemRandom::new();
608 let mut key = [0u8; 32];
609 rng.fill(&mut key)?;
610 Ok(key)
611}
612
613fn hash_password_secure(password: &str) -> Result<String, argon2::password_hash::Error> {
614 let salt = SaltString::generate(&mut OsRng);
615 let argon2 = Argon2::default();
616 let password_hash = argon2.hash_password(password.as_bytes(), &salt)?;
617 Ok(password_hash.to_string())
618}
619
620// Using AES-256-GCM for authenticated encryption
621use aes_gcm::{Aes256Gcm, Key, Nonce};
622use aes_gcm::aead::{Aead, NewAead};
623"#;
624
625 let threats = scanner.scan_text(code).unwrap();
626 assert!(threats.iter().all(|t| {
628 if let ThreatType::Custom(ref s) = t.threat_type {
629 s != "crypto_insecure_rng" && s != "crypto_bad_kdf"
630 } else {
631 true
632 }
633 }));
634 }
635}