refactor: async I/O, input sanitisation, and shared utilities cleanup

This commit is contained in:
2026-05-08 22:59:31 -05:00
parent 52a9621d50
commit f822d6a450
21 changed files with 1351 additions and 2709 deletions
+83 -102
View File
@@ -4,15 +4,9 @@ This plugin provides Shodan.io integration for security research and reconnaissa
import logging
import os
import requests
import aiohttp
import simplematrixbotlib as botlib
from dotenv import load_dotenv
# Load environment variables from .env file
plugin_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(plugin_dir)
dotenv_path = os.path.join(parent_dir, '.env')
load_dotenv(dotenv_path)
from plugins.common import html_escape, collapsible_summary
SHODAN_API_KEY = os.getenv("SHODAN_KEY", "")
SHODAN_API_BASE = "https://api.shodan.io"
@@ -20,16 +14,6 @@ SHODAN_API_BASE = "https://api.shodan.io"
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle Shodan commands.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("shodan"):
@@ -104,35 +88,33 @@ async def show_usage(room, bot):
async def shodan_ip_lookup(room, bot, ip):
"""Look up information about a specific IP address."""
try:
url = f"{SHODAN_API_BASE}/shodan/host/{ip}"
params = {"key": SHODAN_API_KEY}
url = f"{SHODAN_API_BASE}/shodan/host/{ip}?key={SHODAN_API_KEY}"
logging.info(f"Fetching Shodan IP info for: {ip}")
response = requests.get(url, params=params, timeout=15)
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as response:
if response.status == 404:
await bot.api.send_text_message(room.room_id, f"No information found for IP: {html_escape(ip)}")
return
elif response.status == 401:
await bot.api.send_text_message(room.room_id, "Invalid Shodan API key")
return
elif response.status != 200:
await bot.api.send_text_message(room.room_id, f"Shodan API error: {response.status}")
return
if response.status_code == 404:
await bot.api.send_text_message(room.room_id, f"No information found for IP: {ip}")
return
elif response.status_code == 401:
await bot.api.send_text_message(room.room_id, "Invalid Shodan API key")
return
elif response.status_code != 200:
await bot.api.send_text_message(room.room_id, f"Shodan API error: {response.status_code}")
return
data = response.json()
data = await response.json()
# Format the response
output = f"<strong>🔍 Shodan IP Lookup: {ip}</strong><br><br>"
output = f"<strong>🔍 Shodan IP Lookup: {html_escape(ip)}</strong><br><br>"
if data.get('country_name'):
output += f"<strong>📍 Location:</strong> {data.get('city', 'N/A')}, {data.get('country_name', 'N/A')}<br>"
output += f"<strong>📍 Location:</strong> {html_escape(data.get('city', 'N/A'))}, {html_escape(data.get('country_name', 'N/A'))}<br>"
if data.get('org'):
output += f"<strong>🏢 Organization:</strong> {data['org']}<br>"
output += f"<strong>🏢 Organization:</strong> {html_escape(data['org'])}<br>"
if data.get('os'):
output += f"<strong>💻 Operating System:</strong> {data['os']}<br>"
output += f"<strong>💻 Operating System:</strong> {html_escape(data['os'])}<br>"
if data.get('ports'):
output += f"<strong>🔌 Open Ports:</strong> {', '.join(map(str, data['ports']))}<br>"
@@ -148,25 +130,25 @@ async def shodan_ip_lookup(room, bot, ip):
version = service.get('version', '')
banner = service.get('data', '')[:100] + "..." if len(service.get('data', '')) > 100 else service.get('data', '')
output += f" • <strong>Port {port}:</strong> {product} {version}<br>"
output += f" • <strong>Port {port}:</strong> {html_escape(product)} {html_escape(version)}<br>"
if banner:
output += f" <em>{banner}</em><br>"
output += f" <em>{html_escape(banner)}</em><br>"
if len(data['data']) > 5:
output += f" • ... and {len(data['data']) - 5} more services<br>"
# Wrap in collapsible if output is large
if len(output) > 500:
output = f"<details><summary><strong>🔍 Shodan IP Lookup: {ip}</strong></summary>{output}</details>"
output = collapsible_summary(f"🔍 Shodan IP Lookup: {html_escape(ip)}", output)
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan IP info for {ip}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"Error fetching Shodan data: {e}")
logging.error(f"Shodan API error: {e}")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error fetching Shodan data: {str(e)}")
await bot.api.send_text_message(room.room_id, f"Error: {str(e)}")
logging.error(f"Error in shodan_ip_lookup: {e}")
async def shodan_search(room, bot, query):
@@ -176,24 +158,22 @@ async def shodan_search(room, bot, query):
params = {
"key": SHODAN_API_KEY,
"query": query,
"minify": True,
"limit": 5 # Limit results to avoid huge responses
"minify": "true",
"limit": 5
}
logging.info(f"Searching Shodan for: {query}")
response = requests.get(url, params=params, timeout=15)
if response.status_code != 200:
await handle_shodan_error(room, bot, response.status_code)
return
data = response.json()
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=15) as response:
if response.status != 200:
await handle_shodan_error(room, bot, response.status)
return
data = await response.json()
if not data.get('matches'):
await bot.api.send_text_message(room.room_id, f"No results found for: {query}")
await bot.api.send_text_message(room.room_id, f"No results found for: {html_escape(query)}")
return
output = f"<strong>🔍 Shodan Search: '{query}'</strong><br>"
output = f"<strong>🔍 Shodan Search: '{html_escape(query)}'</strong><br>"
output += f"<strong>Total Results:</strong> {data.get('total', 0):,}<br><br>"
for match in data['matches'][:5]: # Show first 5 results
@@ -202,14 +182,14 @@ async def shodan_search(room, bot, query):
org = match.get('org', 'Unknown')
product = match.get('product', 'Unknown')
output += f"<strong>🌐 {ip}:{port}</strong><br>"
output += f" • <strong>Organization:</strong> {org}<br>"
output += f" • <strong>Service:</strong> {product}<br>"
output += f"<strong>🌐 {html_escape(ip)}:{port}</strong><br>"
output += f" • <strong>Organization:</strong> {html_escape(org)}<br>"
output += f" • <strong>Service:</strong> {html_escape(product)}<br>"
if match.get('location'):
loc = match['location']
if loc.get('city') and loc.get('country_name'):
output += f" • <strong>Location:</strong> {loc['city']}, {loc['country_name']}<br>"
output += f" • <strong>Location:</strong> {html_escape(loc['city'])}, {html_escape(loc['country_name'])}<br>"
output += "<br>"
@@ -219,44 +199,41 @@ async def shodan_search(room, bot, query):
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan search results for: {query}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"Error searching Shodan: {e}")
logging.error(f"Shodan API error: {e}")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error searching Shodan: {str(e)}")
await bot.api.send_text_message(room.room_id, f"Error: {str(e)}")
logging.error(f"Error in shodan_search: {e}")
async def shodan_host(room, bot, host):
"""Get host information (domain or IP)."""
try:
url = f"{SHODAN_API_BASE}/dns/domain/{host}"
params = {"key": SHODAN_API_KEY}
url = f"{SHODAN_API_BASE}/dns/domain/{host}?key={SHODAN_API_KEY}"
logging.info(f"Fetching Shodan host info for: {host}")
response = requests.get(url, params=params, timeout=15)
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as response:
if response.status == 404:
# Try IP lookup instead
await shodan_ip_lookup(room, bot, host)
return
elif response.status != 200:
await handle_shodan_error(room, bot, response.status)
return
data = await response.json()
if response.status_code == 404:
# Try IP lookup instead
await shodan_ip_lookup(room, bot, host)
return
elif response.status_code != 200:
await handle_shodan_error(room, bot, response.status_code)
return
data = response.json()
output = f"<strong>🔍 Shodan Host: {host}</strong><br><br>"
output = f"<strong>🔍 Shodan Host: {html_escape(host)}</strong><br><br>"
if data.get('subdomains'):
output += f"<strong>🌐 Subdomains ({len(data['subdomains'])}):</strong><br>"
for subdomain in sorted(data['subdomains'])[:10]: # Show first 10
output += f"{subdomain}.{host}<br>"
output += f"{html_escape(subdomain)}.{html_escape(host)}<br>"
if len(data['subdomains']) > 10:
output += f" • ... and {len(data['subdomains']) - 10} more<br>"
if data.get('tags'):
output += f"<br><strong>🏷️ Tags:</strong> {', '.join(data['tags'])}<br>"
output += f"<br><strong>🏷️ Tags:</strong> {', '.join(html_escape(t) for t in data['tags'])}<br>"
if data.get('data'):
output += f"<br><strong>📊 Records Found:</strong> {len(data['data'])}<br>"
@@ -264,11 +241,11 @@ async def shodan_host(room, bot, host):
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan host info for: {host}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"Error fetching host info: {e}")
logging.error(f"Shodan API error: {e}")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error fetching host info: {str(e)}")
await bot.api.send_text_message(room.room_id, f"Error: {str(e)}")
logging.error(f"Error in shodan_host: {e}")
async def shodan_count(room, bot, query):
@@ -279,39 +256,37 @@ async def shodan_count(room, bot, query):
"key": SHODAN_API_KEY,
"query": query
}
logging.info(f"Counting Shodan results for: {query}")
response = requests.get(url, params=params, timeout=15)
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=15) as response:
if response.status != 200:
await handle_shodan_error(room, bot, response.status)
return
data = await response.json()
if response.status_code != 200:
await handle_shodan_error(room, bot, response.status_code)
return
data = response.json()
output = f"<strong>🔍 Shodan Count: '{query}'</strong><br><br>"
output = f"<strong>🔍 Shodan Count: '{html_escape(query)}'</strong><br><br>"
output += f"<strong>Total Results:</strong> {data.get('total', 0):,}<br>"
# Show top countries if available
if data.get('facets') and 'country' in data['facets']:
output += "<br><strong>🌍 Top Countries:</strong><br>"
for country in data['facets']['country'][:5]:
output += f"{country['value']}: {country['count']:,}<br>"
output += f"{html_escape(country['value'])}: {country['count']:,}<br>"
# Show top organizations if available
if data.get('facets') and 'org' in data['facets']:
output += "<br><strong>🏢 Top Organizations:</strong><br>"
for org in data['facets']['org'][:5]:
output += f"{org['value']}: {org['count']:,}<br>"
output += f"{html_escape(org['value'])}: {org['count']:,}<br>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan count for: {query}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except aiohttp.ClientError as e:
await bot.api.send_text_message(room.room_id, f"Error counting Shodan results: {e}")
logging.error(f"Shodan API error: {e}")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error counting Shodan results: {str(e)}")
await bot.api.send_text_message(room.room_id, f"Error: {str(e)}")
logging.error(f"Error in shodan_count: {e}")
async def handle_shodan_error(room, bot, status_code):
@@ -324,7 +299,6 @@ async def handle_shodan_error(room, bot, status_code):
500: "Shodan API server error",
503: "Shodan API temporarily unavailable"
}
message = error_messages.get(status_code, f"Shodan API error: {status_code}")
await bot.api.send_text_message(room.room_id, message)
logging.error(f"Shodan API error: {status_code}")
@@ -333,7 +307,7 @@ async def handle_shodan_error(room, bot, status_code):
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "Shodan.io reconnaissance"
__help__ = """
@@ -345,6 +319,13 @@ __help__ = """
<li><code>!shodan host &lt;domain&gt;</code> Host & subdomain enumeration</li>
<li><code>!shodan count &lt;query&gt;</code> Result counts</li>
</ul>
<strong>Search Examples:</strong>
<ul>
<li><code>!shodan search apache</code></li>
<li><code>!shodan search "port:22"</code></li>
<li><code>!shodan search "country:US product:nginx"</code></li>
<li><code>!shodan search "net:192.168.1.0/24"</code></li>
</ul>
<p>Requires <strong>SHODAN_KEY</strong> env var.</p>
</details>
"""