diff --git a/EXAMPLES.md b/EXAMPLES.md index 239218c13..78bb019bf 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -75,10 +75,10 @@ python sqlmapcli.py -u "https://demo.owasp-juice.shop/rest/user/login" --data='{ Test multiple endpoints with concurrency: ```bash -# Test multiple endpoints from a JSON file with 5 concurrent scans (default) +# Test multiple endpoints from a JSON file with auto-scaled concurrency (default, typically 2x CPU cores) python sqlmapcli.py -b endpoints.json --level 2 --risk 2 -# Test with higher concurrency (10 concurrent scans) +# Test with specific concurrency (10 concurrent scans) python sqlmapcli.py -b endpoints.json --level 2 --risk 2 --concurrency 10 # Test with custom settings diff --git a/README.md b/README.md index f4389d820..24bb2fa99 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ python sqlmapcli.py --interactive ``` -u, --url Target URL -b, --batch-file JSON file with multiple endpoints --c, --concurrency Concurrent scans for batch mode (default: 5) +-c, --concurrency Concurrent scans for batch mode (default: 0 for auto-scale based on CPU count) --comprehensive Run all risk/level combinations (1-3 risk, 1-5 levels) --level {1-5} Test level (default: 1) --risk {1-3} Test risk (default: 1) diff --git a/sql_cli/__init__.py b/sql_cli/__init__.py new file mode 100644 index 000000000..5ab66d02c --- /dev/null +++ b/sql_cli/__init__.py @@ -0,0 +1,6 @@ +""" +SQLMap CLI Package +A beautiful CLI wrapper for sqlmap with automated testing capabilities +""" + +__version__ = "1.0.0" diff --git a/sql_cli/scanner.py b/sql_cli/scanner.py index 189717d03..cb94e8a2c 100644 --- a/sql_cli/scanner.py +++ b/sql_cli/scanner.py @@ -122,10 +122,21 @@ class SQLMapScanner: # Cleanup temporary output directory try: shutil.rmtree(tmp_output_dir) - except: - pass + except Exception as cleanup_error: + console.log( + f"Failed to remove temporary sqlmap output directory {tmp_output_dir!r}: {cleanup_error}" + ) return process.returncode == 0, full_output + else: + # Run without progress (non-interactive) + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=600 + ) + + # Cleanup temporary output directory + try: + shutil.rmtree(tmp_output_dir) except Exception as cleanup_error: console.log( f"Failed to remove temporary sqlmap output directory {tmp_output_dir!r}: {cleanup_error}" @@ -255,9 +266,33 @@ class SQLMapScanner: ) if parsed["is_vulnerable"]: - self.results["vulnerabilities"].extend( - parsed["vulnerabilities"] - ) + # Deduplicate vulnerabilities across different level/risk combinations + existing_keys = set() + for v in self.results["vulnerabilities"]: + if isinstance(v, dict): + param = v.get("parameter") + vtype = v.get("type") + title = v.get("title") + else: + param = getattr(v, "parameter", None) + vtype = getattr(v, "type", None) + title = getattr(v, "title", None) + existing_keys.add((param, vtype, title)) + + for v in parsed["vulnerabilities"]: + if isinstance(v, dict): + param = v.get("parameter") + vtype = v.get("type") + title = v.get("title") + else: + param = getattr(v, "parameter", None) + vtype = getattr(v, "type", None) + title = getattr(v, "title", None) + + key = (param, vtype, title) + if key not in existing_keys: + self.results["vulnerabilities"].append(v) + existing_keys.add(key) results_table.add_row( str(level), @@ -359,7 +394,7 @@ class SQLMapScanner: headers = endpoint.get("headers") if headers is not None and isinstance(headers, list): - headers = "\\n".join(headers) + headers = "\n".join(headers) try: # Force verbosity 3 for better progress tracking if in batch mode diff --git a/sql_cli/utils.py b/sql_cli/utils.py index ac22f271e..d84dfd8d6 100644 --- a/sql_cli/utils.py +++ b/sql_cli/utils.py @@ -1,4 +1,6 @@ import re +import hashlib +import random from pathlib import Path from datetime import datetime from rich.console import Console @@ -9,11 +11,15 @@ SQLMAP_PATH = Path(__file__).parent.parent / "sqlmap.py" LOGS_DIR = Path(__file__).parent.parent / "logs" def get_log_filename(url: str) -> Path: - """Generate a log filename based on URL and timestamp""" + """Generate a log filename based on URL and timestamp with hash for uniqueness""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - # Sanitize URL for filename - safe_url = re.sub(r'[^\w\-_\.]', '_', url)[:50] - return LOGS_DIR / f"sqlmap_{safe_url}_{timestamp}.log" + # Create a hash of the URL to ensure uniqueness + url_hash = hashlib.md5(url.encode()).hexdigest()[:8] + # Add random component for additional uniqueness in batch scenarios + random_component = random.randint(1000, 9999) + # Sanitize URL for filename (keep it readable but short) + safe_url = re.sub(r'[^\w\-_\.]', '_', url)[:30] + return LOGS_DIR / f"sqlmap_{safe_url}_{url_hash}_{timestamp}_{random_component}.log" def save_log(log_file: Path, content: str): """Save content to log file""" diff --git a/sqlmapcli.py b/sqlmapcli.py index 6262aea4d..00d90cfd6 100755 --- a/sqlmapcli.py +++ b/sqlmapcli.py @@ -73,14 +73,50 @@ def interactive_mode(scanner: SQLMapScanner): ) if scan_type == "quick": - level = int(Prompt.ask("[cyan]Test level (1-5)[/cyan]", default="1")) - risk = int(Prompt.ask("[cyan]Test risk (1-3)[/cyan]", default="1")) + # Input validation for level and risk + while True: + try: + level_str = Prompt.ask("[cyan]Test level (1-5)[/cyan]", default="1") + level = int(level_str) + if 1 <= level <= 5: + break + console.print("[red]Level must be between 1 and 5[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + + while True: + try: + risk_str = Prompt.ask("[cyan]Test risk (1-3)[/cyan]", default="1") + risk = int(risk_str) + if 1 <= risk <= 3: + break + console.print("[red]Risk must be between 1 and 3[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + scanner.quick_scan(url, level, risk, data=data, headers=headers) else: - max_level = int( - Prompt.ask("[cyan]Maximum test level (1-5)[/cyan]", default="5") - ) - max_risk = int(Prompt.ask("[cyan]Maximum test risk (1-3)[/cyan]", default="3")) + # Input validation for max_level and max_risk + while True: + try: + max_level_str = Prompt.ask("[cyan]Maximum test level (1-5)[/cyan]", default="5") + max_level = int(max_level_str) + if 1 <= max_level <= 5: + break + console.print("[red]Level must be between 1 and 5[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + + while True: + try: + max_risk_str = Prompt.ask("[cyan]Maximum test risk (1-3)[/cyan]", default="3") + max_risk = int(max_risk_str) + if 1 <= max_risk <= 3: + break + console.print("[red]Risk must be between 1 and 3[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + scanner.comprehensive_scan(url, max_level, max_risk, data=data, headers=headers) @@ -197,6 +233,21 @@ Examples: verbose=verbose_level, ) return + except FileNotFoundError: + console.print( + f"[bold red]Error: Batch file not found: {args.batch_file}[/bold red]" + ) + sys.exit(1) + except json.JSONDecodeError as e: + console.print( + f"[bold red]Error: Invalid JSON in batch file '{args.batch_file}': {e}[/bold red]" + ) + sys.exit(1) + except PermissionError: + console.print( + f"[bold red]Error: Permission denied when reading batch file: {args.batch_file}[/bold red]" + ) + sys.exit(1) except Exception as e: console.print(f"[bold red]Error loading batch file: {e}[/bold red]") sys.exit(1) diff --git a/test_endpoints.json b/test_endpoints.json.example similarity index 100% rename from test_endpoints.json rename to test_endpoints.json.example