hippox-drivers 0.3.5

🦛All indivisible atomic driver units in Hippox.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Network common utilities
//!
//! This module provides network-related utilities including port parsing,
//! service detection, DNS lookup, and TCP connection functions.
use crate::DriverError;
use crate::result::DriverResult;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::net::ToSocketAddrs;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::timeout;
use tracing::{debug, info, warn};
/// Parse port specification string into vector of ports
/// Supports: "80", "1-1024", "22,80,443", "22,80-90,443"
pub fn parse_ports(ports_spec: &str) -> DriverResult<Vec<u16>> {
    debug!("Parsing ports specification: {}", ports_spec);
    let mut ports = Vec::new();
    for part in ports_spec.split(',') {
        let part = part.trim();
        if part.contains('-') {
            let range: Vec<&str> = part.split('-').collect();
            if range.len() == 2 {
                let start = match range[0].parse::<u16>() {
                    Ok(s) => s,
                    Err(e) => {
                        let err_msg = format!("Invalid port range start: {}", e);
                        warn!("{}", err_msg);
                        return Err(DriverError::validation("ports_spec", err_msg));
                    }
                };
                let end = match range[1].parse::<u16>() {
                    Ok(e) => e,
                    Err(e) => {
                        let err_msg = format!("Invalid port range end: {}", e);
                        warn!("{}", err_msg);
                        return Err(DriverError::validation("ports_spec", err_msg));
                    }
                };
                for port in start..=end {
                    ports.push(port);
                }
            }
        } else if !part.is_empty() {
            let port = match part.parse::<u16>() {
                Ok(p) => p,
                Err(e) => {
                    let err_msg = format!("Invalid port: {}", e);
                    warn!("{}", err_msg);
                    return Err(DriverError::validation("ports_spec", err_msg));
                }
            };
            ports.push(port);
        }
    }
    ports.sort();
    ports.dedup();
    info!("Parsed {} ports", ports.len());
    return Ok(ports);
}
/// Get service name for a port using range matching
pub fn get_service_name(port: u16) -> &'static str {
    match port {
        20 | 21 => "FTP",
        22 => "SSH",
        23 => "Telnet",
        25 | 465 | 587 => "SMTP",
        53 => "DNS",
        67 | 68 => "DHCP",
        69 => "TFTP",
        80 | 8000 | 8080 | 8081 | 8888 => "HTTP",
        110 | 995 => "POP3",
        111 | 135 => "RPC",
        123 => "NTP",
        137 | 138 | 139 => "NetBIOS",
        143 | 993 => "IMAP",
        161 | 162 => "SNMP",
        179 => "BGP",
        389 | 636 => "LDAP",
        443 | 8443 | 9443 => "HTTPS",
        445 => "SMB",
        514 => "Syslog",
        873 => "rsync",
        990 => "FTPS",
        1080 => "SOCKS",
        1433 => "MSSQL",
        1521 => "Oracle",
        1723 => "PPTP",
        1883 | 8883 => "MQTT",
        2049 => "NFS",
        2082 | 2083 => "cPanel",
        2222 => "SSH",
        2375 | 2376 => "Docker",
        2379 | 2380 => "etcd",
        2480 => "OrientDB",
        3000 => "Grafana",
        3306 => "MySQL",
        3389 => "RDP",
        4000 => "Zabbix",
        5000 | 5001 | 5002 => "Flask/Django",
        5432 => "PostgreSQL",
        5672 => "RabbitMQ",
        5900 | 5901 => "VNC",
        5984 => "CouchDB",
        5985 | 5986 => "WinRM",
        6379 => "Redis",
        7000 | 7001 | 7199 | 9042 => "Cassandra",
        8086 => "InfluxDB",
        8091..=8099 => "Couchbase",
        8111 => "Artifactory",
        8125 => "StatsD",
        8126 => "Datadog",
        8140 => "Puppet",
        8161 => "ActiveMQ",
        8200 => "Vault",
        8333 | 8334 => "Bitcoin",
        8384 => "Syncthing",
        8500 | 8600 => "Consul",
        8761 => "Eureka",
        8983 => "Solr",
        9000 => "Portainer",
        9090 | 9091 => "Prometheus",
        9092..=9099 => "Kafka",
        9100.. => "NodeExporter",
        9200..=9299 => "Elasticsearch",
        9300..=9399 => "Elasticsearch",
        9411 => "Zipkin",
        _ => "Unknown",
    }
}
/// Get probe string for a port
pub fn get_probe_for_port(port: u16) -> Option<&'static [u8]> {
    match port {
        21 => Some(b"QUIT\r\n"),
        22 => Some(b"SSH-2.0-Client\r\n"),
        25 => Some(b"EHLO example.com\r\n"),
        80 | 8080 | 8000 => Some(b"HEAD / HTTP/1.0\r\n\r\n"),
        110 => Some(b"QUIT\r\n"),
        143 => Some(b"A001 CAPABILITY\r\n"),
        443 | 8443 => Some(b"HEAD / HTTP/1.0\r\n\r\n"),
        3306 => Some(b"\x00\x00\x00\x01"),
        5432 => Some(b"\x00\x00\x00\x08\x04\xd2\x16\x2f"),
        6379 => Some(b"PING\r\n"),
        _ => None,
    }
}
/// Identify service from banner
pub fn identify_service(port: u16, banner: &str) -> (String, Option<String>, u8) {
    let banner_lower = banner.to_lowercase();
    let service = get_service_name(port);
    if service == "Unknown" {
        return ("Unknown".to_string(), None, 0);
    }
    // Extract version from banner
    let version = extract_version_from_banner(&banner_lower);
    let confidence = if version.is_some() { 90 } else { 70 };
    return (service.to_string(), version, confidence);
}
fn extract_version_from_banner(banner: &str) -> Option<String> {
    let patterns = [
        (r"nginx/([\d\.]+)", "nginx"),
        (r"Apache/([\d\.]+)", "apache"),
        (r"openssh[_\-]?([\d\.]+)", "openssh"),
        (r"OpenSSH[_\-]?([\d\.]+)", "openssh"),
        (r"vsftpd/([\d\.]+)", "vsftpd"),
        (r"proftpd[_\-]?([\d\.]+)", "proftpd"),
        (r"postfix/([\d\.]+)", "postfix"),
        (r"sendmail[_\-]?([\d\.]+)", "sendmail"),
        (r"exim[_\-]?([\d\.]+)", "exim"),
        (r"dovecot[_\-]?([\d\.]+)", "dovecot"),
        (r"MySQL[_\-]?([\d\.]+)", "mysql"),
        (r"MariaDB[_\-]?([\d\.]+)", "mariadb"),
        (r"PostgreSQL[_\-]?([\d\.]+)", "postgresql"),
        (r"Redis[_\-]?([\d\.]+)", "redis"),
        (r"Lighttpd/([\d\.]+)", "lighttpd"),
        (r"IIS/([\d\.]+)", "iis"),
        (r"Dropbear[_\-]?([\d\.]+)", "dropbear"),
        (r"OpenSSL/([\d\.]+)", "openssl"),
    ];
    for (pattern, _) in &patterns {
        let re = regex::Regex::new(pattern).ok()?;
        if let Some(cap) = re.captures(banner) {
            if let Some(m) = cap.get(1) {
                return Some(m.as_str().to_string());
            }
        }
    }
    return None;
}
/// Resolve hostname to IP address
pub fn resolve_host(host: &str) -> DriverResult<std::net::IpAddr> {
    debug!("Resolving host: {}", host);
    let addr = format!("{}:0", host);
    let mut addrs = match addr.to_socket_addrs() {
        Ok(a) => a,
        Err(e) => {
            let err_msg = format!("Failed to resolve host {}: {}", host, e);
            warn!("{}", err_msg);
            return Err(DriverError::execution(err_msg));
        }
    };
    match addrs.next() {
        Some(s) => {
            info!("Resolved host {} to {}", host, s.ip());
            return Ok(s.ip());
        }
        None => {
            let err_msg = format!("Failed to resolve host: {}", host);
            warn!("{}", err_msg);
            return Err(DriverError::execution(err_msg));
        }
    }
}
/// TCP connect with timeout
pub async fn tcp_connect(ip: std::net::IpAddr, port: u16, timeout_secs: u64) -> DriverResult<TcpStream> {
    debug!("Connecting to {}:{} with timeout {}s", ip, port, timeout_secs);
    let addr = std::net::SocketAddr::new(ip, port);
    let timeout_dur = Duration::from_secs(timeout_secs);
    let stream = match timeout(timeout_dur, TcpStream::connect(&addr)).await {
        Ok(Ok(s)) => s,
        Ok(Err(e)) => {
            let err_msg = format!("Failed to connect to {}:{}: {}", ip, port, e);
            warn!("{}", err_msg);
            return Err(DriverError::execution(err_msg));
        }
        Err(_) => {
            let err_msg = format!("Connection to {}:{} timed out after {}s", ip, port, timeout_secs);
            warn!("{}", err_msg);
            return Err(DriverError::timeout(Some(timeout_secs.to_string())));
        }
    };
    info!("Connected to {}:{}", ip, port);
    return Ok(stream);
}
/// Get string parameter from HashMap
pub fn get_param_string(params: &HashMap<String, Value>, name: &str) -> DriverResult<String> {
    debug!("Getting string parameter: {}", name);
    match params.get(name).and_then(|v| v.as_str()) {
        Some(s) => {
            info!("Parameter {}: {}", name, s);
            return Ok(s.to_string());
        }
        None => {
            let err_msg = format!("Missing parameter: {}", name);
            warn!("{}", err_msg);
            return Err(DriverError::missing_parameter(name));
        }
    }
}
/// Get u64 parameter from HashMap with default
pub fn get_param_u64(params: &HashMap<String, Value>, name: &str, default: u64) -> u64 {
    let value = params.get(name).and_then(|v| v.as_u64()).unwrap_or(default);
    debug!("Parameter {}: {} (default: {})", name, value, default);
    return value;
}
/// Get bool parameter from HashMap with default
pub fn get_param_bool(params: &HashMap<String, Value>, name: &str, default: bool) -> bool {
    let value = params.get(name).and_then(|v| v.as_bool()).unwrap_or(default);
    debug!("Parameter {}: {} (default: {})", name, value, default);
    return value;
}
/// Nslookup result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NslookupResult {
    pub domain: String,
    pub dns_server: String,
    pub a_records: Vec<String>,
    pub aaaa_records: Vec<String>,
    pub mx_records: Vec<(String, u16)>,
    pub txt_records: Vec<String>,
    pub cname_records: Vec<String>,
    pub ns_records: Vec<String>,
    pub soa_record: Option<String>,
}
/// Perform detailed DNS lookup (nslookup style)
pub async fn nslookup(domain: &str, dns_server: Option<&str>) -> DriverResult<NslookupResult> {
    use trust_dns_proto::rr::{RData, RecordType};
    use trust_dns_resolver::Resolver;
    use trust_dns_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts};
    debug!("Performing nslookup for domain: {}, dns_server: {:?}", domain, dns_server);
    let dns_server = dns_server.unwrap_or("8.8.8.8");
    let resolver_config = ResolverConfig::from_parts(
        None,
        vec![],
        NameServerConfigGroup::from_ips_clear(
            &[dns_server.parse().map_err(|e| DriverError::execution(format!("Invalid DNS server: {}", e)))?],
            53,
            true,
        ),
    );
    let resolver_opts = ResolverOpts::default();
    let resolver = match Resolver::new(resolver_config, resolver_opts) {
        Ok(r) => r,
        Err(e) => {
            let err_msg = format!("Failed to create DNS resolver: {}", e);
            warn!("{}", err_msg);
            return Err(DriverError::execution(err_msg));
        }
    };
    let mut a_records = Vec::new();
    let mut aaaa_records = Vec::new();
    let mut mx_records = Vec::new();
    let mut txt_records = Vec::new();
    let mut cname_records = Vec::new();
    let mut ns_records = Vec::new();
    let mut soa_record = None;
    // A records
    if let Ok(response) = resolver.lookup(domain, RecordType::A) {
        for record in response.iter() {
            if let RData::A(ip) = record {
                a_records.push(ip.to_string());
            }
        }
        debug!("Found {} A records", a_records.len());
    }
    // AAAA records
    if let Ok(response) = resolver.lookup(domain, RecordType::AAAA) {
        for record in response.iter() {
            if let RData::AAAA(ip) = record {
                aaaa_records.push(ip.to_string());
            }
        }
        debug!("Found {} AAAA records", aaaa_records.len());
    }
    // MX records
    if let Ok(response) = resolver.lookup(domain, RecordType::MX) {
        for record in response.iter() {
            if let RData::MX(mx) = record {
                mx_records.push((mx.exchange().to_string(), mx.preference()));
            }
        }
        mx_records.sort_by_key(|(_, priority)| *priority);
        debug!("Found {} MX records", mx_records.len());
    }
    // TXT records
    if let Ok(response) = resolver.lookup(domain, RecordType::TXT) {
        for record in response.iter() {
            if let RData::TXT(txt) = record {
                let text: String = txt.txt_data().iter().map(|d| String::from_utf8_lossy(d)).collect::<Vec<_>>().join("");
                txt_records.push(text);
            }
        }
        debug!("Found {} TXT records", txt_records.len());
    }
    // CNAME records
    if let Ok(response) = resolver.lookup(domain, RecordType::CNAME) {
        for record in response.iter() {
            if let RData::CNAME(cname) = record {
                cname_records.push(cname.to_string());
            }
        }
        debug!("Found {} CNAME records", cname_records.len());
    }
    // NS records
    if let Ok(response) = resolver.lookup(domain, RecordType::NS) {
        for record in response.iter() {
            if let RData::NS(ns) = record {
                ns_records.push(ns.to_string());
            }
        }
        debug!("Found {} NS records", ns_records.len());
    }
    // SOA record
    if let Ok(response) = resolver.lookup(domain, RecordType::SOA) {
        for record in response.iter() {
            if let RData::SOA(soa) = record {
                soa_record = Some(format!("{} (serial: {})", soa.mname(), soa.serial()));
                break;
            }
        }
        debug!("SOA record found: {:?}", soa_record);
    }
    let result = NslookupResult {
        domain: domain.to_string(),
        dns_server: dns_server.to_string(),
        a_records,
        aaaa_records,
        mx_records,
        txt_records,
        cname_records,
        ns_records,
        soa_record,
    };
    info!("nslookup completed for domain: {}", domain);
    return Ok(result);
}
/// Get local network connections (netstat style)
pub fn get_network_connections() -> DriverResult<Vec<HashMap<String, String>>> {
    debug!("Getting network connections");
    #[cfg(target_os = "linux")]
    {
        let mut connections = Vec::new();
        let content = match std::fs::read_to_string("/proc/net/tcp") {
            Ok(c) => c,
            Err(e) => {
                let err_msg = format!("Failed to read /proc/net/tcp: {}", e);
                warn!("{}", err_msg);
                return Err(DriverError::io(err_msg));
            }
        };
        for line in content.lines().skip(1) {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 4 {
                let mut conn = HashMap::new();
                conn.insert("local_address".to_string(), parts[1].to_string());
                conn.insert("remote_address".to_string(), parts[2].to_string());
                conn.insert("state".to_string(), parts[3].to_string());
                connections.push(conn);
            }
        }
        info!("Found {} network connections", connections.len());
        return Ok(connections);
    }
    #[cfg(target_os = "macos")]
    {
        let output = match hidden_cmd("netstat").args(["-n", "-t"]).output() {
            Ok(o) => o,
            Err(e) => {
                let err_msg = format!("Failed to execute netstat: {}", e);
                warn!("{}", err_msg);
                return Err(DriverError::execution(err_msg));
            }
        };
        let mut connections = Vec::new();
        if let Ok(text) = String::from_utf8(output.stdout) {
            for line in text.lines().skip(1) {
                let parts: Vec<&str> = line.split_whitespace().collect();
                if parts.len() >= 4 {
                    let mut conn = HashMap::new();
                    conn.insert("protocol".to_string(), parts[0].to_string());
                    conn.insert("recvq".to_string(), parts[1].to_string());
                    conn.insert("sendq".to_string(), parts[2].to_string());
                    conn.insert("local_address".to_string(), parts[3].to_string());
                    conn.insert("foreign_address".to_string(), parts[4].to_string());
                    if parts.len() > 5 {
                        conn.insert("state".to_string(), parts[5].to_string());
                    }
                    connections.push(conn);
                }
            }
        }
        info!("Found {} network connections", connections.len());
        return Ok(connections);
    }
    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    {
        let err_msg = "netstat not supported on this platform".to_string();
        warn!("{}", err_msg);
        return Err(DriverError::internal(err_msg));
    }
}