New plugins
This commit is contained in:
@@ -131,6 +131,12 @@ Fetches current weather information for any location using OpenWeatherMap API.
|
|||||||
**📖 !ud [term] [index]**
|
**📖 !ud [term] [index]**
|
||||||
Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index.
|
Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index.
|
||||||
|
|
||||||
|
**🔍 !dns [domain]**
|
||||||
|
Performs comprehensive DNS reconnaissance on a domain. Shows A, AAAA, MX, NS, TXT, CNAME, SOA, and other DNS records.
|
||||||
|
|
||||||
|
**💰 !btc**
|
||||||
|
Fetches the current Bitcoin price in USD from bitcointicker.co API.
|
||||||
|
|
||||||
### AI & Generation Commands
|
### AI & Generation Commands
|
||||||
|
|
||||||
**🤖 AI Commands (!tech, !music, !eth, etc.)**
|
**🤖 AI Commands (!tech, !music, !eth, etc.)**
|
||||||
|
@@ -18,9 +18,12 @@ import toml # Library for parsing TOML configuration files
|
|||||||
from plugins.config import FunguyConfig
|
from plugins.config import FunguyConfig
|
||||||
|
|
||||||
# Whitelist of allowed plugins to prevent arbitrary code execution
|
# Whitelist of allowed plugins to prevent arbitrary code execution
|
||||||
ALLOWED_PLUGINS = {'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
|
ALLOWED_PLUGINS = {
|
||||||
|
'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
|
||||||
'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
|
'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
|
||||||
'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary'}
|
'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary',
|
||||||
|
'bitcoin', 'dns', 'shodan'
|
||||||
|
}
|
||||||
|
|
||||||
class FunguyBot:
|
class FunguyBot:
|
||||||
"""
|
"""
|
||||||
|
109
plugins/bitcoin.py
Normal file
109
plugins/bitcoin.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to fetch the current Bitcoin price.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
|
BITCOIN_API_URL = "https://api.bitcointicker.co/trades/bitstamp/btcusd/60/"
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !btc command.
|
||||||
|
|
||||||
|
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("btc"):
|
||||||
|
logging.info("Received !btc command")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch Bitcoin price data
|
||||||
|
headers = {
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'User-Agent': 'FunguyBot/1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Fetching Bitcoin price from {BITCOIN_API_URL}")
|
||||||
|
response = requests.get(BITCOIN_API_URL, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if not data or len(data) == 0:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"No Bitcoin price data available."
|
||||||
|
)
|
||||||
|
logging.warning("No Bitcoin price data returned from API")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the most recent trade (last item in the array)
|
||||||
|
latest_trade = data[-1]
|
||||||
|
price = latest_trade.get('price')
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Could not extract Bitcoin price from API response."
|
||||||
|
)
|
||||||
|
logging.error("Price field not found in API response")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert to float and format with commas
|
||||||
|
try:
|
||||||
|
price_float = float(price)
|
||||||
|
price_formatted = f"${price_float:,.2f}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
price_formatted = f"${price}"
|
||||||
|
|
||||||
|
# Optional: Get additional info if available
|
||||||
|
timestamp = latest_trade.get('timestamp', '')
|
||||||
|
volume = latest_trade.get('volume', '')
|
||||||
|
|
||||||
|
# Build the message
|
||||||
|
message_text = f"<strong>₿ BTC/USD</strong>"
|
||||||
|
message_text += f"<strong> Current Price:</strong> {price_formatted}"
|
||||||
|
|
||||||
|
message_text += ", <em>bitcointicker.co</em>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, message_text)
|
||||||
|
logging.info(f"Sent Bitcoin price: {price_formatted}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Request timed out. Bitcoin price API may be slow or unavailable."
|
||||||
|
)
|
||||||
|
logging.error("Bitcoin API timeout")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Error fetching Bitcoin price: {e}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error fetching Bitcoin price: {e}")
|
||||||
|
|
||||||
|
except (KeyError, IndexError, ValueError) as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Error parsing Bitcoin price data."
|
||||||
|
)
|
||||||
|
logging.error(f"Error parsing Bitcoin API response: {e}", exc_info=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"An unexpected error occurred while fetching Bitcoin price."
|
||||||
|
)
|
||||||
|
logging.error(f"Unexpected error in Bitcoin plugin: {e}", exc_info=True)
|
209
plugins/dns.py
Normal file
209
plugins/dns.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to perform DNS reconnaissance on a domain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import dns.resolver
|
||||||
|
import dns.reversename
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Common DNS record types to query
|
||||||
|
RECORD_TYPES = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', 'SRV']
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_domain(domain):
|
||||||
|
"""
|
||||||
|
Validate if the provided string is a valid domain name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain (str): The domain to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise.
|
||||||
|
"""
|
||||||
|
# Basic domain validation regex
|
||||||
|
pattern = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
||||||
|
return re.match(pattern, domain) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def format_dns_record(record_type, records):
|
||||||
|
"""
|
||||||
|
Format DNS records for display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
record_type (str): The type of DNS record.
|
||||||
|
records (list): List of DNS record values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted HTML string.
|
||||||
|
"""
|
||||||
|
if not records:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
output = f"<strong>{record_type} Records:</strong><br>"
|
||||||
|
for record in records:
|
||||||
|
output += f" • {record}<br>"
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
async def query_dns_records(domain):
|
||||||
|
"""
|
||||||
|
Query all common DNS record types for a domain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain (str): The domain to query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary with record types as keys and lists of records as values.
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
resolver = dns.resolver.Resolver()
|
||||||
|
resolver.timeout = 5
|
||||||
|
resolver.lifetime = 5
|
||||||
|
|
||||||
|
for record_type in RECORD_TYPES:
|
||||||
|
try:
|
||||||
|
logging.info(f"Querying {record_type} records for {domain}")
|
||||||
|
answers = resolver.resolve(domain, record_type)
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rdata in answers:
|
||||||
|
if record_type == 'MX':
|
||||||
|
# MX records have preference and exchange
|
||||||
|
records.append(f"{rdata.preference} {rdata.exchange}")
|
||||||
|
elif record_type == 'SOA':
|
||||||
|
# SOA records have multiple fields
|
||||||
|
records.append(f"{rdata.mname} {rdata.rname}")
|
||||||
|
elif record_type == 'SRV':
|
||||||
|
# SRV records have priority, weight, port, and target
|
||||||
|
records.append(f"{rdata.priority} {rdata.weight} {rdata.port} {rdata.target}")
|
||||||
|
elif record_type == 'TXT':
|
||||||
|
# TXT records can have multiple strings
|
||||||
|
txt_data = ' '.join([s.decode() if isinstance(s, bytes) else str(s) for s in rdata.strings])
|
||||||
|
records.append(txt_data)
|
||||||
|
else:
|
||||||
|
records.append(str(rdata))
|
||||||
|
|
||||||
|
if records:
|
||||||
|
results[record_type] = records
|
||||||
|
logging.info(f"Found {len(records)} {record_type} record(s)")
|
||||||
|
|
||||||
|
except dns.resolver.NoAnswer:
|
||||||
|
logging.debug(f"No {record_type} records found for {domain}")
|
||||||
|
continue
|
||||||
|
except dns.resolver.NXDOMAIN:
|
||||||
|
logging.warning(f"Domain {domain} does not exist")
|
||||||
|
return None
|
||||||
|
except dns.resolver.Timeout:
|
||||||
|
logging.warning(f"Timeout querying {record_type} for {domain}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error querying {record_type} for {domain}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !dns command.
|
||||||
|
|
||||||
|
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("dns"):
|
||||||
|
logging.info("Received !dns command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) != 1:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Usage: !dns <domain>\nExample: !dns example.com"
|
||||||
|
)
|
||||||
|
logging.info("Sent usage message for !dns")
|
||||||
|
return
|
||||||
|
|
||||||
|
domain = args[0].lower().strip()
|
||||||
|
|
||||||
|
# Remove protocol if present
|
||||||
|
domain = domain.replace('http://', '').replace('https://', '')
|
||||||
|
# Remove trailing slash if present
|
||||||
|
domain = domain.rstrip('/')
|
||||||
|
# Remove www. prefix if present (optional - you can keep it if you want)
|
||||||
|
# domain = domain.replace('www.', '')
|
||||||
|
|
||||||
|
# Validate domain
|
||||||
|
if not is_valid_domain(domain):
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Invalid domain name: {domain}"
|
||||||
|
)
|
||||||
|
logging.warning(f"Invalid domain provided: {domain}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f"Starting DNS reconnaissance for {domain}")
|
||||||
|
|
||||||
|
# Send "working on it" message for longer queries
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"🔍 Performing DNS reconnaissance on {domain}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Query DNS records
|
||||||
|
results = await query_dns_records(domain)
|
||||||
|
|
||||||
|
if results is None:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Domain {domain} does not exist (NXDOMAIN)"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"No DNS records found for {domain}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Format the output
|
||||||
|
output = f"<strong>🔍 DNS Records for {domain}</strong><br><br>"
|
||||||
|
|
||||||
|
# Order the records in a logical way
|
||||||
|
preferred_order = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'SRV', 'PTR']
|
||||||
|
|
||||||
|
for record_type in preferred_order:
|
||||||
|
if record_type in results:
|
||||||
|
output += format_dns_record(record_type, results[record_type])
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Add any remaining record types not in preferred order
|
||||||
|
for record_type in results:
|
||||||
|
if record_type not in preferred_order:
|
||||||
|
output += format_dns_record(record_type, results[record_type])
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Wrap in collapsible details if output is large
|
||||||
|
if output.count('<br>') > 15:
|
||||||
|
output = f"<details><summary><strong>🔍 DNS Records for {domain}</strong></summary>{output}</details>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent DNS records for {domain}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"An error occurred while performing DNS lookup: {str(e)}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error in DNS plugin for {domain}: {e}", exc_info=True)
|
@@ -69,6 +69,14 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
<p>Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index number. Shows definition, example, author, votes, and permalink.</p>
|
<p>Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index number. Shows definition, example, author, votes, and permalink.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔍 <strong>!dns [domain]</strong></summary>
|
||||||
|
<p>Performs comprehensive DNS reconnaissance on a domain. Queries multiple DNS record types including A, AAAA, MX, NS, TXT, CNAME, SOA, and SRV records. Validates domain format and provides formatted results.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>💰 <strong>!btc</strong></summary>
|
||||||
|
<p>Fetches the current Bitcoin price in USD from bitcointicker.co API. Shows real-time BTC/USD price with proper formatting. Includes error handling for API timeouts and data parsing issues.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details><summary>📸 <strong>!sd [prompt]</strong></summary>
|
<details><summary>📸 <strong>!sd [prompt]</strong></summary>
|
||||||
<p>Generates images using self-hosted Stable Diffusion. Supports options: --steps, --cfg, --h, --w, --neg, --sampler. Uses queuing system to handle multiple requests. See available options using just '!sd'.</p>
|
<p>Generates images using self-hosted Stable Diffusion. Supports options: --steps, --cfg, --h, --w, --neg, --sampler. Uses queuing system to handle multiple requests. See available options using just '!sd'.</p>
|
||||||
</details>
|
</details>
|
||||||
|
@@ -57,7 +57,10 @@ async def load_plugin(plugin_name):
|
|||||||
'youtube-preview': 'plugins.youtube-preview',
|
'youtube-preview': 'plugins.youtube-preview',
|
||||||
'youtube-search': 'plugins.youtube-search',
|
'youtube-search': 'plugins.youtube-search',
|
||||||
'weather': 'plugins.weather',
|
'weather': 'plugins.weather',
|
||||||
'urbandictionary': 'plugins.urbandictionary'
|
'urbandictionary': 'plugins.urbandictionary',
|
||||||
|
'bitcoin':'plugins.bitcoin',
|
||||||
|
'dns':'plugins.dns',
|
||||||
|
'shodan':'plugins.shodan'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the module path from the mapping
|
# Get the module path from the mapping
|
||||||
|
330
plugins/shodan.py
Normal file
330
plugins/shodan.py
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides Shodan.io integration for security research and reconnaissance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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"):
|
||||||
|
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")
|
||||||
|
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:])
|
||||||
|
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:])
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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
|
||||||
|
<strong>!shodan count <query></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):
|
||||||
|
"""Look up information about a specific IP address."""
|
||||||
|
try:
|
||||||
|
url = f"{SHODAN_API_BASE}/shodan/host/{ip}"
|
||||||
|
params = {"key": SHODAN_API_KEY}
|
||||||
|
|
||||||
|
logging.info(f"Fetching Shodan IP info for: {ip}")
|
||||||
|
response = requests.get(url, params=params, timeout=15)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
output = f"<strong>🔍 Shodan IP Lookup: {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>"
|
||||||
|
|
||||||
|
if data.get('org'):
|
||||||
|
output += f"<strong>🏢 Organization:</strong> {data['org']}<br>"
|
||||||
|
|
||||||
|
if data.get('os'):
|
||||||
|
output += f"<strong>💻 Operating System:</strong> {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
|
||||||
|
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> {product} {version}<br>"
|
||||||
|
if banner:
|
||||||
|
output += f" <em>{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>"
|
||||||
|
|
||||||
|
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 Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error fetching Shodan data: {str(e)}")
|
||||||
|
logging.error(f"Error in shodan_ip_lookup: {e}")
|
||||||
|
|
||||||
|
async def shodan_search(room, bot, query):
|
||||||
|
"""Search the Shodan database."""
|
||||||
|
try:
|
||||||
|
url = f"{SHODAN_API_BASE}/shodan/host/search"
|
||||||
|
params = {
|
||||||
|
"key": SHODAN_API_KEY,
|
||||||
|
"query": query,
|
||||||
|
"minify": True,
|
||||||
|
"limit": 5 # Limit results to avoid huge responses
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if not data.get('matches'):
|
||||||
|
await bot.api.send_text_message(room.room_id, f"No results found for: {query}")
|
||||||
|
return
|
||||||
|
|
||||||
|
output = f"<strong>🔍 Shodan Search: '{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
|
||||||
|
ip = match.get('ip_str', 'N/A')
|
||||||
|
port = match.get('port', 'N/A')
|
||||||
|
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>"
|
||||||
|
|
||||||
|
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 += "<br>"
|
||||||
|
|
||||||
|
if data.get('total', 0) > 5:
|
||||||
|
output += f"<em>Showing 5 of {data['total']:,} results. Refine your search for more specific results.</em>"
|
||||||
|
|
||||||
|
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 Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error searching Shodan: {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}
|
||||||
|
|
||||||
|
logging.info(f"Fetching Shodan host info for: {host}")
|
||||||
|
response = requests.get(url, params=params, timeout=15)
|
||||||
|
|
||||||
|
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>"
|
||||||
|
|
||||||
|
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>"
|
||||||
|
|
||||||
|
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>"
|
||||||
|
|
||||||
|
if data.get('data'):
|
||||||
|
output += f"<br><strong>📊 Records Found:</strong> {len(data['data'])}<br>"
|
||||||
|
|
||||||
|
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 Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error fetching host info: {str(e)}")
|
||||||
|
logging.error(f"Error in shodan_host: {e}")
|
||||||
|
|
||||||
|
async def shodan_count(room, bot, query):
|
||||||
|
"""Count results for a search 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}")
|
||||||
|
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()
|
||||||
|
|
||||||
|
output = f"<strong>🔍 Shodan Count: '{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>"
|
||||||
|
|
||||||
|
# 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>"
|
||||||
|
|
||||||
|
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 Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error counting Shodan results: {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}")
|
Reference in New Issue
Block a user