Macbook: Return a list of processes using a specific remote port number

I find this script useful for debugging which processes are talking to which remote port.

#!/bin/bash

# Network Connection Monitor with Color Coding
# Shows TCP/UDP connections with state and process info
# Refreshes every 5 seconds
# Usage: ./netmon.sh [--port PORT] [--ip IP_ADDRESS]

# Parse command line arguments
FILTER_PORT=""
FILTER_IP=""

while [[ $# -gt 0 ]]; do
    case $1 in
        --port|-p)
            FILTER_PORT="$2"
            shift 2
            ;;
        --ip|-i)
            FILTER_IP="$2"
            shift 2
            ;;
        --help|-h)
            echo "Usage: $0 [OPTIONS]"
            echo "Options:"
            echo "  --port, -p PORT    Filter by remote port"
            echo "  --ip, -i IP        Filter by remote IP address"
            echo "  --help, -h         Show this help message"
            echo ""
            echo "Examples:"
            echo "  $0 --port 443      Show only connections to port 443"
            echo "  $0 --ip 1.1.1.1    Show only connections to IP 1.1.1.1"
            echo "  $0 -p 80 -i 192.168.1.1  Show connections to 192.168.1.1:80"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            echo "Use --help for usage information"
            exit 1
            ;;
    esac
done

# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
GRAY='\033[0;90m'
NC='\033[0m' # No Color
BOLD='\033[1m'

# Function to get process name from PID
get_process_name() {
    local pid=$1
    if [ "$pid" != "-" ] && [ "$pid" != "0" ] && [ -n "$pid" ]; then
        ps -p "$pid" -o comm= 2>/dev/null || echo "unknown"
    else
        echo "-"
    fi
}

# Function to color-code based on state
get_state_color() {
    local state=$1
    case "$state" in
        "ESTABLISHED")
            echo "${GREEN}"
            ;;
        "LISTEN")
            echo "${BLUE}"
            ;;
        "TIME_WAIT")
            echo "${YELLOW}"
            ;;
        "CLOSE_WAIT")
            echo "${MAGENTA}"
            ;;
        "SYN_SENT"|"SYN_RCVD")
            echo "${CYAN}"
            ;;
        "FIN_WAIT"*)
            echo "${GRAY}"
            ;;
        "CLOSING"|"LAST_ACK")
            echo "${RED}"
            ;;
        *)
            echo "${WHITE}"
            ;;
    esac
}

# Function to split address into IP and port
split_address() {
    local addr=$1
    local ip=""
    local port=""
    
    if [[ "$addr" == "*"* ]]; then
        ip="*"
        port="*"
    elif [[ "$addr" =~ ^(.+)\.([0-9]+)$ ]]; then
        ip="${BASH_REMATCH[1]}"
        port="${BASH_REMATCH[2]}"
    elif [[ "$addr" =~ ^(.+):([0-9]+)$ ]]; then
        # Handle IPv6 format
        ip="${BASH_REMATCH[1]}"
        port="${BASH_REMATCH[2]}"
    else
        ip="$addr"
        port="-"
    fi
    
    echo "$ip|$port"
}

# Function to check if connection matches filters
matches_filter() {
    local remote_ip=$1
    local remote_port=$2
    
    # Check port filter
    if [ -n "$FILTER_PORT" ] && [ "$remote_port" != "$FILTER_PORT" ]; then
        return 1
    fi
    
    # Check IP filter
    if [ -n "$FILTER_IP" ]; then
        # Handle partial IP matching
        if [[ "$remote_ip" != *"$FILTER_IP"* ]]; then
            return 1
        fi
    fi
    
    return 0
}

# Function to display connections
show_connections() {
    clear
    
    # Header
    echo -e "${BOLD}${WHITE}=== Network Connections Monitor ===${NC}"
    echo -e "${BOLD}${WHITE}$(date '+%Y-%m-%d %H:%M:%S')${NC}"
    
    # Show active filters
    if [ -n "$FILTER_PORT" ] || [ -n "$FILTER_IP" ]; then
        echo -e "${YELLOW}Active Filters:${NC}"
        [ -n "$FILTER_PORT" ] && echo -e "  Remote Port: ${BOLD}$FILTER_PORT${NC}"
        [ -n "$FILTER_IP" ] && echo -e "  Remote IP: ${BOLD}$FILTER_IP${NC}"
    fi
    echo ""
    
    # Legend
    echo -e "${BOLD}Color Legend:${NC}"
    echo -e "  ${GREEN}●${NC} ESTABLISHED    ${BLUE}●${NC} LISTEN         ${YELLOW}●${NC} TIME_WAIT"
    echo -e "  ${CYAN}●${NC} SYN_SENT/RCVD  ${MAGENTA}●${NC} CLOSE_WAIT     ${RED}●${NC} CLOSING/LAST_ACK"
    echo -e "  ${GRAY}●${NC} FIN_WAIT       ${WHITE}●${NC} OTHER/UDP"
    echo ""
    
    # Table header
    printf "${BOLD}%-6s %-22s %-22s %-7s %-12s %-8s %-30s${NC}\n" \
        "PROTO" "LOCAL ADDRESS" "REMOTE IP" "R.PORT" "STATE" "PID" "PROCESS"
    echo "$(printf '%.0s-' {1..120})"
    
    # Temporary file for storing connections
    TMPFILE=$(mktemp)
    
    # Get TCP connections with netstat
    # Note: On macOS, we need sudo to see process info for all connections
    if command -v sudo >/dev/null 2>&1; then
        # Try with sudo first (will show all processes)
        sudo netstat -anp tcp 2>/dev/null | grep -E '^tcp' > "$TMPFILE" 2>/dev/null || \
        netstat -an -p tcp 2>/dev/null | grep -E '^tcp' > "$TMPFILE"
    else
        netstat -an -p tcp 2>/dev/null | grep -E '^tcp' > "$TMPFILE"
    fi
    
    # Process TCP connections
    while IFS= read -r line; do
        # Parse netstat output (macOS format)
        proto=$(echo "$line" | awk '{print $1}')
        local_addr=$(echo "$line" | awk '{print $4}')
        remote_addr=$(echo "$line" | awk '{print $5}')
        state=$(echo "$line" | awk '{print $6}')
        
        # Split remote address into IP and port
        IFS='|' read -r remote_ip remote_port <<< "$(split_address "$remote_addr")"
        
        # Apply filters
        if ! matches_filter "$remote_ip" "$remote_port"; then
            continue
        fi
        
        # Try to get PID using lsof for the local address
        if [[ "$local_addr" =~ ([0-9.]+|[*])\.([0-9]+)$ ]] || [[ "$local_addr" =~ ([0-9a-f:]+)\.([0-9]+)$ ]]; then
            port="${BASH_REMATCH[2]}"
            # Use lsof to find the PID
            pid=$(sudo lsof -i TCP:$port -sTCP:$state 2>/dev/null | grep -v PID | head -1 | awk '{print $2}')
            if [ -z "$pid" ]; then
                pid="-"
                process="-"
            else
                process=$(get_process_name "$pid")
            fi
        else
            pid="-"
            process="-"
        fi
        
        # Get color based on state
        color=$(get_state_color "$state")
        
        # Format and print
        printf "${color}%-6s %-22s %-22s %-7s %-12s %-8s %-30s${NC}\n" \
            "$proto" \
            "${local_addr:0:22}" \
            "${remote_ip:0:22}" \
            "${remote_port:0:7}" \
            "$state" \
            "$pid" \
            "${process:0:30}"
    done < "$TMPFILE"
    
    # Get UDP connections
    echo ""
    if command -v sudo >/dev/null 2>&1; then
        sudo netstat -anp udp 2>/dev/null | grep -E '^udp' > "$TMPFILE" 2>/dev/null || \
        netstat -an -p udp 2>/dev/null | grep -E '^udp' > "$TMPFILE"
    else
        netstat -an -p udp 2>/dev/null | grep -E '^udp' > "$TMPFILE"
    fi
    
    # Process UDP connections
    while IFS= read -r line; do
        # Parse netstat output for UDP
        proto=$(echo "$line" | awk '{print $1}')
        local_addr=$(echo "$line" | awk '{print $4}')
        remote_addr=$(echo "$line" | awk '{print $5}')
        
        # Split remote address into IP and port
        IFS='|' read -r remote_ip remote_port <<< "$(split_address "$remote_addr")"
        
        # Apply filters
        if ! matches_filter "$remote_ip" "$remote_port"; then
            continue
        fi
        
        # UDP doesn't have state
        state="*"
        
        # Try to get PID using lsof for the local address
        if [[ "$local_addr" =~ ([0-9.]+|[*])\.([0-9]+)$ ]] || [[ "$local_addr" =~ ([0-9a-f:]+)\.([0-9]+)$ ]]; then
            port="${BASH_REMATCH[2]}"
            # Use lsof to find the PID
            pid=$(sudo lsof -i UDP:$port 2>/dev/null | grep -v PID | head -1 | awk '{print $2}')
            if [ -z "$pid" ]; then
                pid="-"
                process="-"
            else
                process=$(get_process_name "$pid")
            fi
        else
            pid="-"
            process="-"
        fi
        
        # White color for UDP
        printf "${WHITE}%-6s %-22s %-22s %-7s %-12s %-8s %-30s${NC}\n" \
            "$proto" \
            "${local_addr:0:22}" \
            "${remote_ip:0:22}" \
            "${remote_port:0:7}" \
            "$state" \
            "$pid" \
            "${process:0:30}"
    done < "$TMPFILE"
    
    # Clean up
    rm -f "$TMPFILE"
    
    # Footer
    echo ""
    echo "$(printf '%.0s-' {1..120})"
    echo -e "${BOLD}Press Ctrl+C to exit${NC} | Refreshing every 5 seconds..."
    
    # Show filter hint if no filters active
    if [ -z "$FILTER_PORT" ] && [ -z "$FILTER_IP" ]; then
        echo -e "${GRAY}Tip: Use --port PORT or --ip IP to filter connections${NC}"
    fi
}

