use crate::executors::types::{Skill, SkillParameter};
use anyhow::Result;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::sync::Semaphore;
use tokio::time::timeout;
#[derive(Debug)]
pub struct PortScanSkill;
#[async_trait::async_trait]
impl Skill for PortScanSkill {
fn name(&self) -> &str {
"port_scan"
}
fn description(&self) -> &str {
"Scan ports on a target host to discover open ports and services"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user wants to find open ports, check service availability, \
or perform network reconnaissance on a specific host. Supports single port, port ranges, \
comma-separated ports, and concurrent scanning."
}
fn parameters(&self) -> Vec<SkillParameter> {
vec![
SkillParameter {
name: "target".to_string(),
param_type: "string".to_string(),
description: "Target hostname or IP address to scan".to_string(),
required: true,
default: None,
example: Some(Value::String("scanme.nmap.org".to_string())),
enum_values: None,
},
SkillParameter {
name: "ports".to_string(),
param_type: "string".to_string(),
description: "Ports to scan (e.g., '80', '1-1024', '22,80,443')".to_string(),
required: false,
default: Some(Value::String("1-1024".to_string())),
example: Some(Value::String("22,80,443,8080".to_string())),
enum_values: None,
},
SkillParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Connection timeout in seconds for each port".to_string(),
required: false,
default: Some(Value::Number(1.into())),
example: Some(Value::Number(2.into())),
enum_values: None,
},
SkillParameter {
name: "concurrency".to_string(),
param_type: "integer".to_string(),
description: "Number of concurrent connection attempts".to_string(),
required: false,
default: Some(Value::Number(100.into())),
example: Some(Value::Number(50.into())),
enum_values: None,
},
SkillParameter {
name: "show_closed".to_string(),
param_type: "boolean".to_string(),
description: "Show closed ports in results".to_string(),
required: false,
default: Some(Value::Bool(false)),
example: Some(Value::Bool(false)),
enum_values: None,
},
]
}
fn example_call(&self) -> Value {
json!({
"action": "port_scan",
"parameters": {
"target": "localhost",
"ports": "1-1000",
"timeout": 1
}
})
}
fn example_output(&self) -> String {
"Scanning localhost (127.0.0.1)\nPort 22: Open - SSH\nPort 80: Open - HTTP\nPort 443: Open - HTTPS\nTotal open ports: 3\nScan completed in 2.5 seconds".to_string()
}
fn category(&self) -> &str {
"net"
}
async fn execute(&self, parameters: &HashMap<String, Value>) -> Result<String> {
let target = parameters
.get("target")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: target"))?;
let ports_spec = parameters
.get("ports")
.and_then(|v| v.as_str())
.unwrap_or("1-1024");
let timeout_secs = parameters
.get("timeout")
.and_then(|v| v.as_u64())
.unwrap_or(1);
let concurrency = parameters
.get("concurrency")
.and_then(|v| v.as_u64())
.unwrap_or(100) as usize;
let show_closed = parameters
.get("show_closed")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let addr = format!("{}:0", target);
let resolved = match addr.to_socket_addrs() {
Ok(mut addrs) => addrs.next().map(|s| s.ip()),
Err(_) => None,
};
let ip = match resolved {
Some(ip) => ip,
None => return Ok(format!("Failed to resolve target: {}", target)),
};
let ports = parse_ports(ports_spec)?;
let total_ports = ports.len();
let start_time = std::time::Instant::now();
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = vec![];
for port in ports {
let permit = semaphore.clone().acquire_owned().await?;
let target_ip = ip;
let timeout_dur = Duration::from_secs(timeout_secs);
let task = tokio::spawn(async move {
let result = scan_port(target_ip, port, timeout_dur).await;
drop(permit);
(port, result)
});
tasks.push(task);
}
let mut open_ports = Vec::new();
let mut closed_ports = Vec::new();
for task in tasks {
match task.await {
Ok((port, is_open)) => {
if is_open {
open_ports.push(port);
} else if show_closed {
closed_ports.push(port);
}
}
Err(e) => eprintln!("Task failed: {}", e),
}
}
open_ports.sort();
closed_ports.sort();
let duration = start_time.elapsed();
let mut result = format!("Scanning {} ({})\n", target, ip);
result.push_str(&format!("Total ports scanned: {}\n", total_ports));
if !open_ports.is_empty() {
result.push_str(&format!("\nOpen ports: {}\n", open_ports.len()));
for port in &open_ports {
let service = get_service_name(*port);
result.push_str(&format!(" Port {}: Open - {}\n", port, service));
}
} else {
result.push_str("\nNo open ports found\n");
}
if show_closed && !closed_ports.is_empty() {
result.push_str(&format!("\nClosed ports: {}\n", closed_ports.len()));
for port in closed_ports.iter().take(20) {
result.push_str(&format!(" Port {}\n", port));
}
if closed_ports.len() > 20 {
result.push_str(&format!(" ... and {} more\n", closed_ports.len() - 20));
}
}
result.push_str(&format!(
"\nScan completed in {:.2} seconds",
duration.as_secs_f64()
));
Ok(result)
}
}
fn get_service_name(port: u16) -> &'static str {
match port {
20 => "FTP-data",
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "RPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1433 => "MSSQL",
1521 => "Oracle",
3306 => "MySQL",
3389 => "RDP",
5432 => "PostgreSQL",
6379 => "Redis",
8080 => "HTTP-Alt",
8443 => "HTTPS-Alt",
27017 => "MongoDB",
_ => "Unknown",
}
}
async fn scan_port(ip: IpAddr, port: u16, timeout_dur: Duration) -> bool {
let addr = SocketAddr::new(ip, port);
match timeout(timeout_dur, TcpStream::connect(&addr)).await {
Ok(Ok(_)) => true,
_ => false,
}
}
fn parse_ports(ports_spec: &str) -> Result<Vec<u16>> {
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 = range[0].parse::<u16>()?;
let end = range[1].parse::<u16>()?;
for port in start..=end {
ports.push(port);
}
}
} else {
let port = part.parse::<u16>()?;
ports.push(port);
}
}
ports.sort();
ports.dedup();
Ok(ports)
}
#[derive(Debug)]
pub struct PortLookupSkill;
#[async_trait::async_trait]
impl Skill for PortLookupSkill {
fn name(&self) -> &str {
"port_lookup"
}
fn description(&self) -> &str {
"Look up information about a specific port number, including common services and vulnerabilities"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user wants to know what service runs on a specific port"
}
fn parameters(&self) -> Vec<SkillParameter> {
vec![SkillParameter {
name: "port".to_string(),
param_type: "integer".to_string(),
description: "Port number to look up".to_string(),
required: true,
default: None,
example: Some(Value::Number(22.into())),
enum_values: None,
}]
}
fn example_call(&self) -> Value {
json!({
"action": "port_lookup",
"parameters": {
"port": 443
}
})
}
fn example_output(&self) -> String {
"Port 443: HTTPS - HTTP over TLS/SSL\nCommon services: HTTPS, SSL/TLS encrypted web traffic\nDefault protocol: TCP".to_string()
}
fn category(&self) -> &str {
"net"
}
async fn execute(&self, parameters: &HashMap<String, Value>) -> Result<String> {
let port = parameters
.get("port")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: port"))?
as u16;
let service = get_service_name(port);
let protocol = match port {
53 | 123 | 161 | 162 | 514 => "UDP/TCP",
_ => "TCP",
};
let description = match port {
20 | 21 => "File Transfer Protocol",
22 => "Secure Shell for secure remote administration",
23 => "Telnet - insecure remote access",
25 => "Simple Mail Transfer Protocol for email routing",
53 => "Domain Name System for domain resolution",
80 => "Hypertext Transfer Protocol for web traffic",
110 => "Post Office Protocol v3 for email retrieval",
143 => "Internet Message Access Protocol for email",
443 => "HTTP over TLS/SSL for secure web traffic",
445 => "Server Message Block for file sharing",
3306 => "MySQL database",
5432 => "PostgreSQL database",
6379 => "Redis in-memory data store",
8080 => "HTTP alternative port for web proxies and servers",
_ => "Unknown service",
};
Ok(format!(
"Port {}: {} - {}\nProtocol: {}\nCommon services: {}\n",
port, service, description, protocol, service
))
}
}
#[derive(Debug)]
pub struct PortTestSkill;
#[async_trait::async_trait]
impl Skill for PortTestSkill {
fn name(&self) -> &str {
"port_test"
}
fn description(&self) -> &str {
"Test connectivity to a specific port on a target host"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user wants to check if a specific port is reachable on a host"
}
fn parameters(&self) -> Vec<SkillParameter> {
vec![
SkillParameter {
name: "host".to_string(),
param_type: "string".to_string(),
description: "Target hostname or IP address".to_string(),
required: true,
default: None,
example: Some(Value::String("google.com".to_string())),
enum_values: None,
},
SkillParameter {
name: "port".to_string(),
param_type: "integer".to_string(),
description: "Port number to test".to_string(),
required: true,
default: None,
example: Some(Value::Number(80.into())),
enum_values: None,
},
SkillParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Connection timeout in seconds".to_string(),
required: false,
default: Some(Value::Number(3.into())),
example: Some(Value::Number(5.into())),
enum_values: None,
},
]
}
fn example_call(&self) -> Value {
json!({
"action": "port_test",
"parameters": {
"host": "example.com",
"port": 443
}
})
}
fn example_output(&self) -> String {
"Testing connection to example.com:443...\nConnection successful! Port 443 is open and accepting connections\nResponse time: 45ms".to_string()
}
fn category(&self) -> &str {
"net"
}
async fn execute(&self, parameters: &HashMap<String, Value>) -> Result<String> {
let host = parameters
.get("host")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: host"))?;
let port = parameters
.get("port")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: port"))?
as u16;
let timeout_secs = parameters
.get("timeout")
.and_then(|v| v.as_u64())
.unwrap_or(3);
let addr = format!("{}:{}", host, port);
let start = std::time::Instant::now();
match timeout(Duration::from_secs(timeout_secs), async {
let addrs: Vec<SocketAddr> = match addr.to_socket_addrs() {
Ok(addrs) => addrs.collect(),
Err(e) => return Err(anyhow::anyhow!("Failed to resolve host: {}", e)),
};
for addr in addrs {
if tokio::time::timeout(
Duration::from_secs(timeout_secs),
tokio::net::TcpStream::connect(&addr),
)
.await
.is_ok()
{
return Ok(addr);
}
}
Err(anyhow::anyhow!("Connection refused or timeout"))
})
.await
{
Ok(Ok(resolved_addr)) => {
let elapsed = start.elapsed();
let service = get_service_name(port);
Ok(format!(
"Testing connection to {}:{}...\n✓ Connection successful! Port {} ({}) is open and accepting connections\nResponse time: {}ms\nResolved to: {}",
host,
port,
port,
service,
elapsed.as_millis(),
resolved_addr.ip()
))
}
Ok(Err(e)) => Ok(format!(
"Testing connection to {}:{}...\n✗ Connection failed: {}\nPort {} is likely closed or filtered",
host, port, e, port
)),
Err(_) => Ok(format!(
"Testing connection to {}:{}...\n✗ Connection timeout after {} seconds\nPort {} is unreachable or filtered",
host, port, timeout_secs, port
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_parse_ports() {
let ports = parse_ports("80").unwrap();
assert_eq!(ports, vec![80]);
let ports = parse_ports("1-5").unwrap();
assert_eq!(ports, vec![1, 2, 3, 4, 5]);
let ports = parse_ports("22,80,443").unwrap();
assert_eq!(ports, vec![22, 80, 443]);
let ports = parse_ports("22,80-82,443").unwrap();
assert_eq!(ports, vec![22, 80, 81, 82, 443]);
let ports = parse_ports("80,80,443,443").unwrap();
assert_eq!(ports, vec![80, 443]);
}
#[tokio::test]
async fn test_port_lookup_well_known() {
let skill = PortLookupSkill;
let mut params = HashMap::new();
params.insert("port".to_string(), json!(22));
let result = skill.execute(¶ms).await.unwrap();
assert!(result.contains("Port 22: SSH"));
assert!(result.contains("Secure Shell"));
let mut params = HashMap::new();
params.insert("port".to_string(), json!(80));
let result = skill.execute(¶ms).await.unwrap();
assert!(result.contains("Port 80: HTTP"));
assert!(result.contains("Hypertext Transfer Protocol"));
let mut params = HashMap::new();
params.insert("port".to_string(), json!(443));
let result = skill.execute(¶ms).await.unwrap();
assert!(result.contains("Port 443: HTTPS"));
}
#[tokio::test]
async fn test_port_test_open_port() {
let skill = PortTestSkill;
let mut params = HashMap::new();
params.insert("host".to_string(), json!("google.com"));
params.insert("port".to_string(), json!(80));
params.insert("timeout".to_string(), json!(5));
let result = skill.execute(¶ms).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Testing connection to google.com:80"));
assert!(output.contains("✓ Connection successful"));
}
}