# Directory Structure ``` ├── .dockerignore ├── .gitignore ├── .vscode │ └── mcp.json ├── banner.sh ├── demos │ ├── holehe.gif │ ├── nmap.gif │ ├── ocr2text.png │ ├── README.md │ ├── sherlock.gif │ └── sqlmap.gif ├── Dockerfile ├── docs │ └── index.html ├── LICENSE ├── README.md ├── requirements.txt ├── server.py └── toolkit ├── dnsrecon.py ├── holehe.py ├── nmap.py ├── ocr2text.py ├── sherlock.py ├── sqlmap.py ├── sublist3r.py ├── wpscan.py └── zmap.py ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | venv/ 8 | env/ 9 | ENV/ 10 | .env 11 | .venv 12 | .eggs/ 13 | *.egg-info/ 14 | 15 | # Development 16 | .git 17 | .github 18 | .vscode 19 | .idea 20 | 21 | # Build artifacts 22 | dist/ 23 | build/ 24 | *.log ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python virtual environment 2 | venv/ 3 | .venv/ 4 | env/ 5 | ENV/ 6 | 7 | # Python bytecode 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | 20 | # OS specific files 21 | .DS_Store 22 | Thumbs.db ``` -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- ```markdown 1 | ### Ocr2Text 2 |  ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | mcp==1.6.0 2 | numpy==1.26.4 3 | opencv-python-headless==4.9.0.80 4 | openai==1.70.0 5 | openai-agents==0.0.7 6 | pdf2image==1.17.0 7 | pytesseract 8 | python-dotenv==1.1.0 9 | requests==2.32.4 10 | tqdm==4.67.1 11 | typing-inspection==0.4.0 12 | typing_extensions==4.13.1 13 | uvicorn==0.34.0 14 | sublist3r ``` -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "servers": { 3 | "HydraMCP": { 4 | "command": "docker", 5 | "args": [ 6 | "run", 7 | "--rm", 8 | "-i", 9 | "--net=host", 10 | "--privileged", 11 | "--name", 12 | "hydramcp", 13 | "hydramcp" 14 | ] 15 | } 16 | } 17 | } ``` -------------------------------------------------------------------------------- /banner.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | cat << "BANNER" 4 | • Title: HydraΜCP — The Model Context Protocol (MCP) Pentesting Toolkit 5 | • Version: 0.1.0 6 | • License: MIT 7 | • Description: A lightweight, extensible cybersecurity toolkit that connects AI assistants 8 | to security tools through the Model Context Protocol (MCP), enabling 9 | AI-assisted security research, scanning, and analysis. 10 | • Community: @happyhackingspace | https://happyhacking.space 11 | • Author: Built with ❤️ by @atiilla 12 | BANNER 13 | ``` -------------------------------------------------------------------------------- /toolkit/holehe.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: Holehe 3 | # - Description: Email account checker 4 | # - Usage: Checks if an email address is registered on various websites 5 | # - Parameters: email (required) 6 | 7 | import subprocess 8 | import logging 9 | from typing import Dict, Any 10 | # Logging setup 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 14 | ) 15 | logger = logging.getLogger("holehe-tool") 16 | 17 | 18 | class Holehe: 19 | """ 20 | Holehe wrapper class for checking email registrations. 21 | """ 22 | 23 | def __init__(self): 24 | """Initialize Holehe wrapper.""" 25 | logger.info("Holehe wrapper initialized.") 26 | 27 | def scan(self, email: str) -> Dict[str, Any]: 28 | """ 29 | Run the holehe scan for a given email. 30 | 31 | Args: 32 | email (str): The email address to check. 33 | 34 | Returns: 35 | Dict[str, Any]: Result or error from the holehe command. 36 | """ 37 | logger.info(f"Running Holehe scan for: {email}") 38 | try: 39 | result = subprocess.run(["holehe", email], capture_output=True, text=True, check=True) 40 | output = result.stdout.strip() 41 | return { 42 | "email": email, 43 | "success": True, 44 | "output": output 45 | } 46 | except subprocess.CalledProcessError as e: 47 | logger.error(f"Holehe scan failed: {e.stderr.strip()}") 48 | return { 49 | "email": email, 50 | "success": False, 51 | "error": e.stderr.strip() 52 | } 53 | 54 | 55 | def ExecHolehe(email: str) -> Dict[str, Any]: 56 | """ 57 | Convenience wrapper to run a holehe scan. 58 | 59 | Args: 60 | email (str): Email address to scan. 61 | 62 | Returns: 63 | Dict[str, Any]: Holehe scan results. 64 | """ 65 | scanner = Holehe() 66 | return scanner.scan(email) 67 | ``` -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- ```python 1 | from typing import List, Optional, Dict, Any 2 | 3 | from mcp.server.fastmcp import FastMCP 4 | 5 | from toolkit.holehe import ExecHolehe 6 | from toolkit.nmap import ExecNmap 7 | from toolkit.wpscan import ExecWpscan 8 | from toolkit.zmap import ExecZmap 9 | from toolkit.sqlmap import ExecSqlmap 10 | from toolkit.ocr2text import ExecOcr2Text 11 | from toolkit.sublist3r import ExecSublist3r 12 | from toolkit.dnsrecon import ExecDNSRecon 13 | from toolkit.sherlock import ExecSherlock 14 | 15 | # Create server 16 | mcp = FastMCP(name="HydraΜCP", 17 | version="0.1.0" 18 | ) 19 | 20 | 21 | @mcp.tool() 22 | def ZmapScanner( 23 | target: str, 24 | port: int, 25 | bandwidth: Optional[str] = "1M", 26 | ) -> Dict[str, Any]: 27 | """Wrapper for running ZMap network scanning.""" 28 | return ExecZmap(target, port, bandwidth) 29 | 30 | 31 | @mcp.tool() 32 | def WPScanScanner( 33 | url: str, 34 | ) -> Dict[str, Any]: 35 | """Wrapper for running WPScan vulnerability scanning.""" 36 | return ExecWpscan(url) 37 | 38 | 39 | @mcp.tool() 40 | def HoleheScanner( 41 | email: str, 42 | ) -> Dict[str, Any]: 43 | """Wrapper for running Holehe email registration checking.""" 44 | return ExecHolehe(email) 45 | 46 | 47 | @mcp.tool() 48 | def NmapScanner( 49 | target: str, 50 | ports: Optional[str] = None, 51 | ) -> Dict[str, Any]: 52 | """Wrapper for running Nmap network scanning.""" 53 | return ExecNmap(target, ports) 54 | 55 | 56 | @mcp.tool() 57 | def SqlmapScanner( 58 | url: str, 59 | data: Optional[str] = None, 60 | ) -> Dict[str, Any]: 61 | """Wrapper for running Sqlmap vulnerability scanning.""" 62 | return ExecSqlmap(url, data) 63 | 64 | @mcp.tool() 65 | def OcrScanner( 66 | file_path: str, 67 | ) -> Dict[str, Any]: 68 | """Wrapper for running OCR (Optical Character Recognition) on images and PDFs. 69 | 70 | The file_path can be: 71 | - A local file path 72 | - A direct URL (http/https) 73 | - A URL prefixed with @ symbol 74 | """ 75 | return ExecOcr2Text(file_path) 76 | 77 | @mcp.tool() 78 | def Sublist3rScanner( 79 | domain: str, 80 | output_dir: Optional[str] = "output", 81 | ) -> List[str]: 82 | """Wrapper for running Sublist3r subdomain enumeration.""" 83 | return ExecSublist3r(domain, output_dir) 84 | 85 | @mcp.tool() 86 | def DNSReconScanner( 87 | domain: str, 88 | scan_type: Optional[str] = "std", 89 | name_server: Optional[str] = None, 90 | range: Optional[str] = None, 91 | dictionary: Optional[str] = None, 92 | ) -> Dict[str, Any]: 93 | """Wrapper for running DNSRecon for DNS reconnaissance.""" 94 | kwargs = {} 95 | if name_server: 96 | kwargs["name_server"] = name_server 97 | if range: 98 | kwargs["range"] = range 99 | if dictionary: 100 | kwargs["dictionary"] = dictionary 101 | 102 | return ExecDNSRecon(domain, scan_type, **kwargs) 103 | 104 | @mcp.tool() 105 | def SherlockScanner( 106 | usernames: List[str], 107 | ) -> Dict[str, Any]: 108 | """Wrapper for running Sherlock username enumeration.""" 109 | return ExecSherlock(usernames) 110 | 111 | 112 | if __name__ == "__main__": 113 | mcp.run(transport="stdio") ``` -------------------------------------------------------------------------------- /toolkit/nmap.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: Nmap 3 | # - Description: Network scanning tool 4 | # - Usage: Scans networks for hosts and services 5 | # - Parameters: target (required), ports (optional) 6 | 7 | import subprocess 8 | import logging 9 | import re 10 | from typing import List, Optional, Tuple 11 | 12 | # Configure logging 13 | logging.basicConfig(level=logging.INFO) 14 | logger = logging.getLogger(__name__) 15 | 16 | # Define dynamic mappings of phrases to Nmap options 17 | KEYWORD_MAP = { 18 | "no ping": "-Pn", 19 | "skip ping": "-Pn", 20 | "version": "-sV", 21 | "service detection": "-sV", 22 | "os detection": "-O", 23 | "aggressive": "-A", 24 | "verbose": "-v", 25 | "top ports": "--top-ports 100", 26 | "udp": "-sU", 27 | "quick": "-T4", 28 | } 29 | 30 | def parse_nmap_prompt(prompt: str) -> Tuple[str, Optional[str], List[str]]: 31 | prompt = prompt.lower() 32 | options = [] 33 | ports = None 34 | 35 | # Extract IP address (supports v4 only here) 36 | ip_match = re.search(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", prompt) 37 | target = ip_match.group(0) if ip_match else "" 38 | 39 | # Extract ports if mentioned like 'ports 22,80' or 'scan port 443' 40 | port_match = re.search(r"port[s]?\s*(\d{1,5}(?:\s*,\s*\d{1,5})*)", prompt) 41 | if port_match: 42 | ports = port_match.group(1).replace(" ", "") 43 | 44 | # Match keywords to options 45 | for phrase, option in KEYWORD_MAP.items(): 46 | if phrase in prompt: 47 | options.append(option) 48 | 49 | return target, ports, options 50 | 51 | def ExecNmap( 52 | target: str, 53 | ports: Optional[str] = None, 54 | options: Optional[List[str]] = None, 55 | ) -> str: 56 | """Run an Nmap network scan on the specified target.""" 57 | logger.debug("Running Nmap with target=%s, ports=%s, options=%s", target, ports, options) 58 | 59 | if subprocess.run(["which", "nmap"], capture_output=True).returncode != 0: 60 | return {"error": "Nmap is not installed. Install it with 'sudo apt install nmap'."} 61 | 62 | cmd = ["nmap", target] 63 | if ports: 64 | cmd += ["-p", ports] 65 | if options: 66 | cmd += options 67 | 68 | logger.info("Executing command: %s", ' '.join(cmd)) 69 | 70 | try: 71 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) 72 | return {"success": True, "output": result.stdout} 73 | except subprocess.CalledProcessError as e: 74 | logger.error("Nmap command failed: %s", e.stderr) 75 | return {"success": False, "error": e.stderr.strip()} 76 | except Exception as e: 77 | logger.exception("Unexpected error during Nmap execution") 78 | return {"success": False, "error": f"Error executing Nmap scan: {str(e)}"} 79 | 80 | def scan_from_prompt(prompt: str) -> str: 81 | target, ports, options = parse_nmap_prompt(prompt) 82 | if not target: 83 | return {"success": False, "error": "Could not detect a valid IP address in the prompt."} 84 | return ExecNmap(target, ports, options) 85 | 86 | ``` -------------------------------------------------------------------------------- /toolkit/wpscan.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: WPScan 3 | # - Description: WordPress vulnerability scanner 4 | # - Usage: Scans WordPress sites for vulnerabilities and security issues 5 | # - Parameters: url (required), format (optional), api_token (optional) 6 | 7 | import subprocess 8 | import logging 9 | from typing import Any, Dict, List, Optional 10 | import json 11 | 12 | # Logging setup 13 | logging.basicConfig( 14 | level=logging.INFO, 15 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 16 | ) 17 | logger = logging.getLogger("wpscan-tool") 18 | 19 | class WPScan: 20 | """WPScan tool wrapper class.""" 21 | 22 | def __init__(self): 23 | """Initialize WPScan wrapper.""" 24 | self._check_installed() 25 | 26 | def _check_installed(self) -> None: 27 | """Verify WPScan is installed.""" 28 | try: 29 | subprocess.run(["wpscan", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 30 | logger.info("WPScan installation verified.") 31 | except (subprocess.SubprocessError, FileNotFoundError): 32 | logger.warning("wpscan command not found. Make sure WPScan is installed and in your PATH.") 33 | raise RuntimeError("WPScan is not installed or not in PATH.") 34 | 35 | def get_version(self) -> Optional[str]: 36 | """Get installed WPScan version.""" 37 | try: 38 | result = subprocess.run(["wpscan", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 39 | version_line = result.stdout.strip().split('\n')[-1] 40 | logger.info(f"WPScan version: {version_line}") 41 | return version_line 42 | except (subprocess.SubprocessError, FileNotFoundError) as e: 43 | logger.error(f"WPScan version check failed: {e}") 44 | return None 45 | 46 | def scan(self, url: str, **kwargs: Any) -> Dict[str, Any]: 47 | """Run the WPScan scan.""" 48 | if not url: 49 | raise ValueError("Target URL must be provided.") 50 | 51 | cmd = ["wpscan", "--url", url,"--random-user-agent","--ignore-main-redirect"] 52 | 53 | # Append additional options to the command 54 | if "format" in kwargs: 55 | cmd.append("--format") 56 | cmd.append(kwargs["format"]) 57 | if kwargs.get("verbose", False): 58 | cmd.append("--verbose") 59 | if kwargs.get("random_user_agent", False): 60 | cmd.append("--random-user-agent") 61 | if "max_threads" in kwargs: 62 | cmd.append("--max-threads") 63 | cmd.append(str(kwargs["max_threads"])) 64 | if "api_token" in kwargs: 65 | cmd.append("--api-token") 66 | cmd.append(kwargs["api_token"]) 67 | if "enumerate_opts" in kwargs: 68 | cmd.append("--enumerate") 69 | cmd.append(",".join(kwargs["enumerate_opts"])) 70 | 71 | logger.info(f"Preparing WPScan command: {' '.join(cmd)}") 72 | 73 | # Run the scan 74 | try: 75 | result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 76 | output = result.stdout 77 | # If format is json, parse it 78 | if kwargs.get("format", "json") == "json": 79 | return json.loads(output) 80 | return {"status": "success", "output": output} 81 | except subprocess.SubprocessError as e: 82 | logger.error(f"Error during WPScan scan: {e}") 83 | return {"status": "error", "message": str(e)} 84 | 85 | # Example usage (optional convenience function) 86 | def ExecWpscan(url: str, **kwargs: Any) -> Dict[str, Any]: 87 | """Convenience wrapper to run a WPScan scan.""" 88 | scanner = WPScan() 89 | try: 90 | return scanner.scan(url, **kwargs) 91 | except Exception as e: 92 | logger.error(f"WPScan scan failed: {e}") 93 | return {"error": str(e)} 94 | ``` -------------------------------------------------------------------------------- /toolkit/zmap.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: ZMap 3 | # - Description: High-speed network scanner 4 | # - Usage: Scans large network ranges quickly 5 | # - Parameters: target (required), port (required), bandwidth (optional) 6 | 7 | from typing import Any, Dict, List 8 | import ipaddress 9 | import logging 10 | import subprocess 11 | import re 12 | import random 13 | 14 | # Logging setup 15 | logging.basicConfig( 16 | level=logging.INFO, 17 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 18 | ) 19 | logger = logging.getLogger("zmap-tool") 20 | 21 | 22 | class ZMap: 23 | """ZMap scanner class for network scanning operations.""" 24 | 25 | def __init__(self): 26 | """Initialize ZMap scanner.""" 27 | self.interface = "eth0" 28 | self.gateway_mac = self._generate_random_mac() 29 | self._check_installed() 30 | 31 | def _check_installed(self) -> None: 32 | """Verify ZMap is installed.""" 33 | try: 34 | subprocess.run(["zmap", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 35 | except (subprocess.SubprocessError, FileNotFoundError): 36 | logger.warning("ZMap is not installed or not in PATH.") 37 | 38 | def _generate_random_mac(self) -> str: 39 | """Generate a random locally administered unicast MAC address.""" 40 | mac = [0x02, 0x00, 0x00, 41 | random.randint(0x00, 0x7F), 42 | random.randint(0x00, 0xFF), 43 | random.randint(0x00, 0xFF)] 44 | return ':'.join(f'{b:02x}' for b in mac) 45 | 46 | def _convert_ip_range_to_cidr(self, ip_range: str) -> str: 47 | """Convert IP range or CIDR to canonical CIDR format.""" 48 | try: 49 | network = ipaddress.IPv4Network(ip_range, strict=False) 50 | return str(network) 51 | except ValueError: 52 | pass 53 | 54 | match = re.match(r'(\d+\.\d+\.\d+\.\d+)-(\d+\.\d+\.\d+\.\d+)', ip_range) 55 | if not match: 56 | raise ValueError(f"Invalid IP range format: {ip_range}") 57 | 58 | start_ip, end_ip = match.group(1), match.group(2) 59 | start, end = ipaddress.IPv4Address(start_ip), ipaddress.IPv4Address(end_ip) 60 | 61 | for prefixlen in range(32, -1, -1): 62 | network = ipaddress.IPv4Network(f"{start_ip}/{prefixlen}", strict=False) 63 | if network[0] <= start and network[-1] >= end: 64 | return str(network) 65 | 66 | raise ValueError(f"Could not convert IP range {ip_range} to CIDR notation") 67 | 68 | def get_version(self) -> str: 69 | """Get installed ZMap version.""" 70 | try: 71 | result = subprocess.run(["zmap", "--version"], check=True, stdout=subprocess.PIPE, text=True) 72 | return result.stdout.strip() 73 | except subprocess.SubprocessError as e: 74 | logger.error(f"ZMap version check failed: {e}") 75 | raise RuntimeError("ZMap is not installed or misconfigured.") 76 | 77 | def scan(self, target_port: int, subnets: List[str], bandwidth: str = "1M") -> Dict[str, Any]: 78 | """Run the ZMap scan.""" 79 | cidr_subnets = [self._convert_ip_range_to_cidr(subnet) for subnet in subnets] 80 | 81 | if not (0 < target_port <= 65535): 82 | raise ValueError("Port must be between 1 and 65535") 83 | 84 | cmd = [ 85 | "zmap", 86 | "-p", str(target_port), 87 | "-B", bandwidth, 88 | "-i", self.interface, 89 | "-G", self.gateway_mac, 90 | "--blacklist-file=/dev/null", 91 | "-o", "-" 92 | ] 93 | cmd.extend(cidr_subnets) 94 | 95 | logger.info(f"Running ZMap scan: {' '.join(cmd)}") 96 | 97 | try: 98 | result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 99 | hosts = [line.strip() for line in result.stdout.splitlines() if line.strip()] 100 | return { 101 | "port": target_port, 102 | "hosts": hosts, 103 | "total_hosts": len(hosts), 104 | "subnets_scanned": cidr_subnets 105 | } 106 | except subprocess.CalledProcessError as e: 107 | logger.error(f"ZMap scan failed: {e.stderr.strip()}") 108 | return { 109 | "error": "Scan failed", 110 | "details": e.stderr, 111 | "exit_code": e.returncode 112 | } 113 | 114 | 115 | def ExecZmap(target: str, port: int, bandwidth: str = "1M") -> Dict[str, Any]: 116 | """Convenience wrapper to run a ZMap scan.""" 117 | zmap = ZMap() 118 | try: 119 | return zmap.scan(target_port=port, subnets=[target], bandwidth=bandwidth) 120 | except Exception as e: 121 | logger.error(f"Scan error: {e}") 122 | return {"error": str(e)} 123 | ``` -------------------------------------------------------------------------------- /toolkit/sublist3r.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: Sublist3r 3 | # - Description: Fast subdomain enumeration tool 4 | # - Usage: Finds subdomains via search engines 5 | # - Parameters: domain (required), output_dir (optional) 6 | 7 | import subprocess 8 | import os 9 | import json 10 | import logging 11 | import re 12 | from typing import List, Dict, Any 13 | 14 | # Logging setup 15 | logging.basicConfig( 16 | level=logging.INFO, 17 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 18 | ) 19 | logger = logging.getLogger("sublist3r-tool") 20 | 21 | 22 | class Sublist3r: 23 | """ 24 | Sublist3r wrapper class for subdomain enumeration. 25 | """ 26 | 27 | def __init__(self): 28 | """Initialize Sublist3r wrapper.""" 29 | logger.info("Sublist3r wrapper initialized.") 30 | 31 | def extract_subdomains_from_output(self, output: str) -> List[str]: 32 | """ 33 | Extract subdomains from the command output. 34 | 35 | Args: 36 | output (str): Command output text 37 | 38 | Returns: 39 | List[str]: List of extracted subdomains 40 | """ 41 | # Remove ANSI color codes 42 | ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') 43 | clean_output = ansi_escape.sub('', output) 44 | 45 | # Extract subdomains using regex 46 | # Look for lines that appear to be domains (contain at least one dot and no special chars) 47 | domain_pattern = re.compile(r'^(?:[\w-]+\.)+[\w-]+$', re.MULTILINE) 48 | return domain_pattern.findall(clean_output) 49 | 50 | def scan(self, domain: str, output_dir: str) -> Dict[str, Any]: 51 | """ 52 | Run Sublist3r tool to enumerate subdomains of a given domain. 53 | 54 | Args: 55 | domain (str): The target domain to enumerate subdomains for. 56 | output_dir (str): The directory to save the output files. 57 | 58 | Returns: 59 | Dict[str, Any]: Results or error from the Sublist3r command. 60 | """ 61 | # Create output directory if it doesn't exist 62 | os.makedirs(output_dir, exist_ok=True) 63 | 64 | output_file = os.path.join(output_dir, f"{domain}_subdomains.txt") 65 | 66 | logger.info(f"Running Sublist3r scan for domain: {domain}") 67 | try: 68 | # Run without output file first to capture stdout 69 | result = subprocess.run( 70 | ["sublist3r", "-d", domain], 71 | capture_output=True, 72 | text=True, 73 | check=True 74 | ) 75 | output = result.stdout.strip() 76 | 77 | # Try to extract subdomains from the output 78 | subdomains_from_output = self.extract_subdomains_from_output(output) 79 | 80 | # Also try to run with output file as fallback 81 | subprocess.run( 82 | ["sublist3r", "-d", domain, "-o", output_file], 83 | capture_output=True, 84 | text=True, 85 | check=True 86 | ) 87 | 88 | # Parse the output file to get subdomains 89 | subdomains_from_file = [] 90 | if os.path.exists(output_file): 91 | with open(output_file, 'r') as f: 92 | subdomains_from_file = [line.strip() for line in f if line.strip()] 93 | 94 | # Combine subdomains from both sources 95 | subdomains = list(set(subdomains_from_output + subdomains_from_file)) 96 | 97 | # Save results to JSON file 98 | json_output_file = os.path.join(output_dir, f"{domain}_subdomains.json") 99 | with open(json_output_file, 'w') as f: 100 | json.dump(subdomains, f) 101 | 102 | logger.info(f"Sublist3r found {len(subdomains)} subdomains for {domain}") 103 | logger.info(f"Sublist3r results saved to {json_output_file}") 104 | 105 | return { 106 | "domain": domain, 107 | "success": True, 108 | "subdomains": subdomains, 109 | "output": output 110 | } 111 | 112 | except subprocess.CalledProcessError as e: 113 | logger.error(f"Sublist3r scan failed: {e.stderr.strip()}") 114 | return { 115 | "domain": domain, 116 | "success": False, 117 | "error": e.stderr.strip() 118 | } 119 | 120 | 121 | def ExecSublist3r(domain: str, output_dir: str = "results") -> Dict[str, Any]: 122 | """ 123 | Convenience wrapper to run a Sublist3r scan. 124 | 125 | Args: 126 | domain (str): The target domain to enumerate subdomains for. 127 | output_dir (str): The directory to save the output files, defaults to "results". 128 | 129 | Returns: 130 | Dict[str, Any]: Sublist3r scan results. 131 | """ 132 | scanner = Sublist3r() 133 | return scanner.scan(domain, output_dir) ``` -------------------------------------------------------------------------------- /toolkit/sherlock.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: Sherlock 3 | # - Description: Username hunter for social networks 4 | # - Usage: Finds usernames across multiple social networks 5 | # - Parameters: usernames (required), various optional parameters 6 | 7 | import subprocess 8 | import logging 9 | from typing import Dict, Any, List, Optional 10 | 11 | # Logging setup 12 | logging.basicConfig( 13 | level=logging.INFO, 14 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 15 | ) 16 | logger = logging.getLogger("sherlock-tool") 17 | 18 | 19 | class Sherlock: 20 | """ 21 | Sherlock wrapper class for hunting usernames across social networks. 22 | """ 23 | 24 | def __init__(self): 25 | """Initialize Sherlock wrapper.""" 26 | logger.info("Sherlock wrapper initialized.") 27 | 28 | def hunt(self, 29 | usernames: List[str], 30 | output: Optional[str] = None, 31 | folderoutput: Optional[str] = None, 32 | verbose: bool = False, 33 | tor: bool = False, 34 | unique_tor: bool = False, 35 | csv: bool = False, 36 | xlsx: bool = False, 37 | sites: Optional[List[str]] = None, 38 | proxy: Optional[str] = None, 39 | json_file: Optional[str] = None, 40 | timeout: Optional[int] = None, 41 | print_all: bool = False, 42 | print_found: bool = False, 43 | no_color: bool = False, 44 | browse: bool = False, 45 | local: bool = False, 46 | nsfw: bool = False 47 | ) -> Dict[str, Any]: 48 | """ 49 | Run the sherlock hunt for given usernames. 50 | 51 | Args: 52 | usernames (List[str]): List of usernames to check. 53 | output (Optional[str]): Output file for single username results. 54 | folderoutput (Optional[str]): Output folder for multiple username results. 55 | verbose (bool): Display extra debugging information. 56 | tor (bool): Make requests over Tor. 57 | unique_tor (bool): Make requests over Tor with new circuit per request. 58 | csv (bool): Create CSV output file. 59 | xlsx (bool): Create XLSX output file. 60 | sites (Optional[List[str]]): Specific sites to check. 61 | proxy (Optional[str]): Proxy URL to use. 62 | json_file (Optional[str]): JSON file for data input. 63 | timeout (Optional[int]): Request timeout in seconds. 64 | print_all (bool): Output sites where username was not found. 65 | print_found (bool): Output sites where username was found. 66 | no_color (bool): Disable colored terminal output. 67 | browse (bool): Open results in browser. 68 | local (bool): Force use of local data.json file. 69 | nsfw (bool): Include NSFW sites in checks. 70 | 71 | Returns: 72 | Dict[str, Any]: Result or error from the sherlock command. 73 | """ 74 | logger.info(f"Running Sherlock hunt for: {', '.join(usernames)}") 75 | 76 | cmd = ["sherlock"] 77 | 78 | # Add all flags and their values 79 | if verbose: 80 | cmd.append("--verbose") 81 | if folderoutput: 82 | cmd.extend(["--folderoutput", folderoutput]) 83 | if output: 84 | cmd.extend(["--output", output]) 85 | if tor: 86 | cmd.append("--tor") 87 | if unique_tor: 88 | cmd.append("--unique-tor") 89 | if csv: 90 | cmd.append("--csv") 91 | if xlsx: 92 | cmd.append("--xlsx") 93 | if sites: 94 | for site in sites: 95 | cmd.extend(["--site", site]) 96 | if proxy: 97 | cmd.extend(["--proxy", proxy]) 98 | if json_file: 99 | cmd.extend(["--json", json_file]) 100 | if timeout: 101 | cmd.extend(["--timeout", str(timeout)]) 102 | if print_all: 103 | cmd.append("--print-all") 104 | if print_found: 105 | cmd.append("--print-found") 106 | if no_color: 107 | cmd.append("--no-color") 108 | if browse: 109 | cmd.append("--browse") 110 | if local: 111 | cmd.append("--local") 112 | if nsfw: 113 | cmd.append("--nsfw") 114 | 115 | # Add usernames 116 | cmd.extend(usernames) 117 | 118 | try: 119 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) 120 | output = result.stdout.strip() 121 | return { 122 | "usernames": usernames, 123 | "success": True, 124 | "output": output 125 | } 126 | except subprocess.CalledProcessError as e: 127 | logger.error(f"Sherlock hunt failed: {e.stderr.strip()}") 128 | return { 129 | "usernames": usernames, 130 | "success": False, 131 | "error": e.stderr.strip() 132 | } 133 | 134 | 135 | def ExecSherlock(usernames: List[str], **kwargs) -> Dict[str, Any]: 136 | """ 137 | Convenience wrapper to run a sherlock username hunt. 138 | 139 | Args: 140 | usernames (List[str]): Usernames to hunt. 141 | **kwargs: Additional parameters to pass to Sherlock.hunt(). 142 | 143 | Returns: 144 | Dict[str, Any]: Sherlock hunt results. 145 | """ 146 | scanner = Sherlock() 147 | return scanner.hunt(usernames, **kwargs) ``` -------------------------------------------------------------------------------- /toolkit/ocr2text.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: OCR Scanner 3 | # - Description: Optical Character Recognition tool 4 | # - Usage: Extracts text from images and PDF files 5 | # - Parameters: file_path (required) - can be a local file or URL prefixed with @ 6 | 7 | import cv2 8 | import pytesseract 9 | from pdf2image import convert_from_path 10 | import os 11 | import numpy as np 12 | from typing import Dict, Any 13 | import requests 14 | import tempfile 15 | from urllib.parse import urlparse 16 | 17 | # Configure Tesseract path - use system path for Kali Linux 18 | if os.path.exists('/usr/bin/tesseract'): 19 | pytesseract.pytesseract.tesseract_cmd = r'/usr/bin/tesseract' 20 | elif os.path.exists('/usr/local/bin/tesseract'): 21 | pytesseract.pytesseract.tesseract_cmd = r'/usr/local/bin/tesseract' 22 | elif os.path.exists('C:\\Program Files\\Tesseract-OCR\\tesseract.exe'): 23 | pytesseract.pytesseract.tesseract_cmd = r'C:\\Program Files\\Tesseract-OCR\\tesseract.exe' 24 | 25 | def download_image(url): 26 | """Download image from a URL and save to a temporary file""" 27 | try: 28 | response = requests.get(url, stream=True) 29 | response.raise_for_status() 30 | 31 | # Get the file extension from the URL 32 | parsed_url = urlparse(url) 33 | filename = os.path.basename(parsed_url.path) 34 | _, ext = os.path.splitext(filename) 35 | 36 | if not ext: 37 | # Default to .png if no extension found 38 | ext = '.png' 39 | 40 | # Create a temporary file with the correct extension 41 | temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False) 42 | temp_filename = temp_file.name 43 | 44 | # Write the image data to the temporary file 45 | with open(temp_filename, 'wb') as f: 46 | for chunk in response.iter_content(chunk_size=8192): 47 | f.write(chunk) 48 | 49 | return temp_filename 50 | except Exception as e: 51 | print(f"Error downloading image: {str(e)}") 52 | return None 53 | 54 | def process_image(img): 55 | """Process image for better OCR results""" 56 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 57 | gray, img_bin = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) 58 | gray = cv2.bitwise_not(img_bin) 59 | 60 | kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 1)) 61 | dilation = cv2.dilate(gray, kernel, iterations=1) 62 | erosion = cv2.erode(dilation, kernel, iterations=1) 63 | 64 | text = pytesseract.image_to_string(erosion) 65 | return text 66 | 67 | def extract_text_from_pdf(pdf_path): 68 | """Extract text from a PDF file using OCR""" 69 | try: 70 | # Use poppler_path if on Windows, but we're in Kali Linux so not needed 71 | images = convert_from_path(pdf_path) 72 | 73 | all_text = [] 74 | for i, image in enumerate(images): 75 | opencv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) 76 | text = process_image(opencv_image) 77 | all_text.append(f"=== Page {i+1} ===\n{text}\n") 78 | 79 | return "\n".join(all_text) 80 | except Exception as e: 81 | return f"Error processing PDF: {str(e)}" 82 | 83 | def extract_text_from_image(image_path): 84 | """Extract text from an image file using OCR""" 85 | try: 86 | img = cv2.imread(image_path) 87 | if img is None: 88 | return f"Error: Could not read image file {image_path}" 89 | return process_image(img) 90 | except Exception as e: 91 | return f"Error processing image: {str(e)}" 92 | 93 | def ExecOcr2Text(file_path: str) -> Dict[str, Any]: 94 | """Execute OCR on a file (image or PDF) or URL""" 95 | temp_file = None 96 | 97 | try: 98 | # Check if the file_path is a URL (starts with @ or http/https) 99 | if file_path.startswith('@'): 100 | url = file_path[1:] # Remove the @ symbol 101 | print(f"Downloading image from URL: {url}") 102 | temp_file = download_image(url) 103 | elif file_path.startswith(('http://', 'https://')): 104 | url = file_path 105 | print(f"Downloading image from URL: {url}") 106 | temp_file = download_image(url) 107 | 108 | if temp_file is None and (file_path.startswith('@') or file_path.startswith(('http://', 'https://'))): 109 | return { 110 | "success": False, 111 | "error": f"Failed to download image from URL: {url}", 112 | "text": "" 113 | } 114 | 115 | # Use the temp file if a URL was provided 116 | if temp_file: 117 | file_path = temp_file 118 | 119 | # Check if the file exists 120 | if not os.path.exists(file_path): 121 | return { 122 | "success": False, 123 | "error": f"File not found: {file_path}", 124 | "text": "" 125 | } 126 | 127 | file_ext = os.path.splitext(file_path)[1].lower() 128 | 129 | if file_ext == '.pdf': 130 | text = extract_text_from_pdf(file_path) 131 | elif file_ext in ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif']: 132 | text = extract_text_from_image(file_path) 133 | else: 134 | return { 135 | "success": False, 136 | "error": f"Unsupported file format: {file_ext}", 137 | "text": "" 138 | } 139 | 140 | return { 141 | "success": True, 142 | "error": "", 143 | "text": text 144 | } 145 | except Exception as e: 146 | return { 147 | "success": False, 148 | "error": str(e), 149 | "text": "" 150 | } 151 | finally: 152 | # Clean up temporary file if it was created 153 | if temp_file and os.path.exists(temp_file): 154 | try: 155 | os.unlink(temp_file) 156 | except: 157 | pass 158 | ``` -------------------------------------------------------------------------------- /toolkit/dnsrecon.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: DNSRecon 3 | # - Description: DNS reconnaissance tool 4 | # - Usage: Performs various DNS-related scans and enumeration 5 | # - Parameters: domain (required), scan_type (optional), name_server (optional), range (optional), dictionary (optional) 6 | 7 | import subprocess 8 | import logging 9 | from typing import Any, Dict, List, Optional 10 | import json 11 | 12 | # Logging setup 13 | logging.basicConfig( 14 | level=logging.INFO, 15 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 16 | ) 17 | logger = logging.getLogger("dnsrecon-tool") 18 | 19 | class DNSRecon: 20 | """DNSRecon tool wrapper class.""" 21 | 22 | def __init__(self): 23 | """Initialize DNSRecon wrapper.""" 24 | self._check_installed() 25 | 26 | def _check_installed(self) -> None: 27 | """Verify DNSRecon is installed.""" 28 | try: 29 | subprocess.run(["dnsrecon", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 30 | logger.info("DNSRecon installation verified.") 31 | except (subprocess.SubprocessError, FileNotFoundError): 32 | logger.warning("dnsrecon command not found. Make sure DNSRecon is installed and in your PATH.") 33 | raise RuntimeError("DNSRecon is not installed or not in PATH.") 34 | 35 | def get_version(self) -> Optional[str]: 36 | """Get installed DNSRecon version.""" 37 | try: 38 | result = subprocess.run(["dnsrecon", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 39 | version = result.stdout.strip() if result.stdout else result.stderr.strip() 40 | logger.info(f"DNSRecon version: {version}") 41 | return version 42 | except (subprocess.SubprocessError, FileNotFoundError) as e: 43 | logger.error(f"DNSRecon version check failed: {e}") 44 | return None 45 | 46 | def scan(self, domain: Optional[str] = None, scan_type: Optional[str] = "std", **kwargs: Any) -> Dict[str, Any]: 47 | """Run the DNSRecon scan.""" 48 | cmd = ["dnsrecon"] 49 | 50 | # Handle required parameters based on scan type 51 | if scan_type == "rvl": 52 | if "range" not in kwargs: 53 | raise ValueError("IP range must be provided for reverse lookup scan type.") 54 | cmd.extend(["-t", scan_type, "-r", kwargs["range"]]) 55 | elif scan_type not in ["tld", "zonewalk"]: 56 | if not domain: 57 | raise ValueError("Target domain must be provided for this scan type.") 58 | cmd.extend(["-t", scan_type, "-d", domain]) 59 | else: 60 | if not domain: 61 | raise ValueError("Target domain must be provided.") 62 | cmd.extend(["-t", scan_type, "-d", domain]) 63 | 64 | # Handle optional parameters 65 | if "name_server" in kwargs: 66 | cmd.extend(["-n", kwargs["name_server"]]) 67 | if "dictionary" in kwargs: 68 | cmd.extend(["-D", kwargs["dictionary"]]) 69 | if kwargs.get("filter_wildcard", False): 70 | cmd.append("-f") 71 | if kwargs.get("axfr", False): 72 | cmd.append("-a") 73 | if kwargs.get("spf", False): 74 | cmd.append("-s") 75 | if kwargs.get("bing", False): 76 | cmd.append("-b") 77 | if kwargs.get("yandex", False): 78 | cmd.append("-y") 79 | if kwargs.get("crt", False): 80 | cmd.append("-k") 81 | if kwargs.get("whois", False): 82 | cmd.append("-w") 83 | if kwargs.get("dnssec", False): 84 | cmd.append("-z") 85 | if "threads" in kwargs: 86 | cmd.extend(["--threads", str(kwargs["threads"])]) 87 | if "lifetime" in kwargs: 88 | cmd.extend(["--lifetime", str(kwargs["lifetime"])]) 89 | if kwargs.get("tcp", False): 90 | cmd.append("--tcp") 91 | if "db" in kwargs: 92 | cmd.extend(["--db", kwargs["db"]]) 93 | if "xml" in kwargs: 94 | cmd.extend(["-x", kwargs["xml"]]) 95 | if "csv" in kwargs: 96 | cmd.extend(["-c", kwargs["csv"]]) 97 | if "json" in kwargs: 98 | cmd.extend(["-j", kwargs["json"]]) 99 | if kwargs.get("ignore_wildcard", False): 100 | cmd.append("--iw") 101 | if kwargs.get("disable_check_recursion", False): 102 | cmd.append("--disable_check_recursion") 103 | if kwargs.get("disable_check_bindversion", False): 104 | cmd.append("--disable_check_bindversion") 105 | if kwargs.get("verbose", False): 106 | cmd.append("-v") 107 | 108 | logger.info(f"Preparing DNSRecon command: {' '.join(cmd)}") 109 | 110 | # Run the scan 111 | try: 112 | result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 113 | output = result.stdout 114 | 115 | # Parse results 116 | parse_result = self._parse_output(output) 117 | return { 118 | "status": "success", 119 | "command": " ".join(cmd), 120 | "raw_output": output, 121 | "results": parse_result 122 | } 123 | except subprocess.SubprocessError as e: 124 | logger.error(f"Error during DNSRecon scan: {e}") 125 | return { 126 | "status": "error", 127 | "message": str(e), 128 | "command": " ".join(cmd), 129 | "stderr": e.stderr if hasattr(e, 'stderr') else "" 130 | } 131 | 132 | def _parse_output(self, output: str) -> List[Dict[str, Any]]: 133 | """Parse the output from DNSRecon into structured data.""" 134 | results = [] 135 | lines = output.strip().split('\n') 136 | 137 | for line in lines: 138 | if '[*]' in line or '[-]' in line or '[+]' in line: 139 | # Skip the log/status lines 140 | continue 141 | 142 | parts = line.strip().split() 143 | if len(parts) >= 4: 144 | try: 145 | record = { 146 | "record_type": parts[0], 147 | "name": parts[1], 148 | "data": " ".join(parts[2:]) 149 | } 150 | results.append(record) 151 | except Exception as e: 152 | logger.error(f"Error parsing output line: {line}, error: {e}") 153 | 154 | return results 155 | 156 | # Example usage (optional convenience function) 157 | def ExecDNSRecon(domain: Optional[str] = None, scan_type: str = "std", **kwargs: Any) -> Dict[str, Any]: 158 | """Convenience wrapper to run a DNSRecon scan.""" 159 | scanner = DNSRecon() 160 | try: 161 | return scanner.scan(domain, scan_type, **kwargs) 162 | except Exception as e: 163 | logger.error(f"DNSRecon scan failed: {e}") 164 | return {"error": str(e)} 165 | ``` -------------------------------------------------------------------------------- /toolkit/sqlmap.py: -------------------------------------------------------------------------------- ```python 1 | # INFORMATION: 2 | # - Tool: SQLMap 3 | # - Description: SQL injection vulnerability scanner 4 | # - Usage: Detects and exploits SQL injection vulnerabilities in web applications 5 | # - Parameters: url (required), data (optional) 6 | 7 | import subprocess 8 | import json 9 | import logging 10 | import re 11 | from typing import List, Optional, Dict, Any, Union 12 | 13 | # Configure logging 14 | logging.basicConfig(level=logging.INFO) 15 | logger = logging.getLogger(__name__) 16 | 17 | # Database Management System (DBMS) info variables 18 | isDbmsFound = False 19 | dbms = "" 20 | dbmsVersion = "" 21 | dbmsVersionFound = False 22 | 23 | # Tamper scripts for various DBMS 24 | tamperscripts = { 25 | "MySQL": [ 26 | "union2urls", "randomcase", "space2comment", "between", "charencode" 27 | ], 28 | "PostgreSQL": [ 29 | "randomcase", "space2comment", "postgreSQLbool" 30 | ], 31 | "Microsoft SQL Server": [ 32 | "charencode", "space2comment", "union2urls", "mssql08" 33 | ], 34 | "Oracle": [ 35 | "oracle2", "space2comment", "union2urls" 36 | ], 37 | "SQLite": [ 38 | "randomcase", "space2comment", "union2urls", "sqliteunicode" 39 | ], 40 | "Generic": [ 41 | "charencode", "space2comment", "union2urls" 42 | ] 43 | } 44 | 45 | 46 | def run_sqlmap(cmd: List[str]) -> subprocess.CompletedProcess: 47 | """ 48 | Helper function to run the sqlmap command. 49 | 50 | Args: 51 | cmd: List of command-line arguments for sqlmap. 52 | 53 | Returns: 54 | subprocess.CompletedProcess: The result of running sqlmap. 55 | """ 56 | logger.debug("Running sqlmap command: %s", " ".join(cmd)) 57 | try: 58 | result = subprocess.run( 59 | cmd, 60 | capture_output=True, 61 | text=True, 62 | check=True 63 | ) 64 | return result 65 | except subprocess.CalledProcessError as e: 66 | logger.error("SQLMap command failed: %s", e.stderr) 67 | raise e 68 | 69 | 70 | def check_sqlmap_installed() -> bool: 71 | """ 72 | Check if sqlmap is installed on the system. 73 | 74 | Returns: 75 | bool: True if sqlmap is installed, False otherwise 76 | """ 77 | try: 78 | subprocess.run(["which", "sqlmap"], capture_output=True, check=True) 79 | return True 80 | except subprocess.CalledProcessError: 81 | return False 82 | 83 | 84 | def gather_information(url: str) -> bool: 85 | """ 86 | Gather information about the target URL by running sqlmap with the --batch and -v 0 options. 87 | 88 | Args: 89 | url: Target URL to scan 90 | 91 | Returns: 92 | bool: True if the operation was successful, False otherwise 93 | """ 94 | cmd = ["sqlmap", "-u", url, "--batch", "-v", "0"] 95 | 96 | try: 97 | result = run_sqlmap(cmd) 98 | return result.returncode == 0 99 | except subprocess.CalledProcessError: 100 | return False 101 | 102 | 103 | def try_tamper(url: str, tamper: str) -> bool: 104 | """ 105 | Try a specific tamper script on the target URL by running sqlmap with the --batch and -v 0 options. 106 | 107 | Args: 108 | url: Target URL to scan 109 | tamper: Tamper script to use 110 | 111 | Returns: 112 | bool: True if the operation was successful, False otherwise 113 | """ 114 | cmd = ["sqlmap", "-u", url, "--batch", "-v", "0", "--tamper", tamper] 115 | 116 | try: 117 | result = run_sqlmap(cmd) 118 | return result.returncode == 0 119 | except subprocess.CalledProcessError: 120 | return False 121 | 122 | 123 | def try_with_risk_and_level(url: str, risk: int, level: int) -> bool: 124 | """ 125 | Try a specific risk and level on the target URL by running sqlmap with the --batch and -v 0 options. 126 | 127 | Args: 128 | url: Target URL to scan 129 | risk: Risk level (1-3) 130 | level: Level (1-5) 131 | 132 | Returns: 133 | bool: True if the operation was successful, False otherwise 134 | """ 135 | cmd = ["sqlmap", "-u", url, "--batch", "-v", "0", "--level", str(level), "--risk", str(risk)] 136 | 137 | try: 138 | result = run_sqlmap(cmd) 139 | return result.returncode == 0 140 | except subprocess.CalledProcessError: 141 | return False 142 | 143 | 144 | def try_with_technique(url: str, technique: str) -> bool: 145 | """ 146 | Try a specific technique on the target URL by running sqlmap with the --batch and -v 0 options. 147 | 148 | Args: 149 | url: Target URL to scan 150 | technique: Technique to use (e.g., "B", "E", "T", "U") 151 | 152 | Returns: 153 | bool: True if the operation was successful, False otherwise 154 | """ 155 | cmd = ["sqlmap", "-u", url, "--batch", "-v", "0", "--technique", technique] 156 | 157 | try: 158 | result = run_sqlmap(cmd) 159 | return result.returncode == 0 160 | except subprocess.CalledProcessError: 161 | return False 162 | 163 | 164 | def ExecSqlmap(url: str, data: Optional[str] = None) -> Dict[str, Any]: 165 | """ 166 | Run sqlmap with the given URL and data. 167 | 168 | Args: 169 | url: Target URL to scan 170 | data: POST data to include in the request 171 | 172 | Returns: 173 | Dict[str, Any]: Result or error from the sqlmap command 174 | """ 175 | options = [] 176 | if data: 177 | options.extend(["--data", data]) 178 | 179 | cmd = ["sqlmap", "-u", url, "--batch", "-v", "0", "--output-dir=/tmp/sqlmap"] 180 | cmd.extend(options) 181 | 182 | try: 183 | result = run_sqlmap(cmd) 184 | parsed_output = parse_sqlmap_output(result.stdout) 185 | return { 186 | "success": True, 187 | "url": url, 188 | "results": parsed_output 189 | } 190 | except subprocess.CalledProcessError as e: 191 | return { 192 | "success": False, 193 | "error": str(e), 194 | "stderr": e.stderr 195 | } 196 | except Exception as e: 197 | return { 198 | "success": False, 199 | "error": str(e) 200 | } 201 | 202 | 203 | def categorize_tamperscript(db_tech: str) -> str: 204 | """ 205 | Categorizes tamper scripts by database technology. 206 | 207 | Args: 208 | db_tech: The name of the database technology (e.g., MySQL, PostgreSQL, etc.) 209 | 210 | Returns: 211 | str: JSON string containing the tamper scripts for the provided DB technology. 212 | """ 213 | try: 214 | if db_tech in tamperscripts: 215 | return json.dumps({ 216 | "success": True, 217 | "database": db_tech, 218 | "tamper_scripts": tamperscripts[db_tech] 219 | }) 220 | else: 221 | return json.dumps({ 222 | "success": False, 223 | "error": f"Tamper scripts for {db_tech} not found." 224 | }) 225 | except Exception as e: 226 | return json.dumps({ 227 | "success": False, 228 | "error": str(e) 229 | }) 230 | 231 | 232 | def parse_sqlmap_output(output: str) -> Dict[str, Any]: 233 | """ 234 | Parse the output from sqlmap to extract useful information. 235 | 236 | Args: 237 | output: The stdout from sqlmap 238 | 239 | Returns: 240 | Dict: A dictionary containing parsed information 241 | """ 242 | result = { 243 | "vulnerable": False, 244 | "dbms": None, 245 | "payloads": [], 246 | "tables": [], 247 | "raw_output": output 248 | } 249 | 250 | # Check if any vulnerability was found 251 | if "is vulnerable to" in output: 252 | result["vulnerable"] = True 253 | 254 | # Try to extract DBMS information 255 | dbms_match = re.search(r"back-end DBMS: (.+?)(?:\n|\[)", output) 256 | if dbms_match: 257 | result["dbms"] = dbms_match.group(1).strip() 258 | 259 | # Try to extract payload information 260 | payload_matches = re.findall(r"Payload: (.+?)(?:\n|$)", output) 261 | result["payloads"] = [p.strip() for p in payload_matches] 262 | 263 | # Extract tables if available 264 | tables_section = re.search(r"Database: .*?\nTable: (.*?)(?:\n\n|\Z)", output, re.DOTALL) 265 | if tables_section: 266 | tables_text = tables_section.group(1) 267 | tables = re.findall(r"\|\s+([^\|]+?)\s+\|", tables_text) 268 | result["tables"] = [t.strip() for t in tables] 269 | 270 | return result 271 | 272 | 273 | def ExecSqlmap(url: str, data: Optional[str] = None) -> Dict[str, Any]: 274 | """ 275 | Run sqlmap vulnerability scan on a specified target URL. 276 | Main entry point for the MCP server to call this tool. 277 | 278 | Args: 279 | url: Target URL to scan 280 | data: Optional POST data to include in the request 281 | 282 | Returns: 283 | Dict: JSON-serializable dictionary with scan results 284 | """ 285 | logger.info(f"Starting SQLMap scan on {url}") 286 | 287 | # Check if sqlmap is installed 288 | if not check_sqlmap_installed(): 289 | return { 290 | "success": False, 291 | "error": "SQLMap is not installed. Install it with 'pip install sqlmap' or 'apt-get install sqlmap'." 292 | } 293 | 294 | # Define base command 295 | cmd = ["sqlmap", "-u", url, "--batch", "--forms", "--json-output"] 296 | 297 | # Add data parameter if provided 298 | if data: 299 | cmd.extend(["--data", data]) 300 | 301 | # Run initial scan 302 | try: 303 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) 304 | 305 | # Check if JSON output is available 306 | try: 307 | # Try to parse JSON output 308 | json_output_path = re.search(r"JSON report saved to: (.+)", result.stdout) 309 | if json_output_path: 310 | with open(json_output_path.group(1), 'r') as json_file: 311 | json_data = json.load(json_file) 312 | return { 313 | "success": True, 314 | "url": url, 315 | "vulnerable": bool(json_data.get("vulnerabilities", [])), 316 | "data": json_data 317 | } 318 | except (json.JSONDecodeError, FileNotFoundError): 319 | pass 320 | 321 | # If JSON parsing failed, parse the output manually 322 | parsed_results = parse_sqlmap_output(result.stdout) 323 | return { 324 | "success": True, 325 | "url": url, 326 | "vulnerable": parsed_results["vulnerable"], 327 | "dbms": parsed_results["dbms"], 328 | "payloads": parsed_results["payloads"], 329 | "tables": parsed_results["tables"], 330 | "output": result.stdout 331 | } 332 | 333 | except subprocess.CalledProcessError as e: 334 | logger.error(f"SQLMap scan failed: {e.stderr}") 335 | return { 336 | "success": False, 337 | "error": "SQLMap scan failed", 338 | "details": e.stderr 339 | } 340 | except Exception as e: 341 | logger.exception("Unexpected error during SQLMap execution") 342 | return { 343 | "success": False, 344 | "error": f"Error executing SQLMap scan: {str(e)}" 345 | } 346 | 347 | 348 | ``` -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 7 | <title>MCPHydra</title> 8 | <style> 9 | body { 10 | background-color: #000000; 11 | margin: 0; 12 | 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | min-height: 100vh; 17 | } 18 | 19 | #flag-container { 20 | position: relative; 21 | overflow: hidden; 22 | max-width: 100%; 23 | height: 100vh; 24 | } 25 | 26 | 27 | #tiresult { 28 | font-size: 16px; 29 | background-color: #000000; 30 | font-weight: bold; 31 | padding: 3px 5px; 32 | margin: 0; 33 | display: inline-block; 34 | white-space: pre; 35 | width: 100%; 36 | overflow-x: auto; 37 | } 38 | 39 | #mcphydra { 40 | background-color: #000000; 41 | font-weight: bold; 42 | padding: 3px 5px; 43 | margin: 0; 44 | display: inline-block; 45 | font-size: 18px; 46 | white-space: pre; 47 | width: 100%; 48 | overflow-x: auto; 49 | color: #07df07; 50 | } 51 | 52 | /* Responsive font sizes */ 53 | @media screen and (max-width: 1024px) { 54 | 55 | #tiresult, 56 | #mcphydra { 57 | font-size: 20px; 58 | } 59 | 60 | #mcphydra { 61 | font-size: 10px; 62 | } 63 | } 64 | 65 | @media screen and (max-width: 768px) { 66 | 67 | #tiresult, 68 | #mcphydra { 69 | font-size: 20px; 70 | } 71 | 72 | #mcphydra { 73 | font-size: 10px; 74 | } 75 | } 76 | 77 | @media screen and (max-width: 480px) { 78 | 79 | #tiresult, 80 | #mcphydra { 81 | font-size: 14px; 82 | } 83 | 84 | #mcphydra { 85 | font-size: 10px; 86 | } 87 | } 88 | 89 | /* Animation for each character */ 90 | #tiresult b { 91 | display: inline-block; 92 | animation: wave 1s infinite ease-in-out; 93 | transform-origin: center bottom; 94 | position: relative; 95 | } 96 | 97 | /* Base animation keyframes - will be overridden by JavaScript for responsive sizing */ 98 | @keyframes wave { 99 | 0% { 100 | transform: translateY(0) rotate(0deg); 101 | } 102 | 103 | 20% { 104 | transform: translateY(-1px) rotate(1.5deg) translateX(0.8px); 105 | } 106 | 107 | 40% { 108 | transform: translateY(-0.5px) rotate(0.5deg) translateX(0.4px); 109 | } 110 | 111 | 60% { 112 | transform: translateY(0) rotate(0deg); 113 | } 114 | 115 | 80% { 116 | transform: translateY(0.8px) rotate(-1.5deg) translateX(-0.8px); 117 | } 118 | 119 | 100% { 120 | transform: translateY(0) rotate(0deg); 121 | } 122 | } 123 | </style> 124 | </head> 125 | 126 | <body> 127 | <div id="flag-container"> 128 | <pre id="mcphydra"> 129 | __ __ 130 | ____ ___ _________ / /_ __ ______/ /________ _ 131 | / __ `__ \/ ___/ __ \/ __ \/ / / / __ / ___/ __ `/ 132 | / / / / / / /__/ /_/ / / / / /_/ / /_/ / / / /_/ / 133 | /_/ /_/ /_/\___/ .___/_/ /_/\__, /\__,_/_/ \__,_/ 134 | /_/ /____/ v0.1.0 135 | </pre> 136 | <pre id="tiresult"> 137 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⢿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 138 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⠟</b><b style="color:#07df07">⠙</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠋</b><b style="color:#07df07">⠙</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⠷</b><b style="color:#07df07">⠄</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢸</b><b style="color:#07df07">⣿</b> 139 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⢿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠋</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢠</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 140 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⡿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣀</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣴</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣇</b><b style="color:#07df07">⡀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 141 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⣠</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⢿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣷</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 142 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠟</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣠</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⡿</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠟</b><b style="color:#07df07">⠙</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠟</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⡆</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 143 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⠏</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠁</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 144 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠁</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⣠</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣦</b><b style="color:#07df07">⡄</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 145 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⣠</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣦</b><b style="color:#07df07">⡀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣆</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 146 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">mcphydra</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠘</b><b style="color:#07df07">⠛</b><b style="color:#07df07">⠛</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣦</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⣻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 147 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⢻</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⡿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⢿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 148 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣠</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣄</b><b style="color:#07df07">⣀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠛</b><b style="color:#07df07">⠹</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⠷</b><b style="color:#07df07">⣄</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⠉</b><b style="color:#07df07">⣹</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 149 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣾</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣷</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣴</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 150 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⢀</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣷</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣆</b><b style="color:#07df07">⡀</b><b style="color:#07df07">⠀</b><b style="color:#07df07">⠈</b><b style="color:#07df07">⠻</b><b style="color:#07df07">⠿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 151 | <b style="color:#07df07">⣿</b><b style="color:#07df07">⣤</b><b style="color:#07df07">⣼</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣶</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b><b style="color:#07df07">⣿</b> 152 | </pre> 153 | <script> 154 | document.addEventListener('DOMContentLoaded', function () { 155 | const chars = document.querySelectorAll('#tiresult b'); 156 | 157 | // Handle responsive animation scaling 158 | function adjustAnimation() { 159 | const viewportWidth = window.innerWidth; 160 | let scaleFactor = 1; 161 | 162 | if (viewportWidth <= 480) { 163 | scaleFactor = 0.5; 164 | } else if (viewportWidth <= 768) { 165 | scaleFactor = 0.7; 166 | } else if (viewportWidth <= 1024) { 167 | scaleFactor = 0.85; 168 | } 169 | 170 | function getRandomColor() { 171 | return '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); 172 | } 173 | 174 | let randomColors = Array.from({ length: 10 }, getRandomColor); 175 | setInterval(() => { 176 | randomColors = Array.from({ length: 10 }, getRandomColor); 177 | chars.forEach((char, index) => { 178 | const colorIndex = Math.floor(Math.random() * randomColors.length); 179 | char.style.color = randomColors[colorIndex]; 180 | }); 181 | }, 2000); 182 | 183 | console.log(randomColors); 184 | 185 | chars.forEach((char, index) => { 186 | const row = Math.floor(index / 130); 187 | const col = index % 130; 188 | const delay = (col * 0.015 + row * 0.06) % 1.5; 189 | 190 | char.style.animationDelay = delay + 's'; 191 | const durationVariation = 0.9 + Math.random() * 0.2; // 0.9-1.1 192 | char.style.animationDuration = (1.5 * durationVariation) + 's'; 193 | 194 | // Scale transform values based on viewport 195 | const style = document.createElement('style'); 196 | style.innerHTML = ` 197 | @keyframes wave { 198 | 0% { transform: translateY(0) rotate(0deg); } 199 | 20% { transform: translateY(${-1 * scaleFactor}px) rotate(${1.5 * scaleFactor}deg) translateX(${0.8 * scaleFactor}px); } 200 | 40% { transform: translateY(${-0.5 * scaleFactor}px) rotate(${0.5 * scaleFactor}deg) translateX(${0.4 * scaleFactor}px); } 201 | 60% { transform: translateY(0) rotate(0deg); } 202 | 80% { transform: translateY(${0.8 * scaleFactor}px) rotate(${-1.5 * scaleFactor}deg) translateX(${-0.8 * scaleFactor}px); } 203 | 100% { transform: translateY(0) rotate(0deg); } 204 | } 205 | 206 | `; 207 | document.head.appendChild(style); 208 | }); 209 | } 210 | 211 | // Initial setup 212 | adjustAnimation(); 213 | 214 | // Re-adjust on window resize 215 | window.addEventListener('resize', adjustAnimation); 216 | }); 217 | </script> 218 | </body> 219 | 220 | </html> ```