# Trap Ctrl+C to exit cleanly
trap 'echo -e "\n${BOLD}Exiting...${NC}"; exit 0' INT

# Main loop
echo -e "${BOLD}${CYAN}Starting Network Connection Monitor...${NC}"
echo -e "${YELLOW}Note: Run with sudo for complete process information${NC}"

# Show active filters on startup
if [ -n "$FILTER_PORT" ] || [ -n "$FILTER_IP" ]; then
    echo -e "${GREEN}Filtering enabled:${NC}"
    [ -n "$FILTER_PORT" ] && echo -e "  Remote Port: ${BOLD}$FILTER_PORT${NC}"
    [ -n "$FILTER_IP" ] && echo -e "  Remote IP: ${BOLD}$FILTER_IP${NC}"
fi

sleep 2

while true; do
    show_connections
    sleep 5
done

Macbook: Useful/Basic NMAP script to check for vulnerabilities and create a formatted report

If you want to quickly health check your website, then the following script is a simple NMAP script that scans your site for common issues and formats the results in a nice report style.

#!/bin/bash

# Nmap Vulnerability Scanner with Severity Grouping, TLS checks, and Directory Discovery
# Usage: ./vunscan.sh <target_domain>

# Colors for output
RED='\033[0;31m'
ORANGE='\033[0;33m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color

