diff --git a/deny.toml b/deny.toml index a2f1f97..9eae6e6 100644 --- a/deny.toml +++ b/deny.toml @@ -1,7 +1,7 @@ # CryptoTrace — cargo-deny configuration [advisories] vulnerability = "deny" -unmaintained = "warn" +unmaintained = "all" notice = "warn" yanked = "warn" ignore = [] diff --git a/src/analyzers/file.rs b/src/analyzers/file.rs index c3a04a1..29cea57 100644 --- a/src/analyzers/file.rs +++ b/src/analyzers/file.rs @@ -152,7 +152,7 @@ mod tests { let result = analyze_bytes(b"%PDF-1.4\n...", crate::types::SourceType::File).unwrap(); assert_eq!(result.detected_type, "document"); assert_eq!(result.algorithm.as_deref(), Some("pdf")); - assert!(result.signals.map_or(false, |s| s.magic_bytes > 0.5)); + assert!(result.signals.is_some_and(|s| s.magic_bytes > 0.5)); } #[test] diff --git a/src/analyzers/recursive.rs b/src/analyzers/recursive.rs index 1b7b2cc..eec3dac 100644 --- a/src/analyzers/recursive.rs +++ b/src/analyzers/recursive.rs @@ -101,7 +101,7 @@ pub fn analyze_recursive(data: &[u8], config: &RecursiveConfig) -> Result 0 { + if !decoded.is_empty() && !current_data.is_empty() { let ratio = decoded.len() as f64 / current_data.len() as f64; if ratio > config.max_expansion_ratio { return Err(CryptoTraceError::CompressionBomb { @@ -113,7 +113,7 @@ pub fn analyze_recursive(data: &[u8], config: &RecursiveConfig) -> Result 0 { + if !current_data.is_empty() { d.len() as f64 / current_data.len() as f64 } else { 1.0 diff --git a/src/cache.rs b/src/cache.rs index 163f25d..ac424bd 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -63,6 +63,10 @@ impl LruCache { self.entries.len() } + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + pub fn capacity(&self) -> usize { self.max_entries } diff --git a/src/cli.rs b/src/cli.rs index a556db5..049f4ef 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -187,10 +187,10 @@ pub async fn run_with_cli(cli: &Cli) -> Result Result Result Result Result> { if let Ok(model) = std::env::var("ANTHROPIC_MODEL") { config.model = model; } - } else if std::env::var("AI_PROVIDER").map_or(false, |v| v == "local") { + } else if std::env::var("AI_PROVIDER").is_ok_and(|v| v == "local") { config.provider_type = "local".to_string(); config.base_url = std::env::var("AI_BASE_URL").ok(); if let Ok(model) = std::env::var("AI_MODEL") { diff --git a/src/core/calibration.rs b/src/core/calibration.rs index 9b0a1de..ba92afa 100644 --- a/src/core/calibration.rs +++ b/src/core/calibration.rs @@ -116,7 +116,7 @@ pub fn train( grad_w[i] = grad_w[i] / n + l2_lambda * weights[i]; weights[i] -= learning_rate * grad_w[i]; } - grad_b = grad_b / n; + grad_b /= n; intercept -= learning_rate * grad_b; } @@ -346,7 +346,7 @@ mod tests { window_variance: Some(0.0), }; let p = predict_proba(&model, &signals); - assert!(p >= 0.0 && p <= 1.0); + assert!((0.0..=1.0).contains(&p)); } #[test] diff --git a/src/core/confidence.rs b/src/core/confidence.rs index f247218..844748c 100644 --- a/src/core/confidence.rs +++ b/src/core/confidence.rs @@ -135,11 +135,10 @@ fn compute_conflicting_signals( h.algorithm, entropy )); } - if encoding_detection.is_some() { + if let Some(ed) = &encoding_detection { conflicts.push(format!( "Type conflict: hash ({}) and encoding ({}) both detected", - h.algorithm, - encoding_detection.unwrap().encoding_type + h.algorithm, ed.encoding_type )); } } @@ -188,7 +187,7 @@ pub fn build_detection_result( "hash".to_string(), Some(h.algorithm.clone()), Some(h.weakness_flags.join(", ")), - h.risk_level.clone(), + h.risk_level, match h.algorithm.as_str() { "MD5" => vec!["Replace with bcrypt (cost ≥ 12) or Argon2id.".to_string()], "SHA1" => vec!["Upgrade to SHA256 or stronger.".to_string()], @@ -247,10 +246,10 @@ pub fn build_detection_result( let ext_cves = crate::intelligence::risk::build_cve_map("signatures/cve_map.yaml", "cve-db.json"); for (cve_id, desc) in &ext_cves { - if algo.contains(cve_id) || desc.to_lowercase().contains(&algo.to_lowercase()) { - if !weakness_cve.contains(cve_id) { - weakness_cve.push(cve_id.clone()); - } + if (algo.contains(cve_id) || desc.to_lowercase().contains(&algo.to_lowercase())) + && !weakness_cve.contains(cve_id) + { + weakness_cve.push(cve_id.clone()); } } } @@ -272,7 +271,7 @@ pub fn build_detection_result( sliding_entropy: sliding.cloned(), detected_type, algorithm, - confidence: calibrated_conf.min(1.0).max(0.0), + confidence: calibrated_conf.clamp(0.0, 1.0), calibrated: is_calibrated, calibration_samples: model.as_ref().map(|m| m.dataset_size), heuristic_raw: Some(heuristic_confidence), @@ -303,7 +302,7 @@ mod tests { #[test] fn test_confidence_bounds() { let result = compute_confidence(None, None, 5.0, None); - assert!(result >= 0.0 && result <= 1.0); + assert!((0.0..=1.0).contains(&result)); } #[test] diff --git a/src/core/encoding.rs b/src/core/encoding.rs index 040a7c2..a4e73bd 100644 --- a/src/core/encoding.rs +++ b/src/core/encoding.rs @@ -125,10 +125,7 @@ fn detect_hex(input: &str) -> Option { if input.is_empty() || input.len() % 2 != 0 { return None; } - if !input - .chars() - .all(|c| matches!(c, '0'..='9' | 'a'..='f' | 'A'..='F')) - { + if !input.chars().all(|c: char| c.is_ascii_hexdigit()) { return None; } @@ -302,7 +299,7 @@ fn detect_base91(input: &str) -> Option { } let is_valid = |c: char| { let b = c as u8; - b >= 0x21 && b <= 0x7E && c != '\'' && c != '-' + (0x21..=0x7E).contains(&b) && c != '\'' && c != '-' }; let count_valid = input.chars().filter(|c| is_valid(*c)).count(); let valid_ratio = count_valid as f64 / input.len() as f64; diff --git a/src/core/hashing.rs b/src/core/hashing.rs index 223e049..0c6ba44 100644 --- a/src/core/hashing.rs +++ b/src/core/hashing.rs @@ -28,9 +28,7 @@ pub fn detect_hash(input: &str) -> Option { fn try_detect(s: &str) -> Option { let len = s.len(); - let is_hex = s - .chars() - .all(|c| matches!(c, '0'..='9' | 'a'..='f' | 'A'..='F')); + let is_hex = s.chars().all(|c: char| c.is_ascii_hexdigit()); if !is_hex && !is_prefix_based(s) { return None; diff --git a/src/format/mod.rs b/src/format/mod.rs index 65a1cc6..ed691e0 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -81,12 +81,11 @@ pub fn format_tree_string(hierarchy: &FormatHierarchy) -> String { /// Detect ZIP subtype by looking for characteristic files. fn detect_subtype<'a>(entry: &'a MagicEntry, data: &[u8]) -> Option<&'a SubtypeEntry> { let text = String::from_utf8_lossy(data); - for sub in &entry.subtypes { - if text.contains(&sub.detect) { - return Some(sub); - } - } - None + entry + .subtypes + .iter() + .find(|&sub| text.contains(&sub.detect)) + .map(|v| v as _) } /// PE subsystem detection from the optional header. diff --git a/src/intelligence/audit.rs b/src/intelligence/audit.rs index ad76404..4d540d0 100644 --- a/src/intelligence/audit.rs +++ b/src/intelligence/audit.rs @@ -31,7 +31,7 @@ static AUDIT_FILE: std::sync::LazyLock>> = fn ensure_audit_file() -> Option { let mut guard = AUDIT_FILE.lock().ok()?; if guard.is_some() { - return guard.as_ref().map(|f| f.try_clone().ok()).flatten(); + return guard.as_ref().and_then(|f| f.try_clone().ok()); } let dir = audit_dir(); let _ = std::fs::create_dir_all(&dir); diff --git a/src/intelligence/narrative.rs b/src/intelligence/narrative.rs index 94e988d..b164e3e 100644 --- a/src/intelligence/narrative.rs +++ b/src/intelligence/narrative.rs @@ -68,7 +68,7 @@ fn extract_field( if trimmed.is_empty() || contains_hallucination(&trimmed) { fallback.to_string() } else { - validate(&trimmed).unwrap_or_else(|| trimmed) + validate(&trimmed).unwrap_or(trimmed) } } _ => fallback.to_string(), @@ -105,10 +105,7 @@ fn contains_hallucination(text: &str) -> bool { /// Validate summary: max 2 sentences, no hallucinated algorithms not in known list. fn validate_summary(s: &str) -> Option { - let sentence_count = s - .matches(|c: char| c == '.' || c == '!' || c == '?') - .count() - .max(1); + let sentence_count = s.matches(['.', '!', '?']).count().max(1); if sentence_count > 3 { return None; // Allow up to 3 sentences } @@ -167,6 +164,7 @@ fn build_fallback(reason: &str) -> Result { } /// Build a constrained prompt from detection fields (no raw input bytes). +#[allow(clippy::too_many_arguments)] pub fn build_prompt( algorithm: Option<&str>, detected_type: &str, diff --git a/src/intelligence/risk.rs b/src/intelligence/risk.rs index e83ed65..441cff7 100644 --- a/src/intelligence/risk.rs +++ b/src/intelligence/risk.rs @@ -47,7 +47,7 @@ pub fn resolve_risk_level( overrides: &HashMap, ) -> (RiskLevel, Vec) { if let Some(overridden) = overrides.get(algorithm) { - return (overridden.clone(), vec![]); + return (*overridden, vec![]); } default_risk_level(algorithm) } @@ -93,6 +93,7 @@ pub fn load_cve_yaml_database(path: &str) -> HashMap { } #[derive(serde::Deserialize)] +#[allow(dead_code)] struct CveMapFile { version: String, cves: Vec, @@ -102,6 +103,7 @@ struct CveMapFile { struct CveEntry { algorithm: String, cve_ids: Vec, + #[allow(dead_code)] severity: String, cvss_v3_base: Option, description: String, diff --git a/src/intelligence/threat_intel.rs b/src/intelligence/threat_intel.rs index ca1e9be..6ea8ab8 100644 --- a/src/intelligence/threat_intel.rs +++ b/src/intelligence/threat_intel.rs @@ -2,23 +2,13 @@ use crate::error::{CryptoTraceError, Result}; use std::collections::HashMap; /// Configuration for threat intelligence providers. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ThreatIntelConfig { pub vt_api_key: Option, pub yara_rules_path: Option, pub enable_scan: bool, } -impl Default for ThreatIntelConfig { - fn default() -> Self { - Self { - vt_api_key: None, - yara_rules_path: None, - enable_scan: false, - } - } -} - /// A threat intelligence report for a given hash or file. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ThreatReport { @@ -95,9 +85,7 @@ pub async fn query_virustotal(hash: &str, api_key: &str) -> Result total_scanners: total as u32, malicious: positives > 0, source: "VirusTotal".to_string(), - scan_date: attributes["last_analysis_date"] - .as_i64() - .map(|ts| unix_to_iso(ts)), + scan_date: attributes["last_analysis_date"].as_i64().map(unix_to_iso), threat_labels: { if labels.len() > 10 { labels.truncate(10); diff --git a/src/sanitization/guard.rs b/src/sanitization/guard.rs index 68f827a..7582bfa 100644 --- a/src/sanitization/guard.rs +++ b/src/sanitization/guard.rs @@ -11,6 +11,12 @@ pub struct InputGuard { allowed_base_dir: Option, } +impl Default for InputGuard { + fn default() -> Self { + Self::new() + } +} + impl InputGuard { pub fn new() -> Self { Self { diff --git a/src/sanitization/sandbox.rs b/src/sanitization/sandbox.rs index 509ef85..67ca3f9 100644 --- a/src/sanitization/sandbox.rs +++ b/src/sanitization/sandbox.rs @@ -77,7 +77,7 @@ impl Default for SandboxConfig { /// /// - Windows: Job Object with memory limit, active process limit, kill-on-close /// - Linux: subprocess with seccomp-bpf (blocks execve, clone, socket, etc.) -/// + RLIMIT_AS memory enforcement +/// + RLIMIT_AS memory enforcement /// - macOS: subprocess with sandbox-init (deny network, fs-write, proc-spawn) /// /// The worker process is a separate binary (`cryptotrace-worker`) that @@ -275,7 +275,7 @@ fn apply_platform_sandbox(cmd: &mut Command, _max_memory_mb: u64) { ); let profile_bytes = profile.as_bytes(); let mut error: *mut libc::c_char = std::ptr::null_mut(); - extern "C" { + unsafe extern "C" { fn sandbox_init( profile: *const libc::c_char, flags: u64, @@ -283,21 +283,17 @@ fn apply_platform_sandbox(cmd: &mut Command, _max_memory_mb: u64) { ) -> libc::c_int; fn sandbox_free_error(errorbuf: *mut libc::c_char); } - let ret = unsafe { - sandbox_init( - profile_bytes.as_ptr() as *const libc::c_char, - 0u64, - &mut error, - ) - }; + let ret = sandbox_init( + profile_bytes.as_ptr() as *const libc::c_char, + 0u64, + &mut error, + ); if ret != 0 { if !error.is_null() { let msg = std::ffi::CStr::from_ptr(error) .to_string_lossy() .into_owned(); - unsafe { - sandbox_free_error(error); - } + sandbox_free_error(error); Err(std::io::Error::new(std::io::ErrorKind::Other, msg)) } else { Err(std::io::Error::last_os_error()) @@ -332,10 +328,15 @@ fn apply_post_spawn_sandbox( use std::ffi::c_void; use std::ptr; + #[allow(clippy::upper_case_acronyms)] type HANDLE = *mut c_void; + #[allow(clippy::upper_case_acronyms)] type BOOL = i32; + #[allow(clippy::upper_case_acronyms)] type DWORD = u32; + #[allow(clippy::upper_case_acronyms)] type LPCWSTR = *const u16; + #[allow(clippy::upper_case_acronyms)] type LPVOID = *mut c_void; const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: DWORD = 0x2000; @@ -389,10 +390,7 @@ fn apply_post_spawn_sandbox( // Create job object let job = CreateJobObjectW(ptr::null(), ptr::null()); if job.is_null() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to create Windows Job Object", - )); + return Err(std::io::Error::other("Failed to create Windows Job Object")); } // Configure limits @@ -426,8 +424,7 @@ fn apply_post_spawn_sandbox( ); if ret == 0 { CloseHandle(job); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, + return Err(std::io::Error::other( "Failed to set Windows Job Object limits", )); } @@ -440,8 +437,7 @@ fn apply_post_spawn_sandbox( ); if process.is_null() { CloseHandle(job); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, + return Err(std::io::Error::other( "Failed to open worker process handle for Job Object", )); } @@ -450,8 +446,7 @@ fn apply_post_spawn_sandbox( CloseHandle(process); if ret == 0 { CloseHandle(job); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, + return Err(std::io::Error::other( "Failed to assign process to Windows Job Object", )); } diff --git a/src/signatures/mod.rs b/src/signatures/mod.rs index d5d8264..64c6beb 100644 --- a/src/signatures/mod.rs +++ b/src/signatures/mod.rs @@ -153,7 +153,7 @@ mod tests { let data = b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03"; let matches = match_signatures(data, ®); // Multiple entries may match GZIP magic (gzip, gzip_tar, dockertar) - assert!(matches.len() >= 1); + assert!(!matches.is_empty()); assert!(matches.iter().any(|m| m.id == "gzip")); } @@ -162,7 +162,7 @@ mod tests { let reg = default_registry().unwrap(); let data = b"%PDF-1.4"; let matches = match_signatures(data, ®); - assert!(matches.len() >= 1); + assert!(!matches.is_empty()); assert!(matches.iter().any(|m| m.id == "pdf")); } @@ -171,7 +171,7 @@ mod tests { let reg = default_registry().unwrap(); let data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"; let matches = match_signatures(data, ®); - assert!(matches.len() >= 1); + assert!(!matches.is_empty()); assert!(matches.iter().any(|m| m.id == "png")); } @@ -181,7 +181,7 @@ mod tests { let data = b"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"; let matches = match_signatures(data, ®); // ELF magic matches elf, elf_s390, elf_core - assert!(matches.len() >= 1); + assert!(!matches.is_empty()); assert!(matches.iter().any(|m| m.id == "elf")); } @@ -191,7 +191,7 @@ mod tests { let data = b"MZ\x90\x00\x03\x00\x00\x00\x04\x00\x00\x00\xff\xff\x00\x00"; let matches = match_signatures(data, ®); // MZ magic matches pe, pe32, msdos_stub - assert!(matches.len() >= 1); + assert!(!matches.is_empty()); assert!(matches.iter().any(|m| m.id == "pe")); } diff --git a/src/workers.rs b/src/workers.rs index 6e0c87f..1e89687 100644 --- a/src/workers.rs +++ b/src/workers.rs @@ -18,9 +18,11 @@ impl WorkerPool { input: &[u8], timeout: Duration, ) -> Result> { - let mut config = SandboxConfig::default(); - config.enabled = true; - config.timeout_seconds = timeout.as_secs().max(1); + let config = SandboxConfig { + enabled: true, + timeout_seconds: timeout.as_secs().max(1), + ..Default::default() + }; let sandbox = crate::sanitization::sandbox::Sandbox::new(config); sandbox.run_worker(operation, input) } diff --git a/tests/compression_bomb_test.rs b/tests/compression_bomb_test.rs index b2b6633..e154282 100644 --- a/tests/compression_bomb_test.rs +++ b/tests/compression_bomb_test.rs @@ -12,7 +12,7 @@ fn compression_bomb_payload() -> Vec { // GZIP-compress 10 MB of repeated 'A' bytes. // The compressed output should be tiny (well under 100 KB), giving // an expansion ratio > 100× when decompressed. - let huge: Vec = std::iter::repeat(b'A').take(10_000_000).collect(); + let huge: Vec = std::iter::repeat_n(b'A', 10_000_000).collect(); let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best()); use std::io::Write; encoder.write_all(&huge).unwrap(); diff --git a/tests/proptest.rs b/tests/proptest.rs index 89ee5d3..c02185e 100644 --- a/tests/proptest.rs +++ b/tests/proptest.rs @@ -97,12 +97,12 @@ proptest! { fn test_encoding_negative_cases(input: String) { if input.chars().all(|c| c.is_ascii_alphanumeric() || c.is_ascii_punctuation()) { if let Some(result) = cryptotrace::core::encoding::detect_encoding(&input) { - if input.contains('=') && result.encoding_type == "Base64" { - if let Ok(_) = base64::Engine::decode( + if input.contains('=') && result.encoding_type == "Base64" + && base64::Engine::decode( &base64::engine::general_purpose::STANDARD, input.as_bytes(), - ) { - } + ).is_ok() + { } } }