diff --git a/docs/superpowers/contracts/pi-rustscan-instead-of-nmap-2026-11-10.json b/docs/superpowers/contracts/pi-rustscan-instead-of-nmap-2026-11-10.json new file mode 100644 index 00000000..967b26c5 --- /dev/null +++ b/docs/superpowers/contracts/pi-rustscan-instead-of-nmap-2026-11-10.json @@ -0,0 +1,13 @@ +{ + "title": "Pi flashing: discover SSH hosts with rustscan, not nmap", + "objective": "The Pi flash/discovery scripts find SSH hosts on the LAN with `rustscan -a -p 22 -g`, install/check for rustscan instead of nmap, and rustscan is configured (scripts = \"None\") so it never pipes into nmap, which the pacman wrapper blocks.", + "acceptance_criteria": [ + "No nmap references remain in the Pi flashing scripts and rpi_nc_deps checks for rustscan (test_pi_discovery_rustscan.sh)", + "fresh-install/main.sh installs fresh-install/rustscan.toml to ~/.rustscan.toml" + ], + "out_of_scope": [ + "The pacman nmap block and /usr/local/bin/nmap shim (digital-wellbeing repo)", + "Live scan of a real LAN" + ], + "verifier": "linux_configuration/tests/test_pi_discovery_rustscan.sh; pre-commit run --files " +} diff --git a/docs/superpowers/evidence/dns-boot-race-and-startup-monitor-2026-10-02.json b/docs/superpowers/evidence/dns-boot-race-and-startup-monitor-2026-10-02.json deleted file mode 100644 index 3ae9cf85..00000000 --- a/docs/superpowers/evidence/dns-boot-race-and-startup-monitor-2026-10-02.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "intent": "DNS-over-TLS (stunnel-dot) comes up at every boot, dnsmasq has no ordering cycle, and the service checker stops expecting the removed pc_startup feature and finds the extracted repos under ~/src.", - "scope": [ - "linux_configuration/features/lib/dns_units.sh, dot_resolver_install.sh and their tests", - "linux_configuration/periodic_background check_and_enable_services.sh, services_units.sh, services_report.sh and their tests", - "Non-goal: the firewall, the blocklist feed, the guard warnings (compulsive-block, LeechBlock, workout-locker) this checker reports" - ], - "changes": [ - "dnsmasq: bind-dynamic instead of bind-interfaces, so starting before enp6s0 exists no longer exits with 'unknown interface'", - "dnsmasq drop-in: drop After/Wants=network-online.target, which cycled with the stock Before=network-online.target", - "stunnel-dot.service: Wants=dnsmasq.service instead of Requires=, so one failed dnsmasq start no longer kills DoT until a manual restart", - "check_and_enable_services: remove check_startup_monitor (pc_startup was removed in 765fa925) and default HOSTS_BLOCKER/DW/WORKOUT_LOCKER repos to ~/src/" - ], - "verification": [ - { - "command": "pre-commit run --files (shell-coverage-ratchet runs the suites in the jail)", - "result": "pass", - "evidence": "all hooks passed, ratchet 100%" - }, - { - "command": "vmbox: dnsmasq with interface=, bind-interfaces vs bind-dynamic; Requires= vs Wants= on a failing dependency", - "result": "pass", - "evidence": "bind-interfaces: DIED 'unknown interface lanx0'; bind-dynamic: survived, bound 10.199.0.1:5353 after the interface appeared; Requires=: inactive; Wants=: active" - }, - { - "command": "host: setup_dot_resolver.sh + dns_units functions + validate_and_enable", - "result": "pass", - "evidence": "dnsmasq --test OK; stunnel-dot active on 10.8.0.1:853, TLSv1.3 Verification OK; DNS answers on 127.0.0.1 and 192.168.1.43; no network-online in dnsmasq After=" - }, - { - "command": "sudo check_and_enable_services.sh --status (host) and vmbox diff against HEAD", - "result": "pass", - "evidence": "full run, no 'not found'; only diff vs HEAD is the removed PC Startup Monitor section" - } - ], - "risks": [ - "bind-dynamic also listens on addresses that appear later on enp6s0 (intended)", - "stunnel now starts even if dnsmasq is down; queries fail until dnsmasq is back, which Restart=always plus the watchdog already handle" - ], - "rollback": [ - "git revert this commit, then rerun setup_dns_blocker.sh setup and setup_dot_resolver.sh", - "Validate with: systemctl is-active dnsmasq stunnel-dot; setup_dot_resolver.sh --check" - ] -} diff --git a/docs/superpowers/evidence/pi-rustscan-instead-of-nmap-2026-11-10.json b/docs/superpowers/evidence/pi-rustscan-instead-of-nmap-2026-11-10.json new file mode 100644 index 00000000..6aef8e28 --- /dev/null +++ b/docs/superpowers/evidence/pi-rustscan-instead-of-nmap-2026-11-10.json @@ -0,0 +1,35 @@ +{ + "intent": "Pi flashing scripts discover SSH hosts with rustscan instead of nmap; rustscan runs with scripts = \"None\" so it never calls nmap.", + "scope": [ + "linux_configuration/features/lib/{nc_discover,nc_flash,nc_image,nc_remote,pi_flash_remote,rpi_nc_deps}.sh", + "linux_configuration/features/raspberry_pi_flash_sd.sh", + "linux_configuration/fresh-install/main.sh, rustscan.toml", + "linux_configuration/tests/test_pi_discovery_rustscan.sh", + "Not touched: pacman wrapper (digital-wellbeing), live LAN scanning" + ], + "changes": [ + "nmap ping sweep + port scan replaced by `rustscan -a -p 22 -g | awk '{print $1}'` (greppable output, one IP per line)", + "Dependency checks and hint messages name rustscan instead of nmap", + "fresh-install/rustscan.toml (scripts = \"None\") copied to ~/.rustscan.toml by main.sh" + ], + "verification": [ + { + "command": "linux_configuration/tests/test_pi_discovery_rustscan.sh", + "result": "pass", + "evidence": "ok per discovery script, ok rpi_nc_deps dependencies, ok no nmap references" + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "shellcheck, codespell and secrets hooks passed on the shell files" + } + ], + "risks": [ + "rustscan -g output format change would break the awk IP extraction", + "Hosts that drop rustscan's fast SYN probing may be missed versus nmap" + ], + "rollback": [ + "git revert the commit", + "Re-run a Pi discovery and confirm hosts are listed" + ] +} diff --git a/linux_configuration/features/lib/nc_discover.sh b/linux_configuration/features/lib/nc_discover.sh index 3382c626..83d4f2da 100755 --- a/linux_configuration/features/lib/nc_discover.sh +++ b/linux_configuration/features/lib/nc_discover.sh @@ -39,13 +39,11 @@ discover_raspberry_pi() { return fi - # Ping sweep to wake up hosts log_info "Hostname resolution failed, scanning network..." - nmap -sn -T4 "$network" &>/dev/null || true # Scan for SSH-enabled devices (excluding our IP and known laptop) local ssh_hosts - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2>/dev/null | sort -u) || true + ssh_hosts=$(rustscan -a "$network" -p 22 -g 2>/dev/null | awk '{print $1}' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2>/dev/null | sort -u) || true if [[ -z $ssh_hosts ]]; then die "No new SSH-enabled devices found. Is the Pi connected and booted?" diff --git a/linux_configuration/features/lib/nc_flash.sh b/linux_configuration/features/lib/nc_flash.sh index cf30cb21..3492eff4 100755 --- a/linux_configuration/features/lib/nc_flash.sh +++ b/linux_configuration/features/lib/nc_flash.sh @@ -67,9 +67,9 @@ ensure_dependencies() { local missing_packages=() - # Check for nmap (fast network scanning) - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") + # Check for rustscan (fast network scanning) + if ! command -v rustscan &>/dev/null; then + missing_packages+=("rustscan") fi # Check for sshpass (for initial SSH key setup) diff --git a/linux_configuration/features/lib/nc_image.sh b/linux_configuration/features/lib/nc_image.sh index b2ffbbc8..002f6d26 100755 --- a/linux_configuration/features/lib/nc_image.sh +++ b/linux_configuration/features/lib/nc_image.sh @@ -176,5 +176,5 @@ EOF log_success "SD card configured for headless boot!" log_info "Insert the SD card into your Raspberry Pi and power it on." - log_info "Find the Pi's IP address from your router or use: nmap -sn 192.168.1.0/24" + log_info "Find the Pi's IP address from your router or use: rustscan -a 192.168.1.0/24 -p 22 -g" } diff --git a/linux_configuration/features/lib/nc_remote.sh b/linux_configuration/features/lib/nc_remote.sh index 46bed5ef..68b5aaa8 100755 --- a/linux_configuration/features/lib/nc_remote.sh +++ b/linux_configuration/features/lib/nc_remote.sh @@ -22,13 +22,11 @@ discover_remote_laptop() { log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" - # Use nmap for fast parallel SSH port scanning - log_info "Scanning network for SSH-enabled devices (using nmap)..." + # Use rustscan for fast parallel SSH port scanning + log_info "Scanning network for SSH-enabled devices (using rustscan)..." local ssh_hosts - # First do a ping sweep to wake up hosts, then scan SSH port - nmap -sn -T4 "$network" &>/dev/null || true - # Extract IPs from nmap output - grep for report lines then extract IP - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) + # Greppable output: one "IP -> [22]" line per host with the port open + ssh_hosts=$(rustscan -a "$network" -p 22 -g 2>/dev/null | awk '{print $1}' | grep -vw "$my_ip" | sort -u) || true if [[ -z $ssh_hosts ]]; then die "No SSH-enabled devices found on network" diff --git a/linux_configuration/features/lib/pi_flash_remote.sh b/linux_configuration/features/lib/pi_flash_remote.sh index d7df234c..9bba6fc4 100755 --- a/linux_configuration/features/lib/pi_flash_remote.sh +++ b/linux_configuration/features/lib/pi_flash_remote.sh @@ -6,136 +6,136 @@ # and the variables defined above the source line. discover_remote_laptop() { - log_info "Auto-discovering remote laptop on local network..." + log_info "Auto-discovering remote laptop on local network..." - ensure_dependencies + ensure_dependencies - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" - log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" - log_info "Scanning network for SSH-enabled devices (using nmap)..." + log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" + log_info "Scanning network for SSH-enabled devices (using rustscan)..." - local ssh_hosts - nmap -sn -T4 "$network" &> /dev/null || true - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) + local ssh_hosts + # Greppable output: one "IP -> [22]" line per host with the port open + ssh_hosts=$(rustscan -a "$network" -p 22 -g 2>/dev/null | awk '{print $1}' | grep -vw "$my_ip" | sort -u) || true - if [[ -z $ssh_hosts ]]; then - die "No SSH-enabled devices found on network" - fi + if [[ -z $ssh_hosts ]]; then + die "No SSH-enabled devices found on network" + fi - local host_count - host_count=$(echo "$ssh_hosts" | wc -l) - log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" + local host_count + host_count=$(echo "$ssh_hosts" | wc -l) + log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" - local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") - local users=() - for u in "${common_users[@]}"; do - local is_dup=0 - for existing in "${users[@]}"; do - if [[ $u == "$existing" ]]; then - is_dup=1 - break - fi - done - if [[ $is_dup -eq 0 ]]; then - users+=("$u") - fi - done + local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") + local users=() + for u in "${common_users[@]}"; do + local is_dup=0 + for existing in "${users[@]}"; do + if [[ $u == "$existing" ]]; then + is_dup=1 + break + fi + done + if [[ $is_dup -eq 0 ]]; then + users+=("$u") + fi + done - log_info "Will try usernames: ${users[*]}" + log_info "Will try usernames: ${users[*]}" - local found_laptop="" - local found_user="" - local idx=0 + local found_laptop="" + local found_user="" + local idx=0 - for ip in $ssh_hosts; do - idx=$((idx + 1)) + for ip in $ssh_hosts; do + idx=$((idx + 1)) - if [[ $ip == "$gateway" ]]; then - log_info "[$idx/$host_count] Skipping $ip (gateway)" - continue - fi + if [[ $ip == "$gateway" ]]; then + log_info "[$idx/$host_count] Skipping $ip (gateway)" + continue + fi - log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." + log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." - for try_user in "${users[@]}"; do - if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then - log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" - found_user="$try_user" + for try_user in "${users[@]}"; do + if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then + log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" + found_user="$try_user" - log_info "[$idx/$host_count] $ip - Checking for SD card..." - local has_sd - has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2> /dev/null || true) + log_info "[$idx/$host_count] $ip - Checking for SD card..." + local has_sd + has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true) - if [[ -n $has_sd ]]; then - log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" - found_laptop="$ip" - break 2 - else - log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." - if [[ -z $found_laptop ]]; then - found_laptop="$ip" - fi - fi - break - fi - done - done + if [[ -n $has_sd ]]; then + log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" + found_laptop="$ip" + break 2 + else + log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." + if [[ -z $found_laptop ]]; then + found_laptop="$ip" + fi + fi + break + fi + done + done - if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then - log_warning "No device with passwordless SSH found using common usernames." + if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then + log_warning "No device with passwordless SSH found using common usernames." - found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) + found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) - if [[ -z $found_laptop ]]; then - die "Could not find any suitable SSH-enabled device" - fi + if [[ -z $found_laptop ]]; then + die "Could not find any suitable SSH-enabled device" + fi - log_info "Found SSH host at $found_laptop but need credentials." - read -r -p "Enter username for $found_laptop: " found_user + log_info "Found SSH host at $found_laptop but need credentials." + read -r -p "Enter username for $found_laptop: " found_user - if [[ -z $found_user ]]; then - die "No username provided" - fi - fi + if [[ -z $found_user ]]; then + die "No username provided" + fi + fi - REMOTE_LAPTOP_IP="$found_laptop" - REMOTE_LAPTOP_USER="$found_user" - log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + REMOTE_LAPTOP_IP="$found_laptop" + REMOTE_LAPTOP_USER="$found_user" + log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - save_config + save_config } setup_ssh_key_to_remote() { - local remote_host="$1" - local remote_user="$2" + local remote_host="$1" + local remote_user="$2" - if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2> /dev/null; then - log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" - return 0 - fi + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then + log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" + return 0 + fi - log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." + log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." - if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then - log_info "Generating SSH key..." - ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q - fi + if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then + log_info "Generating SSH key..." + ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q + fi - log_info "Copying SSH key to remote host (you may be prompted for password)..." + log_info "Copying SSH key to remote host (you may be prompted for password)..." - if command -v ssh-copy-id &> /dev/null; then - ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" - else - local pub_key - pub_key=$(cat ~/.ssh/id_ed25519.pub 2> /dev/null || cat ~/.ssh/id_rsa.pub) - ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" - fi + if command -v ssh-copy-id &>/dev/null; then + ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" + else + local pub_key + pub_key=$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub) + ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" + fi - log_success "SSH key authentication configured" + log_success "SSH key authentication configured" } diff --git a/linux_configuration/features/lib/rpi_nc_deps.sh b/linux_configuration/features/lib/rpi_nc_deps.sh index e59117e7..4bdd0e68 100755 --- a/linux_configuration/features/lib/rpi_nc_deps.sh +++ b/linux_configuration/features/lib/rpi_nc_deps.sh @@ -36,8 +36,8 @@ wait_for_apt_lock() { ensure_dependencies() { local missing_packages=() - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") + if ! command -v rustscan &>/dev/null; then + missing_packages+=("rustscan") fi if ! command -v sshpass &>/dev/null; then @@ -88,10 +88,9 @@ discover_raspberry_pi() { fi log_info "Hostname resolution failed, scanning network..." - nmap -sn -T4 "$network" &>/dev/null || true local ssh_hosts - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true + ssh_hosts=$(rustscan -a "$network" -p 22 -g 2>/dev/null | awk '{print $1}' | grep -vw "$my_ip" | sort -u) || true if [[ -z $ssh_hosts ]]; then die "No SSH-enabled devices found. Is the Pi connected and booted?" diff --git a/linux_configuration/features/raspberry_pi_flash_sd.sh b/linux_configuration/features/raspberry_pi_flash_sd.sh index 5b46c492..f62e701e 100755 --- a/linux_configuration/features/raspberry_pi_flash_sd.sh +++ b/linux_configuration/features/raspberry_pi_flash_sd.sh @@ -14,8 +14,8 @@ CONFIG_FILE="${SCRIPT_DIR}/.raspberry_pi.conf" # Load configuration from gitignored config file if it exists if [[ -f $CONFIG_FILE ]]; then - # shellcheck source=/dev/null - source "$CONFIG_FILE" + # shellcheck source=/dev/null + source "$CONFIG_FILE" fi # Configuration - Customize these values (or set in config file) @@ -38,34 +38,34 @@ NC='\033[0m' # All log functions output to stderr so they don't interfere with function return values log_info() { - echo -e "${BLUE}[INFO]${NC} $1" >&2 + echo -e "${BLUE}[INFO]${NC} $1" >&2 } log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 + echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 } log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" >&2 + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } log_error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 + echo -e "${RED}[ERROR]${NC} $1" >&2 } die() { - log_error "$1" - exit 1 + log_error "$1" + exit 1 } check_root() { - if [[ $EUID -ne 0 ]]; then - die "This script must be run as root. Use: sudo $0" - fi + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root. Use: sudo $0" + fi } save_config() { - cat > "$CONFIG_FILE" << EOF + cat >"$CONFIG_FILE" < /dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") - echo "$chars" + local length="${1:-16}" + local chars + chars=$(dd if=/dev/urandom bs=256 count=1 2>/dev/null | tr -dc 'A-Za-z0-9!@#$%&*' | cut -c1-"$length") + echo "$chars" } auto_generate_pi_password() { - if [[ -z $PI_PASSWORD ]]; then - PI_PASSWORD=$(generate_password 16) - log_info "Auto-generated Pi password (will be saved to config file)" - fi + if [[ -z $PI_PASSWORD ]]; then + PI_PASSWORD=$(generate_password 16) + log_info "Auto-generated Pi password (will be saved to config file)" + fi } # ============================================================================= @@ -104,31 +104,31 @@ auto_generate_pi_password() { # ============================================================================= ensure_dependencies() { - local missing_packages=() + local missing_packages=() - if ! command -v nmap &> /dev/null; then - missing_packages+=("nmap") - fi + if ! command -v rustscan &>/dev/null; then + missing_packages+=("rustscan") + fi - if ! command -v sshpass &> /dev/null; then - missing_packages+=("sshpass") - fi + if ! command -v sshpass &>/dev/null; then + missing_packages+=("sshpass") + fi - if [[ ${#missing_packages[@]} -gt 0 ]]; then - log_info "Installing missing packages: ${missing_packages[*]}" + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" - if command -v pacman &> /dev/null; then - sudo pacman -S --noconfirm "${missing_packages[@]}" - elif command -v apt-get &> /dev/null; then - sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" - elif command -v dnf &> /dev/null; then - sudo dnf install -y "${missing_packages[@]}" - else - die "Could not detect package manager. Please install manually: ${missing_packages[*]}" - fi + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &>/dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &>/dev/null; then + sudo dnf install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi - log_success "Dependencies installed" - fi + log_success "Dependencies installed" + fi } # shellcheck source=lib/pi_flash_remote.sh @@ -138,29 +138,28 @@ source "$SCRIPT_DIR/lib/pi_flash_image.sh" # shellcheck source=lib/pi_flash_exec.sh source "$SCRIPT_DIR/lib/pi_flash_exec.sh" - main() { - local command="${1:-help}" + local command="${1:-help}" - case "$command" in - local) - phase_flash_local - ;; - remote) - phase_flash_remote - ;; - execute-remote) - phase_execute_remote "${2:-}" - ;; - help | --help | -h) - show_help - ;; - *) - log_error "Unknown command: $command" - show_help - exit 1 - ;; - esac + case "$command" in + local) + phase_flash_local + ;; + remote) + phase_flash_remote + ;; + execute-remote) + phase_execute_remote "${2:-}" + ;; + help | --help | -h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac } main "$@" diff --git a/linux_configuration/fresh-install/main.sh b/linux_configuration/fresh-install/main.sh index 81d514fb..109981be 100755 --- a/linux_configuration/fresh-install/main.sh +++ b/linux_configuration/fresh-install/main.sh @@ -169,6 +169,8 @@ if [ ! -d "$HOME/.config/mpv" ]; then mkdir -p "$HOME/.config/mpv" fi cp mpv.conf "$HOME/.config/mpv/mpv.conf" +# rustscan must never call nmap (nonfree, blocked by the pacman wrapper) +cp rustscan.toml "$HOME/.rustscan.toml" if [ ! -d "$HOME/.oh-my-zsh" ]; then yes | sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" diff --git a/linux_configuration/fresh-install/rustscan.toml b/linux_configuration/fresh-install/rustscan.toml new file mode 100644 index 00000000..a4e146fb --- /dev/null +++ b/linux_configuration/fresh-install/rustscan.toml @@ -0,0 +1,4 @@ +# RustScan config (copied to ~/.rustscan.toml by main.sh). +# "None" keeps rustscan from piping open ports into nmap (nonfree, blocked by +# the pacman wrapper; the /usr/local/bin/nmap shim would only exit 1). +scripts = "None" diff --git a/linux_configuration/tests/test_pi_discovery_rustscan.sh b/linux_configuration/tests/test_pi_discovery_rustscan.sh new file mode 100755 index 00000000..182c2c00 --- /dev/null +++ b/linux_configuration/tests/test_pi_discovery_rustscan.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# Regression test: Raspberry Pi / laptop SSH discovery uses rustscan, never +# nmap (nonfree NPSL licence; the pacman wrapper refuses to install it). +# +# Runs the real discover/ensure_dependencies functions with stubbed +# ip/getent/sshpass/rustscan/nmap/pacman on PATH and asserts: +# - rustscan is called as `-a -p 22 -g` and nmap is never executed +# - our own IP is excluded and the Pi (answering the hostname probe) is found +# - ensure_dependencies asks for rustscan, not nmap +# - no script in the set still invokes nmap + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +FEATURES="$(cd -- "$SCRIPT_DIR/.." && pwd)/features" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT +STUBS="$TMP_DIR/stubs" +mkdir -p "$STUBS" +export STUB_LOG="$TMP_DIR/calls.log" +: >"$STUB_LOG" + +stub() { # name; body on stdin + { + printf '#!/bin/bash\n' + cat + } >"$STUBS/$1" + chmod +x "$STUBS/$1" +} +stub ip <<'STUB' +if [[ $1 == -4 ]]; then + echo " inet 192.168.1.5/24 brd 192.168.1.255 scope global eth0" +else + echo "default via 192.168.1.1 dev eth0" +fi +STUB +stub getent <<'STUB' +exit 2 +STUB +stub nmap <<'STUB' +echo "nmap $*" >>"$STUB_LOG" +exit 1 +STUB +stub rustscan <<'STUB' +echo "rustscan $*" >>"$STUB_LOG" +printf '192.168.1.5 -> [22]\n192.168.1.43 -> [22]\n192.168.1.26 -> [22]\n' +STUB +stub pacman <<'STUB' +echo "pacman $*" >>"$STUB_LOG" +STUB +stub sudo <<'STUB' +"$@" +STUB +# sshpass -p PW ssh ... user@IP CMD : only .43 is the Pi. +stub sshpass <<'STUB' +for a in "$@"; do [[ $a == *@* ]] && ip=${a#*@}; done +echo "ssh $ip" >>"$STUB_LOG" +if [[ $ip == 192.168.1.43 ]]; then + case "${*: -1}" in hostname) echo nextcloud-pi ;; *) echo ok ;; esac +fi +STUB + +log_info() { :; } +log_success() { :; } +die() { + printf 'die: %s\n' "$*" >&2 + exit 1 +} +export PI_HOSTNAME=nextcloud-pi PI_USER=pi PI_PASSWORD=pw REMOTE_LAPTOP_IP=192.168.1.17 + +# Runs discover_raspberry_pi from the given lib with every external stubbed. +check_discover() { # lib file + : >"$STUB_LOG" + ( + PATH="$STUBS:$PATH" + # Real ensure_dependencies (no-op: rustscan/sshpass stubs are on PATH). + # shellcheck source=../features/lib/rpi_nc_deps.sh + source "$FEATURES/lib/rpi_nc_deps.sh" + # shellcheck source=/dev/null + source "$FEATURES/lib/$1" + found=$(discover_raspberry_pi) + [[ $found == 192.168.1.43 ]] || fail "$1 found '$found', want 192.168.1.43" + ) + grep -qx 'rustscan -a 192.168.1.0/24 -p 22 -g' "$STUB_LOG" || + fail "$1: rustscan not called with '-a 192.168.1.0/24 -p 22 -g': $(tr '\n' '|' <"$STUB_LOG")" + if grep -q '^nmap' "$STUB_LOG"; then fail "$1 ran nmap"; fi + if grep -qx 'ssh 192.168.1.5' "$STUB_LOG"; then fail "$1 tried our own IP"; fi + printf 'ok discover %s\n' "$1" +} +check_discover nc_discover.sh +check_discover rpi_nc_deps.sh + +# --- ensure_dependencies asks for rustscan, not nmap ------------------------ +: >"$STUB_LOG" +rm -f "$STUBS/rustscan" "$STUBS/sshpass" # hide them so the install branch runs +( + PATH="$STUBS" + hash -r + # shellcheck source=../features/lib/rpi_nc_deps.sh + source "$FEATURES/lib/rpi_nc_deps.sh" + ensure_dependencies >/dev/null 2>&1 || true +) +grep -q '^pacman -S --noconfirm rustscan' "$STUB_LOG" || + fail "ensure_dependencies did not request rustscan: $(tr '\n' '|' <"$STUB_LOG")" +grep -E '^pacman .*nmap' "$STUB_LOG" && fail "ensure_dependencies still requests nmap" +printf 'ok rpi_nc_deps dependencies\n' + +# --- no remaining nmap invocation in the set -------------------------------- +for f in lib/nc_discover.sh lib/nc_remote.sh lib/rpi_nc_deps.sh lib/nc_flash.sh \ + lib/nc_image.sh lib/pi_flash_remote.sh raspberry_pi_flash_sd.sh; do + if grep -nE '(^|[^[:alnum:]_-])nmap([^[:alnum:]_-]|$)' "$FEATURES/$f"; then + fail "$f still mentions nmap" + fi +done +printf 'ok no nmap references\n'