# 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>
```