1use std::collections::HashSet;
2use std::env;
3use std::io::{self, Write};
4use std::path::PathBuf;
5use std::thread;
6use std::time::Duration;
7
8use crate::config::ConfigManager;
9use crate::engine::{dedup, matcher, msfrpc, parser};
10use crate::error::{AppError, Result};
11use crate::models::{DedupStats, ModuleSuggestion, PayloadRecipe};
12use crate::nvd::{EnrichmentStats, NVDClient};
13use crate::recipe;
14use crate::report;
15use crate::share;
16
17pub fn main() {
18 if let Err(err) = run() {
19 eprintln!("[!] {}", err);
20 std::process::exit(1);
21 }
22}
23
24pub fn run() -> Result<()> {
25 let mut config_mgr = ConfigManager::new();
26 let config_loaded = config_mgr.load();
27
28 let args = env::args().skip(1).collect::<Vec<_>>();
29 if args.is_empty() {
30 print_banner();
31 return Ok(());
32 }
33
34 match args[0].as_str() {
35 "process" => {
36 cmd_process(&args[1..], &mut config_mgr, config_loaded)?;
37 }
38 "share" => {
39 cmd_share(&args[1..])?;
40 }
41 "reset-config" => {
42 cmd_reset_config(&mut config_mgr)?;
43 }
44 "setup" => {
45 cmd_setup(&mut config_mgr)?;
46 }
47 "-h" | "--help" => {
48 print_banner();
49 }
50 other => {
51 eprintln!("[!] Unknown command: {}", other);
52 print_banner();
53 }
54 }
55
56 Ok(())
57}
58
59fn cmd_setup(config_mgr: &mut ConfigManager) -> Result<()> {
60 println!("PloitMalper Configuration Setup");
61 println!();
62 println!("--- MSF-RPC Configuration ---");
63 println!("Enter your Metasploit RPC server details.");
64 let current = config_mgr.get_msfrpc_config().clone();
65
66 let host = prompt_text("MSF-RPC Host", ¤t.host)?;
67 let port = prompt_u16("MSF-RPC Port", current.port)?;
68 let username = prompt_text("MSF-RPC Username", ¤t.username)?;
69 let password = prompt_text("MSF-RPC Password", ¤t.password)?;
70 let ssl = prompt_bool("Use SSL?", current.ssl)?;
71 let workspace = prompt_text("Workspace", ¤t.workspace)?;
72
73 config_mgr.config.msfrpc.host = host;
74 config_mgr.config.msfrpc.port = port;
75 config_mgr.config.msfrpc.username = username;
76 config_mgr.config.msfrpc.password = password;
77 config_mgr.config.msfrpc.ssl = ssl;
78 config_mgr.config.msfrpc.workspace = workspace;
79
80 println!();
81 println!("--- NVD API Configuration ---");
82 println!("Enter your NVD API key for CVE enrichment (optional but recommended).");
83 println!("Get one at: https://nvd.nist.gov/developers/request-an-api-key");
84 let current_nvd = config_mgr.get_nvd_config().clone();
85 let nvd_key = prompt_text("NVD API Key", ¤t_nvd.api_key)?;
86 config_mgr.config.nvd.api_key = nvd_key;
87
88 config_mgr.save()?;
89 println!();
90 println!(
91 "[+] Configuration saved to {}",
92 crate::config::config_file().display()
93 );
94 Ok(())
95}
96
97fn cmd_process(args: &[String], config_mgr: &mut ConfigManager, config_loaded: bool) -> Result<()> {
98 let input_file = match args.first() {
99 Some(path) => path,
100 None => {
101 eprintln!("[!] No input file specified. Use: ploit-malper process <scan.json>");
102 return Ok(());
103 }
104 };
105
106 let input_path = PathBuf::from(input_file);
107 if !input_path.exists() {
108 eprintln!("[!] File not found: {}", input_path.display());
109 return Ok(());
110 }
111
112 println!("[+] Loading scan results from: {}", input_path.display());
113 let raw_json = std::fs::read_to_string(&input_path)?;
114
115 println!("[+] Parsing with Rust engine...");
116 let parsed = parser::parse_vulnmalper_json(&raw_json)?;
117 println!("[+] Found {} raw findings. Deduplicating...", parsed.len());
118
119 let dedup_result = dedup::deduplicate_records(parsed);
120 let mut records = dedup_result.records;
121 let dedup_stats = DedupStats {
122 total: records.len() + dedup_result.removed_count,
123 unique: dedup_result.unique_count,
124 removed: dedup_result.removed_count,
125 };
126
127 println!(
128 "[+] Deduplication complete: {} unique, {} removed",
129 dedup_stats.unique, dedup_stats.removed
130 );
131 println!();
132
133 if config_mgr.is_nvd_configured() {
134 let nvd_cfg = config_mgr.get_nvd_config().clone();
135 let mut nvd_client = NVDClient::new(&nvd_cfg.api_key);
136 println!("[+] Enriching CVEs via NVD API...");
137 let stats: EnrichmentStats = nvd_client.enrich_records(&mut records);
138 println!(
139 "[+] NVD enrichment complete: {} fetched, {} from cache, {} not found",
140 stats.fetched, stats.cache_hits, stats.not_found
141 );
142 println!();
143 }
144
145 let mut all_suggestions = Vec::<ModuleSuggestion>::new();
146 let mut seen_suggestions = HashSet::<(String, String)>::new();
147
148 println!("[+] Analyzing service banners and titles for module suggestions...");
149 for record in &records {
150 if let Some(service) = record.service.as_deref() {
151 for suggestion in matcher::suggest_modules(service) {
152 push_unique_suggestion(&mut all_suggestions, &mut seen_suggestions, suggestion);
153 }
154 }
155 if !record.title.is_empty() {
156 for suggestion in matcher::suggest_from_title(&record.title, "") {
157 push_unique_suggestion(&mut all_suggestions, &mut seen_suggestions, suggestion);
158 }
159 }
160 }
161
162 if all_suggestions.is_empty() {
163 println!("[?] No module suggestions for detected services.");
164 }
165
166 println!();
167 println!(
168 "{}",
169 report::render_console_report(&records, &all_suggestions, &[])
170 );
171 println!();
172
173 let mut msf_connected = false;
174 let mut msf_client = None;
175 if !config_loaded || !config_mgr.is_configured() {
176 println!("[?] No MSF-RPC configuration found.");
177 if prompt_bool_with_default("Configure MSF-RPC and NVD API now?", true)? {
178 cmd_setup(config_mgr)?;
179 } else {
180 println!("[?] Continuing without MSF-RPC integration.");
181 println!();
182 }
183 }
184
185 if config_mgr.is_configured() {
186 let msf_cfg = config_mgr.get_msfrpc_config().clone();
187 println!();
188 println!("[+] Connecting to MSF-RPC for workspace verification...");
189 let mut client = msfrpc::MsfRpcClient::new(
190 &msf_cfg.host,
191 msf_cfg.port,
192 &msf_cfg.username,
193 &msf_cfg.password,
194 msf_cfg.ssl,
195 );
196
197 let login_result = client.login()?;
198 if login_result.success {
199 msf_connected = true;
200 println!(
201 "[+] Authenticated to MSF-RPC at {}:{}",
202 msf_cfg.host, msf_cfg.port
203 );
204
205 let ws_result = client.get_workspaces()?;
206 if !ws_result.workspaces.is_empty() {
207 println!();
208 println!("MSF Workspaces");
209 println!("{:<24} {:<16} {:>8}", "Name", "Scope", "Hosts");
210 println!("{}", "-".repeat(52));
211 for workspace in &ws_result.workspaces {
212 println!(
213 "{:<24} {:<16} {:>8}",
214 truncate(&workspace.name, 24),
215 truncate(&workspace.scope, 16),
216 workspace.host_count
217 );
218 }
219 }
220
221 for record in records.iter().take(5) {
222 if let Some(target) = record.target.as_str().split_whitespace().next() {
223 let check_result =
224 client.check_host_exists(target, Some(&msf_cfg.workspace))?;
225 if check_result.exists {
226 println!(
227 "[?] Host {} already exists in workspace '{}'",
228 target, msf_cfg.workspace
229 );
230 }
231 }
232 }
233 } else {
234 println!(
235 "[!] Remote MSF-RPC unreachable. Switched entirely to offline matching matrix."
236 );
237 println!(" Error: {}", login_result.error);
238 }
239
240 msf_client = Some(client);
241 }
242
243 let mut recipes: Vec<PayloadRecipe> = Vec::new();
244 if prompt_bool_with_default("\nGenerate msfvenom payload recipes?", false)? {
245 println!();
246 println!("Payload Recipe Builder");
247 let platforms = recipe::get_available_platforms();
248 println!("Available platforms: {}", platforms.join(", "));
249 let platform = prompt_choice("Platform", &platforms, "windows")?;
250 let arches = recipe::get_available_arches(&platform);
251 println!("Available architectures: {}", arches.join(", "));
252 let arch = prompt_choice("Architecture", &arches, "x64")?;
253 let lhost = prompt_text("LHOST", "10.0.0.1")?;
254 let lport = prompt_u16("LPORT", 4444)?;
255 let output = prompt_text("Output path", "/tmp/payload")?;
256
257 let recipe = recipe::build_recipe(&platform, &arch, &lhost, lport, &output, None, None);
258 println!();
259 println!("Generated recipe:");
260 println!("{}", recipe.command);
261 recipes.push(recipe);
262 }
263
264 if prompt_bool_with_default("\nWrite findings to Markdown report (report.md)?", true)? {
265 let output_path = report::generate_markdown_report(
266 &records,
267 &all_suggestions,
268 &recipes,
269 &dedup_stats,
270 "report.md",
271 )?;
272 println!();
273 println!("[+] Report written to: {}", output_path.display());
274 }
275
276 if msf_connected {
277 if let Some(mut client) = msf_client {
278 client.logout();
279 }
280 }
281
282 config_mgr.config.last_scan_file = Some(input_path.to_string_lossy().to_string());
283 config_mgr.save()?;
284 Ok(())
285}
286
287fn cmd_share(args: &[String]) -> Result<()> {
288 let mut directory = PathBuf::from(".");
289 let mut port = 8888u16;
290
291 let mut index = 0usize;
292 while index < args.len() {
293 match args[index].as_str() {
294 "--directory" | "-d" => {
295 if let Some(value) = args.get(index + 1) {
296 directory = PathBuf::from(value);
297 index += 2;
298 continue;
299 }
300 }
301 "--port" | "-p" => {
302 if let Some(value) = args.get(index + 1) {
303 port = value
304 .parse::<u16>()
305 .map_err(|_| AppError::Message(format!("Invalid port value: {}", value)))?;
306 index += 2;
307 continue;
308 }
309 }
310 _ => {}
311 }
312 index += 1;
313 }
314
315 println!("[+] Starting file server in: {}", directory.display());
316 println!("[+] Port: {}", port);
317 println!();
318
319 let (server, actual_port) = share::start_file_server(directory, port)?;
320 println!(
321 "File server running at http://0.0.0.0:{} (press Ctrl+C to stop)",
322 actual_port
323 );
324
325 let _server = server;
326 loop {
327 thread::sleep(Duration::from_secs(1));
328 }
329}
330
331fn cmd_reset_config(config_mgr: &mut ConfigManager) -> Result<()> {
332 println!("[?] Resetting all stored configuration...");
333 config_mgr.reset()?;
334 println!("[+] Configuration reset. Run ploit-malper to set up credentials.");
335 Ok(())
336}
337
338fn push_unique_suggestion(
339 suggestions: &mut Vec<ModuleSuggestion>,
340 seen: &mut HashSet<(String, String)>,
341 suggestion: ModuleSuggestion,
342) {
343 let key = (
344 suggestion.service_banner.clone(),
345 suggestion.suggested_module.clone(),
346 );
347 if seen.insert(key) {
348 suggestions.push(suggestion);
349 }
350}
351
352fn prompt_text(prompt: &str, default: &str) -> Result<String> {
353 print!(" {} [{}]: ", prompt, default);
354 io::stdout().flush()?;
355 let mut buffer = String::new();
356 io::stdin().read_line(&mut buffer)?;
357 let value = buffer.trim();
358 Ok(if value.is_empty() {
359 default.to_string()
360 } else {
361 value.to_string()
362 })
363}
364
365fn prompt_u16(prompt: &str, default: u16) -> Result<u16> {
366 loop {
367 let value = prompt_text(prompt, &default.to_string())?;
368 if value.is_empty() {
369 return Ok(default);
370 }
371 match value.parse::<u16>() {
372 Ok(parsed) => return Ok(parsed),
373 Err(_) => println!(" Please enter a valid number."),
374 }
375 }
376}
377
378fn prompt_bool(prompt: &str, default: bool) -> Result<bool> {
379 prompt_bool_with_default(prompt, default)
380}
381
382fn prompt_bool_with_default(prompt: &str, default: bool) -> Result<bool> {
383 let suffix = if default { "[Y/n]" } else { "[y/N]" };
384 loop {
385 print!(" {} {}: ", prompt, suffix);
386 io::stdout().flush()?;
387 let mut buffer = String::new();
388 io::stdin().read_line(&mut buffer)?;
389 let value = buffer.trim().to_lowercase();
390 if value.is_empty() {
391 return Ok(default);
392 }
393 match value.as_str() {
394 "y" | "yes" | "true" => return Ok(true),
395 "n" | "no" | "false" => return Ok(false),
396 _ => println!(" Please answer yes or no."),
397 }
398 }
399}
400
401fn prompt_choice(prompt: &str, choices: &[String], default: &str) -> Result<String> {
402 let resolved_default = if choices.iter().any(|c| c == default) {
403 default.to_string()
404 } else {
405 choices.first().cloned().unwrap_or_default()
406 };
407 loop {
408 print!(" {} [{}]: ", prompt, resolved_default);
409 io::stdout().flush()?;
410 let mut buffer = String::new();
411 io::stdin().read_line(&mut buffer)?;
412 let value = buffer.trim();
413 if value.is_empty() {
414 return Ok(resolved_default);
415 }
416 if choices.iter().any(|choice| choice == value) {
417 return Ok(value.to_string());
418 }
419 println!(" Please choose one of: {}", choices.join(", "));
420 }
421}
422
423fn truncate(value: &str, width: usize) -> String {
424 let count = value.chars().count();
425 if count <= width {
426 return value.to_string();
427 }
428 if width <= 1 {
429 return "…".to_string();
430 }
431 let mut truncated = value.chars().take(width - 1).collect::<String>();
432 truncated.push('…');
433 truncated
434}
435
436fn print_banner() {
437 println!("PloitMalper v{}", env!("CARGO_PKG_VERSION"));
438 println!("Vulnerability Post-Processing and Analysis Toolkit");
439 println!();
440 println!("Commands:");
441 println!(" process <file> Process and deduplicate scan results");
442 println!(" share Start temporary file server");
443 println!(" setup Configure MSF-RPC and NVD API credentials");
444 println!(" reset-config Reset stored configuration");
445}