diff --git a/funguy.py b/funguy.py index 38effcc..6733289 100755 --- a/funguy.py +++ b/funguy.py @@ -22,7 +22,7 @@ ALLOWED_PLUGINS = { 'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma', 'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion', 'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary', - 'bitcoin', 'dns', 'shodan' + 'bitcoin', 'dns', 'shodan', 'dnsdumpster' } class FunguyBot: diff --git a/plugins/dnsdumpster.py b/plugins/dnsdumpster.py new file mode 100644 index 0000000..01b7f56 --- /dev/null +++ b/plugins/dnsdumpster.py @@ -0,0 +1,270 @@ +""" +This plugin provides DNSDumpster.com integration for domain reconnaissance and DNS mapping. +""" + +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) + +DNSDUMPSTER_API_KEY = os.getenv("DNSDUMPSTER_KEY", "") +DNSDUMPSTER_API_BASE = "https://api.dnsdumpster.com" + +async def handle_command(room, message, bot, prefix, config): + """ + Function to handle DNSDumpster 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("dnsdumpster"): + logging.info("Received !dnsdumpster command") + + # Check if API key is configured + if not DNSDUMPSTER_API_KEY: + await bot.api.send_text_message( + room.room_id, + "DNSDumpster API key not configured. Please set DNSDUMPSTER_KEY environment variable." + ) + logging.error("DNSDumpster API key not configured") + return + + args = match.args() + + if len(args) < 1: + await show_usage(room, bot) + return + + # Check if it's a test command or domain lookup + if args[0].lower() == "test": + await test_dnsdumpster_connection(room, bot) + else: + # Treat the first argument as the domain + domain = args[0].lower().strip() + await dnsdumpster_domain_lookup(room, bot, domain) + +async def show_usage(room, bot): + """Display DNSDumpster command usage.""" + usage = """ +🔍 DNSDumpster Commands: + +!dnsdumpster <domain_name> - Get comprehensive DNS reconnaissance for a domain +!dnsdumpster test - Test API connection + +Examples: +• !dnsdumpster google.com +• !dnsdumpster github.com +• !dnsdumpster example.com + +Rate Limit: 1 request per 2 seconds +""" + await bot.api.send_markdown_message(room.room_id, usage) + +async def test_dnsdumpster_connection(room, bot): + """Test DNSDumpster API connection.""" + try: + test_domain = "google.com" # Changed from example.com to google.com + url = f"{DNSDUMPSTER_API_BASE}/domain/{test_domain}" + headers = { + "X-API-Key": DNSDUMPSTER_API_KEY + } + + logging.info(f"Testing DNSDumpster API with domain: {test_domain}") + response = requests.get(url, headers=headers, timeout=15) + + debug_info = f"🔧 DNSDumpster API Test
" + debug_info += f"Status Code: {response.status_code}
" + debug_info += f"Test Domain: {test_domain}
" + debug_info += f"Headers Used: X-API-Key
" + + if response.status_code == 200: + data = response.json() + debug_info += "✅ SUCCESS - API is working!
" + debug_info += f"Response Keys: {list(data.keys())}
" + + # Show some sample data + if data.get('a'): + debug_info += f"A Records Found: {len(data['a'])}
" + if data.get('ns'): + debug_info += f"NS Records Found: {len(data['ns'])}
" + if data.get('total_a_recs'): + debug_info += f"Total A Records: {data['total_a_recs']}
" + + elif response.status_code == 400: + debug_info += "❌ Bad Request - Check domain format
" + debug_info += f"Response: {response.text[:200]}
" + elif response.status_code == 401: + debug_info += "❌ Unauthorized - Invalid API key
" + elif response.status_code == 429: + debug_info += "⚠️ Rate Limit Exceeded - Wait 2 seconds
" + else: + debug_info += f"❌ Error: {response.status_code} - {response.text[:200]}
" + + await bot.api.send_markdown_message(room.room_id, debug_info) + + except Exception as e: + await bot.api.send_text_message(room.room_id, f"Test failed: {str(e)}") + +async def dnsdumpster_domain_lookup(room, bot, domain): + """Get comprehensive DNS reconnaissance for a domain.""" + try: + url = f"{DNSDUMPSTER_API_BASE}/domain/{domain}" + headers = { + "X-API-Key": DNSDUMPSTER_API_KEY + } + + logging.info(f"Fetching DNSDumpster data for domain: {domain}") + + # Send initial processing message + await bot.api.send_text_message(room.room_id, f"🔍 Processing DNS reconnaissance for {domain}...") + + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 400: + await bot.api.send_text_message(room.room_id, f"Bad request - check domain format: {domain}") + return + elif response.status_code == 401: + await bot.api.send_text_message(room.room_id, "Invalid DNSDumpster API key") + return + elif response.status_code == 403: + await bot.api.send_text_message(room.room_id, "Access denied - check API key permissions") + return + elif response.status_code == 429: + await bot.api.send_text_message(room.room_id, "Rate limit exceeded - wait 2 seconds between requests") + return + elif response.status_code != 200: + await bot.api.send_text_message(room.room_id, f"DNSDumpster API error: {response.status_code} - {response.text[:100]}") + return + + data = response.json() + logging.info(f"DNSDumpster response keys: {list(data.keys())}") + + # Format the comprehensive DNS report + output = await format_dnsdumpster_report(domain, data) + + await bot.api.send_markdown_message(room.room_id, output) + logging.info(f"Sent DNSDumpster data for {domain}") + + except requests.exceptions.Timeout: + await bot.api.send_text_message(room.room_id, "DNSDumpster API request timed out") + logging.error("DNSDumpster API timeout") + except Exception as e: + await bot.api.send_text_message(room.room_id, f"Error fetching DNSDumpster data: {str(e)}") + logging.error(f"Error in dnsdumpster_domain_lookup: {e}") + +async def format_dnsdumpster_report(domain, data): + """Format DNSDumpster JSON response into a readable report.""" + output = f"🔍 DNSDumpster Report: {domain}

" + + # Summary statistics + if data.get('total_a_recs'): + output += f"📊 Summary
" + output += f" • Total A Records: {data['total_a_recs']}
" + + # A Records - Show ALL records + if data.get('a') and data['a']: + output += f"
📍 A Records (IPv4) - {len(data['a'])} found
" + for record in data['a']: # Show ALL A records + host = record.get('host', 'N/A') + ips = record.get('ips', []) + + output += f" • {host}
" + for ip_info in ips: # Show ALL IPs per host + ip = ip_info.get('ip', 'N/A') + country = ip_info.get('country', 'Unknown') + asn_name = ip_info.get('asn_name', 'Unknown') + + output += f" └─ {ip} ({country})
" + output += f" └─ {asn_name}
" + + # Show banner information if available + banners = ip_info.get('banners', {}) + if banners.get('http') or banners.get('https'): + output += f" └─ Web Services: " + services = [] + if banners.get('http'): + services.append("HTTP") + if banners.get('https'): + services.append("HTTPS") + output += f"{', '.join(services)}
" + + # NS Records - Show ALL records + if data.get('ns') and data['ns']: + output += f"
🔗 NS Records (Name Servers) - {len(data['ns'])} found
" + for record in data['ns']: # Show ALL NS records + host = record.get('host', 'N/A') + ips = record.get('ips', []) + + output += f" • {host}
" + for ip_info in ips: # Show ALL IPs + ip = ip_info.get('ip', 'N/A') + country = ip_info.get('country', 'Unknown') + output += f" └─ {ip} ({country})
" + + # MX Records - Show ALL records + if data.get('mx') and data['mx']: + output += f"
📧 MX Records (Mail Servers) - {len(data['mx'])} found
" + for record in data['mx']: # Show ALL MX records + host = record.get('host', 'N/A') + ips = record.get('ips', []) + + output += f" • {host}
" + for ip_info in ips: # Show ALL IPs + ip = ip_info.get('ip', 'N/A') + country = ip_info.get('country', 'Unknown') + output += f" └─ {ip} ({country})
" + + # CNAME Records - Show ALL records + if data.get('cname') and data['cname']: + output += f"
🔀 CNAME Records - {len(data['cname'])} found
" + for record in data['cname']: # Show ALL CNAME records + host = record.get('host', 'N/A') + target = record.get('target', 'N/A') + output += f" • {host} → {target}
" + + # TXT Records - Show ALL records + if data.get('txt') and data['txt']: + output += f"
📄 TXT Records - {len(data['txt'])} found
" + for txt in data['txt']: # Show ALL TXT records + # Truncate very long TXT records but show more content + if len(txt) > 200: + txt = txt[:200] + "..." + output += f" • {txt}
" + + # Additional record types that might be present - Show ALL records + other_records = ['aaaa', 'srv', 'soa', 'ptr'] + for record_type in other_records: + if data.get(record_type) and data[record_type]: + output += f"
🔧 {record_type.upper()} Records - {len(data[record_type])} found
" + for record in data[record_type]: # Show ALL records + if isinstance(record, dict): + # Format dictionary records nicely + record_str = ", ".join([f"{k}: {v}" for k, v in record.items()]) + if len(record_str) > 150: + record_str = record_str[:150] + "..." + output += f" • {record_str}
" + else: + output += f" • {record}
" + + # Add rate limit reminder + output += "
💡 Rate Limit: 1 request per 2 seconds" + + # Always wrap in collapsible details since we're showing all results + output = f"
🔍 DNSDumpster Report: {domain} (Click to expand){output}
" + + return output diff --git a/plugins/help.py b/plugins/help.py index b5dc203..013684c 100644 --- a/plugins/help.py +++ b/plugins/help.py @@ -99,6 +99,33 @@ async def handle_command(room, message, bot, prefix, config):

Requires SHODAN_KEY environment variable

+
🌐 !dnsdumpster [domain] +

Comprehensive DNS reconnaissance and attack surface mapping using DNSDumpster.com API.

+

Commands:

+ +

Features:

+ +

Examples:

+ +

Requires DNSDUMPSTER_KEY environment variable
+Rate Limit: 1 request per 2 seconds

+
+
📸 !sd [prompt]

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'.

diff --git a/plugins/loadplugin.py b/plugins/loadplugin.py index d538939..902d63a 100644 --- a/plugins/loadplugin.py +++ b/plugins/loadplugin.py @@ -60,7 +60,8 @@ async def load_plugin(plugin_name): 'urbandictionary': 'plugins.urbandictionary', 'bitcoin':'plugins.bitcoin', 'dns':'plugins.dns', - 'shodan':'plugins.shodan' + 'shodan':'plugins.shodan', + 'dnsdumpster': 'plugins.dnsdumpster' } # Get the module path from the mapping