Skip to main content

aurora_modules/
net.rs

1use aurora_core::{AuroraResult, Pipeline, Value};
2use std::process::Command;
3
4fn check_tool(name: &str) -> AuroraResult<()> {
5    let status = Command::new("which")
6        .arg(name)
7        .output()
8        .ok()
9        .map(|o| o.status.success())
10        .unwrap_or(false);
11    if !status {
12        return Err(aurora_core::AuroraError::CommandNotFound(
13            format!("{} is not installed", name)
14        ));
15    }
16    Ok(())
17}
18
19pub fn net_ping(host: &str) -> AuroraResult<Pipeline> {
20    check_tool("ping")?;
21    let output = Command::new("ping")
22        .args(["-c", "4", host])
23        .output()
24        .map_err(|e| aurora_core::AuroraError::ModuleError(
25            format!("ping failed: {}", e)
26        ))?;
27
28    let stdout = String::from_utf8_lossy(&output.stdout);
29    let stderr = String::from_utf8_lossy(&output.stderr);
30    let combined = format!("{}{}", stdout, stderr);
31
32    let sent = 4i64;
33    let received = combined.lines()
34        .filter(|l| l.contains("bytes from") || l.contains("icmp_seq"))
35        .count() as i64;
36
37    let mut min_ms = String::new();
38    let mut avg_ms = String::new();
39    let mut max_ms = String::new();
40
41    for line in combined.lines() {
42        if line.contains("rtt min/avg/max") || line.contains("round-trip") {
43            let parts: Vec<&str> = line.split('=').collect();
44            if parts.len() >= 2 {
45                let stats = parts[1].trim();
46                let nums: Vec<&str> = stats.split('/').collect();
47                if nums.len() >= 3 {
48                    min_ms = nums[0].trim().to_string();
49                    avg_ms = nums[1].trim().to_string();
50                    max_ms = nums[2].trim().to_string();
51                }
52            }
53        }
54    }
55
56    Ok(Pipeline::table(
57        vec!["sent".into(), "received".into(), "min_ms".into(), "avg_ms".into(), "max_ms".into()],
58        vec![vec![
59            Value::Int(sent),
60            Value::Int(received),
61            Value::String(min_ms),
62            Value::String(avg_ms),
63            Value::String(max_ms),
64        ]],
65    ))
66}
67
68pub fn net_dns(domain: &str) -> AuroraResult<Pipeline> {
69    if find_tool("dig") {
70        let output = Command::new("dig")
71            .args([domain, "+short"])
72            .output()
73            .map_err(|e| aurora_core::AuroraError::ModuleError(
74                format!("dig failed: {}", e)
75            ))?;
76        let stdout = String::from_utf8_lossy(&output.stdout);
77        let rows: Vec<Vec<Value>> = stdout.lines()
78            .filter(|l| !l.is_empty())
79            .map(|l| vec![Value::String(l.into())])
80            .collect();
81        return Ok(Pipeline::table(vec!["record".into()], rows));
82    }
83
84    if find_tool("nslookup") {
85        let output = Command::new("nslookup")
86            .arg(domain)
87            .output()
88            .map_err(|e| aurora_core::AuroraError::ModuleError(
89                format!("nslookup failed: {}", e)
90            ))?;
91        let stdout = String::from_utf8_lossy(&output.stdout);
92        return Ok(Pipeline::table(
93            vec!["record".into()],
94            vec![vec![Value::String(stdout.trim().into())]],
95        ));
96    }
97
98    Err(aurora_core::AuroraError::CommandNotFound(
99        "neither dig nor nslookup are installed".into()
100    ))
101}
102
103fn find_tool(name: &str) -> bool {
104    Command::new("which")
105        .arg(name)
106        .output()
107        .ok()
108        .map(|o| o.status.success())
109        .unwrap_or(false)
110}
111
112pub fn net_http(url: &str) -> AuroraResult<Pipeline> {
113    check_tool("curl")?;
114    let output = Command::new("curl")
115        .args(["-sI", url])
116        .output()
117        .map_err(|e| aurora_core::AuroraError::ModuleError(
118            format!("curl failed: {}", e)
119        ))?;
120
121    let stdout = String::from_utf8_lossy(&output.stdout);
122    let mut rows: Vec<Vec<Value>> = Vec::new();
123
124    let mut status = String::new();
125    for line in stdout.lines() {
126        if line.is_empty() { continue; }
127        if line.starts_with("HTTP/") {
128            status = line.to_string();
129        } else {
130            if let Some(idx) = line.find(':') {
131                let key = &line[..idx];
132                let val = &line[idx + 1..];
133                rows.push(vec![
134                    Value::String(key.trim().into()),
135                    Value::String(val.trim().into()),
136                ]);
137            }
138        }
139    }
140
141    if !status.is_empty() {
142        rows.insert(0, vec![
143            Value::String("Status".into()),
144            Value::String(status),
145        ]);
146    }
147
148    Ok(Pipeline::table(
149        vec!["header".into(), "value".into()],
150        rows,
151    ))
152}
153
154pub fn net_ip() -> AuroraResult<Pipeline> {
155    if find_tool("curl") {
156        let output = Command::new("curl")
157            .args(["-s", "https://api.ipify.org?format=json"])
158            .output()
159            .map_err(|e| aurora_core::AuroraError::ModuleError(
160                format!("curl failed: {}", e)
161            ))?;
162        let stdout = String::from_utf8_lossy(&output.stdout);
163        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stdout) {
164            if let Some(ip) = parsed.get("ip").and_then(|v| v.as_str()) {
165                return Ok(Pipeline::table(
166                    vec!["ip".into()],
167                    vec![vec![Value::String(ip.into())]],
168                ));
169            }
170        }
171    }
172
173    let output = Command::new("hostname")
174        .args(["-I"])
175        .output()
176        .ok();
177    if let Some(out) = output {
178        let stdout = String::from_utf8_lossy(&out.stdout);
179        let ip = stdout.trim().split(' ').next().unwrap_or("").to_string();
180        if !ip.is_empty() {
181            return Ok(Pipeline::table(
182                vec!["ip".into()],
183                vec![vec![Value::String(ip)]],
184            ));
185        }
186    }
187
188    Ok(Pipeline::table(
189        vec!["ip".into()],
190        vec![vec![Value::String("unknown".into())]],
191    ))
192}
193
194pub fn net_scan(host: &str) -> AuroraResult<Pipeline> {
195    if find_tool("nmap") {
196        let output = Command::new("nmap")
197            .args(["-F", host])
198            .output()
199            .map_err(|e| aurora_core::AuroraError::ModuleError(
200                format!("nmap failed: {}", e)
201            ))?;
202        let stdout = String::from_utf8_lossy(&output.stdout);
203        let mut rows: Vec<Vec<Value>> = Vec::new();
204
205        for line in stdout.lines() {
206            let line = line.trim();
207            if line.contains("/tcp") || line.contains("/udp") {
208                let parts: Vec<&str> = line.split_whitespace().collect();
209                if let Some(port_part) = parts.first() {
210                    rows.push(vec![
211                        Value::String(port_part.to_string()),
212                        Value::String(parts.get(1).copied().unwrap_or("").into()),
213                        Value::String(parts.get(2).copied().unwrap_or("").into()),
214                    ]);
215                }
216            }
217        }
218
219        return Ok(Pipeline::table(
220            vec!["port".into(), "state".into(), "service".into()],
221            rows,
222        ));
223    }
224
225    Err(aurora_core::AuroraError::CommandNotFound(
226        "nmap is not installed".into()
227    ))
228}