Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 168 additions & 15 deletions internal/scanner/scanner.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package scanner

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"strings"
"time"

"github.com/shirou/gopsutil/v3/net"
Expand All @@ -12,15 +16,27 @@ import (

// PortInfo represents information about a listening port
type PortInfo struct {
Port int
PID int32
Process string
Status string
HTTPStatus int // HTTP response status code (0 if not checked)
Latency time.Duration // Response latency
CPUPercent float64 // CPU usage percentage
MemoryMB float64 // Memory usage in MB
Selected bool // For multi-select mode
Port int
PID int32
Process string
Status string
HTTPStatus int // HTTP response status code (0 if not checked)
Latency time.Duration // Response latency
CPUPercent float64 // CPU usage percentage
MemoryMB float64 // Memory usage in MB
Selected bool // For multi-select mode
ContainerID string // Docker container ID (short form)
ContainerName string // Docker container name
IsContainer bool // Whether this process is in a container
}

// DockerContainer represents a Docker container
type DockerContainer struct {
ID string `json:"ID"`
Name string `json:"Names"`
Image string `json:"Image"`
Ports string `json:"Ports"`
Status string `json:"Status"`
}

// ScanPorts scans for all active network connections
Expand All @@ -44,6 +60,9 @@ func ScanPorts() ([]PortInfo, error) {

pName := "Unknown"
var cpuPercent, memoryMB float64
var containerID, containerName string
var isContainer bool

if conn.Pid != 0 {
p, err := process.NewProcess(conn.Pid)
if err == nil {
Expand All @@ -54,16 +73,22 @@ func ScanPorts() ([]PortInfo, error) {
if err == nil {
memoryMB = float64(memInfo.RSS) / 1024 / 1024
}

// Check if process is in a Docker container
containerID, containerName, isContainer = getContainerInfo(conn.Pid)
}
}

portInfo := PortInfo{
Port: port,
PID: conn.Pid,
Process: pName,
Status: conn.Status,
CPUPercent: cpuPercent,
MemoryMB: memoryMB,
Port: port,
PID: conn.Pid,
Process: pName,
Status: conn.Status,
CPUPercent: cpuPercent,
MemoryMB: memoryMB,
ContainerID: containerID,
ContainerName: containerName,
IsContainer: isContainer,
}

// Check HTTP health for common web ports
Expand Down Expand Up @@ -177,3 +202,131 @@ func KillMultipleProcesses(pids []int32) error {
}
return nil
}

// getContainerInfo checks if a PID is running in a Docker container
// Returns containerID (short), containerName, and isContainer bool
func getContainerInfo(pid int32) (string, string, bool) {
// Check if Docker is available
if !isDockerAvailable() {
return "", "", false
}

// Read cgroup file to detect container ID
containerID := getContainerIDFromCgroup(pid)
if containerID == "" {
return "", "", false
}

// Get container name from Docker
containerName := getContainerNameByID(containerID)

// Return short container ID (first 12 chars)
shortID := containerID
if len(containerID) > 12 {
shortID = containerID[:12]
}

return shortID, containerName, true
}

// isDockerAvailable checks if Docker CLI is available
func isDockerAvailable() bool {
cmd := exec.Command("docker", "version")
err := cmd.Run()
return err == nil
}

// getContainerIDFromCgroup reads the cgroup file to extract container ID
func getContainerIDFromCgroup(pid int32) string {
cgroupPath := fmt.Sprintf("/proc/%d/cgroup", pid)
data, err := os.ReadFile(cgroupPath)
if err != nil {
return ""
}

// Look for docker container ID in cgroup
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.Contains(line, "docker") {
// Extract container ID from paths like:
// 0::/docker/1234567890abcdef...
// 0::/system.slice/docker-1234567890abcdef.scope
parts := strings.Split(line, "/")
for _, part := range parts {
if strings.HasPrefix(part, "docker-") {
// Remove "docker-" prefix and ".scope" suffix
id := strings.TrimPrefix(part, "docker-")
id = strings.TrimSuffix(id, ".scope")
return id
}
// Check if part is a long hex string (container ID)
if len(part) == 64 {
return part
}
}
}
}

return ""
}

// getContainerNameByID gets the container name using Docker CLI
func getContainerNameByID(containerID string) string {
cmd := exec.Command("docker", "inspect", "--format={{.Name}}", containerID)
output, err := cmd.Output()
if err != nil {
return ""
}

name := strings.TrimSpace(string(output))
// Remove leading slash from container name
name = strings.TrimPrefix(name, "/")
return name
}

// ListDockerContainers returns all running Docker containers
func ListDockerContainers() ([]DockerContainer, error) {
if !isDockerAvailable() {
return nil, fmt.Errorf("docker is not available")
}

cmd := exec.Command("docker", "ps", "--format", "{{json .}}")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}

var containers []DockerContainer
lines := bytes.Split(output, []byte("\n"))
for _, line := range lines {
if len(line) == 0 {
continue
}
var container DockerContainer
if err := json.Unmarshal(line, &container); err == nil {
containers = append(containers, container)
}
}

return containers, nil
}

// StopContainer stops a Docker container by ID or name
func StopContainer(containerID string) error {
if !isDockerAvailable() {
return fmt.Errorf("docker is not available")
}

cmd := exec.Command("docker", "stop", containerID)
return cmd.Run()
}

// RestartContainer restarts a Docker container by ID or name
func RestartContainer(containerID string) error {
if !isDockerAvailable() {
return fmt.Errorf("docker is not available")
}

cmd := exec.Command("docker", "restart", containerID)
return cmd.Run()
}
Loading