Files

8.2 KiB
Raw Permalink Blame History

name, description, version, tags
name description version tags
network-reconnaissance 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. 1.0.0
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.

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)

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)

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

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

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

{
  "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

getent hosts cloud.familie-schoen.com
host cloud.familie-schoen.com

2.2 Port Scanning (Bash /dev/tcp)

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

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

openssl s_client -connect HOST:443 -servername HOST 2>&1 </dev/null
openssl s_client -connect HOST:443 -servername HOST 2>&1 </dev/null | \
  openssl x509 -noout -subject -issuer -dates -ext subjectAltName

2.5 API/Service Discovery

for endpoint in /api/v1/version /api/health /healthz /version /status /robots.txt; do
  r=$(curl -ks https://HOST$endpoint)
  [ -n "$r" ] && echo "$endpoint: $r"
done

Common services to fingerprint:

  • Seafile: /api2/server-info/, /seafhttp/
  • GitLab: /api/v4/version
  • Nextcloud: /.well-known/caldav
  • Grafana: /api/health
  • Prometheus: /metrics

2.6 HTML Content Analysis

curl -ks https://DOMAIN | grep -ioE '<title>[^<]+'
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:

# 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 25 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/)