Add deep-check-device.sh (host-level, invoked via SSH forced-command)
Part of #15's fourth piece: on-demand active investigation of a single unknown device, admin-triggered from the dashboard. mDNS resolve, targeted SSDP/UPnP query (many smart-home devices announce a friendlyName/manufacturer this way), curated port scan, HTTP title/server grab on anything open. Runs on the CT122 host for the same multicast-needs-real-network-access reason discover-devices.sh does. Not wired into the API yet -- that's next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Forced command for a dedicated SSH key entry on CT122's OWN root user (see
|
||||||
|
# docs/device-discovery.md). Unlike the generic collector forced-commands on
|
||||||
|
# omv/ripper, this one is parameterized: forced commands ignore whatever the
|
||||||
|
# client literally requested, but OpenSSH still exposes it via
|
||||||
|
# $SSH_ORIGINAL_COMMAND, which this script reads and validates strictly
|
||||||
|
# before using -- never passed to a shell for interpretation.
|
||||||
|
#
|
||||||
|
# Runs a handful of *targeted, on-demand* active probes against a single LAN
|
||||||
|
# IP (admin-triggered from the dashboard, not automatic/scheduled): mDNS
|
||||||
|
# reverse resolve, SSDP/UPnP query (many smart-home devices announce a
|
||||||
|
# friendlyName/manufacturer this way), a small curated TCP port scan, and an
|
||||||
|
# HTTP title/server grab on anything open. Bounded timeouts throughout so a
|
||||||
|
# single check finishes in well under 15s.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IP="${SSH_ORIGINAL_COMMAND:-}"
|
||||||
|
if ! [[ "$IP" =~ ^192\.168\.1\.([0-9]{1,3})$ ]]; then
|
||||||
|
echo '{"error":"invalid or missing target IP"}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
OCTET="${BASH_REMATCH[1]}"
|
||||||
|
if (( OCTET < 1 || OCTET > 254 )); then
|
||||||
|
echo '{"error":"target IP out of range"}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MDNS=$(timeout 2 avahi-resolve -a "$IP" 2>/dev/null | awk '{print $2}' || true)
|
||||||
|
RDNS=$(timeout 2 getent hosts "$IP" 2>/dev/null | awk '{print $2}' | head -1 || true)
|
||||||
|
|
||||||
|
SSDP_LOCATION=$(timeout 4 python3 - "$IP" <<'PYEOF' || true
|
||||||
|
import socket, sys, time
|
||||||
|
target = sys.argv[1]
|
||||||
|
msg = "\r\n".join([
|
||||||
|
"M-SEARCH * HTTP/1.1", "HOST: 239.255.255.250:1900",
|
||||||
|
'MAN: "ssdp:discover"', "MX: 3", "ST: ssdp:all", "", ""
|
||||||
|
]).encode()
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.settimeout(3)
|
||||||
|
sock.sendto(msg, ("239.255.255.250", 1900))
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < 3:
|
||||||
|
try:
|
||||||
|
data, addr = sock.recvfrom(4096)
|
||||||
|
if addr[0] == target:
|
||||||
|
for line in data.decode(errors="replace").split("\r\n"):
|
||||||
|
if line.lower().startswith("location:"):
|
||||||
|
print(line.split(":", 1)[1].strip())
|
||||||
|
sys.exit(0)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
|
||||||
|
SSDP_FRIENDLY="" SSDP_MANUFACTURER="" SSDP_MODEL=""
|
||||||
|
if [[ -n "$SSDP_LOCATION" ]]; then
|
||||||
|
XML=$(timeout 3 curl -s -m 2 "$SSDP_LOCATION" 2>/dev/null || true)
|
||||||
|
SSDP_FRIENDLY=$(echo "$XML" | grep -o -m1 '<friendlyName>[^<]*' | sed 's/<friendlyName>//' || true)
|
||||||
|
SSDP_MANUFACTURER=$(echo "$XML" | grep -o -m1 '<manufacturer>[^<]*' | sed 's/<manufacturer>//' || true)
|
||||||
|
SSDP_MODEL=$(echo "$XML" | grep -o -m1 '<modelName>[^<]*' | sed 's/<modelName>//' || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORTS=(21 22 23 80 443 554 5000 8000 8008 8009 8060 8080 8443 9100 32400 62078)
|
||||||
|
OPEN_PORTS_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$OPEN_PORTS_DIR"' EXIT
|
||||||
|
for p in "${PORTS[@]}"; do
|
||||||
|
( timeout 1 bash -c "echo >/dev/tcp/$IP/$p" 2>/dev/null && touch "$OPEN_PORTS_DIR/$p" ) &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
open_ports=()
|
||||||
|
for p in "${PORTS[@]}"; do
|
||||||
|
[[ -f "$OPEN_PORTS_DIR/$p" ]] && open_ports+=("$p")
|
||||||
|
done
|
||||||
|
|
||||||
|
http_titles=()
|
||||||
|
for p in "${open_ports[@]}"; do
|
||||||
|
case "$p" in
|
||||||
|
80|8000|8008|8080|8060|5000) scheme="http" ;;
|
||||||
|
443|8443) scheme="https" ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
title=$(timeout 3 curl -sk -m 2 "$scheme://$IP:$p/" 2>/dev/null | grep -o -m1 -i '<title>[^<]*' | sed -E 's/<title>//i' || true)
|
||||||
|
server=$(timeout 3 curl -sIk -m 2 "$scheme://$IP:$p/" 2>/dev/null | grep -i '^server:' | head -1 | cut -d: -f2- | tr -d '\r' | sed 's/^ *//' || true)
|
||||||
|
http_titles+=("$p|$title|$server")
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 - "$IP" "$MDNS" "$RDNS" "$SSDP_LOCATION" "$SSDP_FRIENDLY" "$SSDP_MANUFACTURER" "$SSDP_MODEL" \
|
||||||
|
"$(IFS=,; echo "${open_ports[*]}")" "$(printf '%s\n' "${http_titles[@]}")" <<'PYEOF'
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
ip, mdns, rdns, ssdp_loc, ssdp_friendly, ssdp_mfr, ssdp_model, ports_csv, http_raw = sys.argv[1:10]
|
||||||
|
|
||||||
|
http = []
|
||||||
|
for line in http_raw.splitlines():
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
port, title, server = (line.split("|", 2) + ["", ""])[:3]
|
||||||
|
http.append({
|
||||||
|
"port": int(port),
|
||||||
|
"title": title or None,
|
||||||
|
"server": server or None,
|
||||||
|
})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"ip": ip,
|
||||||
|
"mdnsHostname": mdns or None,
|
||||||
|
"reverseDns": rdns or None,
|
||||||
|
"ssdp": {
|
||||||
|
"location": ssdp_loc or None,
|
||||||
|
"friendlyName": ssdp_friendly or None,
|
||||||
|
"manufacturer": ssdp_mfr or None,
|
||||||
|
"modelName": ssdp_model or None,
|
||||||
|
} if ssdp_loc else None,
|
||||||
|
"openPorts": [int(p) for p in ports_csv.split(",") if p],
|
||||||
|
"http": http,
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
|
PYEOF
|
||||||
Reference in New Issue
Block a user