various plugin refactors and fixes
This commit is contained in:
+75
-232
@@ -1,77 +1,43 @@
|
||||
"""
|
||||
This plugin provides Shodan.io integration for security research and reconnaissance.
|
||||
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, collapsible_summary
|
||||
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):
|
||||
"""
|
||||
Function to handle Shodan commands.
|
||||
"""
|
||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||
if match.is_not_from_this_bot() and match.prefix() and match.command("shodan"):
|
||||
logging.info("Received !shodan command")
|
||||
|
||||
# Check if API key is configured
|
||||
if not SHODAN_API_KEY:
|
||||
await bot.api.send_text_message(
|
||||
room.room_id,
|
||||
"Shodan API key not configured. Please set SHODAN_KEY environment variable."
|
||||
)
|
||||
logging.error("Shodan API key not configured")
|
||||
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
|
||||
|
||||
subcommand = args[0].lower()
|
||||
|
||||
if subcommand == "ip":
|
||||
if len(args) < 2:
|
||||
await bot.api.send_text_message(room.room_id, "Usage: !shodan ip <ip_address>")
|
||||
return
|
||||
ip = args[1]
|
||||
await shodan_ip_lookup(room, bot, ip)
|
||||
|
||||
elif subcommand == "search":
|
||||
if len(args) < 2:
|
||||
await bot.api.send_text_message(room.room_id, "Usage: !shodan search <query>")
|
||||
return
|
||||
query = ' '.join(args[1:])
|
||||
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 subcommand == "host":
|
||||
if len(args) < 2:
|
||||
await bot.api.send_text_message(room.room_id, "Usage: !shodan host <domain/ip>")
|
||||
return
|
||||
host = args[1]
|
||||
await shodan_host(room, bot, host)
|
||||
|
||||
elif subcommand == "count":
|
||||
if len(args) < 2:
|
||||
await bot.api.send_text_message(room.room_id, "Usage: !shodan count <query>")
|
||||
return
|
||||
query = ' '.join(args[1:])
|
||||
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):
|
||||
"""Display Shodan command usage."""
|
||||
usage = """
|
||||
<strong>🔍 Shodan Commands:</strong>
|
||||
|
||||
usage = """<strong>🔍 Shodan Commands:</strong>
|
||||
<strong>!shodan ip <ip_address></strong> - Get detailed information about an IP
|
||||
<strong>!shodan search <query></strong> - Search Shodan database
|
||||
<strong>!shodan host <domain/ip></strong> - Get host information
|
||||
@@ -86,228 +52,112 @@ async def show_usage(room, bot):
|
||||
await bot.api.send_markdown_message(room.room_id, usage)
|
||||
|
||||
async def shodan_ip_lookup(room, bot, ip):
|
||||
"""Look up information about a specific IP address."""
|
||||
safe_ip = html_escape(ip)
|
||||
try:
|
||||
url = f"{SHODAN_API_BASE}/shodan/host/{ip}?key={SHODAN_API_KEY}"
|
||||
logging.info(f"Fetching Shodan IP info for: {ip}")
|
||||
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}")
|
||||
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()
|
||||
|
||||
data = await response.json()
|
||||
|
||||
# Format the response
|
||||
output = f"<strong>🔍 Shodan IP Lookup: {html_escape(ip)}</strong><br><br>"
|
||||
|
||||
if data.get('country_name'):
|
||||
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> {html_escape(data['org'])}<br>"
|
||||
|
||||
if data.get('os'):
|
||||
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>"
|
||||
|
||||
output += f"<strong>🕒 Last Update:</strong> {data.get('last_update', 'N/A')}<br><br>"
|
||||
|
||||
# Show services
|
||||
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'):
|
||||
output += "<strong>📡 Services:</strong><br>"
|
||||
for service in data['data'][:5]: # Limit to first 5 services
|
||||
port = service.get('port', 'N/A')
|
||||
product = service.get('product', 'Unknown')
|
||||
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> {html_escape(product)} {html_escape(version)}<br>"
|
||||
if banner:
|
||||
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 = collapsible_summary(f"🔍 Shodan IP Lookup: {html_escape(ip)}", output)
|
||||
|
||||
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)
|
||||
logging.info(f"Sent Shodan IP info for {ip}")
|
||||
|
||||
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: {str(e)}")
|
||||
logging.error(f"Error in shodan_ip_lookup: {e}")
|
||||
await bot.api.send_text_message(room.room_id, f"API error: {e}")
|
||||
|
||||
async def shodan_search(room, bot, query):
|
||||
"""Search the Shodan database."""
|
||||
safe_query = html_escape(query)
|
||||
try:
|
||||
url = f"{SHODAN_API_BASE}/shodan/host/search"
|
||||
params = {
|
||||
"key": SHODAN_API_KEY,
|
||||
"query": query,
|
||||
"minify": "true",
|
||||
"limit": 5
|
||||
}
|
||||
logging.info(f"Searching Shodan for: {query}")
|
||||
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, params=params, timeout=15) as response:
|
||||
if response.status != 200:
|
||||
await handle_shodan_error(room, bot, response.status)
|
||||
return
|
||||
data = await response.json()
|
||||
|
||||
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 found for: {html_escape(query)}")
|
||||
await bot.api.send_text_message(room.room_id, f"No results for '{safe_query}'.")
|
||||
return
|
||||
|
||||
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
|
||||
rows = []
|
||||
for match in data['matches'][:5]:
|
||||
ip = match.get('ip_str', 'N/A')
|
||||
port = match.get('port', 'N/A')
|
||||
port = match.get('port', '')
|
||||
org = match.get('org', 'Unknown')
|
||||
product = match.get('product', 'Unknown')
|
||||
|
||||
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> {html_escape(loc['city'])}, {html_escape(loc['country_name'])}<br>"
|
||||
|
||||
output += "<br>"
|
||||
|
||||
if data.get('total', 0) > 5:
|
||||
output += f"<em>Showing 5 of {data['total']:,} results. Refine your search for more specific results.</em>"
|
||||
|
||||
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)
|
||||
logging.info(f"Sent Shodan search results for: {query}")
|
||||
|
||||
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: {str(e)}")
|
||||
logging.error(f"Error in shodan_search: {e}")
|
||||
await bot.api.send_text_message(room.room_id, f"API error: {e}")
|
||||
|
||||
async def shodan_host(room, bot, host):
|
||||
"""Get host information (domain or IP)."""
|
||||
safe_host = html_escape(host)
|
||||
try:
|
||||
url = f"{SHODAN_API_BASE}/dns/domain/{host}?key={SHODAN_API_KEY}"
|
||||
logging.info(f"Fetching Shodan host info for: {host}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=15) as response:
|
||||
if response.status == 404:
|
||||
# Try IP lookup instead
|
||||
async with session.get(url, timeout=15) as resp:
|
||||
if resp.status == 404:
|
||||
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()
|
||||
|
||||
output = f"<strong>🔍 Shodan Host: {html_escape(host)}</strong><br><br>"
|
||||
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
rows = [("🌐", "Domain", safe_host)]
|
||||
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" • {html_escape(subdomain)}.{html_escape(host)}<br>"
|
||||
|
||||
for sub in sorted(data['subdomains'])[:10]:
|
||||
rows.append(("", "Subdomain", f"{sub}.{safe_host}"))
|
||||
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(html_escape(t) for t in data['tags'])}<br>"
|
||||
|
||||
if data.get('data'):
|
||||
output += f"<br><strong>📊 Records Found:</strong> {len(data['data'])}<br>"
|
||||
|
||||
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)
|
||||
logging.info(f"Sent Shodan host info for: {host}")
|
||||
|
||||
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: {str(e)}")
|
||||
logging.error(f"Error in shodan_host: {e}")
|
||||
await bot.api.send_text_message(room.room_id, f"API error: {e}")
|
||||
|
||||
async def shodan_count(room, bot, query):
|
||||
"""Count results for a search query."""
|
||||
safe_query = html_escape(query)
|
||||
try:
|
||||
url = f"{SHODAN_API_BASE}/shodan/host/count"
|
||||
params = {
|
||||
"key": SHODAN_API_KEY,
|
||||
"query": query
|
||||
}
|
||||
logging.info(f"Counting Shodan results for: {query}")
|
||||
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, params=params, timeout=15) as response:
|
||||
if response.status != 200:
|
||||
await handle_shodan_error(room, bot, response.status)
|
||||
return
|
||||
data = await response.json()
|
||||
|
||||
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" • {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" • {html_escape(org['value'])}: {org['count']:,}<br>"
|
||||
|
||||
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)
|
||||
logging.info(f"Sent Shodan count for: {query}")
|
||||
|
||||
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: {str(e)}")
|
||||
logging.error(f"Error in shodan_count: {e}")
|
||||
|
||||
async def handle_shodan_error(room, bot, status_code):
|
||||
"""Handle Shodan API errors."""
|
||||
error_messages = {
|
||||
401: "Invalid Shodan API key",
|
||||
403: "Access denied - check API key permissions",
|
||||
404: "No results found",
|
||||
429: "Rate limit exceeded - try again later",
|
||||
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}")
|
||||
await bot.api.send_text_message(room.room_id, f"API error: {e}")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin Metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
__version__ = "1.0.1"
|
||||
__version__ = "1.0.2"
|
||||
__author__ = "Funguy Bot"
|
||||
__description__ = "Shodan.io reconnaissance"
|
||||
__help__ = """
|
||||
@@ -319,13 +169,6 @@ __help__ = """
|
||||
<li><code>!shodan host <domain></code> – Host & subdomain enumeration</li>
|
||||
<li><code>!shodan count <query></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>
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user