feat: add internetdb plugin (Shodan InternetDB host recon)

Free, keyless API returning open ports, hostnames, CPEs, tags, and
known CVEs for any public IP. All requests routed through SOCKS5.
21 test cases (927 total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-20 17:41:51 +01:00
parent 442fea703c
commit 3de3f054df
6 changed files with 487 additions and 8 deletions

105
plugins/internetdb.py Normal file
View File

@@ -0,0 +1,105 @@
"""Plugin: Shodan InternetDB -- free host reconnaissance (no API key)."""
from __future__ import annotations
import asyncio
import ipaddress
import json
import logging
from derp.http import urlopen as _urlopen
from derp.plugin import command
log = logging.getLogger(__name__)
_API_URL = "https://internetdb.shodan.io"
_TIMEOUT = 15
def _fetch(addr: str) -> dict | None:
"""Fetch InternetDB data for an IP address.
Returns parsed JSON dict, or None on 404 (no data).
Raises on network/server errors.
"""
import urllib.error
try:
resp = _urlopen(f"{_API_URL}/{addr}", timeout=_TIMEOUT)
return json.loads(resp.read())
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
def _format_result(addr: str, data: dict) -> str:
"""Format InternetDB response into a compact IRC message."""
lines = []
hostnames = data.get("hostnames", [])
if hostnames:
lines.append(f"{addr} -- {', '.join(hostnames[:5])}")
else:
lines.append(addr)
ports = data.get("ports", [])
if ports:
lines.append(f"Ports: {', '.join(str(p) for p in sorted(ports))}")
cpes = data.get("cpes", [])
if cpes:
lines.append(f"CPEs: {', '.join(cpes[:8])}")
tags = data.get("tags", [])
if tags:
lines.append(f"Tags: {', '.join(tags)}")
vulns = data.get("vulns", [])
if vulns:
shown = vulns[:10]
suffix = f" (+{len(vulns) - 10} more)" if len(vulns) > 10 else ""
lines.append(f"CVEs: {', '.join(shown)}{suffix}")
return " | ".join(lines)
@command("internetdb", help="Shodan InternetDB: !internetdb <ip>")
async def cmd_internetdb(bot, message):
"""Look up host information from Shodan InternetDB.
Returns open ports, hostnames, CPEs, tags, and known CVEs.
Free API, no key required.
Usage:
!internetdb 8.8.8.8
"""
parts = message.text.split(None, 2)
if len(parts) < 2:
await bot.reply(message, "Usage: !internetdb <ip>")
return
addr = parts[1].strip()
try:
ip = ipaddress.ip_address(addr)
except ValueError:
await bot.reply(message, f"Invalid IP address: {addr}")
return
if ip.is_private or ip.is_loopback:
await bot.reply(message, f"{addr}: private/loopback address")
return
loop = asyncio.get_running_loop()
try:
data = await loop.run_in_executor(None, _fetch, str(ip))
except Exception as exc:
log.error("internetdb: lookup failed for %s: %s", addr, exc)
await bot.reply(message, f"{addr}: lookup failed ({exc})")
return
if data is None:
await bot.reply(message, f"{addr}: no data available")
return
await bot.reply(message, _format_result(str(ip), data))