1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4use crate::config::ConfigManager;
5use crate::engine::{dedup, matcher, parser};
6use crate::error::{AppError, Result};
7use crate::models::{DedupStats, InjectableEndpoint, ModuleSuggestion, ScanRecord};
8use crate::nvd::{EnrichmentStats, NVDClient};
9use crate::report;
10
11#[derive(Debug, Clone)]
13pub struct ProcessOptions {
14 pub verbose: bool,
15 pub print_console: bool,
17}
18
19impl Default for ProcessOptions {
20 fn default() -> Self {
21 Self {
22 verbose: false,
23 print_console: true,
24 }
25 }
26}
27
28#[derive(Debug)]
30pub struct ProcessResult {
31 pub records: Vec<ScanRecord>,
32 pub injectable: Vec<InjectableEndpoint>,
33 pub suggestions: Vec<ModuleSuggestion>,
34 pub dedup_stats: DedupStats,
35}
36
37pub fn process_scan_file(
41 input_path: &Path,
42 config_mgr: &mut ConfigManager,
43 opts: &ProcessOptions,
44) -> Result<ProcessResult> {
45 if opts.verbose {
46 println!("[dbg] processing {}", input_path.display());
47 }
48 println!("[+] Loading scan results from: {}", input_path.display());
49 let raw_json = std::fs::read_to_string(input_path)?;
50
51 println!("[+] Parsing with Rust engine...");
52 let mut parsed = parser::parse_vulnmalper_scan(&raw_json)?;
53 let injectable = parsed.injectable_endpoints.clone();
54 let raw_count = parsed.records.len();
55
56 println!(
57 "[+] Found {} raw findings ({} injectable endpoint{} flagged). Normalizing...",
58 raw_count,
59 injectable.len(),
60 if injectable.len() == 1 { "" } else { "s" }
61 );
62
63 for record in &mut parsed.records {
65 crate::cve::extract_into_record(record);
66 }
67 let discovered_cves: std::collections::BTreeSet<String> = parsed
68 .records
69 .iter()
70 .flat_map(|record| record.cves.iter().cloned())
71 .collect();
72 println!(
73 "[+] Discovered {} CVE id{}.",
74 discovered_cves.len(),
75 if discovered_cves.len() == 1 { "" } else { "s" }
76 );
77
78 let dedup_result = dedup::deduplicate_records(parsed.records);
79 let mut records = dedup_result.records;
80 let dedup_stats = DedupStats {
81 total: raw_count,
82 unique: dedup_result.unique_count,
83 removed: dedup_result.removed_count,
84 };
85
86 println!(
87 "[+] Normalization + deduplication complete: {} unique, {} merged",
88 dedup_stats.unique, dedup_stats.removed
89 );
90 println!();
91
92 if config_mgr.is_nvd_configured() {
93 let nvd_cfg = config_mgr.get_nvd_config().clone();
94 let mut nvd_client = NVDClient::new(&nvd_cfg.api_key);
95 println!("[+] Enriching discovered CVEs via NVD API...");
96 let stats: EnrichmentStats = nvd_client.enrich_records(&mut records);
97 println!(
98 "[+] NVD enrichment complete: {} fetched, {} from cache, {} not found",
99 stats.fetched, stats.cache_hits, stats.not_found
100 );
101 println!();
102 }
103
104 let mut all_suggestions = Vec::<ModuleSuggestion>::new();
105 let mut seen_suggestions = HashSet::<(String, String)>::new();
106
107 println!("[+] Analyzing service banners, technologies and CVEs for module suggestions...");
108 for record in &records {
109 if let Some(service) = record.service.as_deref() {
110 if opts.verbose {
111 println!("[dbg] searching modules for service '{}'", service);
112 }
113 for suggestion in matcher::suggest_modules(service) {
114 push_unique_suggestion(&mut all_suggestions, &mut seen_suggestions, suggestion);
115 }
116 }
117 if !record.title.is_empty() {
118 if opts.verbose {
119 println!("[dbg] matching modules for title '{}'", record.title);
120 }
121 for suggestion in matcher::suggest_from_title(&record.title, &record.detail) {
122 push_unique_suggestion(&mut all_suggestions, &mut seen_suggestions, suggestion);
123 }
124 }
125 for cve in &record.cves {
126 if opts.verbose {
127 println!("[dbg] searching modules for {}", cve);
128 }
129 for module in crate::msf::modules_for_cve(cve) {
130 let suggestion = ModuleSuggestion {
131 service_banner: format!("cve:{}", cve),
132 suggested_module: module.name.clone(),
133 confidence: if module.rank == "excellent" || module.rank == "great" {
134 "high".to_string()
135 } else {
136 "medium".to_string()
137 },
138 };
139 push_unique_suggestion(&mut all_suggestions, &mut seen_suggestions, suggestion);
140 }
141 }
142 }
143
144 if all_suggestions.is_empty() {
145 println!("[?] No module suggestions for detected services or CVEs.");
146 } else {
147 println!("[+] {} module suggestion(s) found:", all_suggestions.len());
148 for suggestion in &all_suggestions {
149 println!(
150 " {} (confidence: {})",
151 suggestion.suggested_module, suggestion.confidence
152 );
153 }
154 }
155
156 println!();
157
158 if opts.print_console {
159 println!(
160 "{}",
161 report::render_console_report(
162 &records,
163 &injectable,
164 &all_suggestions,
165 &[],
166 &dedup_stats
167 )
168 );
169 println!();
170 }
171
172 Ok(ProcessResult {
173 records,
174 injectable,
175 suggestions: all_suggestions,
176 dedup_stats,
177 })
178}
179
180pub fn write_markdown_report(result: &ProcessResult, path: &Path) -> Result<PathBuf> {
182 report::generate_markdown_report(
183 &result.records,
184 &result.injectable,
185 &result.suggestions,
186 &[],
187 &result.dedup_stats,
188 path.to_str().unwrap_or("report.md"),
189 )
190}
191
192pub fn process_folder(
196 folder: &Path,
197 config_mgr: &mut ConfigManager,
198 verbose: bool,
199) -> Result<usize> {
200 let scan = crate::ingest::scanner::scan_folder(folder, verbose)?;
201 let jsons: Vec<_> = scan
202 .artifacts
203 .iter()
204 .filter(|a| a.kind == crate::ingest::scanner::ArtifactKind::VulnMalperJson)
205 .collect();
206
207 if jsons.is_empty() {
208 return Err(AppError::Message(format!(
209 "no VulnMalper JSON files found in '{}' to process",
210 folder.display()
211 )));
212 }
213
214 let opts = ProcessOptions {
215 verbose,
216 print_console: false,
217 };
218 let mut written = 0usize;
219 for artifact in &jsons {
220 let input_path = PathBuf::from(&artifact.path);
221 let report_path = report::derive_report_path(&input_path);
222 println!();
223 println!(
224 "[+] Processing {} -> {}",
225 input_path.display(),
226 report_path.display()
227 );
228 let result = process_scan_file(&input_path, config_mgr, &opts)?;
229 write_markdown_report(&result, &report_path)?;
230 println!("[+] Report written to: {}", report_path.display());
231 written += 1;
232 }
233 Ok(written)
234}
235
236fn push_unique_suggestion(
237 suggestions: &mut Vec<ModuleSuggestion>,
238 seen: &mut HashSet<(String, String)>,
239 suggestion: ModuleSuggestion,
240) {
241 let key = (
242 suggestion.service_banner.clone(),
243 suggestion.suggested_module.clone(),
244 );
245 if seen.insert(key) {
246 suggestions.push(suggestion);
247 }
248}