Files
Linux_post_install/bin/pos-network-scan
T
he 5ef38dc46f fix: resolve all 23 MAINTENANCE audit tickets
- deps guards before -h|--help in docker-health/ps, network-scan,
  usb-server, media-mp3/mp4 (--dry-run pre-scan kept); system-firewall
  gains usage()/--help; autostart/usb-automount get flags.sh + template
- install.sh: normalize N-M range syntax in --steps
- bin/pos: INTERACTIVE_CMDS += docker-compose docker-vbox network-hotspot
- common.sh: canonical XDG-aware CONFIG_DIR + DIM color var; notify.sh
  stderr fallback; ent_plugin_* registry renames (runtime plugin API kept)
- docker-compose SCALE_DIR/CONFIG_ENV env seams; ffmpeg in PACKAGES;
  scrcpy.sh exec bit
- docs: health is console-only (--send/--markdown removed), POS.md file
  refs for config/tree/entertainment, DEV.md no-guard exception, docmap/
  filetable regenerated (make gen), hand-maintained line rows bumped
- add scripts/lint-conventions.sh gate + Makefile lint target; record
  all VERIFIED outcomes in MAINTENANCE.md; AGENT_TODO Done entry
  (2026-08-14)
- gates: make gen/check/lint all green (0 FAIL, 0 WARN); bash -n sweep
  clean; restricted-PATH dep tests + step-matrix dry-runs verified
2026-08-14 12:57:00 -04:00

