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}