# Check if target is provided
if [ $# -eq 0 ]; then
    echo "Usage: $0 <target_domain>"
    echo "Example: $0 example.com"
    exit 1
fi

TARGET=$1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="vuln_scan_${TARGET}_${TIMESTAMP}"
RAW_OUTPUT="${OUTPUT_DIR}/raw_scan.xml"
OPEN_PORTS=""

# Debug output
echo "DEBUG: TARGET=$TARGET"
echo "DEBUG: TIMESTAMP=$TIMESTAMP"
echo "DEBUG: OUTPUT_DIR=$OUTPUT_DIR"
echo "DEBUG: RAW_OUTPUT=$RAW_OUTPUT"

# Create output directory
mkdir -p "$OUTPUT_DIR"
if [ ! -d "$OUTPUT_DIR" ]; then
    echo -e "${RED}Error: Failed to create output directory $OUTPUT_DIR${NC}"
    exit 1
fi

echo "================================================================"
echo "         Vulnerability Scanner for $TARGET"
echo "================================================================"
echo "Scan started at: $(date)"
echo "Results will be saved in: $OUTPUT_DIR"
echo ""

# Function to print section headers
print_header() {
    echo -e "\n${BLUE}================================================================${NC}"
    echo -e "${BLUE}$1${NC}"
    echo -e "${BLUE}================================================================${NC}"
}

# Function to run nmap scan
run_scan() {
    print_header "Running Comprehensive Vulnerability Scan"
    echo "This may take several minutes…"
    
    # First, determine which ports are open
    echo "Phase 1: Port discovery..."
    echo "Scanning for open ports (this may take a while)..."
    
    # Try a faster scan first on common ports
    nmap -p 1-1000,8080,8443,3306,5432,27017 --open -T4 "$TARGET" -oG "${OUTPUT_DIR}/open_ports_quick.txt" 2>/dev/null
    
    # If user wants full scan, uncomment the next line and comment the previous one
    # nmap -p- --open -T4 "$TARGET" -oG "${OUTPUT_DIR}/open_ports.txt" 2>/dev/null
    
    # Extract open ports
    if [ -f "${OUTPUT_DIR}/open_ports_quick.txt" ]; then
        OPEN_PORTS=$(grep -oE '[0-9]+/open' "${OUTPUT_DIR}/open_ports_quick.txt" 2>/dev/null | cut -d'/' -f1 | tr '\n' ',' | sed 's/,$//')
    fi
    
    # If no ports found, try common web ports
    if [ -z "$OPEN_PORTS" ] || [ "$OPEN_PORTS" = "" ]; then
        echo -e "${YELLOW}Warning: No open ports found in quick scan. Checking common web ports...${NC}"
        
        # Test common ports individually
        COMMON_PORTS="80,443,8080,8443,22,21,25,3306,5432"
        OPEN_PORTS=""
        
        for port in $(echo $COMMON_PORTS | tr ',' ' '); do
            echo -n "Testing port $port... "
            if nmap -p $port --open "$TARGET" 2>/dev/null | grep -q "open"; then
                echo "open"
                if [ -z "$OPEN_PORTS" ]; then
                    OPEN_PORTS="$port"
                else
                    OPEN_PORTS="$OPEN_PORTS,$port"
                fi
            else
                echo "closed/filtered"
            fi
        done
    fi
    
    # Final fallback
    if [ -z "$OPEN_PORTS" ] || [ "$OPEN_PORTS" = "" ]; then
        echo -e "${YELLOW}Warning: No open ports detected. Using default web ports for scanning.${NC}"
        OPEN_PORTS="80,443"
    fi
    
    echo ""
    echo "Ports to scan: $OPEN_PORTS"
    echo ""
    
    # Main vulnerability scan with http-vulners-regex
    echo "Phase 2: Vulnerability scanning..."
    nmap -sV -sC --script vuln,http-vulners-regex \
         --script-args vulns.showall,http-vulners-regex.paths={/} \
         -p "$OPEN_PORTS" \
         -oX "$RAW_OUTPUT" \
         -oN "${OUTPUT_DIR}/scan_normal.txt" \
         "$TARGET"
    
    if [ $? -ne 0 ]; then
        echo -e "${RED}Error: Nmap scan failed${NC}"
        # Don't exit, continue with other scans
    fi
}

# Function to parse and categorize vulnerabilities
parse_vulnerabilities() {
    print_header "Parsing and Categorizing Vulnerabilities"
    
    # Initialize arrays
    declare -a critical_vulns=()
    declare -a high_vulns=()
    declare -a medium_vulns=()
    declare -a low_vulns=()
    declare -a info_vulns=()
    
    # Create temporary files for each severity
    CRITICAL_FILE="${OUTPUT_DIR}/critical.tmp"
    HIGH_FILE="${OUTPUT_DIR}/high.tmp"
    MEDIUM_FILE="${OUTPUT_DIR}/medium.tmp"
    LOW_FILE="${OUTPUT_DIR}/low.tmp"
    INFO_FILE="${OUTPUT_DIR}/info.tmp"
    
    # Clear temp files
    > "$CRITICAL_FILE"
    > "$HIGH_FILE"
    > "$MEDIUM_FILE"
    > "$LOW_FILE"
    > "$INFO_FILE"
    
    # Parse XML output for vulnerabilities
    if [ -f "$RAW_OUTPUT" ]; then
        # Extract script output and categorize by common vulnerability indicators
        grep -A 20 '<script id=".*vuln.*"' "$RAW_OUTPUT" | while read line; do
            if echo "$line" | grep -qi "CRITICAL\|CVE.*CRITICAL\|score.*9\|score.*10"; then
                echo "$line" >> "$CRITICAL_FILE"
            elif echo "$line" | grep -qi "HIGH\|CVE.*HIGH\|score.*[7-8]"; then
                echo "$line" >> "$HIGH_FILE"
            elif echo "$line" | grep -qi "MEDIUM\|CVE.*MEDIUM\|score.*[4-6]"; then
                echo "$line" >> "$MEDIUM_FILE"
            elif echo "$line" | grep -qi "LOW\|CVE.*LOW\|score.*[1-3]"; then
                echo "$line" >> "$LOW_FILE"
            elif echo "$line" | grep -qi "INFO\|INFORMATION"; then
                echo "$line" >> "$INFO_FILE"
            fi
        done
        
        # Also parse normal output for vulnerability information
        if [ -f "${OUTPUT_DIR}/scan_normal.txt" ]; then
            # Look for common vulnerability patterns in normal output
            grep -E "(CVE-|VULNERABLE|State: VULNERABLE)" "${OUTPUT_DIR}/scan_normal.txt" | while read vuln_line; do
                if echo "$vuln_line" | grep -qi "critical\|9\.[0-9]\|10\.0"; then
                    echo "$vuln_line" >> "$CRITICAL_FILE"
                elif echo "$vuln_line" | grep -qi "high\|[7-8]\.[0-9]"; then
                    echo "$vuln_line" >> "$HIGH_FILE"
                elif echo "$vuln_line" | grep -qi "medium\|[4-6]\.[0-9]"; then
                    echo "$vuln_line" >> "$MEDIUM_FILE"
                elif echo "$vuln_line" | grep -qi "low\|[1-3]\.[0-9]"; then
                    echo "$vuln_line" >> "$LOW_FILE"
                else
                    echo "$vuln_line" >> "$INFO_FILE"
                fi
            done
        fi
    fi
}

# Function to display vulnerabilities by severity
display_results() {
    print_header "VULNERABILITY SCAN RESULTS"
    
    # Critical Vulnerabilities
    echo -e "\n${RED}🔴 CRITICAL SEVERITY VULNERABILITIES${NC}"
    echo "=================================================="
    if [ -s "${OUTPUT_DIR}/critical.tmp" ]; then
        cat "${OUTPUT_DIR}/critical.tmp" | head -20
        CRITICAL_COUNT=$(wc -l < "${OUTPUT_DIR}/critical.tmp")
        echo -e "${RED}Total Critical: $CRITICAL_COUNT${NC}"
    else
        echo -e "${GREEN}✓ No critical vulnerabilities found${NC}"
    fi
    
    # High Vulnerabilities
    echo -e "\n${ORANGE}🟠 HIGH SEVERITY VULNERABILITIES${NC}"
    echo "============================================="
    if [ -s "${OUTPUT_DIR}/high.tmp" ]; then
        cat "${OUTPUT_DIR}/high.tmp" | head -15
        HIGH_COUNT=$(wc -l < "${OUTPUT_DIR}/high.tmp")
        echo -e "${ORANGE}Total High: $HIGH_COUNT${NC}"
    else
        echo -e "${GREEN}✓ No high severity vulnerabilities found${NC}"
    fi
    
    # Medium Vulnerabilities
    echo -e "\n${YELLOW}🟡 MEDIUM SEVERITY VULNERABILITIES${NC}"
    echo "==============================================="
    if [ -s "${OUTPUT_DIR}/medium.tmp" ]; then
        cat "${OUTPUT_DIR}/medium.tmp" | head -10
        MEDIUM_COUNT=$(wc -l < "${OUTPUT_DIR}/medium.tmp")
        echo -e "${YELLOW}Total Medium: $MEDIUM_COUNT${NC}"
    else
        echo -e "${GREEN}✓ No medium severity vulnerabilities found${NC}"
    fi
    
    # Low Vulnerabilities
    echo -e "\n${BLUE}🔵 LOW SEVERITY VULNERABILITIES${NC}"
    echo "=========================================="
    if [ -s "${OUTPUT_DIR}/low.tmp" ]; then
        cat "${OUTPUT_DIR}/low.tmp" | head -8
        LOW_COUNT=$(wc -l < "${OUTPUT_DIR}/low.tmp")
        echo -e "${BLUE}Total Low: $LOW_COUNT${NC}"
    else
        echo -e "${GREEN}✓ No low severity vulnerabilities found${NC}"
    fi
    
    # Information/Other
    echo -e "\n${GREEN}ℹ️  INFORMATIONAL${NC}"
    echo "========================="
    if [ -s "${OUTPUT_DIR}/info.tmp" ]; then
        cat "${OUTPUT_DIR}/info.tmp" | head -5
        INFO_COUNT=$(wc -l < "${OUTPUT_DIR}/info.tmp")
        echo -e "${GREEN}Total Info: $INFO_COUNT${NC}"
    else
        echo "No informational items found"
    fi
}

# Function to run gobuster scan for enhanced directory discovery
run_gobuster_scan() {
    echo "Running gobuster directory scan..."
    
    GOBUSTER_RESULTS="${OUTPUT_DIR}/gobuster_results.txt"
    PERMISSION_ANALYSIS="${OUTPUT_DIR}/gobuster_permissions.txt"
    > "$PERMISSION_ANALYSIS"
    
    for port in $(echo "$WEB_PORTS" | tr ',' ' '); do
        PROTOCOL="http"
        if [[ "$port" == "443" || "$port" == "8443" ]]; then
            PROTOCOL="https"
        fi
        
        echo "Scanning $PROTOCOL://$TARGET:$port with gobuster..."
        
        # Run gobuster with common wordlist
        if [ -f "/usr/share/wordlists/dirb/common.txt" ]; then
            WORDLIST="/usr/share/wordlists/dirb/common.txt"
        elif [ -f "/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt" ]; then
            WORDLIST="/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt"
        else
            # Create a small built-in wordlist
            WORDLIST="${OUTPUT_DIR}/temp_wordlist.txt"
            cat > "$WORDLIST" <<EOF
admin
administrator
api
backup
bin
cgi-bin
config
data
database
db
debug
dev
development
doc
docs
documentation
download
downloads
error
errors
export
files
hidden
images
img
include
includes
js
library
log
logs
manage
management
manager
media
old
private
proc
public
resources
scripts
secret
secure
server-status
staging
static
storage
system
temp
templates
test
testing
tmp
upload
uploads
users
var
vendor
web
webapp
wp-admin
wp-content
.git
.svn
.env
.htaccess
.htpasswd
robots.txt
sitemap.xml
web.config
phpinfo.php
info.php
test.php
EOF
        fi
        
        # Run gobuster with status code analysis
        gobuster dir -u "$PROTOCOL://$TARGET:$port" \
                    -w "$WORDLIST" \
                    -k \
                    -t 10 \
                    --no-error \
                    -o "${GOBUSTER_RESULTS}_${port}.txt" \
                    -s "200,204,301,302,307,401,403,405" 2>/dev/null
        
        # Analyze results for permission issues
        if [ -f "${GOBUSTER_RESULTS}_${port}.txt" ]; then
            echo "Analyzing gobuster results for permission issues..."
            
            # Check for 403 Forbidden directories
            grep "Status: 403" "${GOBUSTER_RESULTS}_${port}.txt" | while read line; do
                dir=$(echo "$line" | awk '{print $1}')
                echo -e "${ORANGE}[403 Forbidden]${NC} $PROTOCOL://$TARGET:$port$dir - Directory exists but access denied" >> "$PERMISSION_ANALYSIS"
                echo -e "${ORANGE}  Permission Issue:${NC} $PROTOCOL://$TARGET:$port$dir (403 Forbidden)"
            done
            
            # Check for 401 Unauthorized directories
            grep "Status: 401" "${GOBUSTER_RESULTS}_${port}.txt" | while read line; do
                dir=$(echo "$line" | awk '{print $1}')
                echo -e "${YELLOW}[401 Unauthorized]${NC} $PROTOCOL://$TARGET:$port$dir - Authentication required" >> "$PERMISSION_ANALYSIS"
                echo -e "${YELLOW}  Auth Required:${NC} $PROTOCOL://$TARGET:$port$dir (401 Unauthorized)"
            done
            
            # Check for directory listing enabled (potentially dangerous)
            grep "Status: 200" "${GOBUSTER_RESULTS}_${port}.txt" | while read line; do
                dir=$(echo "$line" | awk '{print $1}')
                # Check if it's a directory by looking for trailing slash or common directory patterns
                if [[ "$dir" =~ /$ ]] || [[ ! "$dir" =~ \. ]]; then
                    # Test if directory listing is enabled
                    RESPONSE=$(curl -k -s --max-time 5 "$PROTOCOL://$TARGET:$port$dir" 2>/dev/null)
                    if echo "$RESPONSE" | grep -qi "index of\|directory listing\|parent directory\|<pre>\|<dir>"; then
                        echo -e "${RED}[Directory Listing Enabled]${NC} $PROTOCOL://$TARGET:$port$dir - SECURITY RISK" >> "$PERMISSION_ANALYSIS"
                        echo -e "${RED}  🚨 Directory Listing:${NC} $PROTOCOL://$TARGET:$port$dir"
                    fi
                fi
            done
            
            # Check for sensitive files with incorrect permissions
            for sensitive_file in ".git/config" ".env" ".htpasswd" "web.config" "phpinfo.php" "info.php" ".DS_Store" "Thumbs.db"; do
                if grep -q "/$sensitive_file.*Status: 200" "${GOBUSTER_RESULTS}_${port}.txt"; then
                    echo -e "${RED}[Sensitive File Exposed]${NC} $PROTOCOL://$TARGET:$port/$sensitive_file - CRITICAL SECURITY RISK" >> "$PERMISSION_ANALYSIS"
                    echo -e "${RED}  🚨 Sensitive File:${NC} $PROTOCOL://$TARGET:$port/$sensitive_file"
                fi
            done
        fi
    done
    
    # Clean up temporary wordlist if created
    [ -f "${OUTPUT_DIR}/temp_wordlist.txt" ] && rm -f "${OUTPUT_DIR}/temp_wordlist.txt"
    
    # Display permission analysis summary
    if [ -s "$PERMISSION_ANALYSIS" ]; then
        echo ""
        echo -e "${ORANGE}=== Directory Permission Issues Summary ===${NC}"
        cat "$PERMISSION_ANALYSIS"
        
        # Count different types of issues
        FORBIDDEN_COUNT=$(grep -c "403 Forbidden" "$PERMISSION_ANALYSIS" 2>/dev/null || echo 0)
        UNAUTH_COUNT=$(grep -c "401 Unauthorized" "$PERMISSION_ANALYSIS" 2>/dev/null || echo 0)
        LISTING_COUNT=$(grep -c "Directory Listing Enabled" "$PERMISSION_ANALYSIS" 2>/dev/null || echo 0)
        SENSITIVE_COUNT=$(grep -c "Sensitive File Exposed" "$PERMISSION_ANALYSIS" 2>/dev/null || echo 0)
        
        echo ""
        echo "Permission Issue Statistics:"
        echo "  - 403 Forbidden directories: $FORBIDDEN_COUNT"
        echo "  - 401 Unauthorized directories: $UNAUTH_COUNT"
        echo "  - Directory listings enabled: $LISTING_COUNT"
        echo "  - Sensitive files exposed: $SENSITIVE_COUNT"
    fi
}

# Function to run TLS/SSL checks
run_tls_checks() {
    print_header "Running TLS/SSL Security Checks"
    
    # Check for HTTPS ports
    HTTPS_PORTS=$(echo "$OPEN_PORTS" | tr ',' '\n' | grep -E '443|8443' | tr '\n' ',' | sed 's/,$//')
    if [ -z "$HTTPS_PORTS" ]; then
        HTTPS_PORTS="443"
        echo "No HTTPS ports found in scan, checking default port 443..."
    fi
    
    echo "Checking TLS/SSL on ports: $HTTPS_PORTS"
    
    # Run SSL scan using nmap ssl scripts
    nmap -sV --script ssl-cert,ssl-enum-ciphers,ssl-known-key,ssl-ccs-injection,ssl-heartbleed,ssl-poodle,sslv2,tls-alpn,tls-nextprotoneg \
         -p "$HTTPS_PORTS" \
         -oN "${OUTPUT_DIR}/tls_scan.txt" \
         "$TARGET" 2>/dev/null
    
    # Parse TLS results
    TLS_ISSUES_FILE="${OUTPUT_DIR}/tls_issues.txt"
    > "$TLS_ISSUES_FILE"
    
    # Check for weak ciphers
    if grep -q "TLSv1.0\|SSLv2\|SSLv3" "${OUTPUT_DIR}/tls_scan.txt" 2>/dev/null; then
        echo "CRITICAL: Outdated SSL/TLS protocols detected" >> "$TLS_ISSUES_FILE"
    fi
    
    # Check for weak cipher suites
    if grep -q "DES\|RC4\|MD5" "${OUTPUT_DIR}/tls_scan.txt" 2>/dev/null; then
        echo "HIGH: Weak cipher suites detected" >> "$TLS_ISSUES_FILE"
    fi
    
    # Check for certificate issues
    if grep -q "expired\|self-signed" "${OUTPUT_DIR}/tls_scan.txt" 2>/dev/null; then
        echo "MEDIUM: Certificate issues detected" >> "$TLS_ISSUES_FILE"
    fi
    
    # Display TLS results
    echo ""
    if [ -s "$TLS_ISSUES_FILE" ]; then
        echo -e "${RED}TLS/SSL Issues Found:${NC}"
        cat "$TLS_ISSUES_FILE"
    else
        echo -e "${GREEN}✓ No major TLS/SSL issues detected${NC}"
    fi
    echo ""
}

# Function to run directory busting and permission checks
run_dirbuster() {
    print_header "Running Directory Discovery and Permission Checks"
    
    # Check for web ports
    WEB_PORTS=$(echo "$OPEN_PORTS" | tr ',' '\n' | grep -E '^(80|443|8080|8443)$' | tr '\n' ',' | sed 's/,$//')
    if [ -z "$WEB_PORTS" ]; then
        echo "No standard web ports found in open ports, checking defaults..."
        WEB_PORTS="80,443"
    fi
    
    echo "Running directory discovery on web ports: $WEB_PORTS"
    
    # Check if gobuster is available
    if command -v gobuster &> /dev/null; then
        echo -e "${GREEN}Using gobuster for enhanced directory discovery and permission checks${NC}"
        run_gobuster_scan
    else
        echo -e "${YELLOW}Gobuster not found. Using fallback method.${NC}"
        echo -e "${YELLOW}Install gobuster for enhanced directory permission checks: brew install gobuster${NC}"
    fi
    
    # Use nmap's http-enum script for directory discovery
    nmap -sV --script http-enum \
         --script-args http-enum.basepath='/' \
         -p "$WEB_PORTS" \
         -oN "${OUTPUT_DIR}/dirbuster.txt" \
         "$TARGET" 2>/dev/null
    
    # Common directory wordlist (built-in small list)
    COMMON_DIRS="admin administrator backup api config test dev staging uploads download downloads files documents images img css js scripts cgi-bin wp-admin phpmyadmin .git .svn .env .htaccess robots.txt sitemap.xml"
    
    # Quick check for common directories using curl
    DIRS_FOUND_FILE="${OUTPUT_DIR}/directories_found.txt"
    > "$DIRS_FOUND_FILE"
    
    for port in $(echo "$WEB_PORTS" | tr ',' ' '); do
        PROTOCOL="http"
        if [[ "$port" == "443" || "$port" == "8443" ]]; then
            PROTOCOL="https"
        fi
        
        echo "Checking common directories on $PROTOCOL://$TARGET:$port"
        
        for dir in $COMMON_DIRS; do
            URL="$PROTOCOL://$TARGET:$port/$dir"
            STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 3 "$URL" 2>/dev/null)
            
            if [[ "$STATUS" == "200" || "$STATUS" == "301" || "$STATUS" == "302" || "$STATUS" == "401" || "$STATUS" == "403" ]]; then
                echo "[$STATUS] $URL" >> "$DIRS_FOUND_FILE"
                echo -e "${GREEN}Found:${NC} [$STATUS] $URL"
                
                # Check for permission issues
                if [[ "$STATUS" == "403" ]]; then
                    echo -e "${ORANGE}  ⚠️  Permission denied (403) - Possible misconfiguration${NC}"
                    echo "[PERMISSION ISSUE] 403 Forbidden: $URL" >> "${OUTPUT_DIR}/permission_issues.txt"
                elif [[ "$STATUS" == "401" ]]; then
                    echo -e "${YELLOW}  🔒 Authentication required (401)${NC}"
                    echo "[AUTH REQUIRED] 401 Unauthorized: $URL" >> "${OUTPUT_DIR}/permission_issues.txt"
                fi
            fi
        done
    done
    
    # Display results
    echo ""
    if [ -s "$DIRS_FOUND_FILE" ]; then
        echo -e "${YELLOW}Directories/Files discovered:${NC}"
        cat "$DIRS_FOUND_FILE"
    else
        echo "No additional directories found"
    fi
    
    # Display permission issues if found
    if [ -s "${OUTPUT_DIR}/permission_issues.txt" ]; then
        echo ""
        echo -e "${ORANGE}Directory Permission Issues Found:${NC}"
        cat "${OUTPUT_DIR}/permission_issues.txt"
    fi
    echo ""
}

