Files
FunguyBot/plugins/shodan.py
T

175 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Shodan.io integration for security research and reconnaissance.
Output uses shared code_block for aligned columns.
"""
import logging
import os
import aiohttp
import simplematrixbotlib as botlib
from plugins.common import html_escape, code_block, collapsible_summary
SHODAN_API_KEY = os.getenv("SHODAN_KEY", "")
SHODAN_API_BASE = "https://api.shodan.io"
async def handle_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("shodan"):
if not SHODAN_API_KEY:
await bot.api.send_text_message(room.room_id, "Shodan API key not configured.")
return
args = match.args()
if len(args) < 1:
await show_usage(room, bot)
return
sub = args[0].lower()
if sub == "ip" and len(args) >= 2:
await shodan_ip_lookup(room, bot, args[1])
elif sub == "search" and len(args) >= 2:
query = " ".join(args[1:])
await shodan_search(room, bot, query)
elif sub == "host" and len(args) >= 2:
await shodan_host(room, bot, args[1])
elif sub == "count" and len(args) >= 2:
query = " ".join(args[1:])
await shodan_count(room, bot, query)
else:
await show_usage(room, bot)
async def show_usage(room, bot):
usage = """<strong>🔍 Shodan Commands:</strong>
<strong>!shodan ip &lt;ip_address&gt;</strong> - Get detailed information about an IP
<strong>!shodan search &lt;query&gt;</strong> - Search Shodan database
<strong>!shodan host &lt;domain/ip&gt;</strong> - Get host information
<strong>!shodan count &lt;query&gt;</strong> - Count results for a search query
<strong>Search Examples:</strong>
• <code>!shodan search apache</code>
• <code>!shodan search "port:22"</code>
• <code>!shodan search "country:US product:nginx"</code>
• <code>!shodan search "net:192.168.1.0/24"</code>
"""
await bot.api.send_markdown_message(room.room_id, usage)
async def shodan_ip_lookup(room, bot, ip):
safe_ip = html_escape(ip)
try:
url = f"{SHODAN_API_BASE}/shodan/host/{ip}?key={SHODAN_API_KEY}"
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as resp:
if resp.status == 404:
await bot.api.send_text_message(room.room_id, f"No information found for IP: {safe_ip}")
return
resp.raise_for_status()
data = await resp.json()
rows = [
("🌐", "IP", safe_ip),
("📍", "Location", f"{data.get('city','N/A')}, {data.get('country_name','N/A')}"),
("🏢", "Organization", data.get('org', 'N/A')),
("💻", "OS", data.get('os', 'N/A')),
("🔌", "Open Ports", ', '.join(map(str, data.get('ports', []))) or 'None'),
]
if data.get('data'):
for svc in data['data'][:5]:
rows.append(("📡", f"Port {svc.get('port')}", svc.get('product','Unknown')))
sections = [{"title": f"Shodan IP Lookup: {safe_ip}", "rows": rows}]
block = code_block(f"🔍 Shodan IP Lookup: {safe_ip}", sections)
output = collapsible_summary(f"🔍 Shodan: {safe_ip}", block)
await bot.api.send_markdown_message(room.room_id, output)
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"API error: {e}")
async def shodan_search(room, bot, query):
safe_query = html_escape(query)
try:
url = f"{SHODAN_API_BASE}/shodan/host/search?key={SHODAN_API_KEY}&query={query}&minify=true&limit=5"
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as resp:
resp.raise_for_status()
data = await resp.json()
if not data.get('matches'):
await bot.api.send_text_message(room.room_id, f"No results for '{safe_query}'.")
return
rows = []
for match in data['matches'][:5]:
ip = match.get('ip_str', 'N/A')
port = match.get('port', '')
org = match.get('org', 'Unknown')
product = match.get('product', 'Unknown')
rows.append(("🌐", f"{ip}:{port}", f"{product} {org}"))
sections = [{"title": f"Search: {safe_query}", "rows": rows}]
block = code_block(f"🔍 Shodan Search: {safe_query}", sections)
output = collapsible_summary(f"Shodan Search: {safe_query}", block)
await bot.api.send_markdown_message(room.room_id, output)
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"API error: {e}")
async def shodan_host(room, bot, host):
safe_host = html_escape(host)
try:
url = f"{SHODAN_API_BASE}/dns/domain/{host}?key={SHODAN_API_KEY}"
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as resp:
if resp.status == 404:
await shodan_ip_lookup(room, bot, host)
return
resp.raise_for_status()
data = await resp.json()
rows = [("🌐", "Domain", safe_host)]
if data.get('subdomains'):
for sub in sorted(data['subdomains'])[:10]:
rows.append(("", "Subdomain", f"{sub}.{safe_host}"))
if len(data['subdomains']) > 10:
rows.append(("", "", f"... and {len(data['subdomains']) - 10} more"))
sections = [{"title": f"Host: {safe_host}", "rows": rows}]
block = code_block(f"🔍 Shodan Host: {safe_host}", sections)
output = collapsible_summary(f"Shodan Host: {safe_host}", block)
await bot.api.send_markdown_message(room.room_id, output)
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"API error: {e}")
async def shodan_count(room, bot, query):
safe_query = html_escape(query)
try:
url = f"{SHODAN_API_BASE}/shodan/host/count?key={SHODAN_API_KEY}&query={query}"
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as resp:
resp.raise_for_status()
data = await resp.json()
rows = [("🔢", "Total Results", f"{data.get('total', 0):,}")]
if data.get('facets'):
for facet_name, facet_data in data['facets'].items():
for item in facet_data[:5]:
rows.append(("", facet_name.capitalize(), f"{item['value']}: {item['count']:,}"))
sections = [{"title": f"Count: {safe_query}", "rows": rows}]
block = code_block(f"🔍 Shodan Count: {safe_query}", sections)
output = collapsible_summary(f"Shodan Count: {safe_query}", block)
await bot.api.send_markdown_message(room.room_id, output)
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"API error: {e}")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.2"
__author__ = "Funguy Bot"
__description__ = "Shodan.io reconnaissance"
__help__ = """
<details>
<summary><strong>!shodan</strong> Shodan search</summary>
<ul>
<li><code>!shodan ip &lt;ip&gt;</code> IP info with open ports</li>
<li><code>!shodan search &lt;query&gt;</code> Search internet devices</li>
<li><code>!shodan host &lt;domain&gt;</code> Host & subdomain enumeration</li>
<li><code>!shodan count &lt;query&gt;</code> Result counts</li>
</ul>
<p>Requires <strong>SHODAN_KEY</strong> env var.</p>
</details>
"""