Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
Executable
+425
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#########################################################################################
|
||||
#### port-scan-reporter.sh — Scan hosts for open ports and compare against baseline ####
|
||||
#### Reports new open ports, closed ports, and service version changes ####
|
||||
#### Requires: bash 4+, nmap ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version 1.00 ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./port-scan-reporter.sh --target 192.168.1.1 ####
|
||||
#### ./port-scan-reporter.sh --targets-file hosts.txt --save-baseline ####
|
||||
#### ####
|
||||
#### See --help for all options. ####
|
||||
#########################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults & State ──────────────────────────────────────────────────
|
||||
SCRIPT_NAME="$(basename "$0")"; readonly SCRIPT_NAME
|
||||
TARGET=""; TARGETS_FILE=""; BASELINE_DIR="${HOME:-/tmp}/.port-scan-baselines"
|
||||
SAVE_BASELINE=false; OUTPUT_JSON=false; TEXTFILE_PATH=""; PORTS=""; COLOR="auto"
|
||||
TOTAL_HOSTS=0; HOSTS_WITH_CHANGES=0; TOTAL_NEW=0; TOTAL_CLOSED=0; TOTAL_CHANGED=0
|
||||
JSON_RESULTS=(); PROM_METRICS=()
|
||||
|
||||
# ── Colors ────────────────────────────────────────────────────────────
|
||||
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" RESET=""
|
||||
setup_colors() {
|
||||
if [[ "$COLOR" == "never" ]]; then
|
||||
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" RESET=""
|
||||
return
|
||||
fi
|
||||
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
else
|
||||
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" RESET=""
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────
|
||||
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
||||
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
|
||||
# ── Dependency check ─────────────────────────────────────────────────
|
||||
check_deps() {
|
||||
if ! command -v nmap &>/dev/null; then
|
||||
err "nmap is required but not installed"
|
||||
echo " Install: apt install nmap / yum install nmap / brew install nmap" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Usage ─────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${SCRIPT_NAME} — Scan hosts for open ports and compare against baseline
|
||||
|
||||
USAGE:
|
||||
${SCRIPT_NAME} --target <host>
|
||||
${SCRIPT_NAME} --targets-file <file> [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--target <host> Single host to scan (IP or hostname)
|
||||
--targets-file <file> File with one host per line
|
||||
--baseline-dir <path> Baseline directory (default: ~/.port-scan-baselines/)
|
||||
--save-baseline Save current scan as the new baseline
|
||||
--json Output results as JSON
|
||||
--textfile <path> Write Prometheus metrics to file
|
||||
--ports <range> Port range (e.g., "22,80,443" or "1-1024", default: nmap top 1000)
|
||||
--no-color Disable colored output
|
||||
--help Show this help
|
||||
|
||||
EXAMPLES:
|
||||
${SCRIPT_NAME} --target 192.168.1.1
|
||||
${SCRIPT_NAME} --targets-file hosts.txt --save-baseline
|
||||
${SCRIPT_NAME} --target 10.0.0.5 --ports 22,80,443,8080 --json
|
||||
${SCRIPT_NAME} --targets-file /etc/hosts.txt --textfile /var/lib/node_exporter/port_scan.prom
|
||||
|
||||
EXIT CODES: 0 = no changes, 1 = changes detected, 2 = error
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── Argument parsing ─────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
[[ $# -lt 2 ]] && { err "--target requires a host"; exit 2; }
|
||||
TARGET="$2"; shift 2 ;;
|
||||
--targets-file)
|
||||
[[ $# -lt 2 ]] && { err "--targets-file requires a path"; exit 2; }
|
||||
TARGETS_FILE="$2"; shift 2 ;;
|
||||
--baseline-dir)
|
||||
[[ $# -lt 2 ]] && { err "--baseline-dir requires a path"; exit 2; }
|
||||
BASELINE_DIR="$2"; shift 2 ;;
|
||||
--save-baseline)
|
||||
SAVE_BASELINE=true; shift ;;
|
||||
--json)
|
||||
OUTPUT_JSON=true; shift ;;
|
||||
--textfile)
|
||||
[[ $# -lt 2 ]] && { err "--textfile requires a path"; exit 2; }
|
||||
TEXTFILE_PATH="$2"; shift 2 ;;
|
||||
--ports)
|
||||
[[ $# -lt 2 ]] && { err "--ports requires a range"; exit 2; }
|
||||
PORTS="$2"; shift 2 ;;
|
||||
--no-color)
|
||||
COLOR="never"; shift ;;
|
||||
--help|-h)
|
||||
usage; exit 0 ;;
|
||||
*)
|
||||
err "Unknown option: $1"
|
||||
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TARGET" && -z "$TARGETS_FILE" ]]; then
|
||||
err "Must specify --target or --targets-file"
|
||||
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -n "$TARGET" && -n "$TARGETS_FILE" ]]; then
|
||||
err "Cannot use both --target and --targets-file"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -n "$TARGETS_FILE" && ! -f "$TARGETS_FILE" ]]; then
|
||||
err "Targets file not found: ${TARGETS_FILE}"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Build host list ──────────────────────────────────────────────────
|
||||
build_host_list() {
|
||||
local hosts=()
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
hosts+=("$TARGET")
|
||||
else
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="${line// /}"
|
||||
[[ -z "$line" ]] && continue
|
||||
hosts+=("$line")
|
||||
done < "$TARGETS_FILE"
|
||||
fi
|
||||
if [[ ${#hosts[@]} -eq 0 ]]; then
|
||||
err "No hosts to scan"
|
||||
exit 2
|
||||
fi
|
||||
printf '%s\n' "${hosts[@]}"
|
||||
}
|
||||
|
||||
# ── Host to baseline filename ────────────────────────────────────────
|
||||
host_to_filename() { echo "${1//[.:\/ ]/_}.baseline"; }
|
||||
|
||||
# ── Scan a single host ──────────────────────────────────────────────
|
||||
scan_host() {
|
||||
local host="$1"
|
||||
local nmap_args=(-sV --open -T4 --host-timeout 120)
|
||||
[[ -n "$PORTS" ]] && nmap_args+=(-p "$PORTS")
|
||||
nmap_args+=("$host")
|
||||
local raw_output
|
||||
raw_output=$(nmap "${nmap_args[@]}" 2>/dev/null) || {
|
||||
warn "nmap scan failed for ${host}"
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "$raw_output" | awk '
|
||||
/^[0-9]+\/(tcp|udp)/ {
|
||||
port_proto = $1
|
||||
state = $2
|
||||
service = ""
|
||||
for (i = 3; i <= NF; i++) {
|
||||
if (service != "") service = service " "
|
||||
service = service $i
|
||||
}
|
||||
if (service == "") service = "unknown"
|
||||
print port_proto " " state " " service
|
||||
}
|
||||
' | sort -t/ -k1 -n
|
||||
}
|
||||
|
||||
# ── Compare scan to baseline ────────────────────────────────────────
|
||||
compare_to_baseline() {
|
||||
local host="$1"
|
||||
local current_file="$2"
|
||||
local baseline_file="${BASELINE_DIR}/$(host_to_filename "$host")"
|
||||
|
||||
local new_ports=() closed_ports=() changed_services=()
|
||||
if [[ ! -f "$baseline_file" ]]; then
|
||||
if [[ "$OUTPUT_JSON" != true ]]; then
|
||||
echo -e " ${CYAN}No baseline found — all ports reported as new${RESET}"
|
||||
fi
|
||||
local count
|
||||
count=$(wc -l < "$current_file")
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
while IFS= read -r line; do
|
||||
new_ports+=("$line")
|
||||
done < "$current_file"
|
||||
fi
|
||||
else
|
||||
# Find NEW ports (in current, not in baseline)
|
||||
while IFS= read -r line; do
|
||||
local port_proto
|
||||
port_proto=$(echo "$line" | awk '{print $1}')
|
||||
if ! grep -q "^${port_proto} " "$baseline_file" 2>/dev/null; then
|
||||
new_ports+=("$line")
|
||||
fi
|
||||
done < "$current_file"
|
||||
|
||||
# Find CLOSED ports (in baseline, not in current)
|
||||
while IFS= read -r line; do
|
||||
local port_proto
|
||||
port_proto=$(echo "$line" | awk '{print $1}')
|
||||
if ! grep -q "^${port_proto} " "$current_file" 2>/dev/null; then
|
||||
closed_ports+=("$line")
|
||||
fi
|
||||
done < "$baseline_file"
|
||||
|
||||
# Find CHANGED services (same port, different service)
|
||||
while IFS= read -r line; do
|
||||
local port_proto current_rest baseline_line baseline_rest
|
||||
port_proto=$(echo "$line" | awk '{print $1}')
|
||||
current_rest=$(echo "$line" | awk '{$1=""; print}' | sed 's/^ //')
|
||||
baseline_line=$(grep "^${port_proto} " "$baseline_file" 2>/dev/null || true)
|
||||
if [[ -n "$baseline_line" ]]; then
|
||||
baseline_rest=$(echo "$baseline_line" | awk '{$1=""; print}' | sed 's/^ //')
|
||||
if [[ "$current_rest" != "$baseline_rest" ]]; then
|
||||
changed_services+=("${port_proto} ${baseline_rest} → ${current_rest}")
|
||||
fi
|
||||
fi
|
||||
done < "$current_file"
|
||||
fi
|
||||
|
||||
local host_new=${#new_ports[@]} host_closed=${#closed_ports[@]} host_changed=${#changed_services[@]}
|
||||
local host_total_changes=$(( host_new + host_closed + host_changed ))
|
||||
|
||||
if [[ "$OUTPUT_JSON" != true && "$host_total_changes" -gt 0 ]]; then
|
||||
echo ""
|
||||
echo -e " ${BOLD}Comparing against baseline...${RESET}"
|
||||
echo ""
|
||||
for entry in "${new_ports[@]}"; do
|
||||
read -r pp _ svc <<< "$entry"
|
||||
echo -e " ${RED}[NEW]${RESET} ${pp} open ${svc}"
|
||||
done
|
||||
for entry in "${closed_ports[@]}"; do
|
||||
read -r pp _ svc <<< "$entry"
|
||||
echo -e " ${YELLOW}[CLOSED]${RESET} ${pp} open ${svc}"
|
||||
done
|
||||
for entry in "${changed_services[@]}"; do
|
||||
echo -e " ${YELLOW}[CHANGED]${RESET} ${entry}"
|
||||
done
|
||||
elif [[ "$OUTPUT_JSON" != true && "$host_total_changes" -eq 0 ]]; then
|
||||
echo ""
|
||||
echo -e " ${GREEN}No changes from baseline${RESET}"
|
||||
fi
|
||||
|
||||
TOTAL_NEW=$(( TOTAL_NEW + host_new ))
|
||||
TOTAL_CLOSED=$(( TOTAL_CLOSED + host_closed ))
|
||||
TOTAL_CHANGED=$(( TOTAL_CHANGED + host_changed ))
|
||||
if [[ "$host_total_changes" -gt 0 ]]; then
|
||||
HOSTS_WITH_CHANGES=$(( HOSTS_WITH_CHANGES + 1 ))
|
||||
fi
|
||||
|
||||
local open_count
|
||||
open_count=$(wc -l < "$current_file")
|
||||
if [[ "$OUTPUT_JSON" == true ]]; then
|
||||
local json_entry
|
||||
json_entry=$(build_host_json "$host" "$current_file" "$open_count" \
|
||||
"$host_new" "$host_closed" "$host_changed")
|
||||
JSON_RESULTS+=("$json_entry")
|
||||
fi
|
||||
|
||||
if [[ -n "$TEXTFILE_PATH" ]]; then
|
||||
PROM_METRICS+=("port_scan_open_ports{host=\"${host}\"} ${open_count}")
|
||||
PROM_METRICS+=("port_scan_new_ports{host=\"${host}\"} ${host_new}")
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Build JSON for a single host ─────────────────────────────────────
|
||||
build_host_json() {
|
||||
local host="$1" current_file="$2" open_count="$3"
|
||||
local new_count="$4" closed_count="$5" changed_count="$6"
|
||||
local ports_json="[" first=true
|
||||
while IFS= read -r line; do
|
||||
read -r pp state svc_rest <<< "$line"
|
||||
[[ "$first" == true ]] && first=false || ports_json+=","
|
||||
ports_json+="{\"port\":\"${pp}\",\"state\":\"${state}\",\"service\":\"${svc_rest:-unknown}\"}"
|
||||
done < "$current_file"
|
||||
ports_json+="]"
|
||||
echo "{\"host\":\"${host}\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"open_ports\":${open_count},\"new_ports\":${new_count},\"closed_ports\":${closed_count},\"changed_services\":${changed_count},\"ports\":${ports_json}}"
|
||||
}
|
||||
|
||||
# ── Print summary ────────────────────────────────────────────────────
|
||||
print_summary() {
|
||||
if [[ "$OUTPUT_JSON" == true ]]; then
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
echo " ───────────────────────────────────────────"
|
||||
echo -e " ${BOLD}Summary${RESET}"
|
||||
printf " %-22s %d\n" "Hosts scanned:" "$TOTAL_HOSTS"
|
||||
printf " %-22s %d\n" "Hosts with changes:" "$HOSTS_WITH_CHANGES"
|
||||
printf " %-22s %d\n" "New open ports:" "$TOTAL_NEW"
|
||||
printf " %-22s %d\n" "Closed ports:" "$TOTAL_CLOSED"
|
||||
printf " %-22s %d\n" "Changed services:" "$TOTAL_CHANGED"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Write JSON output ───────────────────────────────────────────────
|
||||
write_json() {
|
||||
local first=true; echo "["
|
||||
for entry in "${JSON_RESULTS[@]}"; do
|
||||
[[ "$first" == true ]] && first=false || echo ","
|
||||
echo -n " ${entry}"
|
||||
done
|
||||
echo ""; echo "]"
|
||||
}
|
||||
|
||||
# ── Write Prometheus textfile ────────────────────────────────────────
|
||||
write_textfile() {
|
||||
local tmpfile="${TEXTFILE_PATH}.$$"
|
||||
{
|
||||
echo "# HELP port_scan_open_ports Number of open ports detected on host"
|
||||
echo "# TYPE port_scan_open_ports gauge"
|
||||
echo "# HELP port_scan_new_ports Number of new ports since last baseline"
|
||||
echo "# TYPE port_scan_new_ports gauge"
|
||||
echo "# HELP port_scan_changes_total Total port changes across all hosts"
|
||||
echo "# TYPE port_scan_changes_total gauge"
|
||||
for metric in "${PROM_METRICS[@]}"; do
|
||||
echo "$metric"
|
||||
done
|
||||
local total_changes=$(( TOTAL_NEW + TOTAL_CLOSED + TOTAL_CHANGED ))
|
||||
echo "port_scan_changes_total ${total_changes}"
|
||||
} > "$tmpfile"
|
||||
mv "$tmpfile" "$TEXTFILE_PATH"
|
||||
log "Prometheus metrics written to ${TEXTFILE_PATH}"
|
||||
}
|
||||
|
||||
# ── Save baseline ───────────────────────────────────────────────────
|
||||
save_baseline() {
|
||||
local host="$1" current_file="$2"
|
||||
local baseline_file="${BASELINE_DIR}/$(host_to_filename "$host")"
|
||||
cp "$current_file" "$baseline_file"
|
||||
log "Baseline saved for ${host} → ${baseline_file}"
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
parse_args "$@"
|
||||
setup_colors
|
||||
check_deps
|
||||
mkdir -p "$BASELINE_DIR"
|
||||
local hosts; hosts=$(build_host_list)
|
||||
if [[ "$OUTPUT_JSON" != true ]]; then
|
||||
echo ""
|
||||
echo -e "${BOLD}Port Scan Reporter${RESET}"
|
||||
echo "═══════════════════════════════════════════"
|
||||
fi
|
||||
|
||||
local port_display="default top 1000"
|
||||
[[ -n "$PORTS" ]] && port_display="$PORTS"
|
||||
while IFS= read -r host; do
|
||||
TOTAL_HOSTS=$(( TOTAL_HOSTS + 1 ))
|
||||
if [[ "$OUTPUT_JSON" != true ]]; then
|
||||
echo ""
|
||||
echo -e "Scanning ${BOLD}${host}${RESET} (ports: ${port_display})..."
|
||||
fi
|
||||
|
||||
local tmpfile; tmpfile=$(mktemp)
|
||||
trap "rm -f '$tmpfile'" EXIT
|
||||
if ! scan_host "$host" > "$tmpfile"; then
|
||||
warn "Skipping ${host} — scan failed"
|
||||
rm -f "$tmpfile"
|
||||
continue
|
||||
fi
|
||||
|
||||
local open_count; open_count=$(wc -l < "$tmpfile")
|
||||
if [[ "$OUTPUT_JSON" != true && "$open_count" -gt 0 ]]; then
|
||||
echo ""
|
||||
printf " %-14s %-8s %s\n" "PORT/PROTO" "STATE" "SERVICE"
|
||||
while IFS= read -r line; do
|
||||
read -r pp state svc <<< "$line"
|
||||
printf " %-14s %-8s %s\n" "$pp" "$state" "$svc"
|
||||
done < "$tmpfile"
|
||||
elif [[ "$OUTPUT_JSON" != true ]]; then
|
||||
echo " No open ports found"
|
||||
fi
|
||||
|
||||
compare_to_baseline "$host" "$tmpfile"
|
||||
if [[ "$SAVE_BASELINE" == true ]]; then
|
||||
save_baseline "$host" "$tmpfile"
|
||||
fi
|
||||
rm -f "$tmpfile"
|
||||
done <<< "$hosts"
|
||||
|
||||
if [[ "$OUTPUT_JSON" == true ]]; then
|
||||
write_json
|
||||
else
|
||||
print_summary
|
||||
fi
|
||||
|
||||
[[ -n "$TEXTFILE_PATH" ]] && write_textfile
|
||||
if [[ "$SAVE_BASELINE" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
local total_changes=$(( TOTAL_NEW + TOTAL_CLOSED + TOTAL_CHANGED ))
|
||||
if [[ "$total_changes" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user