1use std::collections::{HashMap, HashSet};
44use std::path::Path;
45
46use once_cell::sync::Lazy;
47use regex::Regex;
48use serde::{Deserialize, Serialize};
49
50use crate::error::{Result, BrrrError};
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub enum WeakCryptoIssue {
59 WeakHash,
61 WeakCipher,
63 InsecureMode,
65 InsufficientKeySize,
67 HardcodedKey,
69 HardcodedIv,
71 PredictableRandom,
73 MissingAuthentication,
75 DeprecatedFunction,
77}
78
79impl std::fmt::Display for WeakCryptoIssue {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 WeakCryptoIssue::WeakHash => write!(f, "Weak Hash Algorithm"),
83 WeakCryptoIssue::WeakCipher => write!(f, "Weak Cipher Algorithm"),
84 WeakCryptoIssue::InsecureMode => write!(f, "Insecure Cipher Mode"),
85 WeakCryptoIssue::InsufficientKeySize => write!(f, "Insufficient Key Size"),
86 WeakCryptoIssue::HardcodedKey => write!(f, "Hardcoded Encryption Key"),
87 WeakCryptoIssue::HardcodedIv => write!(f, "Hardcoded Initialization Vector"),
88 WeakCryptoIssue::PredictableRandom => write!(f, "Predictable Random for Crypto"),
89 WeakCryptoIssue::MissingAuthentication => write!(f, "Encryption Without Authentication"),
90 WeakCryptoIssue::DeprecatedFunction => write!(f, "Deprecated Cryptographic Function"),
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97pub enum Algorithm {
98 Md5,
100 Sha1,
102 Des,
104 TripleDes,
106 Rc4,
108 Blowfish,
110 EcbMode,
112 RsaWeakKey,
114 Dsa,
116 HardcodedKeyPattern,
118 HardcodedIvPattern,
120 InsecureRandom,
122 Other(String),
124}
125
126impl std::fmt::Display for Algorithm {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 match self {
129 Algorithm::Md5 => write!(f, "MD5"),
130 Algorithm::Sha1 => write!(f, "SHA-1"),
131 Algorithm::Des => write!(f, "DES"),
132 Algorithm::TripleDes => write!(f, "3DES/Triple-DES"),
133 Algorithm::Rc4 => write!(f, "RC4"),
134 Algorithm::Blowfish => write!(f, "Blowfish"),
135 Algorithm::EcbMode => write!(f, "ECB Mode"),
136 Algorithm::RsaWeakKey => write!(f, "RSA (weak key)"),
137 Algorithm::Dsa => write!(f, "DSA"),
138 Algorithm::HardcodedKeyPattern => write!(f, "Hardcoded Key"),
139 Algorithm::HardcodedIvPattern => write!(f, "Hardcoded IV"),
140 Algorithm::InsecureRandom => write!(f, "Insecure Random"),
141 Algorithm::Other(s) => write!(f, "{}", s),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
148pub enum Severity {
149 Info,
151 Low,
153 Medium,
155 High,
157 Critical,
159}
160
161impl std::fmt::Display for Severity {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 Severity::Info => write!(f, "INFO"),
165 Severity::Low => write!(f, "LOW"),
166 Severity::Medium => write!(f, "MEDIUM"),
167 Severity::High => write!(f, "HIGH"),
168 Severity::Critical => write!(f, "CRITICAL"),
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
175pub enum Confidence {
176 Low,
178 Medium,
180 High,
182}
183
184impl std::fmt::Display for Confidence {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Confidence::Low => write!(f, "LOW"),
188 Confidence::Medium => write!(f, "MEDIUM"),
189 Confidence::High => write!(f, "HIGH"),
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
196pub enum UsageContext {
197 PasswordHashing,
199 Signature,
201 Encryption,
203 FileChecksum,
205 CacheKey,
207 GitOperation,
209 Hmac,
211 Unknown,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Location {
218 pub file: String,
220 pub line: usize,
222 pub column: usize,
224 pub end_line: usize,
226 pub end_column: usize,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct WeakCryptoFinding {
233 pub location: Location,
235 pub issue_type: WeakCryptoIssue,
237 pub algorithm: Algorithm,
239 pub severity: Severity,
241 pub confidence: Confidence,
243 pub context: UsageContext,
245 pub code_snippet: String,
247 pub description: String,
249 pub remediation: String,
251 pub is_test_file: bool,
253 pub likely_safe: bool,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct ScanResult {
260 pub findings: Vec<WeakCryptoFinding>,
262 pub files_scanned: usize,
264 pub issue_counts: HashMap<String, usize>,
266 pub severity_counts: HashMap<String, usize>,
268 pub algorithm_counts: HashMap<String, usize>,
270}
271
272struct CryptoPattern {
278 regex: Regex,
279 issue_type: WeakCryptoIssue,
280 algorithm: Algorithm,
281 base_severity: Severity,
282 confidence: Confidence,
283 description: &'static str,
284 remediation: &'static str,
285 safe_context_patterns: Vec<&'static str>,
287}
288
289static SAFE_CONTEXT_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
291 vec![
292 Regex::new(r"(?i)checksum").expect("Invalid regex"),
294 Regex::new(r"(?i)file_?hash").expect("Invalid regex"),
295 Regex::new(r"(?i)content_?hash").expect("Invalid regex"),
296 Regex::new(r"(?i)etag").expect("Invalid regex"),
297 Regex::new(r"(?i)cache_?key").expect("Invalid regex"),
298 Regex::new(r"(?i)fingerprint").expect("Invalid regex"),
299 Regex::new(r"(?i)dedup").expect("Invalid regex"),
300 Regex::new(r"(?i)git").expect("Invalid regex"),
302 Regex::new(r"(?i)commit").expect("Invalid regex"),
303 Regex::new(r"(?i)blob").expect("Invalid regex"),
304 Regex::new(r"(?i)tree").expect("Invalid regex"),
305 Regex::new(r"(?i)legacy").expect("Invalid regex"),
307 Regex::new(r"(?i)backward.?compat").expect("Invalid regex"),
308 Regex::new(r"(?i)deprecated.?but.?required").expect("Invalid regex"),
309 Regex::new(r"(?i)verify_?integrity").expect("Invalid regex"),
311 Regex::new(r"(?i)compare_?hash").expect("Invalid regex"),
312 ]
313});
314
315static SECURITY_CONTEXT_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
317 vec![
318 Regex::new(r"(?i)password").expect("Invalid regex"),
320 Regex::new(r"(?i)passwd").expect("Invalid regex"),
321 Regex::new(r"(?i)pwd").expect("Invalid regex"),
322 Regex::new(r"(?i)credential").expect("Invalid regex"),
323 Regex::new(r"(?i)auth").expect("Invalid regex"),
325 Regex::new(r"(?i)token").expect("Invalid regex"),
326 Regex::new(r"(?i)session").expect("Invalid regex"),
327 Regex::new(r"(?i)login").expect("Invalid regex"),
328 Regex::new(r"(?i)sign(ature)?").expect("Invalid regex"),
330 Regex::new(r"(?i)verify").expect("Invalid regex"),
331 Regex::new(r"(?i)certificate").expect("Invalid regex"),
332 Regex::new(r"(?i)encrypt").expect("Invalid regex"),
334 Regex::new(r"(?i)decrypt").expect("Invalid regex"),
335 Regex::new(r"(?i)secret").expect("Invalid regex"),
336 Regex::new(r"(?i)private").expect("Invalid regex"),
337 Regex::new(r"(?i)sensitive").expect("Invalid regex"),
338 ]
339});
340
341static CRYPTO_PATTERNS: Lazy<Vec<CryptoPattern>> = Lazy::new(|| {
343 vec![
344 CryptoPattern {
350 regex: Regex::new(r"(?i)hashlib\s*\.\s*md5\s*\(").expect("Invalid regex"),
351 issue_type: WeakCryptoIssue::WeakHash,
352 algorithm: Algorithm::Md5,
353 base_severity: Severity::Medium,
354 confidence: Confidence::High,
355 description: "MD5 hash detected - collision attacks are practical",
356 remediation: "Use SHA-256 or SHA-3 for checksums, or bcrypt/argon2 for passwords",
357 safe_context_patterns: vec!["checksum", "etag", "cache", "fingerprint"],
358 },
359 CryptoPattern {
360 regex: Regex::new(r#"(?i)hashlib\s*\.\s*new\s*\(\s*["']md5["']"#).expect("Invalid regex"),
361 issue_type: WeakCryptoIssue::WeakHash,
362 algorithm: Algorithm::Md5,
363 base_severity: Severity::Medium,
364 confidence: Confidence::High,
365 description: "MD5 hash via hashlib.new() - collision attacks are practical",
366 remediation: "Use SHA-256 or SHA-3 for checksums, or bcrypt/argon2 for passwords",
367 safe_context_patterns: vec!["checksum", "etag", "cache"],
368 },
369 CryptoPattern {
371 regex: Regex::new(r"(?i)hashlib\s*\.\s*sha1\s*\(").expect("Invalid regex"),
372 issue_type: WeakCryptoIssue::WeakHash,
373 algorithm: Algorithm::Sha1,
374 base_severity: Severity::Medium,
375 confidence: Confidence::High,
376 description: "SHA-1 hash detected - collision attacks demonstrated (SHAttered)",
377 remediation: "Use SHA-256 or SHA-3 for security purposes",
378 safe_context_patterns: vec!["git", "checksum", "legacy"],
379 },
380 CryptoPattern {
381 regex: Regex::new(r#"(?i)hashlib\s*\.\s*new\s*\(\s*["']sha1["']"#).expect("Invalid regex"),
382 issue_type: WeakCryptoIssue::WeakHash,
383 algorithm: Algorithm::Sha1,
384 base_severity: Severity::Medium,
385 confidence: Confidence::High,
386 description: "SHA-1 hash via hashlib.new()",
387 remediation: "Use SHA-256 or SHA-3 for security purposes",
388 safe_context_patterns: vec!["git", "checksum", "legacy"],
389 },
390 CryptoPattern {
392 regex: Regex::new(r"(?i)(?:Crypto|Cryptodome)\s*\.\s*Cipher\s*\.\s*DES\b").expect("Invalid regex"),
393 issue_type: WeakCryptoIssue::WeakCipher,
394 algorithm: Algorithm::Des,
395 base_severity: Severity::High,
396 confidence: Confidence::High,
397 description: "DES cipher detected - 56-bit key is trivially brute-forceable",
398 remediation: "Use AES-256 instead",
399 safe_context_patterns: vec![],
400 },
401 CryptoPattern {
403 regex: Regex::new(r"(?i)from\s+(?:Crypto|Cryptodome)\s*\.\s*Cipher\s+import\s+DES\b").expect("Invalid regex"),
404 issue_type: WeakCryptoIssue::WeakCipher,
405 algorithm: Algorithm::Des,
406 base_severity: Severity::High,
407 confidence: Confidence::High,
408 description: "DES cipher import detected - 56-bit key is trivially brute-forceable",
409 remediation: "Use AES-256 instead",
410 safe_context_patterns: vec![],
411 },
412 CryptoPattern {
414 regex: Regex::new(r"(?i)(?:Crypto|Cryptodome)\s*\.\s*Cipher\s*\.\s*(?:DES3|TripleDES)\b").expect("Invalid regex"),
415 issue_type: WeakCryptoIssue::WeakCipher,
416 algorithm: Algorithm::TripleDes,
417 base_severity: Severity::Medium,
418 confidence: Confidence::High,
419 description: "Triple DES detected - deprecated, use AES instead",
420 remediation: "Use AES-256 instead",
421 safe_context_patterns: vec!["legacy"],
422 },
423 CryptoPattern {
425 regex: Regex::new(r"(?i)(?:Crypto|Cryptodome)\s*\.\s*Cipher\s*\.\s*Blowfish\b").expect("Invalid regex"),
426 issue_type: WeakCryptoIssue::WeakCipher,
427 algorithm: Algorithm::Blowfish,
428 base_severity: Severity::Medium,
429 confidence: Confidence::High,
430 description: "Blowfish cipher detected - 64-bit block size leads to birthday attacks",
431 remediation: "Use AES-256 instead (128-bit block size)",
432 safe_context_patterns: vec!["legacy"],
433 },
434 CryptoPattern {
436 regex: Regex::new(r"(?i)(?:Crypto|Cryptodome)\s*\.\s*Cipher\s*\.\s*(?:ARC4|RC4)\b").expect("Invalid regex"),
437 issue_type: WeakCryptoIssue::WeakCipher,
438 algorithm: Algorithm::Rc4,
439 base_severity: Severity::High,
440 confidence: Confidence::High,
441 description: "RC4 stream cipher detected - biased keystream, multiple attacks",
442 remediation: "Use AES-GCM or ChaCha20-Poly1305 instead",
443 safe_context_patterns: vec![],
444 },
445 CryptoPattern {
447 regex: Regex::new(r"(?i)MODE_ECB\b").expect("Invalid regex"),
448 issue_type: WeakCryptoIssue::InsecureMode,
449 algorithm: Algorithm::EcbMode,
450 base_severity: Severity::High,
451 confidence: Confidence::High,
452 description: "ECB mode detected - reveals patterns in encrypted data",
453 remediation: "Use CBC with random IV, or preferably GCM/CCM for authenticated encryption",
454 safe_context_patterns: vec![],
455 },
456 CryptoPattern {
458 regex: Regex::new(r"(?i)algorithms\s*\.\s*(?:MD5|SHA1)\b").expect("Invalid regex"),
459 issue_type: WeakCryptoIssue::WeakHash,
460 algorithm: Algorithm::Md5,
461 base_severity: Severity::Medium,
462 confidence: Confidence::High,
463 description: "Weak hash algorithm in cryptography library",
464 remediation: "Use SHA256 or SHA512 for security purposes",
465 safe_context_patterns: vec!["checksum", "legacy"],
466 },
467 CryptoPattern {
469 regex: Regex::new(r"(?i)random\s*\.\s*(?:random|randint|choice|randrange)\s*\(").expect("Invalid regex"),
470 issue_type: WeakCryptoIssue::PredictableRandom,
471 algorithm: Algorithm::InsecureRandom,
472 base_severity: Severity::High,
473 confidence: Confidence::Medium,
474 description: "Non-cryptographic random used (random module) - predictable",
475 remediation: "Use secrets module or os.urandom() for cryptographic purposes",
476 safe_context_patterns: vec!["test", "mock", "sample", "shuffle"],
477 },
478
479 CryptoPattern {
485 regex: Regex::new(r#"(?i)crypto\s*\.\s*createHash\s*\(\s*["']md5["']"#).expect("Invalid regex"),
486 issue_type: WeakCryptoIssue::WeakHash,
487 algorithm: Algorithm::Md5,
488 base_severity: Severity::Medium,
489 confidence: Confidence::High,
490 description: "MD5 hash in Node.js crypto - collision attacks are practical",
491 remediation: "Use 'sha256' or 'sha512' instead",
492 safe_context_patterns: vec!["checksum", "etag", "cache"],
493 },
494 CryptoPattern {
496 regex: Regex::new(r#"(?i)crypto\s*\.\s*createHash\s*\(\s*["']sha1["']"#).expect("Invalid regex"),
497 issue_type: WeakCryptoIssue::WeakHash,
498 algorithm: Algorithm::Sha1,
499 base_severity: Severity::Medium,
500 confidence: Confidence::High,
501 description: "SHA-1 hash in Node.js crypto - collision attacks demonstrated",
502 remediation: "Use 'sha256' or 'sha512' for security purposes",
503 safe_context_patterns: vec!["git", "checksum", "legacy"],
504 },
505 CryptoPattern {
507 regex: Regex::new(r#"(?i)crypto\s*\.\s*createCipher(?:iv)?\s*\(\s*["']des(?:-[a-z]+)?["']"#).expect("Invalid regex"),
508 issue_type: WeakCryptoIssue::WeakCipher,
509 algorithm: Algorithm::Des,
510 base_severity: Severity::High,
511 confidence: Confidence::High,
512 description: "DES cipher in Node.js - 56-bit key is trivially brute-forceable",
513 remediation: "Use 'aes-256-gcm' instead",
514 safe_context_patterns: vec![],
515 },
516 CryptoPattern {
518 regex: Regex::new(r#"(?i)crypto\s*\.\s*createCipher(?:iv)?\s*\(\s*["'](?:des-ede3|des3)(?:-[a-z]+)?["']"#).expect("Invalid regex"),
519 issue_type: WeakCryptoIssue::WeakCipher,
520 algorithm: Algorithm::TripleDes,
521 base_severity: Severity::Medium,
522 confidence: Confidence::High,
523 description: "Triple DES in Node.js - deprecated, slow",
524 remediation: "Use 'aes-256-gcm' instead",
525 safe_context_patterns: vec!["legacy"],
526 },
527 CryptoPattern {
529 regex: Regex::new(r#"(?i)crypto\s*\.\s*createCipher(?:iv)?\s*\(\s*["']rc4["']"#).expect("Invalid regex"),
530 issue_type: WeakCryptoIssue::WeakCipher,
531 algorithm: Algorithm::Rc4,
532 base_severity: Severity::High,
533 confidence: Confidence::High,
534 description: "RC4 stream cipher in Node.js - broken, biased keystream",
535 remediation: "Use 'aes-256-gcm' or 'chacha20-poly1305' instead",
536 safe_context_patterns: vec![],
537 },
538 CryptoPattern {
540 regex: Regex::new(r#"(?i)createCipher(?:iv)?\s*\(\s*["'][a-z0-9-]+-ecb["']"#).expect("Invalid regex"),
541 issue_type: WeakCryptoIssue::InsecureMode,
542 algorithm: Algorithm::EcbMode,
543 base_severity: Severity::High,
544 confidence: Confidence::High,
545 description: "ECB mode in Node.js - reveals patterns in encrypted data",
546 remediation: "Use GCM mode for authenticated encryption",
547 safe_context_patterns: vec![],
548 },
549 CryptoPattern {
551 regex: Regex::new(r"(?i)crypto\s*\.\s*createCipher\s*\(").expect("Invalid regex"),
552 issue_type: WeakCryptoIssue::DeprecatedFunction,
553 algorithm: Algorithm::Other("createCipher".to_string()),
554 base_severity: Severity::Medium,
555 confidence: Confidence::High,
556 description: "Deprecated createCipher() - uses weak key derivation",
557 remediation: "Use createCipheriv() with a proper key derivation function",
558 safe_context_patterns: vec![],
559 },
560 CryptoPattern {
562 regex: Regex::new(r"(?i)CryptoJS\s*\.\s*MD5\s*\(").expect("Invalid regex"),
563 issue_type: WeakCryptoIssue::WeakHash,
564 algorithm: Algorithm::Md5,
565 base_severity: Severity::Medium,
566 confidence: Confidence::High,
567 description: "MD5 via crypto-js - collision attacks are practical",
568 remediation: "Use CryptoJS.SHA256() for checksums",
569 safe_context_patterns: vec!["checksum", "etag"],
570 },
571 CryptoPattern {
573 regex: Regex::new(r"(?i)CryptoJS\s*\.\s*SHA1\s*\(").expect("Invalid regex"),
574 issue_type: WeakCryptoIssue::WeakHash,
575 algorithm: Algorithm::Sha1,
576 base_severity: Severity::Medium,
577 confidence: Confidence::High,
578 description: "SHA-1 via crypto-js - collision attacks demonstrated",
579 remediation: "Use CryptoJS.SHA256() for security purposes",
580 safe_context_patterns: vec!["git", "checksum"],
581 },
582 CryptoPattern {
584 regex: Regex::new(r"(?i)CryptoJS\s*\.\s*DES\s*\.").expect("Invalid regex"),
585 issue_type: WeakCryptoIssue::WeakCipher,
586 algorithm: Algorithm::Des,
587 base_severity: Severity::High,
588 confidence: Confidence::High,
589 description: "DES via crypto-js - 56-bit key is trivially brute-forceable",
590 remediation: "Use CryptoJS.AES instead",
591 safe_context_patterns: vec![],
592 },
593 CryptoPattern {
595 regex: Regex::new(r"(?i)CryptoJS\s*\.\s*TripleDES\s*\.").expect("Invalid regex"),
596 issue_type: WeakCryptoIssue::WeakCipher,
597 algorithm: Algorithm::TripleDes,
598 base_severity: Severity::Medium,
599 confidence: Confidence::High,
600 description: "Triple DES via crypto-js - deprecated",
601 remediation: "Use CryptoJS.AES instead",
602 safe_context_patterns: vec!["legacy"],
603 },
604 CryptoPattern {
606 regex: Regex::new(r"(?i)CryptoJS\s*\.\s*(?:RC4|Rabbit)\s*\.").expect("Invalid regex"),
607 issue_type: WeakCryptoIssue::WeakCipher,
608 algorithm: Algorithm::Rc4,
609 base_severity: Severity::High,
610 confidence: Confidence::High,
611 description: "RC4/Rabbit via crypto-js - broken stream cipher",
612 remediation: "Use CryptoJS.AES instead",
613 safe_context_patterns: vec![],
614 },
615 CryptoPattern {
617 regex: Regex::new(r"(?i)CryptoJS\s*\.\s*mode\s*\.\s*ECB").expect("Invalid regex"),
618 issue_type: WeakCryptoIssue::InsecureMode,
619 algorithm: Algorithm::EcbMode,
620 base_severity: Severity::High,
621 confidence: Confidence::High,
622 description: "ECB mode via crypto-js - reveals patterns in encrypted data",
623 remediation: "Use CryptoJS.mode.CBC or CryptoJS.mode.CTR with random IV",
624 safe_context_patterns: vec![],
625 },
626 CryptoPattern {
628 regex: Regex::new(r"(?i)Math\s*\.\s*random\s*\(\s*\)").expect("Invalid regex"),
629 issue_type: WeakCryptoIssue::PredictableRandom,
630 algorithm: Algorithm::InsecureRandom,
631 base_severity: Severity::High,
632 confidence: Confidence::Low, description: "Math.random() is not cryptographically secure",
634 remediation: "Use crypto.randomBytes() or crypto.getRandomValues() for crypto",
635 safe_context_patterns: vec!["ui", "animation", "shuffle", "sample", "test"],
636 },
637
638 CryptoPattern {
644 regex: Regex::new(r"(?i)md5\s*\.\s*(?:New|Sum)\s*\(").expect("Invalid regex"),
645 issue_type: WeakCryptoIssue::WeakHash,
646 algorithm: Algorithm::Md5,
647 base_severity: Severity::Medium,
648 confidence: Confidence::High,
649 description: "MD5 hash in Go (crypto/md5) - collision attacks are practical",
650 remediation: "Use crypto/sha256 instead",
651 safe_context_patterns: vec!["checksum", "etag", "cache"],
652 },
653 CryptoPattern {
655 regex: Regex::new(r"(?i)sha1\s*\.\s*(?:New|Sum)\s*\(").expect("Invalid regex"),
656 issue_type: WeakCryptoIssue::WeakHash,
657 algorithm: Algorithm::Sha1,
658 base_severity: Severity::Medium,
659 confidence: Confidence::High,
660 description: "SHA-1 hash in Go (crypto/sha1) - collision attacks demonstrated",
661 remediation: "Use crypto/sha256 for security purposes",
662 safe_context_patterns: vec!["git", "checksum", "legacy"],
663 },
664 CryptoPattern {
666 regex: Regex::new(r"(?i)des\s*\.\s*NewCipher\s*\(").expect("Invalid regex"),
667 issue_type: WeakCryptoIssue::WeakCipher,
668 algorithm: Algorithm::Des,
669 base_severity: Severity::High,
670 confidence: Confidence::High,
671 description: "DES cipher in Go (crypto/des) - 56-bit key is trivially brute-forceable",
672 remediation: "Use crypto/aes instead",
673 safe_context_patterns: vec![],
674 },
675 CryptoPattern {
677 regex: Regex::new(r"(?i)des\s*\.\s*NewTripleDESCipher\s*\(").expect("Invalid regex"),
678 issue_type: WeakCryptoIssue::WeakCipher,
679 algorithm: Algorithm::TripleDes,
680 base_severity: Severity::Medium,
681 confidence: Confidence::High,
682 description: "Triple DES in Go - deprecated",
683 remediation: "Use crypto/aes instead",
684 safe_context_patterns: vec!["legacy"],
685 },
686 CryptoPattern {
688 regex: Regex::new(r"(?i)rc4\s*\.\s*NewCipher\s*\(").expect("Invalid regex"),
689 issue_type: WeakCryptoIssue::WeakCipher,
690 algorithm: Algorithm::Rc4,
691 base_severity: Severity::High,
692 confidence: Confidence::High,
693 description: "RC4 stream cipher in Go (crypto/rc4) - broken, biased keystream",
694 remediation: "Use crypto/aes with GCM mode instead",
695 safe_context_patterns: vec![],
696 },
697 CryptoPattern {
699 regex: Regex::new(r#"(?i)"math/rand""#).expect("Invalid regex"),
700 issue_type: WeakCryptoIssue::PredictableRandom,
701 algorithm: Algorithm::InsecureRandom,
702 base_severity: Severity::Medium,
703 confidence: Confidence::Low, description: "math/rand import detected - not cryptographically secure",
705 remediation: "Use crypto/rand for cryptographic purposes",
706 safe_context_patterns: vec!["test", "mock", "sample"],
707 },
708
709 CryptoPattern {
715 regex: Regex::new(r"(?i)md5\s*::\s*(?:compute|Md5|digest)").expect("Invalid regex"),
716 issue_type: WeakCryptoIssue::WeakHash,
717 algorithm: Algorithm::Md5,
718 base_severity: Severity::Medium,
719 confidence: Confidence::High,
720 description: "MD5 crate usage in Rust - collision attacks are practical",
721 remediation: "Use sha2 crate (Sha256) instead",
722 safe_context_patterns: vec!["checksum", "etag", "cache"],
723 },
724 CryptoPattern {
726 regex: Regex::new(r"(?i)sha1\s*::\s*(?:Sha1|digest)").expect("Invalid regex"),
727 issue_type: WeakCryptoIssue::WeakHash,
728 algorithm: Algorithm::Sha1,
729 base_severity: Severity::Medium,
730 confidence: Confidence::High,
731 description: "SHA-1 crate usage in Rust - collision attacks demonstrated",
732 remediation: "Use sha2 crate (Sha256) for security purposes",
733 safe_context_patterns: vec!["git", "checksum", "legacy"],
734 },
735 CryptoPattern {
737 regex: Regex::new(r"(?i)des\s*::\s*Des\b").expect("Invalid regex"),
738 issue_type: WeakCryptoIssue::WeakCipher,
739 algorithm: Algorithm::Des,
740 base_severity: Severity::High,
741 confidence: Confidence::High,
742 description: "DES crate usage in Rust - 56-bit key is trivially brute-forceable",
743 remediation: "Use aes crate instead",
744 safe_context_patterns: vec![],
745 },
746 CryptoPattern {
748 regex: Regex::new(r"(?i)rc4\s*::\s*Rc4").expect("Invalid regex"),
749 issue_type: WeakCryptoIssue::WeakCipher,
750 algorithm: Algorithm::Rc4,
751 base_severity: Severity::High,
752 confidence: Confidence::High,
753 description: "RC4 crate usage in Rust - broken, biased keystream",
754 remediation: "Use aes-gcm or chacha20poly1305 crate instead",
755 safe_context_patterns: vec![],
756 },
757 CryptoPattern {
759 regex: Regex::new(r"(?i)blowfish\s*::\s*Blowfish").expect("Invalid regex"),
760 issue_type: WeakCryptoIssue::WeakCipher,
761 algorithm: Algorithm::Blowfish,
762 base_severity: Severity::Medium,
763 confidence: Confidence::High,
764 description: "Blowfish crate usage in Rust - 64-bit block size leads to birthday attacks",
765 remediation: "Use aes crate instead (128-bit block size)",
766 safe_context_patterns: vec!["legacy"],
767 },
768 CryptoPattern {
770 regex: Regex::new(r"(?i)rand\s*::\s*(?:thread_rng|random)").expect("Invalid regex"),
771 issue_type: WeakCryptoIssue::PredictableRandom,
772 algorithm: Algorithm::InsecureRandom,
773 base_severity: Severity::Medium,
774 confidence: Confidence::Low, description: "rand crate may not be suitable for cryptographic purposes",
776 remediation: "Use rand::rngs::OsRng or the getrandom crate for crypto",
777 safe_context_patterns: vec!["test", "mock", "sample", "shuffle"],
778 },
779
780 CryptoPattern {
786 regex: Regex::new(r#"(?i)MessageDigest\s*\.\s*getInstance\s*\(\s*["']MD5["']"#).expect("Invalid regex"),
787 issue_type: WeakCryptoIssue::WeakHash,
788 algorithm: Algorithm::Md5,
789 base_severity: Severity::Medium,
790 confidence: Confidence::High,
791 description: "MD5 MessageDigest in Java - collision attacks are practical",
792 remediation: "Use SHA-256: MessageDigest.getInstance(\"SHA-256\")",
793 safe_context_patterns: vec!["checksum", "etag", "cache"],
794 },
795 CryptoPattern {
797 regex: Regex::new(r#"(?i)MessageDigest\s*\.\s*getInstance\s*\(\s*["']SHA-?1["']"#).expect("Invalid regex"),
798 issue_type: WeakCryptoIssue::WeakHash,
799 algorithm: Algorithm::Sha1,
800 base_severity: Severity::Medium,
801 confidence: Confidence::High,
802 description: "SHA-1 MessageDigest in Java - collision attacks demonstrated",
803 remediation: "Use SHA-256: MessageDigest.getInstance(\"SHA-256\")",
804 safe_context_patterns: vec!["git", "checksum", "legacy"],
805 },
806 CryptoPattern {
808 regex: Regex::new(r#"(?i)Cipher\s*\.\s*getInstance\s*\(\s*["']DES[/"']"#).expect("Invalid regex"),
809 issue_type: WeakCryptoIssue::WeakCipher,
810 algorithm: Algorithm::Des,
811 base_severity: Severity::High,
812 confidence: Confidence::High,
813 description: "DES Cipher in Java - 56-bit key is trivially brute-forceable",
814 remediation: "Use AES: Cipher.getInstance(\"AES/GCM/NoPadding\")",
815 safe_context_patterns: vec![],
816 },
817 CryptoPattern {
819 regex: Regex::new(r#"(?i)Cipher\s*\.\s*getInstance\s*\(\s*["'](?:DESede|TripleDES)[/"']"#).expect("Invalid regex"),
820 issue_type: WeakCryptoIssue::WeakCipher,
821 algorithm: Algorithm::TripleDes,
822 base_severity: Severity::Medium,
823 confidence: Confidence::High,
824 description: "Triple DES Cipher in Java - deprecated",
825 remediation: "Use AES: Cipher.getInstance(\"AES/GCM/NoPadding\")",
826 safe_context_patterns: vec!["legacy"],
827 },
828 CryptoPattern {
830 regex: Regex::new(r#"(?i)Cipher\s*\.\s*getInstance\s*\(\s*["'](?:RC4|ARCFOUR)["']"#).expect("Invalid regex"),
831 issue_type: WeakCryptoIssue::WeakCipher,
832 algorithm: Algorithm::Rc4,
833 base_severity: Severity::High,
834 confidence: Confidence::High,
835 description: "RC4 Cipher in Java - broken, biased keystream",
836 remediation: "Use AES: Cipher.getInstance(\"AES/GCM/NoPadding\")",
837 safe_context_patterns: vec![],
838 },
839 CryptoPattern {
841 regex: Regex::new(r#"(?i)Cipher\s*\.\s*getInstance\s*\(\s*["']Blowfish[/"']"#).expect("Invalid regex"),
842 issue_type: WeakCryptoIssue::WeakCipher,
843 algorithm: Algorithm::Blowfish,
844 base_severity: Severity::Medium,
845 confidence: Confidence::High,
846 description: "Blowfish Cipher in Java - 64-bit block size leads to birthday attacks",
847 remediation: "Use AES: Cipher.getInstance(\"AES/GCM/NoPadding\")",
848 safe_context_patterns: vec!["legacy"],
849 },
850 CryptoPattern {
852 regex: Regex::new(r#"(?i)Cipher\s*\.\s*getInstance\s*\(\s*["'][A-Z]+/ECB/"#).expect("Invalid regex"),
853 issue_type: WeakCryptoIssue::InsecureMode,
854 algorithm: Algorithm::EcbMode,
855 base_severity: Severity::High,
856 confidence: Confidence::High,
857 description: "ECB mode in Java - reveals patterns in encrypted data",
858 remediation: "Use GCM mode: Cipher.getInstance(\"AES/GCM/NoPadding\")",
859 safe_context_patterns: vec![],
860 },
861 CryptoPattern {
863 regex: Regex::new(r"(?i)new\s+Random\s*\(").expect("Invalid regex"),
864 issue_type: WeakCryptoIssue::PredictableRandom,
865 algorithm: Algorithm::InsecureRandom,
866 base_severity: Severity::Medium,
867 confidence: Confidence::Low, description: "java.util.Random is not cryptographically secure",
869 remediation: "Use SecureRandom for cryptographic purposes",
870 safe_context_patterns: vec!["test", "mock", "sample", "shuffle"],
871 },
872 CryptoPattern {
874 regex: Regex::new(r#"(?i)KeyPairGenerator\s*\.\s*getInstance\s*\(\s*["']DSA["']"#).expect("Invalid regex"),
875 issue_type: WeakCryptoIssue::WeakCipher,
876 algorithm: Algorithm::Dsa,
877 base_severity: Severity::Medium,
878 confidence: Confidence::High,
879 description: "DSA key generation in Java - deprecated in favor of ECDSA",
880 remediation: "Use EC or RSA: KeyPairGenerator.getInstance(\"EC\") or KeyPairGenerator.getInstance(\"RSA\")",
881 safe_context_patterns: vec!["legacy"],
882 },
883
884 CryptoPattern {
890 regex: Regex::new(r"(?i)\bMD5(?:_Init|_Update|_Final)?\s*\(").expect("Invalid regex"),
891 issue_type: WeakCryptoIssue::WeakHash,
892 algorithm: Algorithm::Md5,
893 base_severity: Severity::Medium,
894 confidence: Confidence::High,
895 description: "MD5 function in C/OpenSSL - collision attacks are practical",
896 remediation: "Use SHA256_Init/Update/Final or EVP_sha256()",
897 safe_context_patterns: vec!["checksum", "etag", "cache"],
898 },
899 CryptoPattern {
901 regex: Regex::new(r"(?i)\bSHA1(?:_Init|_Update|_Final)?\s*\(").expect("Invalid regex"),
902 issue_type: WeakCryptoIssue::WeakHash,
903 algorithm: Algorithm::Sha1,
904 base_severity: Severity::Medium,
905 confidence: Confidence::High,
906 description: "SHA-1 function in C/OpenSSL - collision attacks demonstrated",
907 remediation: "Use SHA256_Init/Update/Final or EVP_sha256()",
908 safe_context_patterns: vec!["git", "checksum", "legacy"],
909 },
910 CryptoPattern {
912 regex: Regex::new(r"(?i)\bDES_(?:set_key|ecb_encrypt|cbc_encrypt|ncbc_encrypt)\s*\(").expect("Invalid regex"),
913 issue_type: WeakCryptoIssue::WeakCipher,
914 algorithm: Algorithm::Des,
915 base_severity: Severity::High,
916 confidence: Confidence::High,
917 description: "DES function in C/OpenSSL - 56-bit key is trivially brute-forceable",
918 remediation: "Use AES with EVP_aes_256_gcm()",
919 safe_context_patterns: vec![],
920 },
921 CryptoPattern {
923 regex: Regex::new(r"(?i)\bRC4(?:_set_key)?\s*\(").expect("Invalid regex"),
924 issue_type: WeakCryptoIssue::WeakCipher,
925 algorithm: Algorithm::Rc4,
926 base_severity: Severity::High,
927 confidence: Confidence::High,
928 description: "RC4 function in C/OpenSSL - broken, biased keystream",
929 remediation: "Use AES-GCM with EVP_aes_256_gcm()",
930 safe_context_patterns: vec![],
931 },
932 CryptoPattern {
934 regex: Regex::new(r"(?i)\bBF_(?:set_key|ecb_encrypt|cbc_encrypt)\s*\(").expect("Invalid regex"),
935 issue_type: WeakCryptoIssue::WeakCipher,
936 algorithm: Algorithm::Blowfish,
937 base_severity: Severity::Medium,
938 confidence: Confidence::High,
939 description: "Blowfish function in C/OpenSSL - 64-bit block size leads to birthday attacks",
940 remediation: "Use AES with EVP_aes_256_gcm()",
941 safe_context_patterns: vec!["legacy"],
942 },
943 CryptoPattern {
945 regex: Regex::new(r"(?i)EVP_(?:md5|sha1)\s*\(").expect("Invalid regex"),
946 issue_type: WeakCryptoIssue::WeakHash,
947 algorithm: Algorithm::Md5,
948 base_severity: Severity::Medium,
949 confidence: Confidence::High,
950 description: "Weak hash via EVP interface - collision attacks possible",
951 remediation: "Use EVP_sha256() or EVP_sha512()",
952 safe_context_patterns: vec!["checksum", "etag", "cache"],
953 },
954 CryptoPattern {
956 regex: Regex::new(r"(?i)EVP_des_(?:ecb|cbc|cfb|ofb)\s*\(").expect("Invalid regex"),
957 issue_type: WeakCryptoIssue::WeakCipher,
958 algorithm: Algorithm::Des,
959 base_severity: Severity::High,
960 confidence: Confidence::High,
961 description: "DES via EVP interface - 56-bit key is trivially brute-forceable",
962 remediation: "Use EVP_aes_256_gcm()",
963 safe_context_patterns: vec![],
964 },
965 CryptoPattern {
967 regex: Regex::new(r"(?i)EVP_[a-z0-9]+_ecb\s*\(").expect("Invalid regex"),
968 issue_type: WeakCryptoIssue::InsecureMode,
969 algorithm: Algorithm::EcbMode,
970 base_severity: Severity::High,
971 confidence: Confidence::High,
972 description: "ECB mode via EVP interface - reveals patterns in encrypted data",
973 remediation: "Use GCM mode: EVP_aes_256_gcm()",
974 safe_context_patterns: vec![],
975 },
976 CryptoPattern {
978 regex: Regex::new(r"\brand\s*\(\s*\)").expect("Invalid regex"),
979 issue_type: WeakCryptoIssue::PredictableRandom,
980 algorithm: Algorithm::InsecureRandom,
981 base_severity: Severity::High,
982 confidence: Confidence::Medium,
983 description: "rand() is not cryptographically secure",
984 remediation: "Use RAND_bytes() from OpenSSL or /dev/urandom",
985 safe_context_patterns: vec!["test", "mock", "sample"],
986 },
987
988 CryptoPattern {
994 regex: Regex::new(r#"(?i)(?:encryption_?key|secret_?key|aes_?key|cipher_?key)\s*[=:]\s*["'][A-Za-z0-9+/=]{16,}["']"#).expect("Invalid regex"),
995 issue_type: WeakCryptoIssue::HardcodedKey,
996 algorithm: Algorithm::HardcodedKeyPattern,
997 base_severity: Severity::Critical,
998 confidence: Confidence::High,
999 description: "Hardcoded encryption key detected - keys should never be in source code",
1000 remediation: "Store keys in environment variables, key management service, or secure vault",
1001 safe_context_patterns: vec!["test", "example", "mock", "placeholder"],
1002 },
1003 CryptoPattern {
1005 regex: Regex::new(r#"(?i)(?:iv|init(?:ialization)?_?vector|nonce)\s*[=:]\s*["'][A-Za-z0-9+/=]{12,}["']"#).expect("Invalid regex"),
1006 issue_type: WeakCryptoIssue::HardcodedIv,
1007 algorithm: Algorithm::HardcodedIvPattern,
1008 base_severity: Severity::High,
1009 confidence: Confidence::High,
1010 description: "Hardcoded IV/nonce detected - IV should be random for each encryption",
1011 remediation: "Generate random IV using cryptographic random number generator",
1012 safe_context_patterns: vec!["test", "example", "mock"],
1013 },
1014 CryptoPattern {
1016 regex: Regex::new(r#"(?i)(?:key|secret)\s*[=:]\s*["'][0-9a-fA-F]{32,}["']"#).expect("Invalid regex"),
1017 issue_type: WeakCryptoIssue::HardcodedKey,
1018 algorithm: Algorithm::HardcodedKeyPattern,
1019 base_severity: Severity::Critical,
1020 confidence: Confidence::Medium,
1021 description: "Possible hardcoded key (hex string) - keys should never be in source code",
1022 remediation: "Store keys in environment variables, key management service, or secure vault",
1023 safe_context_patterns: vec!["test", "example", "mock", "checksum"],
1024 },
1025 CryptoPattern {
1027 regex: Regex::new(r#"(?i)(?:encryption|cipher|aes|secret).*[=:]\s*["'][A-Za-z0-9+/]{22,}={0,2}["']"#).expect("Invalid regex"),
1028 issue_type: WeakCryptoIssue::HardcodedKey,
1029 algorithm: Algorithm::HardcodedKeyPattern,
1030 base_severity: Severity::High,
1031 confidence: Confidence::Medium,
1032 description: "Possible hardcoded key (base64) - keys should never be in source code",
1033 remediation: "Store keys in environment variables, key management service, or secure vault",
1034 safe_context_patterns: vec!["test", "example", "mock"],
1035 },
1036
1037 CryptoPattern {
1043 regex: Regex::new(r"(?i)RSA\.generate\s*\(\s*(?:512|768|1024)\s*[,)]").expect("Invalid regex"),
1044 issue_type: WeakCryptoIssue::InsufficientKeySize,
1045 algorithm: Algorithm::RsaWeakKey,
1046 base_severity: Severity::High,
1047 confidence: Confidence::High,
1048 description: "RSA key size < 2048 bits - can be factored with modern hardware",
1049 remediation: "Use at least 2048-bit RSA keys, preferably 4096-bit",
1050 safe_context_patterns: vec!["test"],
1051 },
1052 CryptoPattern {
1054 regex: Regex::new(r"(?i)\.initialize\s*\(\s*(?:512|768|1024)\s*[,)]").expect("Invalid regex"),
1055 issue_type: WeakCryptoIssue::InsufficientKeySize,
1056 algorithm: Algorithm::RsaWeakKey,
1057 base_severity: Severity::High,
1058 confidence: Confidence::Medium,
1059 description: "RSA key size < 2048 bits - can be factored with modern hardware",
1060 remediation: "Use at least 2048-bit RSA keys, preferably 4096-bit",
1061 safe_context_patterns: vec!["test"],
1062 },
1063 CryptoPattern {
1065 regex: Regex::new(r"(?i)(?:rsa|key_?size|modulus_?(?:size|length))\s*[=:]\s*(?:512|768|1024)\b").expect("Invalid regex"),
1066 issue_type: WeakCryptoIssue::InsufficientKeySize,
1067 algorithm: Algorithm::RsaWeakKey,
1068 base_severity: Severity::High,
1069 confidence: Confidence::Medium,
1070 description: "RSA key size < 2048 bits specified",
1071 remediation: "Use at least 2048-bit RSA keys, preferably 4096-bit",
1072 safe_context_patterns: vec!["test", "min_", "minimum"],
1073 },
1074 ]
1075});
1076
1077pub struct WeakCryptoDetector {
1083 skip_test_files: bool,
1085 include_safe_patterns: bool,
1087}
1088
1089impl Default for WeakCryptoDetector {
1090 fn default() -> Self {
1091 Self::new()
1092 }
1093}
1094
1095impl WeakCryptoDetector {
1096 pub fn new() -> Self {
1098 Self {
1099 skip_test_files: false,
1100 include_safe_patterns: false, }
1102 }
1103
1104 #[must_use]
1106 pub fn skip_test_files(mut self, skip: bool) -> Self {
1107 self.skip_test_files = skip;
1108 self
1109 }
1110
1111 #[must_use]
1113 pub fn include_safe_patterns(mut self, include: bool) -> Self {
1114 self.include_safe_patterns = include;
1115 self
1116 }
1117
1118 fn is_test_file(path: &Path) -> bool {
1120 let path_str = path.to_string_lossy().to_lowercase();
1121
1122 if path_str.contains("/test/")
1124 || path_str.contains("/tests/")
1125 || path_str.contains("/__tests__/")
1126 || path_str.contains("/spec/")
1127 || path_str.contains("/specs/")
1128 || path_str.contains("/_test/")
1129 || path_str.contains("/test_")
1130 || path_str.contains("/testdata/")
1131 || path_str.contains("/fixtures/")
1132 || path_str.contains("/mocks/")
1133 {
1134 return true;
1135 }
1136
1137 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1139 let name_lower = name.to_lowercase();
1140 if name_lower.starts_with("test_")
1141 || name_lower.ends_with("_test.py")
1142 || name_lower.ends_with("_test.go")
1143 || name_lower.ends_with("_test.rs")
1144 || name_lower.ends_with("_test.ts")
1145 || name_lower.ends_with("_test.js")
1146 || name_lower.ends_with(".test.ts")
1147 || name_lower.ends_with(".test.js")
1148 || name_lower.ends_with(".test.tsx")
1149 || name_lower.ends_with(".test.jsx")
1150 || name_lower.ends_with(".spec.ts")
1151 || name_lower.ends_with(".spec.js")
1152 || name_lower.ends_with("_spec.rb")
1153 || name_lower.ends_with("test.java")
1154 {
1155 return true;
1156 }
1157 }
1158
1159 false
1160 }
1161
1162 fn analyze_context(line: &str, surrounding_lines: &[&str]) -> UsageContext {
1170 let line_lower = line.to_lowercase();
1171 let surrounding = surrounding_lines.join(" ").to_lowercase();
1172
1173 if line_lower.contains("checksum")
1176 || line_lower.contains("file_hash")
1177 || line_lower.contains("content_hash")
1178 || line_lower.contains("filehash")
1179 {
1180 return UsageContext::FileChecksum;
1181 }
1182 if line_lower.contains("cache_key") || line_lower.contains("cache") && line_lower.contains("hash") {
1183 return UsageContext::CacheKey;
1184 }
1185 if line_lower.contains("git") && !line_lower.contains("digit") {
1186 return UsageContext::GitOperation;
1187 }
1188 if line_lower.contains("hmac") {
1189 return UsageContext::Hmac;
1190 }
1191
1192 if line_lower.contains("password") || line_lower.contains("passwd") || line_lower.contains("pwd") {
1194 return UsageContext::PasswordHashing;
1195 }
1196 if line_lower.contains("signature") || (line_lower.contains("sign") && !line_lower.contains("assign")) {
1197 return UsageContext::Signature;
1198 }
1199 if line_lower.contains("encrypt") || line_lower.contains("cipher") {
1200 return UsageContext::Encryption;
1201 }
1202 if line_lower.contains("token") || line_lower.contains("credential") || line_lower.contains("auth") {
1203 return UsageContext::PasswordHashing;
1204 }
1205
1206 if surrounding.contains("password") || surrounding.contains("passwd") {
1209 return UsageContext::PasswordHashing;
1210 }
1211 if surrounding.contains("signature") || (surrounding.contains("sign") && !surrounding.contains("assign")) {
1212 return UsageContext::Signature;
1213 }
1214 if surrounding.contains("encrypt") || surrounding.contains("cipher") {
1215 return UsageContext::Encryption;
1216 }
1217
1218 UsageContext::Unknown
1219 }
1220
1221 fn is_likely_safe(
1223 context: UsageContext,
1224 issue_type: WeakCryptoIssue,
1225 algorithm: &Algorithm,
1226 ) -> bool {
1227 match context {
1228 UsageContext::FileChecksum => matches!(
1230 (issue_type, algorithm),
1231 (WeakCryptoIssue::WeakHash, Algorithm::Md5)
1232 | (WeakCryptoIssue::WeakHash, Algorithm::Sha1)
1233 ),
1234 UsageContext::CacheKey => matches!(
1236 (issue_type, algorithm),
1237 (WeakCryptoIssue::WeakHash, Algorithm::Md5)
1238 | (WeakCryptoIssue::WeakHash, Algorithm::Sha1)
1239 ),
1240 UsageContext::GitOperation => matches!(
1242 (issue_type, algorithm),
1243 (WeakCryptoIssue::WeakHash, Algorithm::Sha1)
1244 ),
1245 UsageContext::Hmac => matches!(
1247 (issue_type, algorithm),
1248 (WeakCryptoIssue::WeakHash, Algorithm::Md5)
1249 | (WeakCryptoIssue::WeakHash, Algorithm::Sha1)
1250 ),
1251 _ => false,
1253 }
1254 }
1255
1256 fn adjust_severity(
1258 base_severity: Severity,
1259 context: UsageContext,
1260 is_test: bool,
1261 likely_safe: bool,
1262 ) -> Severity {
1263 if is_test {
1265 return match base_severity {
1266 Severity::Critical => Severity::Low,
1267 Severity::High => Severity::Low,
1268 Severity::Medium => Severity::Info,
1269 _ => Severity::Info,
1270 };
1271 }
1272
1273 if likely_safe {
1275 return match base_severity {
1276 Severity::Critical | Severity::High => Severity::Info,
1277 _ => Severity::Info,
1278 };
1279 }
1280
1281 match context {
1283 UsageContext::PasswordHashing => match base_severity {
1284 Severity::Medium => Severity::High,
1285 Severity::High => Severity::Critical,
1286 s => s,
1287 },
1288 UsageContext::Signature | UsageContext::Encryption => match base_severity {
1289 Severity::Medium => Severity::High,
1290 s => s,
1291 },
1292 _ => base_severity,
1293 }
1294 }
1295
1296 fn get_remediation(issue_type: WeakCryptoIssue, algorithm: &Algorithm) -> String {
1298 match issue_type {
1299 WeakCryptoIssue::WeakHash => {
1300 match algorithm {
1301 Algorithm::Md5 => "Replace MD5 with SHA-256 for integrity checks, or bcrypt/argon2 for password hashing".to_string(),
1302 Algorithm::Sha1 => "Replace SHA-1 with SHA-256 for security purposes".to_string(),
1303 _ => "Use SHA-256 or SHA-3 family of hash functions".to_string(),
1304 }
1305 }
1306 WeakCryptoIssue::WeakCipher => {
1307 match algorithm {
1308 Algorithm::Des => "Replace DES with AES-256-GCM for authenticated encryption".to_string(),
1309 Algorithm::TripleDes => "Replace 3DES with AES-256-GCM for better performance and security".to_string(),
1310 Algorithm::Rc4 => "Replace RC4 with AES-GCM or ChaCha20-Poly1305".to_string(),
1311 Algorithm::Blowfish => "Replace Blowfish with AES-256 (128-bit block size)".to_string(),
1312 _ => "Use AES-256-GCM or ChaCha20-Poly1305 for authenticated encryption".to_string(),
1313 }
1314 }
1315 WeakCryptoIssue::InsecureMode => {
1316 "Use GCM or CCM mode for authenticated encryption, or at minimum CBC with random IV".to_string()
1317 }
1318 WeakCryptoIssue::InsufficientKeySize => {
1319 "Use at least 2048-bit RSA keys (4096-bit recommended), or switch to ECC (P-256 or better)".to_string()
1320 }
1321 WeakCryptoIssue::HardcodedKey => {
1322 "Store encryption keys in:\n - Environment variables\n - Key management service (AWS KMS, HashiCorp Vault)\n - Hardware security module (HSM)\n Never commit keys to source control".to_string()
1323 }
1324 WeakCryptoIssue::HardcodedIv => {
1325 "Generate a random IV for each encryption operation:\n - Python: os.urandom(16)\n - Node.js: crypto.randomBytes(16)\n - Go: io.ReadFull(rand.Reader, iv)\n Store IV alongside ciphertext".to_string()
1326 }
1327 WeakCryptoIssue::PredictableRandom => {
1328 "Use cryptographically secure random:\n - Python: secrets module or os.urandom()\n - Node.js: crypto.randomBytes()\n - Go: crypto/rand\n - Rust: rand::rngs::OsRng or getrandom".to_string()
1329 }
1330 WeakCryptoIssue::MissingAuthentication => {
1331 "Use authenticated encryption (AEAD):\n - AES-GCM\n - ChaCha20-Poly1305\n Or add HMAC for integrity verification".to_string()
1332 }
1333 WeakCryptoIssue::DeprecatedFunction => {
1334 "Update to use the modern, recommended API for cryptographic operations".to_string()
1335 }
1336 }
1337 }
1338
1339 pub fn scan_file(&self, file_path: &str) -> Result<Vec<WeakCryptoFinding>> {
1341 let path = Path::new(file_path);
1342 let is_test = Self::is_test_file(path);
1343
1344 if self.skip_test_files && is_test {
1346 return Ok(vec![]);
1347 }
1348
1349 let source = std::fs::read_to_string(path).map_err(|e| BrrrError::io_with_path(e, path))?;
1350 let lines: Vec<&str> = source.lines().collect();
1351
1352 let mut findings = Vec::new();
1353 let mut seen_locations: HashSet<(usize, usize)> = HashSet::new();
1354
1355 for (line_num, line) in lines.iter().enumerate() {
1356 let line_number = line_num + 1;
1357
1358 let trimmed = line.trim();
1360 if trimmed.starts_with('#')
1361 || trimmed.starts_with("//")
1362 || trimmed.starts_with('*')
1363 || trimmed.starts_with("/*")
1364 || trimmed.starts_with("'''")
1365 || trimmed.starts_with("\"\"\"")
1366 {
1367 continue;
1368 }
1369
1370 let start = line_num.saturating_sub(3);
1372 let end = (line_num + 4).min(lines.len());
1373 let surrounding: Vec<&str> = lines[start..end].to_vec();
1374
1375 for pattern in CRYPTO_PATTERNS.iter() {
1377 if let Some(m) = pattern.regex.find(line) {
1378 let loc_key = (line_number, m.start());
1380 if seen_locations.contains(&loc_key) {
1381 continue;
1382 }
1383 seen_locations.insert(loc_key);
1384
1385 let context = Self::analyze_context(line, &surrounding);
1387
1388 let pattern_safe = pattern
1391 .safe_context_patterns
1392 .iter()
1393 .any(|p| line.to_lowercase().contains(p));
1394
1395 let is_security_sensitive = matches!(
1398 context,
1399 UsageContext::PasswordHashing
1400 | UsageContext::Signature
1401 | UsageContext::Encryption
1402 );
1403
1404 let likely_safe = !is_security_sensitive
1406 && (pattern_safe
1407 || Self::is_likely_safe(context, pattern.issue_type, &pattern.algorithm));
1408
1409 if likely_safe && !self.include_safe_patterns {
1411 continue;
1412 }
1413
1414 let severity = Self::adjust_severity(
1416 pattern.base_severity,
1417 context,
1418 is_test,
1419 likely_safe,
1420 );
1421
1422 let snippet_start = line_num.saturating_sub(1);
1424 let snippet_end = (line_num + 2).min(lines.len());
1425 let code_snippet = lines[snippet_start..snippet_end].join("\n");
1426
1427 findings.push(WeakCryptoFinding {
1428 location: Location {
1429 file: file_path.to_string(),
1430 line: line_number,
1431 column: m.start() + 1,
1432 end_line: line_number,
1433 end_column: m.end() + 1,
1434 },
1435 issue_type: pattern.issue_type,
1436 algorithm: pattern.algorithm.clone(),
1437 severity,
1438 confidence: pattern.confidence,
1439 context,
1440 code_snippet,
1441 description: pattern.description.to_string(),
1442 remediation: Self::get_remediation(pattern.issue_type, &pattern.algorithm),
1443 is_test_file: is_test,
1444 likely_safe,
1445 });
1446 }
1447 }
1448 }
1449
1450 Ok(findings)
1451 }
1452
1453 pub fn scan_directory(&self, dir_path: &str, language: Option<&str>) -> Result<ScanResult> {
1455 let path = Path::new(dir_path);
1456 if !path.is_dir() {
1457 return Err(BrrrError::InvalidArgument(format!(
1458 "Not a directory: {}",
1459 dir_path
1460 )));
1461 }
1462
1463 let mut findings = Vec::new();
1464 let mut files_scanned = 0;
1465
1466 let mut builder = ignore::WalkBuilder::new(path);
1468 builder.add_custom_ignore_filename(".brrrignore");
1469 builder.hidden(true);
1470
1471 let extensions: HashSet<&str> = match language {
1473 Some("python") => ["py"].iter().copied().collect(),
1474 Some("typescript") | Some("javascript") => ["ts", "tsx", "js", "jsx", "mjs", "cjs"]
1475 .iter()
1476 .copied()
1477 .collect(),
1478 Some("go") => ["go"].iter().copied().collect(),
1479 Some("rust") => ["rs"].iter().copied().collect(),
1480 Some("java") => ["java"].iter().copied().collect(),
1481 Some("c") => ["c", "h"].iter().copied().collect(),
1482 Some("cpp") => ["cpp", "cc", "cxx", "hpp", "h"].iter().copied().collect(),
1483 _ => [
1484 "py", "ts", "tsx", "js", "jsx", "mjs", "cjs", "go", "rs", "java", "c", "h", "cpp",
1485 "cc", "cxx", "hpp",
1486 ]
1487 .iter()
1488 .copied()
1489 .collect(),
1490 };
1491
1492 for entry in builder.build().flatten() {
1493 let entry_path = entry.path();
1494 if !entry_path.is_file() {
1495 continue;
1496 }
1497
1498 let ext = entry_path
1499 .extension()
1500 .and_then(|e| e.to_str())
1501 .unwrap_or("");
1502
1503 if !extensions.contains(ext) {
1504 continue;
1505 }
1506
1507 files_scanned += 1;
1508
1509 if let Ok(file_findings) = self.scan_file(entry_path.to_str().unwrap_or("")) {
1510 findings.extend(file_findings);
1511 }
1512 }
1513
1514 let mut issue_counts: HashMap<String, usize> = HashMap::new();
1516 let mut severity_counts: HashMap<String, usize> = HashMap::new();
1517 let mut algorithm_counts: HashMap<String, usize> = HashMap::new();
1518
1519 for finding in &findings {
1520 *issue_counts
1521 .entry(finding.issue_type.to_string())
1522 .or_insert(0) += 1;
1523 *severity_counts
1524 .entry(finding.severity.to_string())
1525 .or_insert(0) += 1;
1526 *algorithm_counts
1527 .entry(finding.algorithm.to_string())
1528 .or_insert(0) += 1;
1529 }
1530
1531 Ok(ScanResult {
1532 findings,
1533 files_scanned,
1534 issue_counts,
1535 severity_counts,
1536 algorithm_counts,
1537 })
1538 }
1539}
1540
1541pub fn scan_weak_crypto(path: &str, language: Option<&str>) -> Result<ScanResult> {
1556 let detector = WeakCryptoDetector::new();
1557 let path_obj = Path::new(path);
1558
1559 if path_obj.is_file() {
1560 let findings = detector.scan_file(path)?;
1561
1562 let mut issue_counts: HashMap<String, usize> = HashMap::new();
1563 let mut severity_counts: HashMap<String, usize> = HashMap::new();
1564 let mut algorithm_counts: HashMap<String, usize> = HashMap::new();
1565
1566 for finding in &findings {
1567 *issue_counts
1568 .entry(finding.issue_type.to_string())
1569 .or_insert(0) += 1;
1570 *severity_counts
1571 .entry(finding.severity.to_string())
1572 .or_insert(0) += 1;
1573 *algorithm_counts
1574 .entry(finding.algorithm.to_string())
1575 .or_insert(0) += 1;
1576 }
1577
1578 Ok(ScanResult {
1579 findings,
1580 files_scanned: 1,
1581 issue_counts,
1582 severity_counts,
1583 algorithm_counts,
1584 })
1585 } else {
1586 detector.scan_directory(path, language)
1587 }
1588}
1589
1590pub fn scan_file_weak_crypto(path: &Path, _language: Option<&str>) -> Result<Vec<WeakCryptoFinding>> {
1592 let detector = WeakCryptoDetector::new();
1593 detector.scan_file(path.to_str().unwrap_or(""))
1594}
1595
1596#[cfg(test)]
1601mod tests {
1602 use super::*;
1603 use std::io::Write;
1604
1605 fn create_temp_file(content: &str, extension: &str) -> tempfile::NamedTempFile {
1606 let mut file = tempfile::Builder::new()
1607 .suffix(extension)
1608 .tempfile()
1609 .expect("Failed to create temp file");
1610 file.write_all(content.as_bytes())
1611 .expect("Failed to write temp file");
1612 file
1613 }
1614
1615 #[test]
1620 fn test_detect_python_md5() {
1621 let source = r#"
1622import hashlib
1623password_hash = hashlib.md5(password.encode()).hexdigest()
1624 "#;
1625 let file = create_temp_file(source, ".py");
1626 let detector = WeakCryptoDetector::new();
1627 let findings = detector
1628 .scan_file(file.path().to_str().unwrap())
1629 .expect("Scan should succeed");
1630
1631 assert!(!findings.is_empty(), "Should detect MD5 usage");
1632 assert_eq!(findings[0].algorithm, Algorithm::Md5);
1633 assert_eq!(findings[0].issue_type, WeakCryptoIssue::WeakHash);
1634 }
1635
1636 #[test]
1637 fn test_detect_python_md5_for_checksum_safe() {
1638 let source = r#"
1639import hashlib
1640# Calculate file checksum
1641file_checksum = hashlib.md5(file_content).hexdigest()
1642 "#;
1643 let file = create_temp_file(source, ".py");
1644 let detector = WeakCryptoDetector::new();
1645 let findings = detector
1646 .scan_file(file.path().to_str().unwrap())
1647 .expect("Scan should succeed");
1648
1649 assert!(findings.is_empty(), "Should not flag MD5 for checksums by default");
1651 }
1652
1653 #[test]
1654 fn test_detect_python_md5_for_checksum_include_safe() {
1655 let source = r#"
1656import hashlib
1657# Calculate file checksum
1658file_checksum = hashlib.md5(file_content).hexdigest()
1659 "#;
1660 let file = create_temp_file(source, ".py");
1661 let detector = WeakCryptoDetector::new().include_safe_patterns(true);
1662 let findings = detector
1663 .scan_file(file.path().to_str().unwrap())
1664 .expect("Scan should succeed");
1665
1666 assert!(!findings.is_empty(), "Should detect MD5 when including safe patterns");
1667 assert!(findings[0].likely_safe, "Should mark as likely safe");
1668 assert_eq!(findings[0].severity, Severity::Info, "Severity should be reduced");
1669 }
1670
1671 #[test]
1672 fn test_detect_python_sha1() {
1673 let source = r#"
1674import hashlib
1675token = hashlib.sha1(secret.encode()).hexdigest()
1676 "#;
1677 let file = create_temp_file(source, ".py");
1678 let detector = WeakCryptoDetector::new();
1679 let findings = detector
1680 .scan_file(file.path().to_str().unwrap())
1681 .expect("Scan should succeed");
1682
1683 assert!(!findings.is_empty(), "Should detect SHA-1 usage");
1684 assert_eq!(findings[0].algorithm, Algorithm::Sha1);
1685 }
1686
1687 #[test]
1688 fn test_detect_python_des() {
1689 let source = r#"
1690from Crypto.Cipher import DES
1691cipher = DES.new(key, DES.MODE_ECB)
1692 "#;
1693 let file = create_temp_file(source, ".py");
1694 let detector = WeakCryptoDetector::new();
1695 let findings = detector
1696 .scan_file(file.path().to_str().unwrap())
1697 .expect("Scan should succeed");
1698
1699 let des_finding = findings.iter().find(|f| f.algorithm == Algorithm::Des);
1700 assert!(des_finding.is_some(), "Should detect DES cipher");
1701 assert_eq!(des_finding.unwrap().severity, Severity::High);
1702 }
1703
1704 #[test]
1705 fn test_detect_python_ecb_mode() {
1706 let source = r#"
1707from Crypto.Cipher import AES
1708cipher = AES.new(key, AES.MODE_ECB)
1709 "#;
1710 let file = create_temp_file(source, ".py");
1711 let detector = WeakCryptoDetector::new();
1712 let findings = detector
1713 .scan_file(file.path().to_str().unwrap())
1714 .expect("Scan should succeed");
1715
1716 let ecb_finding = findings.iter().find(|f| f.algorithm == Algorithm::EcbMode);
1717 assert!(ecb_finding.is_some(), "Should detect ECB mode");
1718 }
1719
1720 #[test]
1721 fn test_detect_python_predictable_random() {
1722 let source = r#"
1723import random
1724encryption_key = ''.join(random.choice(chars) for _ in range(32))
1725 "#;
1726 let file = create_temp_file(source, ".py");
1727 let detector = WeakCryptoDetector::new();
1728 let findings = detector
1729 .scan_file(file.path().to_str().unwrap())
1730 .expect("Scan should succeed");
1731
1732 let random_finding = findings.iter().find(|f| f.issue_type == WeakCryptoIssue::PredictableRandom);
1733 assert!(random_finding.is_some(), "Should detect insecure random");
1734 }
1735
1736 #[test]
1741 fn test_detect_nodejs_md5() {
1742 let source = r#"
1743const crypto = require('crypto');
1744const hash = crypto.createHash('md5').update(password).digest('hex');
1745 "#;
1746 let file = create_temp_file(source, ".js");
1747 let detector = WeakCryptoDetector::new();
1748 let findings = detector
1749 .scan_file(file.path().to_str().unwrap())
1750 .expect("Scan should succeed");
1751
1752 assert!(!findings.is_empty(), "Should detect MD5 in Node.js");
1753 assert_eq!(findings[0].algorithm, Algorithm::Md5);
1754 }
1755
1756 #[test]
1757 fn test_detect_nodejs_des() {
1758 let source = r#"
1759const crypto = require('crypto');
1760const cipher = crypto.createCipher('des', key);
1761 "#;
1762 let file = create_temp_file(source, ".js");
1763 let detector = WeakCryptoDetector::new();
1764 let findings = detector
1765 .scan_file(file.path().to_str().unwrap())
1766 .expect("Scan should succeed");
1767
1768 let des_finding = findings.iter().find(|f| f.algorithm == Algorithm::Des);
1770 assert!(des_finding.is_some(), "Should detect DES in Node.js");
1771 }
1772
1773 #[test]
1774 fn test_detect_nodejs_ecb_mode() {
1775 let source = r#"
1776const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
1777 "#;
1778 let file = create_temp_file(source, ".js");
1779 let detector = WeakCryptoDetector::new();
1780 let findings = detector
1781 .scan_file(file.path().to_str().unwrap())
1782 .expect("Scan should succeed");
1783
1784 let ecb_finding = findings.iter().find(|f| f.algorithm == Algorithm::EcbMode);
1785 assert!(ecb_finding.is_some(), "Should detect ECB mode in Node.js");
1786 }
1787
1788 #[test]
1789 fn test_detect_cryptojs_md5() {
1790 let source = r#"
1791const hash = CryptoJS.MD5(password).toString();
1792 "#;
1793 let file = create_temp_file(source, ".js");
1794 let detector = WeakCryptoDetector::new();
1795 let findings = detector
1796 .scan_file(file.path().to_str().unwrap())
1797 .expect("Scan should succeed");
1798
1799 assert!(!findings.is_empty(), "Should detect CryptoJS MD5");
1800 }
1801
1802 #[test]
1807 fn test_detect_go_md5() {
1808 let source = r#"
1809package main
1810
1811import "crypto/md5"
1812
1813func hash(data []byte) []byte {
1814 h := md5.New()
1815 h.Write(data)
1816 return h.Sum(nil)
1817}
1818 "#;
1819 let file = create_temp_file(source, ".go");
1820 let detector = WeakCryptoDetector::new();
1821 let findings = detector
1822 .scan_file(file.path().to_str().unwrap())
1823 .expect("Scan should succeed");
1824
1825 assert!(!findings.is_empty(), "Should detect MD5 in Go");
1826 assert_eq!(findings[0].algorithm, Algorithm::Md5);
1827 }
1828
1829 #[test]
1830 fn test_detect_go_des() {
1831 let source = r#"
1832package main
1833
1834import "crypto/des"
1835
1836func encrypt(key, data []byte) {
1837 block, _ := des.NewCipher(key)
1838}
1839 "#;
1840 let file = create_temp_file(source, ".go");
1841 let detector = WeakCryptoDetector::new();
1842 let findings = detector
1843 .scan_file(file.path().to_str().unwrap())
1844 .expect("Scan should succeed");
1845
1846 let des_finding = findings.iter().find(|f| f.algorithm == Algorithm::Des);
1847 assert!(des_finding.is_some(), "Should detect DES in Go");
1848 }
1849
1850 #[test]
1855 fn test_detect_rust_md5() {
1856 let source = r#"
1857use md5::{Md5, Digest};
1858
1859fn hash_password(password: &str) -> String {
1860 let result = md5::compute(password.as_bytes());
1861 format!("{:x}", result)
1862}
1863 "#;
1864 let file = create_temp_file(source, ".rs");
1865 let detector = WeakCryptoDetector::new();
1866 let findings = detector
1867 .scan_file(file.path().to_str().unwrap())
1868 .expect("Scan should succeed");
1869
1870 assert!(!findings.is_empty(), "Should detect MD5 in Rust");
1871 }
1872
1873 #[test]
1878 fn test_detect_java_md5() {
1879 let source = r#"
1880import java.security.MessageDigest;
1881
1882public class HashUtil {
1883 public static byte[] hash(String data) throws Exception {
1884 MessageDigest md = MessageDigest.getInstance("MD5");
1885 return md.digest(data.getBytes());
1886 }
1887}
1888 "#;
1889 let file = create_temp_file(source, ".java");
1890 let detector = WeakCryptoDetector::new();
1891 let findings = detector
1892 .scan_file(file.path().to_str().unwrap())
1893 .expect("Scan should succeed");
1894
1895 assert!(!findings.is_empty(), "Should detect MD5 in Java");
1896 assert_eq!(findings[0].algorithm, Algorithm::Md5);
1897 }
1898
1899 #[test]
1900 fn test_detect_java_des() {
1901 let source = r#"
1902import javax.crypto.Cipher;
1903
1904public class CryptoUtil {
1905 public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
1906 Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
1907 return cipher.doFinal(data);
1908 }
1909}
1910 "#;
1911 let file = create_temp_file(source, ".java");
1912 let detector = WeakCryptoDetector::new();
1913 let findings = detector
1914 .scan_file(file.path().to_str().unwrap())
1915 .expect("Scan should succeed");
1916
1917 let des_finding = findings.iter().find(|f| f.algorithm == Algorithm::Des);
1919 assert!(des_finding.is_some(), "Should detect DES in Java");
1920 }
1921
1922 #[test]
1927 fn test_detect_c_md5() {
1928 let source = r#"
1929#include <openssl/md5.h>
1930
1931void hash_password(const char* password, unsigned char* digest) {
1932 MD5((unsigned char*)password, strlen(password), digest);
1933}
1934 "#;
1935 let file = create_temp_file(source, ".c");
1936 let detector = WeakCryptoDetector::new();
1937 let findings = detector
1938 .scan_file(file.path().to_str().unwrap())
1939 .expect("Scan should succeed");
1940
1941 assert!(!findings.is_empty(), "Should detect MD5 in C");
1942 assert_eq!(findings[0].algorithm, Algorithm::Md5);
1943 }
1944
1945 #[test]
1946 fn test_detect_c_des() {
1947 let source = r#"
1948#include <openssl/des.h>
1949
1950void encrypt(DES_cblock* key, unsigned char* data) {
1951 DES_key_schedule schedule;
1952 DES_set_key(key, &schedule);
1953 DES_ecb_encrypt((DES_cblock*)data, (DES_cblock*)data, &schedule, DES_ENCRYPT);
1954}
1955 "#;
1956 let file = create_temp_file(source, ".c");
1957 let detector = WeakCryptoDetector::new();
1958 let findings = detector
1959 .scan_file(file.path().to_str().unwrap())
1960 .expect("Scan should succeed");
1961
1962 let des_finding = findings.iter().find(|f| f.algorithm == Algorithm::Des);
1963 assert!(des_finding.is_some(), "Should detect DES in C");
1964 }
1965
1966 #[test]
1971 fn test_detect_hardcoded_key() {
1972 let source = r#"
1973const encryption_key = "0123456789abcdef0123456789abcdef";
1974const cipher = crypto.createCipheriv('aes-256-cbc', encryption_key, iv);
1975 "#;
1976 let file = create_temp_file(source, ".js");
1977 let detector = WeakCryptoDetector::new();
1978 let findings = detector
1979 .scan_file(file.path().to_str().unwrap())
1980 .expect("Scan should succeed");
1981
1982 let key_finding = findings.iter().find(|f| f.issue_type == WeakCryptoIssue::HardcodedKey);
1983 assert!(key_finding.is_some(), "Should detect hardcoded key");
1984 assert_eq!(key_finding.unwrap().severity, Severity::Critical);
1985 }
1986
1987 #[test]
1988 fn test_detect_hardcoded_iv() {
1989 let source = r#"
1990iv = "1234567890123456"
1991cipher = AES.new(key, AES.MODE_CBC, iv)
1992 "#;
1993 let file = create_temp_file(source, ".py");
1994 let detector = WeakCryptoDetector::new();
1995 let findings = detector
1996 .scan_file(file.path().to_str().unwrap())
1997 .expect("Scan should succeed");
1998
1999 let iv_finding = findings.iter().find(|f| f.issue_type == WeakCryptoIssue::HardcodedIv);
2000 assert!(iv_finding.is_some(), "Should detect hardcoded IV");
2001 }
2002
2003 #[test]
2008 fn test_detect_weak_rsa_python() {
2009 let source = r#"
2010from Crypto.PublicKey import RSA
2011key = RSA.generate(1024)
2012 "#;
2013 let file = create_temp_file(source, ".py");
2014 let detector = WeakCryptoDetector::new();
2015 let findings = detector
2016 .scan_file(file.path().to_str().unwrap())
2017 .expect("Scan should succeed");
2018
2019 let rsa_finding = findings.iter().find(|f| f.issue_type == WeakCryptoIssue::InsufficientKeySize);
2020 assert!(rsa_finding.is_some(), "Should detect weak RSA key size");
2021 }
2022
2023 #[test]
2028 fn test_password_context_increases_severity() {
2029 let source = r#"
2030import hashlib
2031# Hash password for storage
2032password_hash = hashlib.sha1(password.encode()).hexdigest()
2033 "#;
2034 let file = create_temp_file(source, ".py");
2035 let detector = WeakCryptoDetector::new();
2036 let findings = detector
2037 .scan_file(file.path().to_str().unwrap())
2038 .expect("Scan should succeed");
2039
2040 assert!(!findings.is_empty(), "Should detect SHA-1 usage");
2041 assert!(
2043 findings[0].severity >= Severity::High,
2044 "Severity should be elevated for password context"
2045 );
2046 }
2047
2048 #[test]
2049 fn test_test_file_reduces_severity() {
2050 let source = r#"
2051import hashlib
2052hash = hashlib.md5(data).hexdigest()
2053 "#;
2054 let mut file = tempfile::Builder::new()
2055 .suffix("_test.py")
2056 .tempfile()
2057 .expect("Failed to create temp file");
2058 file.write_all(source.as_bytes())
2059 .expect("Failed to write temp file");
2060
2061 let detector = WeakCryptoDetector::new();
2062 let findings = detector
2063 .scan_file(file.path().to_str().unwrap())
2064 .expect("Scan should succeed");
2065
2066 assert!(!findings.is_empty(), "Should detect MD5 in test file");
2067 assert!(findings[0].is_test_file, "Should mark as test file");
2068 assert!(
2069 findings[0].severity <= Severity::Low,
2070 "Severity should be reduced for test files"
2071 );
2072 }
2073
2074 #[test]
2079 fn test_issue_type_display() {
2080 assert_eq!(WeakCryptoIssue::WeakHash.to_string(), "Weak Hash Algorithm");
2081 assert_eq!(WeakCryptoIssue::WeakCipher.to_string(), "Weak Cipher Algorithm");
2082 assert_eq!(WeakCryptoIssue::InsecureMode.to_string(), "Insecure Cipher Mode");
2083 assert_eq!(WeakCryptoIssue::HardcodedKey.to_string(), "Hardcoded Encryption Key");
2084 }
2085
2086 #[test]
2087 fn test_algorithm_display() {
2088 assert_eq!(Algorithm::Md5.to_string(), "MD5");
2089 assert_eq!(Algorithm::Sha1.to_string(), "SHA-1");
2090 assert_eq!(Algorithm::Des.to_string(), "DES");
2091 assert_eq!(Algorithm::EcbMode.to_string(), "ECB Mode");
2092 }
2093
2094 #[test]
2095 fn test_severity_display() {
2096 assert_eq!(Severity::Critical.to_string(), "CRITICAL");
2097 assert_eq!(Severity::High.to_string(), "HIGH");
2098 assert_eq!(Severity::Medium.to_string(), "MEDIUM");
2099 assert_eq!(Severity::Low.to_string(), "LOW");
2100 assert_eq!(Severity::Info.to_string(), "INFO");
2101 }
2102
2103 #[test]
2108 fn test_scan_result_counts() {
2109 let result = ScanResult {
2110 findings: vec![],
2111 files_scanned: 10,
2112 issue_counts: [("Weak Hash Algorithm".to_string(), 5)]
2113 .into_iter()
2114 .collect(),
2115 severity_counts: [("HIGH".to_string(), 3), ("MEDIUM".to_string(), 2)]
2116 .into_iter()
2117 .collect(),
2118 algorithm_counts: [("MD5".to_string(), 3), ("SHA-1".to_string(), 2)]
2119 .into_iter()
2120 .collect(),
2121 };
2122
2123 assert_eq!(result.files_scanned, 10);
2124 assert_eq!(result.issue_counts.get("Weak Hash Algorithm"), Some(&5));
2125 assert_eq!(result.severity_counts.get("HIGH"), Some(&3));
2126 assert_eq!(result.algorithm_counts.get("MD5"), Some(&3));
2127 }
2128}