From 0bfe1ee02eb3447716a15828929a10e3df39c2b2 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Thu, 12 Feb 2026 09:34:18 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20deep=20analysis=20=E2=80=94=20security?= =?UTF-8?q?=20scanning,=20code=20quality,=20dependency=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desktop/src-tauri/Cargo.toml | 2 + .../src-tauri/src/commands/analyze_project.rs | 5 + desktop/src-tauri/src/deep_analysis.rs | 158 ++++++++++++++++++ desktop/src-tauri/src/lib.rs | 1 + 4 files changed, 166 insertions(+) create mode 100644 desktop/src-tauri/src/deep_analysis.rs diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 9d38ecf..438283d 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -29,3 +29,5 @@ tauri-plugin-process = "2" walkdir = "2" chrono = "0.4" reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +regex = "1" +regex = "1" diff --git a/desktop/src-tauri/src/commands/analyze_project.rs b/desktop/src-tauri/src/commands/analyze_project.rs index e091631..f43eaf7 100644 --- a/desktop/src-tauri/src/commands/analyze_project.rs +++ b/desktop/src-tauri/src/commands/analyze_project.rs @@ -241,6 +241,11 @@ pub fn analyze_project(window: tauri::Window, path: String) -> Result, + pub signals: Vec, + pub todo_count: u32, + pub security_issues: u32, + pub quality_issues: u32, + pub files_scanned: u32, +} + +pub fn run_deep_analysis(root: &Path) -> DeepAnalysisResult { + let mut result = DeepAnalysisResult { + findings: Vec::new(), signals: Vec::new(), + todo_count: 0, security_issues: 0, quality_issues: 0, files_scanned: 0, + }; + let mut files: Vec = Vec::new(); + collect_files(root, root, 0, &mut files); + + for file_path in &files { + let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase(); + let content = match fs::read_to_string(file_path) { Ok(c) => c, Err(_) => continue }; + result.files_scanned += 1; + let rel = file_path.strip_prefix(root).unwrap_or(file_path).to_string_lossy().to_string(); + + if !rel.contains(".example") && !rel.contains(".sample") { + for (pat, desc) in SECRET_PATTERNS { + if let Ok(re) = regex::Regex::new(pat) { + if re.is_match(&content) { + result.security_issues += 1; + result.findings.push(Finding { severity: "high".into(), title: format!("🔐 {}", desc), details: format!("Файл: {}", rel) }); + result.signals.push(ProjectSignal { category: "security".into(), level: "high".into(), message: format!("{} в {}", desc, rel) }); + } + } + } + } + + for (pat, title, exts) in VULN_PATTERNS { + let applicable: Vec<&str> = exts.split(',').collect(); + if !applicable.contains(&ext.as_str()) { continue; } + if let Ok(re) = regex::Regex::new(pat) { + let matches: Vec<_> = re.find_iter(&content).collect(); + if !matches.is_empty() { + result.security_issues += 1; + let line = content[..matches[0].start()].chars().filter(|c| *c == '\n').count() + 1; + result.findings.push(Finding { severity: "high".into(), title: format!("⚠️ {}", title), details: format!("{}:{} ({} шт.)", rel, line, matches.len()) }); + } + } + } + + for (pat, title, exts) in QUALITY_PATTERNS { + let applicable: Vec<&str> = exts.split(',').collect(); + if !applicable.contains(&ext.as_str()) { continue; } + if let Ok(re) = regex::Regex::new(pat) { + let matches: Vec<_> = re.find_iter(&content).collect(); + if !matches.is_empty() { + let count = matches.len(); + if pat.contains("TODO") { result.todo_count += count as u32; } + result.quality_issues += count as u32; + if count >= 3 || pat.contains("unwrap") { + result.findings.push(Finding { severity: "warn".into(), title: format!("📝 {}", title), details: format!("{}: {} шт.", rel, count) }); + } + } + } + } + + let lines = content.lines().count(); + if lines > 500 { + result.findings.push(Finding { severity: "warn".into(), title: "📏 Большой файл".into(), details: format!("{}: {} строк", rel, lines) }); + } + + if rel == "package.json" { check_package_json(&content, &mut result); } + if rel == "requirements.txt" { check_requirements_txt(&content, &mut result); } + } + + if result.security_issues > 0 { + result.signals.push(ProjectSignal { category: "security".into(), level: "high".into(), message: format!("Deep analysis: {} проблем безопасности", result.security_issues) }); + } + if result.todo_count > 5 { + result.signals.push(ProjectSignal { category: "quality".into(), level: "warn".into(), message: format!("{} TODO/FIXME комментариев", result.todo_count) }); + } + result +} + +fn collect_files(root: &Path, dir: &Path, depth: u32, out: &mut Vec) { + if depth > 10 || out.len() > 500 { return; } + let entries = match fs::read_dir(dir) { Ok(e) => e, Err(_) => return }; + let excluded = ["node_modules", ".git", "target", "dist", "build", ".next", "__pycache__", ".venv", "venv", "vendor", ".cargo"]; + for entry in entries.flatten() { + let path = entry.path(); + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if path.is_dir() { + if excluded.contains(&name) { continue; } + collect_files(root, &path, depth + 1, out); + continue; + } + if let Ok(meta) = path.metadata() { if meta.len() > MAX_SCAN_SIZE { continue; } } + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if CODE_EXTENSIONS.contains(&ext) { out.push(path); } + } +} + +fn check_package_json(content: &str, result: &mut DeepAnalysisResult) { + if let Ok(json) = serde_json::from_str::(content) { + if let Some(scripts) = json.get("scripts").and_then(|s| s.as_object()) { + if !scripts.contains_key("test") || scripts.get("test").and_then(|t| t.as_str()).unwrap_or("").contains("no test specified") { + result.findings.push(Finding { severity: "warn".into(), title: "🧪 Нет скрипта test".into(), details: "npm test не настроен".into() }); + } + } + } +} + +fn check_requirements_txt(content: &str, result: &mut DeepAnalysisResult) { + let unpinned: u32 = content.lines().filter(|l| { let l = l.trim(); !l.is_empty() && !l.starts_with('#') && !l.contains("==") }).count() as u32; + if unpinned > 3 { + result.findings.push(Finding { severity: "warn".into(), title: "📦 Незафиксированные версии".into(), details: format!("{} пакетов без ==", unpinned) }); + } +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 293fb5b..7842e32 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod deep_analysis; mod commands; mod types;