# Function to generate summary report
generate_summary() {
    print_header "SCAN SUMMARY"
    
    CRITICAL_COUNT=0
    HIGH_COUNT=0
    MEDIUM_COUNT=0
    LOW_COUNT=0
    INFO_COUNT=0
    
    [ -f "${OUTPUT_DIR}/critical.tmp" ] && CRITICAL_COUNT=$(wc -l < "${OUTPUT_DIR}/critical.tmp")
    [ -f "${OUTPUT_DIR}/high.tmp" ] && HIGH_COUNT=$(wc -l < "${OUTPUT_DIR}/high.tmp")
    [ -f "${OUTPUT_DIR}/medium.tmp" ] && MEDIUM_COUNT=$(wc -l < "${OUTPUT_DIR}/medium.tmp")
    [ -f "${OUTPUT_DIR}/low.tmp" ] && LOW_COUNT=$(wc -l < "${OUTPUT_DIR}/low.tmp")
    [ -f "${OUTPUT_DIR}/info.tmp" ] && INFO_COUNT=$(wc -l < "${OUTPUT_DIR}/info.tmp")
    
    echo "Target: $TARGET"
    echo "Scan Date: $(date)"
    echo ""
    echo -e "${RED}Critical:       $CRITICAL_COUNT${NC}"
    echo -e "${ORANGE}High:           $HIGH_COUNT${NC}"
    echo -e "${YELLOW}Medium:         $MEDIUM_COUNT${NC}"
    echo -e "${BLUE}Low:            $LOW_COUNT${NC}"
    echo -e "${GREEN}Informational:  $INFO_COUNT${NC}"
    echo ""
    
    TOTAL=$((CRITICAL_COUNT + HIGH_COUNT + MEDIUM_COUNT + LOW_COUNT))
    echo "Total Vulnerabilities: $TOTAL"
    
    # Risk assessment
    if [ $CRITICAL_COUNT -gt 0 ]; then
        echo -e "${RED}🚨 RISK LEVEL: CRITICAL - Immediate action required!${NC}"
    elif [ $HIGH_COUNT -gt 0 ]; then
        echo -e "${ORANGE}⚠️  RISK LEVEL: HIGH - Action required soon${NC}"
    elif [ $MEDIUM_COUNT -gt 0 ]; then
        echo -e "${YELLOW}⚡ RISK LEVEL: MEDIUM - Should be addressed${NC}"
    elif [ $LOW_COUNT -gt 0 ]; then
        echo -e "${BLUE}📋 RISK LEVEL: LOW - Monitor and plan fixes${NC}"
    else
        echo -e "${GREEN}✅ RISK LEVEL: MINIMAL - Good security posture${NC}"
    fi
    
    # Save summary to file
    {
        echo "Vulnerability Scan Summary for $TARGET"
        echo "======================================"
        echo "Scan Date: $(date)"
        echo ""
        echo "Critical: $CRITICAL_COUNT"
        echo "High: $HIGH_COUNT"
        echo "Medium: $MEDIUM_COUNT"
        echo "Low: $LOW_COUNT"
        echo "Informational: $INFO_COUNT"
        echo "Total: $TOTAL"
        echo ""
        echo "Additional Checks:"
        [ -f "${OUTPUT_DIR}/tls_issues.txt" ] && [ -s "${OUTPUT_DIR}/tls_issues.txt" ] && echo "TLS/SSL Issues: $(wc -l < "${OUTPUT_DIR}/tls_issues.txt")"
        [ -f "${OUTPUT_DIR}/directories_found.txt" ] && [ -s "${OUTPUT_DIR}/directories_found.txt" ] && echo "Directories Found: $(wc -l < "${OUTPUT_DIR}/directories_found.txt")"
        [ -f "${OUTPUT_DIR}/gobuster_permissions.txt" ] && [ -s "${OUTPUT_DIR}/gobuster_permissions.txt" ] && echo "Directory Permission Issues: $(wc -l < "${OUTPUT_DIR}/gobuster_permissions.txt")"
    } > "${OUTPUT_DIR}/summary.txt"
}

