Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deny.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CryptoTrace — cargo-deny configuration
[advisories]
vulnerability = "deny"
unmaintained = "warn"
unmaintained = "all"
notice = "warn"
yanked = "warn"
ignore = []
Expand Down
2 changes: 1 addition & 1 deletion src/analyzers/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions src/analyzers/recursive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub fn analyze_recursive(data: &[u8], config: &RecursiveConfig) -> Result<Vec<La

// Check expansion ratio
if let Some(ref decoded) = decoded {
if !decoded.is_empty() && current_data.len() > 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 {
Expand All @@ -113,7 +113,7 @@ pub fn analyze_recursive(data: &[u8], config: &RecursiveConfig) -> Result<Vec<La
}

let expansion_ratio = decoded.as_ref().map(|d| {
if current_data.len() > 0 {
if !current_data.is_empty() {
d.len() as f64 / current_data.len() as f64
} else {
1.0
Expand Down
4 changes: 4 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ impl<V> LruCache<V> {
self.entries.len()
}

pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}

pub fn capacity(&self) -> usize {
self.max_entries
}
Expand Down
12 changes: 6 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@ pub async fn run_with_cli(cli: &Cli) -> Result<Option<(DetectionResult, bool, bo
result.detection_context = detection_context;

// Recursive analysis
if *deep && !result.algorithm.as_deref().map_or(true, |a| a.is_empty()) {
if *deep && !result.algorithm.as_deref().is_none_or(|a| a.is_empty()) {
let config = crate::analyzers::recursive::RecursiveConfig::default();
let layers = crate::analyzers::recursive::analyze_recursive(
&result.input_hash.as_bytes(),
result.input_hash.as_bytes(),
&config,
)?;
// Convert recursive layers to DetectionResult layers
Expand Down Expand Up @@ -261,7 +261,7 @@ pub async fn run_with_cli(cli: &Cli) -> Result<Option<(DetectionResult, bool, bo
);
} else if let Some(path) = from_file {
let import_path = std::path::Path::new(path);
let sig_path = verify.as_ref().map(|s| std::path::Path::new(s));
let sig_path = verify.as_ref().map(std::path::Path::new);
update_mgr.import_local(import_path, sig_path)?;
println!("Imported signature DB: {}", update_mgr.current_version());
} else {
Expand Down Expand Up @@ -324,7 +324,7 @@ pub async fn run_with_cli(cli: &Cli) -> Result<Option<(DetectionResult, bool, bo
println!("AI cache TTL days: {}", cache.ttl_days);
println!("AI cache max entries: {}", cache.max_entries);
}
println!("Sandbox enabled: {}", false);
println!("Sandbox enabled: false");
println!("Sandbox max memory: 512 MB");
println!("Sandbox max concurrent: 4");
println!("Sandbox timeout: 30s");
Expand Down Expand Up @@ -384,7 +384,7 @@ pub async fn run_with_cli(cli: &Cli) -> Result<Option<(DetectionResult, bool, bo
let mut wtr = csv::Writer::from_path(output).map_err(|e| {
crate::error::CryptoTraceError::Other(format!("Cannot create CSV: {}", e))
})?;
wtr.write_record(&[
wtr.write_record([
"entropy",
"block_alignment",
"magic_bytes",
Expand Down Expand Up @@ -477,7 +477,7 @@ pub fn load_ai_provider() -> Result<Box<dyn crate::providers::AiProvider>> {
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") {
Expand Down
4 changes: 2 additions & 2 deletions src/core/calibration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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]
Expand Down
19 changes: 9 additions & 10 deletions src/core/confidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
));
}
}
Expand Down Expand Up @@ -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()],
Expand Down Expand Up @@ -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());
}
}
}
Expand All @@ -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),
Expand Down Expand Up @@ -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]
Expand Down
7 changes: 2 additions & 5 deletions src/core/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,7 @@ fn detect_hex(input: &str) -> Option<EncodingDetection> {
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;
}

Expand Down Expand Up @@ -302,7 +299,7 @@ fn detect_base91(input: &str) -> Option<EncodingDetection> {
}
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;
Expand Down
4 changes: 1 addition & 3 deletions src/core/hashing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ pub fn detect_hash(input: &str) -> Option<HashDetection> {

fn try_detect(s: &str) -> Option<HashDetection> {
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;
Expand Down
11 changes: 5 additions & 6 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/intelligence/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ static AUDIT_FILE: std::sync::LazyLock<Mutex<Option<std::fs::File>>> =
fn ensure_audit_file() -> Option<std::fs::File> {
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);
Expand Down
8 changes: 3 additions & 5 deletions src/intelligence/narrative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<String> {
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
}
Expand Down Expand Up @@ -167,6 +164,7 @@ fn build_fallback(reason: &str) -> Result<AiNarrative> {
}

/// 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,
Expand Down
4 changes: 3 additions & 1 deletion src/intelligence/risk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub fn resolve_risk_level(
overrides: &HashMap<String, RiskLevel>,
) -> (RiskLevel, Vec<String>) {
if let Some(overridden) = overrides.get(algorithm) {
return (overridden.clone(), vec![]);
return (*overridden, vec![]);
}
default_risk_level(algorithm)
}
Expand Down Expand Up @@ -93,6 +93,7 @@ pub fn load_cve_yaml_database(path: &str) -> HashMap<String, String> {
}

#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct CveMapFile {
version: String,
cves: Vec<CveEntry>,
Expand All @@ -102,6 +103,7 @@ struct CveMapFile {
struct CveEntry {
algorithm: String,
cve_ids: Vec<String>,
#[allow(dead_code)]
severity: String,
cvss_v3_base: Option<f64>,
description: String,
Expand Down
16 changes: 2 additions & 14 deletions src/intelligence/threat_intel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub yara_rules_path: Option<String>,
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 {
Expand Down Expand Up @@ -95,9 +85,7 @@ pub async fn query_virustotal(hash: &str, api_key: &str) -> Result<ThreatReport>
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);
Expand Down
6 changes: 6 additions & 0 deletions src/sanitization/guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ pub struct InputGuard {
allowed_base_dir: Option<PathBuf>,
}

impl Default for InputGuard {
fn default() -> Self {
Self::new()
}
}

impl InputGuard {
pub fn new() -> Self {
Self {
Expand Down
Loading
Loading