Skip to main content

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