proc_cli/commands/
ports.rs

1//! `proc ports` - List all listening ports
2//!
3//! Examples:
4//!   proc ports              # Show all listening ports
5//!   proc ports --filter node # Filter by process name
6//!   proc ports --exposed    # Only network-accessible ports (0.0.0.0)
7//!   proc ports --local      # Only localhost ports (127.0.0.1)
8//!   proc ports -v           # Show with executable paths
9
10use crate::core::{PortInfo, Process};
11use crate::error::Result;
12use crate::ui::{OutputFormat, Printer};
13use clap::Args;
14use colored::*;
15use serde::Serialize;
16use std::collections::HashMap;
17
18/// List all listening ports
19#[derive(Args, Debug)]
20pub struct PortsCommand {
21    /// Filter by process name
22    #[arg(long, short = 'f')]
23    pub filter: Option<String>,
24
25    /// Only show network-exposed ports (0.0.0.0, ::)
26    #[arg(long, short = 'e')]
27    pub exposed: bool,
28
29    /// Only show localhost ports (127.0.0.1, ::1)
30    #[arg(long, short = 'l')]
31    pub local: bool,
32
33    /// Output as JSON
34    #[arg(long, short = 'j')]
35    pub json: bool,
36
37    /// Show verbose output (includes executable path)
38    #[arg(long, short = 'v')]
39    pub verbose: bool,
40
41    /// Sort by: port, pid, name
42    #[arg(long, short = 's', default_value = "port")]
43    pub sort: String,
44}
45
46impl PortsCommand {
47    pub fn execute(&self) -> Result<()> {
48        let mut ports = PortInfo::get_all_listening()?;
49
50        // Filter by process name if specified
51        if let Some(ref filter) = self.filter {
52            let filter_lower = filter.to_lowercase();
53            ports.retain(|p| p.process_name.to_lowercase().contains(&filter_lower));
54        }
55
56        // Filter by address exposure
57        if self.exposed {
58            ports.retain(|p| {
59                p.address
60                    .as_ref()
61                    .map(|a| a == "0.0.0.0" || a == "::" || a == "*")
62                    .unwrap_or(true)
63            });
64        }
65
66        if self.local {
67            ports.retain(|p| {
68                p.address
69                    .as_ref()
70                    .map(|a| a == "127.0.0.1" || a == "::1" || a.starts_with("[::1]"))
71                    .unwrap_or(false)
72            });
73        }
74
75        // Sort ports
76        match self.sort.to_lowercase().as_str() {
77            "port" => ports.sort_by_key(|p| p.port),
78            "pid" => ports.sort_by_key(|p| p.pid),
79            "name" => ports.sort_by(|a, b| {
80                a.process_name
81                    .to_lowercase()
82                    .cmp(&b.process_name.to_lowercase())
83            }),
84            _ => ports.sort_by_key(|p| p.port),
85        }
86
87        // In verbose mode, fetch process info for paths
88        let process_map: HashMap<u32, Process> = if self.verbose {
89            let mut map = HashMap::new();
90            for port in &ports {
91                if let std::collections::hash_map::Entry::Vacant(e) = map.entry(port.pid) {
92                    if let Ok(Some(proc)) = Process::find_by_pid(port.pid) {
93                        e.insert(proc);
94                    }
95                }
96            }
97            map
98        } else {
99            HashMap::new()
100        };
101
102        if self.json {
103            self.print_json(&ports, &process_map);
104        } else {
105            self.print_human(&ports, &process_map);
106        }
107
108        Ok(())
109    }
110
111    fn print_human(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
112        if ports.is_empty() {
113            println!("{} No listening ports found", "⚠".yellow().bold());
114            return;
115        }
116
117        println!(
118            "{} Found {} listening port{}",
119            "✓".green().bold(),
120            ports.len().to_string().cyan().bold(),
121            if ports.len() == 1 { "" } else { "s" }
122        );
123        println!();
124
125        // Header
126        println!(
127            "{:<8} {:<10} {:<8} {:<20} {:<15}",
128            "PORT".bright_blue().bold(),
129            "PROTO".bright_blue().bold(),
130            "PID".bright_blue().bold(),
131            "PROCESS".bright_blue().bold(),
132            "ADDRESS".bright_blue().bold()
133        );
134        println!("{}", "─".repeat(65).bright_black());
135
136        for port in ports {
137            let addr = port.address.as_deref().unwrap_or("*");
138            let proto = format!("{:?}", port.protocol).to_uppercase();
139
140            println!(
141                "{:<8} {:<10} {:<8} {:<20} {:<15}",
142                port.port.to_string().cyan().bold(),
143                proto.white(),
144                port.pid.to_string().cyan(),
145                truncate_string(&port.process_name, 19).white(),
146                addr.bright_black()
147            );
148
149            // In verbose mode, show path
150            if self.verbose {
151                if let Some(proc) = process_map.get(&port.pid) {
152                    if let Some(ref path) = proc.exe_path {
153                        println!(
154                            "         {} {}",
155                            "↳".bright_black(),
156                            truncate_string(path, 55).bright_black()
157                        );
158                    }
159                }
160            }
161        }
162        println!();
163    }
164
165    fn print_json(&self, ports: &[PortInfo], process_map: &HashMap<u32, Process>) {
166        let printer = Printer::new(OutputFormat::Json, self.verbose);
167
168        #[derive(Serialize)]
169        struct PortWithProcess<'a> {
170            #[serde(flatten)]
171            port: &'a PortInfo,
172            #[serde(skip_serializing_if = "Option::is_none")]
173            exe_path: Option<&'a str>,
174        }
175
176        let enriched: Vec<PortWithProcess> = ports
177            .iter()
178            .map(|p| PortWithProcess {
179                port: p,
180                exe_path: process_map
181                    .get(&p.pid)
182                    .and_then(|proc| proc.exe_path.as_deref()),
183            })
184            .collect();
185
186        #[derive(Serialize)]
187        struct Output<'a> {
188            action: &'static str,
189            success: bool,
190            count: usize,
191            ports: Vec<PortWithProcess<'a>>,
192        }
193
194        printer.print_json(&Output {
195            action: "ports",
196            success: true,
197            count: ports.len(),
198            ports: enriched,
199        });
200    }
201}
202
203fn truncate_string(s: &str, max_len: usize) -> String {
204    if s.len() <= max_len {
205        s.to_string()
206    } else {
207        format!("{}...", &s[..max_len.saturating_sub(3)])
208    }
209}