# Main execution
main() {
    echo "Starting vulnerability scan for $TARGET…"
    
    # Check if required tools are installed
    if ! command -v nmap &> /dev/null; then
        echo -e "${RED}Error: nmap is not installed. Please install nmap first.${NC}"
        exit 1
    fi
    
    if ! command -v curl &> /dev/null; then
        echo -e "${RED}Error: curl is not installed. Please install curl first.${NC}"
        exit 1
    fi
    
    # Check for optional tools
    if command -v gobuster &> /dev/null; then
        echo -e "${GREEN}✓ Gobuster found - Enhanced directory scanning enabled${NC}"
    else
        echo -e "${YELLOW}ℹ️  Gobuster not found - Basic directory scanning will be used${NC}"
        echo -e "${YELLOW}   Install with: brew install gobuster (macOS) or apt install gobuster (Linux)${NC}"
    fi
    
    # Run the main vulnerability scan
    run_scan
    
    # Run TLS/SSL checks
    run_tls_checks
    
    # Run directory discovery
    run_dirbuster
    
    # Parse results
    parse_vulnerabilities
    
    # Display formatted results
    display_results
    
    # Generate summary
    generate_summary
    
    # Cleanup temporary files
    rm -f "${OUTPUT_DIR}"/*.tmp
    
    print_header "SCAN COMPLETE"
    echo "All results saved in: $OUTPUT_DIR"
    echo "Summary saved in: ${OUTPUT_DIR}/summary.txt"
    echo -e "${GREEN}Scan completed at: $(date)${NC}"
}

# Run main function
main

Here’s a comprehensive guide on how to fix each type of directory permission issue that the above script might find (for apache):

## 1. **403 Forbidden Errors**

### What it means:
The directory/file exists but the server is denying access to it.

### How to fix:
# For Apache (.htaccess)
# Add to .htaccess in the directory:
Order deny,allow
Deny from all

# Or remove the directory from web access entirely
# Move sensitive directories outside the web root
mv /var/www/html/backup /var/backups/

# For Nginx
# Add to nginx.conf:
location /admin {
    deny all;
    return 404;  # Return 404 instead of 403 to hide existence
}
## 2. **401 Unauthorized Errors**

### What it means:
Authentication is required but may not be properly configured.

### How to fix:
# For Apache - create .htpasswd file
htpasswd -c /etc/apache2/.htpasswd username

# Add to .htaccess:
AuthType Basic
AuthName "Restricted Access"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user

# For Nginx:
# Install apache2-utils for htpasswd
sudo apt-get install apache2-utils
htpasswd -c /etc/nginx/.htpasswd username

# Add to nginx.conf:
location /admin {
    auth_basic "Restricted Access";
    auth_basic_user_file /etc/nginx/.htpasswd;
}
## 3. **Directory Listing Enabled (CRITICAL)**

### What it means:
Anyone can see all files in the directory - major security risk!

### How to fix:
# For Apache
# Method 1: Add to .htaccess in the directory
Options -Indexes

# Method 2: Add to Apache config (httpd.conf or apache2.conf)
<Directory /var/www/html>
    Options -Indexes
</Directory>

# For Nginx
# Add to nginx.conf (Nginx doesn't have directory listing by default)
# If you see it enabled, remove:
autoindex off;  # This should be the default

# Create index files in empty directories
echo "<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>403 Forbidden</h1></body></html>" > index.html
## 4. **Sensitive Files Exposed (CRITICAL)**

### Common exposed files and fixes:

#### **.git directory**
# Remove .git from production
rm -rf /var/www/html/.git

# Or block access via .htaccess
<Files ~ "^\.git">
    Order allow,deny
    Deny from all
</Files>

# For Nginx:
location ~ /\.git {
    deny all;
    return 404;
}
#### **.env file**
# Move outside web root
mv /var/www/html/.env /var/www/

# Update your application to read from new location
# In PHP: require_once __DIR__ . '/../.env';

# Block via .htaccess
<Files .env>
    Order allow,deny
    Deny from all
</Files>
#### **Configuration files (config.php, settings.php)**
# Move sensitive configs outside web root
mv /var/www/html/config.php /var/www/config/

# Or restrict access via .htaccess
<Files "config.php">
    Order allow,deny
    Deny from all
</Files>
#### **Backup files**
# Remove backup files from web directory
find /var/www/html -name "*.bak" -o -name "*.backup" -o -name "*.old" | xargs rm -f

# Create a cron job to clean regularly
echo "0 2 * * * find /var/www/html -name '*.bak' -o -name '*.backup' -delete" | crontab -
## 5. **General Security Best Practices**

### Create a comprehensive .htaccess file:
# Disable directory browsing
Options -Indexes

# Deny access to hidden files and directories
<Files .*>
    Order allow,deny
    Deny from all
</Files>

# Deny access to backup and source files
<FilesMatch "(\.(bak|backup|config|dist|fla|inc|ini|log|psd|sh|sql|swp)|~)$">
    Order allow,deny
    Deny from all
</FilesMatch>

# Protect sensitive files
location ~ /(\.htaccess|\.htpasswd|\.env|composer\.json|composer\.lock|package\.json|package-lock\.json)$ {
    deny all;
    return 404;
}

## 6. Quick Security Audit Commands
## Run these commands to find and fix common issues:

# Find all .git directories in web root
find /var/www/html -type d -name .git

# Find all .env files
find /var/www/html -name .env

# Find all backup files
find /var/www/html -type f \( -name "*.bak" -o -name "*.backup" -o -name "*.old" -o -name "*~" \)

# Find directories without index files (potential listing)
find /var/www/html -type d -exec sh -c '[ ! -f "$1/index.html" ] && [ ! -f "$1/index.php" ] && echo "$1"' _ {} \;

# Set proper permissions
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;

## 7. Testing Your Fixes
## After implementing fixes, test them:

# Test that sensitive files are blocked
curl -I https://yoursite.com/.git/config
# Should return 403 or 404

# Test that directory listing is disabled
curl https://yoursite.com/images/
# Should not show a file list

# Run the vunscan.sh script again
./vunscan.sh yoursite.com
# Verify issues are resolved


## 8. Preventive Measures
## 1. Use a deployment script that excludes sensitive files:
bash
## 2. Regular security scans:
bash
## 3. Use a Web Application Firewall (WAF) like ModSecurity or Cloudflare

# Remember: The goal is not just to hide these files (security through obscurity) but to properly secure them or remove them from the web-accessible directory entirely.

Macbook: Script to monitor the top disk reads and writes

The script below tracks disk usage of a macbook for 20 seconds and the shows the processes with the highest disk utilisations

#!/bin/bash

# Disk I/O Monitor for macOS
# Shows which processes are using disk I/O the most with full paths

DURATION=20

echo "Disk I/O Monitor for macOS"
echo "========================================"
echo ""

# Check for sudo
if [[ $EUID -ne 0 ]]; then
    echo "ERROR: This script requires sudo privileges"
    echo "Please run: sudo $0"
    exit 1
fi

# Create temp file
TEMP_FILE="/tmp/disk_io_$$.txt"
export TEMP_FILE

# Collect data
echo "Collecting disk I/O data for $DURATION seconds..."
fs_usage -w -f filesys 2>/dev/null > "$TEMP_FILE" &
FS_PID=$!

# Progress bar
for i in $(seq 1 $DURATION); do
    printf "\rProgress: [%-20s] %d/%d seconds" "$(printf '#%.0s' $(seq 1 $((i*20/DURATION))))" $i $DURATION
    sleep 1
done
echo ""

# Stop collection
kill $FS_PID 2>/dev/null
wait $FS_PID 2>/dev/null

echo ""
echo "Processing data..."

# Parse with Python - pass temp file as argument
python3 - "$TEMP_FILE" << 'PYTHON_END'
import re
import os
import sys
from collections import defaultdict
import subprocess

# Get temp file from argument
temp_file = sys.argv[1] if len(sys.argv) > 1 else '/tmp/disk_io_temp.txt'

# Storage for process stats
stats = defaultdict(lambda: {'reads': 0, 'writes': 0, 'process_name': '', 'pid': ''})

# Parse fs_usage output
try:
    with open(temp_file, 'r') as f:
        for line in f:
            # Look for lines with process info (format: processname.pid at end of line)
            match = re.search(r'(\S+)\.(\d+)\s*$', line)
            if match:
                process_name = match.group(1)
                pid = match.group(2)
                key = f"{process_name}|{pid}"
                
                # Store process info
                stats[key]['process_name'] = process_name
                stats[key]['pid'] = pid
                
                # Categorize operation
                if any(op in line for op in ['RdData', 'read', 'READ', 'getattrlist', 'stat64', 'lstat64', 'open']):
                    stats[key]['reads'] += 1
                elif any(op in line for op in ['WrData', 'write', 'WRITE', 'close', 'fsync']):
                    stats[key]['writes'] += 1
except Exception as e:
    print(f"Error reading file: {e}")
    sys.exit(1)

# Calculate totals
total_ops = sum(s['reads'] + s['writes'] for s in stats.values())

# Get executable paths
def get_exe_path(process_name, pid):
    try:
        # Method 1: Try lsof with format output
        result = subprocess.run(['lsof', '-p', pid, '-Fn'], capture_output=True, text=True, stderr=subprocess.DEVNULL)
        paths = []
        for line in result.stdout.split('\n'):
            if line.startswith('n'):
                path = line[1:].strip()
                paths.append(path)
        
        # Look for the best path
        for path in paths:
            if '/Contents/MacOS/' in path and process_name in path:
                return path
            elif path.endswith('.app'):
                return path
            elif any(p in path for p in ['/bin/', '/sbin/', '/usr/']) and not any(path.endswith(ext) for ext in ['.dylib', '.so']):
                return path
        
        # Method 2: Try ps
        result = subprocess.run(['ps', '-p', pid, '-o', 'command='], capture_output=True, text=True, stderr=subprocess.DEVNULL)
        if result.stdout.strip():
            cmd = result.stdout.strip().split()[0]
            if os.path.exists(cmd):
                return cmd
        
        # Method 3: Return command name from ps
        result = subprocess.run(['ps', '-p', pid, '-o', 'comm='], capture_output=True, text=True, stderr=subprocess.DEVNULL)
        if result.stdout.strip():
            return result.stdout.strip()
            
    except Exception:
        pass
    
    # Last resort: return process name
    return process_name

# Sort by total operations
sorted_stats = sorted(stats.items(), key=lambda x: x[1]['reads'] + x[1]['writes'], reverse=True)

# Print header
print("\n%-30s %-8s %-45s %8s %8s %8s %7s %7s" % 
      ("Process Name", "PID", "Executable Path", "Reads", "Writes", "Total", "Read%", "Write%"))
print("=" * 140)

# Print top 20 processes
count = 0
for key, data in sorted_stats:
    if data['reads'] + data['writes'] == 0:
        continue
        
    total = data['reads'] + data['writes']
    read_pct = (data['reads'] * 100.0 / total_ops) if total_ops > 0 else 0
    write_pct = (data['writes'] * 100.0 / total_ops) if total_ops > 0 else 0
    
    # Get executable path
    exe_path = get_exe_path(data['process_name'], data['pid'])
    if len(exe_path) > 45:
        exe_path = "..." + exe_path[-42:]
    
    print("%-30s %-8s %-45s %8d %8d %8d %6.1f%% %6.1f%%" % 
          (data['process_name'][:30], 
           data['pid'], 
           exe_path,
           data['reads'], 
           data['writes'], 
           total,
           read_pct, 
           write_pct))
    
    count += 1
    if count >= 20:
        break

print("=" * 140)
print(f"Total I/O operations captured: {total_ops}")

PYTHON_END

# Cleanup
rm -f "$TEMP_FILE"

echo ""
echo "Monitoring complete."

Example output:

Disk I/O Monitor for macOS
========================================

Collecting disk I/O data for 20 seconds...
Progress: [####################] 20/20 seconds

Processing data...

Process Name                   PID      Executable Path                                  Reads   Writes    Total   Read%  Write%
============================================================================================================================================
Chrome                         4719678  Chrome                                             427      811     1238    3.1%    5.9%
UPMServiceController           4644625  UPMServiceController                               423      587     1010    3.1%    4.3%
UPMServiceController           4014337  UPMServiceController                               468      309      777    3.4%    2.2%
wsdlpd                         3060029  wsdlpd                                             154      370      524    1.1%    2.7%
tccd                           4743441  tccd                                               359       48      407    2.6%    0.3%
tccd                           4742031  tccd                                               358       48      406    2.6%    0.3%
com.crowdstrike.falcon.Agent   6174     com.crowdstrike.falcon.Agent                       301        5      306    2.2%    0.0%
UPMServiceContro               4644625  UPMServiceContro                                    12      285      297    0.1%    2.1%
mds_stores                     4736869  mds_stores                                         204       71      275    1.5%    0.5%
EndPointClassifier             6901     EndPointClassifier                                  40      231      271    0.3%    1.7%

MacOs: How to see which processes are using a specific port (eg 443)

Below is a useful script when you want to see which processes are using a specific port.

#!/bin/bash

# Port Monitor Script for macOS
# Usage: ./port_monitor.sh <port_number>
# Check if port number is provided

if [ $# -eq 0 ]; then
echo "Usage: $0 <port_number>"
echo "Example: $0 8080"
exit 1
fi

PORT=$1

# Validate port number

if ! [[ $PORT =~ ^[0-9]+$ ]] || [ $PORT -lt 1 ] || [ $PORT -gt 65535 ]; then
echo "Error: Please provide a valid port number (1-65535)"
exit 1
fi

# Function to display processes using the port

show_port_usage() {
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")

# Clear screen for better readability
clear

echo "=================================="
echo "Port Monitor - Port $PORT"
echo "Last updated: $timestamp"
echo "Press Ctrl+C to exit"
echo "=================================="
echo

# Check for processes using the port with lsof - both TCP and UDP
if lsof -i :$PORT &>/dev/null || netstat -an | grep -E "[:.]$PORT[[:space:]]" &>/dev/null; then
    echo "Processes using port $PORT:"
    echo
    lsof -i :$PORT -P -n | head -1
    echo "--------------------------------------------------------------------------------"
    lsof -i :$PORT -P -n | tail -n +2
    echo
    
    # Also show netstat information for additional context
    echo "Network connections on port $PORT:"
    echo
    printf "%-6s %-30s %-30s %-12s\n" "PROTO" "LOCAL ADDRESS" "FOREIGN ADDRESS" "STATE"
    echo "--------------------------------------------------------------------------------------------"
    
    # Show all connections (LISTEN, ESTABLISHED, etc.)
    # Use netstat -n to show numeric addresses
    netstat -anp tcp | grep -E "\.$PORT[[:space:]]" | while read line; do
        # Extract the relevant fields from netstat output
        proto=$(echo "$line" | awk '{print $1}')
        local_addr=$(echo "$line" | awk '{print $4}')
        foreign_addr=$(echo "$line" | awk '{print $5}')
        state=$(echo "$line" | awk '{print $6}')
        
        # Only print if we have valid data
        if [ -n "$proto" ] && [ -n "$local_addr" ]; then
            printf "%-6s %-30s %-30s %-12s\n" "$proto" "$local_addr" "$foreign_addr" "$state"
        fi
    done
    
    # Also check UDP connections
    netstat -anp udp | grep -E "\.$PORT[[:space:]]" | while read line; do
        proto=$(echo "$line" | awk '{print $1}')
        local_addr=$(echo "$line" | awk '{print $4}')
        foreign_addr=$(echo "$line" | awk '{print $5}')
        printf "%-6s %-30s %-30s %-12s\n" "$proto" "$local_addr" "$foreign_addr" "-"
    done
    
    # Also check for any established connections using lsof
    echo
    echo "Active connections with processes:"
    echo "--------------------------------------------------------------------------------------------"
    lsof -i :$PORT -P -n 2>/dev/null | grep -v LISTEN | tail -n +2 | while read line; do
        if [ -n "$line" ]; then
            echo "$line"
        fi
    done
    
else
    echo "No processes found using port $PORT"
    echo
    
    # Check if the port might be in use but not showing up in lsof
    local netstat_result=$(netstat -anv | grep -E "\.$PORT ")
    if [ -n "$netstat_result" ]; then
        echo "However, netstat shows activity on port $PORT:"
        echo "$netstat_result"
    fi
fi

echo
echo "Refreshing in 20 seconds... (Press Ctrl+C to exit)"
}

# Trap Ctrl+C to exit gracefully

trap 'echo -e "\n\nExiting port monitor..."; exit 0' INT

# Main loop - refresh every 20 seconds

while true; do
show_port_usage
sleep 20
done

Mac OSX: Altering the OS route table to re-direct the traffic of a website to a different interface (eg re-routing whatsapp traffic to en0)

This was a hard article to figure out the title for! Put simply, your mac book has a route table and if you want to move a specific IP address or dns from one interface to another, then follow the steps below:

First find the IP address of the website that you want to re-route the traffic for:

$ nslookup web.whatsapp.com
Server:		100.64.0.1
Address:	100.64.0.1#53

Non-authoritative answer:
web.whatsapp.com	canonical name = mmx-ds.cdn.whatsapp.net.
Name:	mmx-ds.cdn.whatsapp.net
Address: 102.132.99.60

We want to re-route traffic the traffic from: 102.132.99.60 to the default interface. So first lets find out which interface this traffic is currently being routed to?

$ route -n get web.whatsapp.com
   route to: 102.132.99.60
destination: 102.132.99.60
    gateway: 100.64.0.1
  interface: utun0
      flags: <UP,GATEWAY,HOST,DONE,WASCLONED,IFSCOPE,IFREF>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0        34        21         0      1400         0

So this is currently going to a tunnelled interface called utun0 on gateway 100.64.0.1.

Ok, so I want to move if off this tunnelled interface. So lets first display the kernel routing table. The -n option forces netstat to print the IP addresses. Without this option, netstat attempts to display the host names.

$ netstat - rn | head -n 5
Active Internet connections
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0    126  100.64.0.1.64770       136.226.216.14.https   ESTABLISHED
tcp4       0      0  100.64.0.1.64768       whatsapp-cdn-shv.https ESTABLISHED
tcp4       0      0  100.64.0.1.64766       52.178.17.3.https      ESTABLISHED

Now we want to re-route whatsapp to the default interface. So lets get the IP address of the default interface.

$ netstat -nr | grep default
default            192.168.8.1        UGScg                 en0
default                                 fe80::%utun1                            UGcIg               utun1
default                                 fe80::%utun2                            UGcIg               utun2
default                                 fe80::%utun3                            UGcIg               utun3
default                                 fe80::%utun4                            UGcIg               utun4
default                                 fe80::%utun5                            UGcIg               utun5
default                                 fe80::%utun0                            UGcIg               utun0

We can see that our en0 interface is on IP address: 192.168.8.1. So lets re-route the traffic from Whatsapp’s ip address to this interace’s IP address:

$ sudo route add 102.132.99.60 192.168.0.1
route: writing to routing socket: File exists
add host 102.132.99.60: gateway 192.168.8.1: File exists

Now lets test if we are routing via the correct interface:

$ route -n get 102.132.99.60
   route to: 102.132.99.60
destination: 102.132.99.60
    gateway: 192.168.8.1
  interface: utun6
      flags: <UP,GATEWAY,HOST,DONE,STATIC>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1400         0

Finally delete the route and recheck the routing:

$ sudo route delete 102.132.99.60
delete host 102.132.99.60

$ route -n get 102.132.99.60
   route to: 102.132.99.60
destination: 102.132.99.60
    gateway: 100.64.0.1
  interface: utun6
      flags: <UP,GATEWAY,HOST,DONE,WASCLONED,IFSCOPE,IFREF>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1400         0

Macbook OSX: Using Touch ID / fingerprints to enable SUDO and permanently enabling this after Mac OSX updates

Each day that I wake up I try and figure out if I can do less work than yesterday. With this in mind I was playing around to see if there is a way to save me typing my password each time I SUDO. It turns out this is quite a simple change…

Open Terminal and run the following to edit sudos behaviour:

sudo nano /etc/pam.d/sudo

Next add the following to the top of the file:

auth       sufficient     pam_tid.so

The only issue with this is that /etc/pam.d/sudo is overwritten on every macOS update (major, minor or patch – it is always overwritten and reset back to its default state).

MacOS: Sonoma

In their “What’s new for enterprise in macOS Sonoma” document Apple listed the following in the “Bug fixes and other improvements” section:

Touch ID can be allowed for sudo with a configuration that persists across software updates using /etc/pam.d/sudo_local. See /etc/pam.d/sudo_local.template for details.

So lets create a template file in /etc/pam.d/sudo_local.template:

sudo nano /etc/pam.d/sudo_local.template

Next uncomment the auth line, as per:

# sudo_local: local config file which survives system update and is included fo$
# uncomment following line to enable Touch ID for sudo
auth       sufficient     pam_tid.so

This should mean that Touch ID now survive system updates!

Quick tests:

sudo ls
# exit sudo
sudo -k
sudo ls

To enable Touch ID access on Iterm2. You need to do the following. Go to Prefs -> Advanced -> Allow sessions to survive logging out and back in and set value to no . Restart Iterm2 and touch ID authentication will work on Iterm2.

Macbook OSX: Change the default image type of your screenshots from PNG to JPEG, GIF or PDF

There are a few things that I tweak when I get a new Macbook, one of which is the screenshot format (mainly because it doesnt natively render in Whatsapp). So I thought I would share the code snippet that you can run in Terminal to alter the default image type of your screenshots:

For JPEG use:

$ defaults write com.apple.screencapture type JPG

For GIF use:

$ defaults write com.apple.screencapture type GIF

For PDF use:

$ defaults write com.apple.screencapture type PDF

For PNG use:

$ defaults write com.apple.screencapture type PNG

Mac OSX : Tracing which network interface will be used to route traffic to an IP/DNS address

If you have multiple connections on your device (and maybe you have a zero trust client installed); how do you find out which network interface on your device will be used to route the traffic?

Below is a route get request for googles DNS service:

$ route get 8.8.8.8

   route to: dns.google
destination: dns.google
    gateway: 100.64.0.1
  interface: utun3
      flags: <UP,GATEWAY,HOST,DONE,WASCLONED,IFSCOPE,IFREF>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1400         0

If you have multiple interfaces enabled, then the first item in the Service Order will be used. If you want to see the default interface for your device:

$ route -n get 0.0.0.0 | grep interface
  interface: en0

Lets go an see whats going on in my default interface:

$ netstat utun3 | grep ESTABLISHED
tcp4       0      0  100.64.0.1.65271       jnb02s11-in-f4.1.https ESTABLISHED
tcp4       0      0  100.64.0.1.65269       jnb02s02-in-f14..https ESTABLISHED
tcp4       0      0  100.64.0.1.65262       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65261       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65260       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65259       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65258       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65257       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65256       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65255       192.0.73.2.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65254       192.0.78.23.https      ESTABLISHED
tcp4       0      0  100.64.0.1.65253       192.0.76.3.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65252       192.0.78.23.https      ESTABLISHED
tcp4       0      0  100.64.0.1.65251       192.0.76.3.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65250       192.0.78.23.https      ESTABLISHED
tcp4       0      0  100.64.0.1.65249       192.0.76.3.https       ESTABLISHED
tcp4       0      0  100.64.0.1.65248       ec2-13-244-140-3.https ESTABLISHED
tcp4       0      0  100.64.0.1.65247       192.0.73.2.https       ESTABLISHED

Finding and Setting the Maximum Transmission Unit (MTU) on a Windows Machine

If you have just changed ISPs or moved house and your internet suddenly starts misbehaving the likelihood is your Maximum Transmission Unit (MTU) is set too high for your ISP. The default internet facing MTU is 1500 bytes, BUT depending on your setup, this often needs to be set much lower.

Step 1:

First check your current MTU across all your ipv4 interfaces using netsh:

netsh interface ipv4 show subinterfaces
   MTU  MediaSenseState   Bytes In  Bytes Out  Interface
------  ---------------  ---------  ---------  -------------
4294967295                1          0          0  Loopback Pseudo-Interface 1
  1492                1        675        523  Local Area Connection

As you can see, the Local Area Connection interface is set to a 1492 bytes MTU. So how do we find out what it should be? We are going to send a fixed size Echo packet out, and tell the network not to fragment this packet. If somewhere along the line this packet is too big then this request will fail.

Next enter (if it fails then you know your MTU is too high):

ping 8.8.8.8 -f -l 1492

Procedure to find optimal MTU:

For PPPoE, your Max MTU should be no more than 1492 to allow space for the 8 byte PPPoE “wrapper”. 1492 + 8 = 1500. The ping test we will be doing does not include the IP/ICMP header of 28 bytes. 1500 – 28 = 1472. Include the 8 byte PPPoE wrapper if your ISP uses PPPoE and you get 1500 – 28 – 8 = 1464.

The best value for MTU is that value just before your packets get fragmented. Add 28 to the largest packet size that does not result in fragmenting the packets (since the ping command specifies the ping packet size, not including the IP/ICMP header of 28 bytes), and this is your Max MTU setting.

The below is an automated ping sweep, that tests various packet sizes until it fails (increasing in 10 bytes per iteration):

C:\Windows\system32>for /l %i in (1360,10,1500) do @ping -n 1 -w 8.8.8.8 -l %i -f

Pinging 8.8.8.8. with 1400 bytes of data:
Reply from 8.8.8.8: bytes=1400 time=6ms TTL=64

Ping statistics for 8.8.8.8:
Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 6ms, Maximum = 6ms, Average = 6ms

Pinging 8.8.8.8 with 1401 bytes of data:
Reply from 8.8.8.8: bytes=1401 time<1ms TTL=64

Ping statistics for 8.8.8.8:
Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms

Pinging 8.8.8.8 with 1402 bytes of data:
Reply from 8.8.8.8: bytes=1402 time<1ms TTL=64

Ping statistics for 8.8.8.8:
Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms

Pinging 8.8.8.8 with 1403 bytes of data:
Reply from 8.8.8.8: bytes=1403 time<1ms TTL=64

Ping statistics for 8.8.8.8:
Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms 

Once you find the MTU, you can set it as per below:

set subinterface “Local Area Connection” mtu=1360 store=persistent

Finding and Setting the Maximum Transmission Unit (MTU) on Mac/OSX

If you have just changed ISPs or moved house and your internet suddenly starts misbehaving the likelihood is your Maximum Transmission Unit (MTU) is set too high for your ISP. The default internet facing MTU is 1500 bytes, BUT depending on your setup, this often needs to be set much lower.

Step 1:

First check your current MTU.

$ networksetup -getMTU en0
Active MTU: 1500 (Current Setting: 1500)

As you can see, the Mac is set to 1500 bytes MTU. So how do we find out what it should be? We are going to send a fixed size Echo packet out, and tell the network not to fragment this packet. If somewhere along the line this packet is too big then this request will fail.

Next enter:

$ ping -D -s 1500 www.google.com
PING www.google.com (172.217.170.100): 1500 data bytes
ping: sendto: Message too long
ping: sendto: Message too long
Request timeout for icmp_seq 0
ping: sendto: Message too long
Request timeout for icmp_seq 1
ping: sendto: Message too long

Ok, so our MTU is too high.

Procedure to find optimal MTU:

Hint: For PPPoE, your Max MTU should be no more than 1492 to allow space for the 8 byte PPPoE “wrapper”. 1492 + 8 = 1500. The ping test we will be doing does not include the IP/ICMP header of 28 bytes. 1500 – 28 = 1472. Include the 8 byte PPPoE wrapper if your ISP uses PPPoE and you get 1500 – 28 – 8 = 1464.

The best value for MTU is that value just before your packets get fragmented. Add 28 to the largest packet size that does not result in fragmenting the packets (since the ping command specifies the ping packet size, not including the IP/ICMP header of 28 bytes), and this is your Max MTU setting.

The below is an automated ping sweep, that tests various packet sizes until it fails (increasing in 10 bytes per iteration):

$ ping -g 1300 -G 1600 -h 10 -D www.google.com
PING www.google.com (172.217.170.100): (1300 ... 1600) data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4
Request timeout for icmp_seq 5
Request timeout for icmp_seq 6
ping: sendto: Message too long
Request timeout for icmp_seq 7

As you can see it failed on the 7th attempt (giving you a 1300 + 60 MTU).

Once you find the MTU, you can set it as per below:

$ ping -D -s 1360 www.google.com
PING www.google.com (172.217.170.100): 1370 data bytes
Request timeout for icmp_seq 0

So I can set my MTU as 1360 + 28 = 1386:

networksetup -setMTU en0 1386