Add optional Fingerbank device fingerprinting to deep-check
CI / web (push) Successful in 16s
CI / api (push) Successful in 21s

Folded into the existing on-demand Deep check button: queries
Fingerbank's interrogate API with the device's MAC plus the SSDP
SERVER header when deep-check-device.sh finds one, showing the
confidence band alongside the result. Runs directly from the API
container (no host-level access needed, just an outbound HTTPS call),
unlike the SSDP/mDNS steps.

Confirmed via direct testing: without DHCP fingerprint data (which we
structurally don't have, not being the DHCP server), MAC-only queries
often can't get past manufacturer-level confidence -- same info the
free OUI lookup already provides. Documented honestly in
docs/device-discovery.md rather than overselling it. Still worth
having as opt-in enrichment for devices that do expose richer signals.

Gated behind optional FINGERBANK_API_KEY -- missing key, API errors,
or no match all degrade gracefully without affecting the rest of
deep-check's local findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 23:35:35 -06:00
parent ee9b6d15cd
commit d0d6ae95f1
11 changed files with 183 additions and 13 deletions
+17 -5
View File
@@ -28,7 +28,11 @@ 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
# Prints LOCATION and SERVER headers (one per line) from the first ssdp:all
# response the target IP sends -- SERVER is a useful extra fingerprinting
# signal (e.g. "Linux/3.10 UPnP/1.0 MiniDLNA/1.1") beyond just the device
# descriptor XML, fed to Fingerbank as an upnp_user_agents hint.
SSDP_RAW=$(timeout 4 python3 - "$IP" <<'PYEOF' || true
import socket, sys, time
target = sys.argv[1]
msg = "\r\n".join([
@@ -44,14 +48,21 @@ while time.time() - start < 3:
try:
data, addr = sock.recvfrom(4096)
if addr[0] == target:
location = server = ""
for line in data.decode(errors="replace").split("\r\n"):
if line.lower().startswith("location:"):
print(line.split(":", 1)[1].strip())
sys.exit(0)
location = line.split(":", 1)[1].strip()
elif line.lower().startswith("server:"):
server = line.split(":", 1)[1].strip()
print(location)
print(server)
sys.exit(0)
except socket.timeout:
break
PYEOF
)
SSDP_LOCATION=$(echo "$SSDP_RAW" | sed -n '1p')
SSDP_SERVER=$(echo "$SSDP_RAW" | sed -n '2p')
SSDP_FRIENDLY="" SSDP_MANUFACTURER="" SSDP_MODEL=""
if [[ -n "$SSDP_LOCATION" ]]; then
@@ -85,11 +96,11 @@ for p in "${open_ports[@]}"; do
http_titles+=("$p|$title|$server")
done
python3 - "$IP" "$MDNS" "$RDNS" "$SSDP_LOCATION" "$SSDP_FRIENDLY" "$SSDP_MANUFACTURER" "$SSDP_MODEL" \
python3 - "$IP" "$MDNS" "$RDNS" "$SSDP_LOCATION" "$SSDP_SERVER" "$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]
ip, mdns, rdns, ssdp_loc, ssdp_server, ssdp_friendly, ssdp_mfr, ssdp_model, ports_csv, http_raw = sys.argv[1:11]
http = []
for line in http_raw.splitlines():
@@ -108,6 +119,7 @@ result = {
"reverseDns": rdns or None,
"ssdp": {
"location": ssdp_loc or None,
"server": ssdp_server or None,
"friendlyName": ssdp_friendly or None,
"manufacturer": ssdp_mfr or None,
"modelName": ssdp_model or None,