Apply code review fixes: add __init__.py, fix filename collisions, improve error handling, add input validation, fix header concatenation, deduplicate vulnerabilities, rename test file

Co-authored-by: GilbertKrantz <90319182+GilbertKrantz@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-07 13:26:23 +00:00
parent ae3920f0b3
commit 1336215439
7 changed files with 117 additions and 19 deletions

View File

@ -75,10 +75,10 @@ python sqlmapcli.py -u "https://demo.owasp-juice.shop/rest/user/login" --data='{
Test multiple endpoints with concurrency: Test multiple endpoints with concurrency:
```bash ```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 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 python sqlmapcli.py -b endpoints.json --level 2 --risk 2 --concurrency 10
# Test with custom settings # Test with custom settings

View File

@ -74,7 +74,7 @@ python sqlmapcli.py --interactive
``` ```
-u, --url Target URL -u, --url Target URL
-b, --batch-file JSON file with multiple endpoints -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) --comprehensive Run all risk/level combinations (1-3 risk, 1-5 levels)
--level {1-5} Test level (default: 1) --level {1-5} Test level (default: 1)
--risk {1-3} Test risk (default: 1) --risk {1-3} Test risk (default: 1)

6
sql_cli/__init__.py Normal file
View File

@ -0,0 +1,6 @@
"""
SQLMap CLI Package
A beautiful CLI wrapper for sqlmap with automated testing capabilities
"""
__version__ = "1.0.0"

View File

@ -122,10 +122,21 @@ class SQLMapScanner:
# Cleanup temporary output directory # Cleanup temporary output directory
try: try:
shutil.rmtree(tmp_output_dir) shutil.rmtree(tmp_output_dir)
except: except Exception as cleanup_error:
pass console.log(
f"Failed to remove temporary sqlmap output directory {tmp_output_dir!r}: {cleanup_error}"
)
return process.returncode == 0, full_output 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: except Exception as cleanup_error:
console.log( console.log(
f"Failed to remove temporary sqlmap output directory {tmp_output_dir!r}: {cleanup_error}" f"Failed to remove temporary sqlmap output directory {tmp_output_dir!r}: {cleanup_error}"
@ -255,9 +266,33 @@ class SQLMapScanner:
) )
if parsed["is_vulnerable"]: if parsed["is_vulnerable"]:
self.results["vulnerabilities"].extend( # Deduplicate vulnerabilities across different level/risk combinations
parsed["vulnerabilities"] 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( results_table.add_row(
str(level), str(level),
@ -359,7 +394,7 @@ class SQLMapScanner:
headers = endpoint.get("headers") headers = endpoint.get("headers")
if headers is not None and isinstance(headers, list): if headers is not None and isinstance(headers, list):
headers = "\\n".join(headers) headers = "\n".join(headers)
try: try:
# Force verbosity 3 for better progress tracking if in batch mode # Force verbosity 3 for better progress tracking if in batch mode

View File

@ -1,4 +1,6 @@
import re import re
import hashlib
import random
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from rich.console import Console from rich.console import Console
@ -9,11 +11,15 @@ SQLMAP_PATH = Path(__file__).parent.parent / "sqlmap.py"
LOGS_DIR = Path(__file__).parent.parent / "logs" LOGS_DIR = Path(__file__).parent.parent / "logs"
def get_log_filename(url: str) -> Path: 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") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Sanitize URL for filename # Create a hash of the URL to ensure uniqueness
safe_url = re.sub(r'[^\w\-_\.]', '_', url)[:50] url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
return LOGS_DIR / f"sqlmap_{safe_url}_{timestamp}.log" # 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): def save_log(log_file: Path, content: str):
"""Save content to log file""" """Save content to log file"""

View File

@ -73,14 +73,50 @@ def interactive_mode(scanner: SQLMapScanner):
) )
if scan_type == "quick": if scan_type == "quick":
level = int(Prompt.ask("[cyan]Test level (1-5)[/cyan]", default="1")) # Input validation for level and risk
risk = int(Prompt.ask("[cyan]Test risk (1-3)[/cyan]", default="1")) 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) scanner.quick_scan(url, level, risk, data=data, headers=headers)
else: else:
max_level = int( # Input validation for max_level and max_risk
Prompt.ask("[cyan]Maximum test level (1-5)[/cyan]", default="5") while True:
) try:
max_risk = int(Prompt.ask("[cyan]Maximum test risk (1-3)[/cyan]", default="3")) 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) scanner.comprehensive_scan(url, max_level, max_risk, data=data, headers=headers)
@ -197,6 +233,21 @@ Examples:
verbose=verbose_level, verbose=verbose_level,
) )
return 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: except Exception as e:
console.print(f"[bold red]Error loading batch file: {e}[/bold red]") console.print(f"[bold red]Error loading batch file: {e}[/bold red]")
sys.exit(1) sys.exit(1)