273 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# POS: network scan — Parallel ping sweep of CIDR
usage() {
cat <<EOF
Usage: pos network scan <cidr> [--full] [--retries N]
Two-phase network scan using nmap.
Phase 1: Fast host discovery (finds alive hosts)
Phase 2: Full metadata scan on alive hosts only (--full only)
Options:
--full Detailed scan: OS, ports, services, NSE scripts (slower)
--retries N Retries per host in discovery (default: 1)
Examples:
pos network scan 192.168.1.0/24
pos network scan 10.0.0.0/28 --full
pos network scan 172.1.1.104
pos network scan 192.168.1.0/24 --retries 3
EOF
exit 0
}
# Deps guard before -h|--help (help also errors without nmap).
if ! command -v nmap &>/dev/null; then
echo "ERROR: nmap is required. Install with: sudo apt install nmap"
exit 1
fi
case "${1:-}" in
-h|--help|"") usage ;;
esac
net=""
full=0
retries=1
while [[ $# -gt 0 ]]; do
case "$1" in
--full) full=1; shift ;;
--retries)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo "ERROR: --retries requires a number"
exit 1
fi
retries="$2"; shift 2 ;;
*) net="$1"; shift ;;
esac
done
if [[ -z "$net" ]]; then
echo "ERROR: Missing CIDR (e.g. 192.168.1.0/24)"
exit 1
fi
# ── Input validation ───────────────────────────────────────────
# Bare IP → /32
if [[ "$net" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
net="${net}/32"
# Valid CIDR
elif [[ "$net" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
: # ok
else
echo "ERROR: Invalid target '$net'"
echo "Expected: IP (172.1.1.104) or CIDR (192.168.1.0/24)"
exit 1
fi
# ── Estimate host count ────────────────────────────────────────
cidr_bits="${net##*/}"
if [[ "$cidr_bits" -ge 24 ]]; then
host_estimate=$(( 1 << (32 - cidr_bits) ))
elif [[ "$cidr_bits" -ge 16 ]]; then
host_estimate="$(( 1 << (32 - cidr_bits) ))+"
else
host_estimate="many"
fi
is_root=0
[[ $EUID -eq 0 ]] && is_root=1
can_sudo=0
if [[ "$is_root" -eq 1 ]]; then
can_sudo=1
elif sudo -n nmap -V &>/dev/null; then
can_sudo=1
elif [[ "$full" -eq 1 && -t 0 ]]; then
can_sudo=1
fi
tmpfile=$(mktemp /tmp/scan-XXXXXX.txt)
trap 'rm -f "$tmpfile"' EXIT
# ── Phase 1: Fast host discovery ───────────────────────────────
nmap_cmd="nmap"
[[ "$can_sudo" -eq 1 ]] && nmap_cmd="sudo nmap"
echo "Discovering hosts in $net (~$host_estimate) ..."
echo
$nmap_cmd -sn -T5 -n \
--min-rate 1000 \
--min-parallelism 1024 \
--min-hostgroup 1024 \
--max-retries "$retries" \
--host-timeout 5s \
"$net" 2>/dev/null | awk '/^Nmap scan report for/ {
ip = $(NF);
gsub(/[()]/, "", ip);
print ip;
}' > "$tmpfile"
host_count=$(wc -l < "$tmpfile")
if [[ "$host_count" -eq 0 ]]; then
echo "No hosts found."
exit 0
fi
echo "Found $host_count host(s)."
echo
if [[ "$full" -eq 0 ]]; then
cat "$tmpfile"
echo
echo "Done."
exit 0
fi
# ── Phase 2: Full metadata scan ────────────────────────────────
echo "Scanning $host_count host(s) (full) ..."
echo
nmap_args="-sV --version-intensity 9 -sC -T4 -n"
nmap_args="$nmap_args --min-parallelism 256 --max-parallelism 512"
nmap_args="$nmap_args --min-hostgroup 256 --max-hostgroup 512"
nmap_args="$nmap_args --min-rate 1000 --max-retries 1"
nmap_args="$nmap_args --host-timeout 60s --max-rtt-timeout 200ms"
nmap_args="$nmap_args --script ssh-hostkey,ssl-cert,http-title,http-server-header,smb-os-discovery,nbstat,rpcinfo"
[[ "$can_sudo" -eq 1 ]] && nmap_args="$nmap_args -O --osscan-guess"
# shellcheck disable=SC2086
$nmap_cmd $nmap_args -iL "$tmpfile" 2>/dev/null | awk '
BEGIN { ip_count = 0; has_os = 0 }
/^Nmap scan report for/ {
ip = $(NF);
gsub(/[()]/, "", ip);
hostname = "";
if ($(NF) ~ /^\(/) {
hostname = $(NF-1);
} else if (NF > 4) {
if (ip != $(NF-1) && $(NF-1) !~ /^(for|[0-9])/) {
hostname = $(NF-1);
}
}
if (ip_count > 0) printf "\n";
ip_count++;
has_os = 0;
if (hostname != "" && hostname != ip)
printf "\033[1;36m%s\033[0m (%s)\n", ip, hostname;
else
printf "\033[1;36m%s\033[0m\n", ip;
}
/^MAC Address/ {
vendor = $0;
sub(/.*\(/, "", vendor);
sub(/\).*/, "", vendor);
printf " \033[2m%-10s\033[0m %s %s\n", "MAC:", $3, vendor;
}
/^Aggressive OS guesses:/ {
has_os = 1;
line = $0;
sub(/.*guesses: /, "", line);
gsub(/\s*\(.*/, "", line);
printf " \033[2m%-10s\033[0m %s\n", "OS:", line;
}
/^OS details:/ {
has_os = 1;
sub(/.*OS details: /, "");
printf " \033[2m%-10s\033[0m %s\n", "OS:", $0;
}
/^Running:/ {
sub(/.*Running: /, "");
printf " \033[2m%-10s\033[0m %s\n", "OS:", $0;
}
/^Service Info:/ {
line = $0;
sub(/.*Service Info:/, "", line);
gsub(/^ +/, "", line);
if (has_os == 0)
printf " \033[2m%-10s\033[0m %s\n", "Info:", line;
}
/^\| ssh-hostkey:/ {
line = $0;
sub(/.*ssh-hostkey:/, "", line);
gsub(/^ +/, "", line);
if (line ~ /SHA256/) {
match(line, /SHA256:[A-Za-z0-9+\/=]+/);
key = substr(line, RSTART, RLENGTH);
printf " \033[2m%-10s\033[0m %s\n", "SSH:", key;
}
}
/^\| http-title:/ {
line = $0;
sub(/.*http-title:/, "", line);
gsub(/^ +/, "", line);
gsub(/\s*\[.*$/, "", line);
if (line != "" && line !~ /^No/)
printf " \033[2m%-10s\033[0m %s\n", "HTTP Title:", line;
}
/^\| http-server-header:/ {
line = $0;
sub(/.*http-server-header:/, "", line);
gsub(/^ +/, "", line);
printf " \033[2m%-10s\033[0m %s\n", "HTTP Server:", line;
}
/^\|_?NetBIOS name:/ {
line = $0;
sub(/.*NetBIOS name:/, "", line);
sub(/,.*$/, "", line);
gsub(/^ +/, "", line);
printf " \033[2m%-10s\033[0m %s\n", "NetBIOS:", line;
}
/^\|_?SMB OS:/ {
line = $0;
sub(/.*SMB OS:/, "", line);
gsub(/^ +/, "", line);
printf " \033[2m%-10s\033[0m %s\n", "SMB:", line;
}
/^\|_?Domain:/ {
line = $0;
sub(/.*Domain:/, "", line);
gsub(/^ +/, "", line);
if (line != "" && line !~ /^WORKGROUP/)
printf " \033[2m%-10s\033[0m %s\n", "Domain:", line;
}
/^\| [0-9]+\/tcp/ {
line = $0;
gsub(/^ *\| */, "", line);
printf " \033[2m%-10s\033[0m %s\n", "RPC:", line;
}
/^[0-9]+\/tcp[[:space:]]+open/ {
port = $1; service = $3;
version = "";
for (i = 4; i <= NF; i++) version = version " " $i;
gsub(/^ +/, "", version);
if (version != "")
printf " \033[2m%-10s\033[0m %s — %s\n", port, service, version;
else
printf " \033[2m%-10s\033[0m %s\n", port, service;
}
'
echo
echo "Done."