--- name: network-reconnaissance description: "Class-level skill for network reconnaissance from containerized and general environments. Covers internal subnet scanning (TCP sweep, port scan, service detection, TLS/SSH analysis), external domain/service reconnaissance (DNS, SSL, HTTP headers, API discovery), and automated recurring scan setups." version: 1.0.0 tags: [network, reconnaissance, scanning, security, dns, ssl, ports, containers] --- ## Overview This umbrella skill consolidates all network scanning and reconnaissance capabilities: - **Internal Subnet Scanning** — TCP sweep, port scanning, banner grabbing, SSH/TLS analysis from containers or Python - **External Domain Reconnaissance** — DNS, SSL certificates, HTTP headers, API/service discovery - **Automated Recurring Scans** — Weekly cronjob-based scanning with change detection No nmap, no root privileges required. Pure Python stdlib (`socket`, `ssl`, `concurrent.futures`) for container environments. --- ## Section 1: Internal Subnet Scanning ### 1.1 Network Layout Discovery **Always discover the actual IP format first.** Users may describe segments as "50.x" but the real network may be `10.0.X.Y`. ```bash ip addr show eth0 | grep inet ip route show ping -c 1 -W 2 10.0.50.1 ping -c 1 -W 2 50.0.50.1 ``` Common formats: - `10.0.X.Y` where X = segment index (e.g., 10.0.20.10 = Hypervisor) - Container usually sits in one segment (e.g., 10.0.30.230) ### 1.2 Two-Phase Scan Workflow **Phase 1 — Host Discovery (fast TCP sweep)** ```python import socket, concurrent.futures PROBE_PORTS = [443, 80, 22, 2222, 3306, 8080, 8443] def host_alive(ip, ports=PROBE_PORTS, timeout=0.3): for p in ports: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) if sock.connect_ex((ip, p)) == 0: sock.close(); return True sock.close() return False with concurrent.futures.ThreadPoolExecutor(100) as pool: alive = [ip for ip, ok in zip(segment_ips, pool.map(host_alive, segment_ips)) if ok] ``` **Phase 2 — Deep Scan (per host)** ```python IMPORTANT_PORTS = { 22: "SSH", 23: "Telnet", 25: "SMTP", 53: "DNS", 80: "HTTP", 443: "HTTPS/TLS", 1883: "MQTT", 2222: "SSH-alt", 2375: "Docker", 2376: "Docker-TLS", 3306: "MySQL", 5432: "PostgreSQL", 6379: "Redis", 8080: "HTTP-alt", 8443: "HTTPS-alt", 8883: "MQTT-TLS", 9090: "Prometheus/Grafana", 9200: "Elasticsearch", 11211: "Memcached", 27017: "MongoDB", } def deep_scan(ip): hostname = socket.gethostbyaddr(ip)[0] # may be None ports = scan_host_ports(ip, IMPORTANT_PORTS, workers=30) tls = analyze_tls(ip) if any(p[0]==443 for p in ports) else None ssh = get_ssh_banner(ip, 22) or get_ssh_banner(ip, 2222) web = get_web_header(ip, 80) return {"ip": ip, "hostname": hostname, "ports": ports, "tls": tls, "ssh": ssh, "web_server": web} ``` ### 1.3 TLS Analysis ```python def analyze_tls(ip, port=443, timeout=5.0): ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE try: with socket.create_connection((ip, port), timeout=timeout) as sock: with ctx.wrap_socket(sock, server_hostname=ip) as s: return { "protocol": s.version(), "cipher": s.cipher()[0], "cert_cn": next((f[0][1] for f in s.getpeercert().get("subject",[]) if f[0][0]=="commonName"), None), "cert_issuer": next((f[0][1] for f in s.getpeercert().get("issuer",[]) if f[0][0]=="commonName"), None), "valid_from": s.getpeercert().get("notBefore"), "valid_until": s.getpeercert().get("notAfter"), } except Exception as e: return {"error": str(e)[:80]} ``` ### 1.4 SSH Banner Grab ```python def get_ssh_banner(ip, port=22, timeout=5.0): sock = socket.create_connection((ip, port), timeout=timeout) banner = sock.recv(256).decode("utf-8", errors="ignore").strip() sock.close(); return banner ``` ### 1.5 Output Format ```json { "scan_date": "2026-05-17T08:03:11.184435+00:00", "total_hosts": 60, "scan_duration_seconds": 94.4, "segments": { "Hypervisor": {"subnet": "10.0.20.0/24", "alive_count": 10, "hosts": [...]}, "Container": {"subnet": "10.0.30.0/24", "alive_count": 26, "hosts": [...]}, "Smart Home": {"subnet": "10.0.50.0/24", "alive_count": 24, "hosts": [...]} } } ``` - `hostname` is usually `null` (no reverse-DNS in internal networks) - `vendor` and `mac` are NOT included (no MAC access from container) --- ## Section 2: External Domain & Service Reconnaissance ### 2.1 DNS Resolution ```bash getent hosts cloud.familie-schoen.com host cloud.familie-schoen.com ``` ### 2.2 Port Scanning (Bash /dev/tcp) ```bash bash -c 'echo >/dev/tcp/HOST/PORT 2>&1 && echo open || echo closed' # Common ports: 22, 25, 53, 80, 443, 587, 993, 995, 3306, 5432, 8080, 8443, 9090 ``` ### 2.3 HTTP/HTTPS Analysis ```bash curl -sI http://DOMAIN curl -ksI https://DOMAIN curl -kI -L https/DOMAIN curl -ks https://DOMAIN/path # Security headers: curl -ksI https://DOMAIN | grep -iE 'strict-transport|content-security|x-frame|x-xss|cache-control' ``` ### 2.4 SSL Certificate Extraction ```bash openssl s_client -connect HOST:443 -servername HOST 2>&1 &1 [^<]+' curl -ks https://DOMAIN | grep -ioE 'wordpress|django|flask|react|angular|nextcloud|seafile|gitea|jenkins|grafana|prometheus' ``` --- ## Section 3: Automated Recurring Scanning For SRE-style weekly network scans with change detection, set up a `no_agent` cronjob: ```bash # 1. Write the scan script to ~/.hermes/scripts/network-scan.py # 2. Create the cronjob: cronjob action=create \ name="weekly-network-scan" \ script="network-scan.py" \ schedule="0 8 * * 0" \ no_agent=true \ deliver="origin" ``` **Critical rules:** - `script` parameter accepts **filename only** (relative to `~/.hermes/scripts/`), NOT an absolute path. - Prefer `--no-agent` for pure script execution — faster, avoids model format issues. - After any Hermes update, verify cron job model format (see `references/cron-model-fix.md`). ### Change Detection Load the previous scan file and diff: - New IPs → new hosts - Missing IPs → gone hosts - New ports on existing hosts → new services - Different SSH banners → version changes --- ## Pitfalls | Pitfall | Symptom | Solution | |---------|---------|----------| | Using nmap | Permission denied | Use Python stdlib only | | Using arping | Hangs forever | NEVER use arping in containers | | Using `ip neigh` for MAC | No MACs returned | OUI vendor detection is useless from container | | Sequential TCP scan | 14+ min for /24 | Use ThreadPoolExecutor (100 workers sweep, 30 deep scan) | | Wrong IP format | Can't reach targets | Verify with `ip route` and `ping` first | | `script` path absolute | Cronjob fails | Use filename only in `script` param | | Cron model null after update | `Error code: 400` | Update with `provider/model` format | | ShareGPT first-run delay | 2–5 min wait | Pre-download once before benchmark suite | | NumPy X86_V2 crash | `RuntimeError: NumPy...X86_V2` | `pip install "numpy<2.0" --force-reinstall` | | Prefill throughput bias | First request inflates latency | Pre-warm with throwaway run, or filter `turn_index=0` from JSONL | --- ## References - `references/container-network-recon-session-2026-04.md` — Session notes from initial network discovery - `references/domain-service-recon-familie-schoen.md` — External domain scan example - `references/cron-model-fix.md` — Post-Hermes-update cron job model fix - `scripts/network-scan.py` — Full internal scan script (copy to `~/.hermes/scripts/`)