mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
298 lines
12 KiB
Python
298 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Nikto Web Application Scanning Automation
|
|
|
|
Automates Nikto scanning across multiple targets, parses results,
|
|
and generates consolidated vulnerability reports.
|
|
|
|
Requirements:
|
|
pip install pandas defusedxml jinja2
|
|
System: nikto installed and in PATH
|
|
|
|
Usage:
|
|
python process.py scan --targets targets.txt --output-dir ./results
|
|
python process.py parse --xml-dir ./results --report report.html
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
import defusedxml.ElementTree as ET
|
|
import pandas as pd
|
|
|
|
|
|
class NiktoScanner:
|
|
"""Automated Nikto web scanning manager."""
|
|
|
|
def __init__(self, output_dir: str = "./nikto_results", timeout: int = 600):
|
|
self.output_dir = Path(output_dir)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
self.timeout = timeout
|
|
self.results = []
|
|
|
|
def scan_target(self, target: str, tuning: str = "123456789abc",
|
|
pause: int = 1, ssl: bool = False) -> dict:
|
|
"""Run Nikto scan against a single target."""
|
|
parsed = urlparse(target if "://" in target else f"http://{target}")
|
|
safe_name = parsed.netloc.replace(":", "_").replace("/", "_")
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
xml_output = self.output_dir / f"{safe_name}_{timestamp}.xml"
|
|
|
|
cmd = [
|
|
"nikto",
|
|
"-h", target,
|
|
"-Tuning", tuning,
|
|
"-Pause", str(pause),
|
|
"-timeout", "10",
|
|
"-nointeractive",
|
|
"-output", str(xml_output),
|
|
"-Format", "xml",
|
|
]
|
|
|
|
if ssl or parsed.scheme == "https":
|
|
cmd.append("-ssl")
|
|
|
|
result = {
|
|
"target": target,
|
|
"status": "unknown",
|
|
"output_file": str(xml_output),
|
|
"findings": [],
|
|
"start_time": datetime.now().isoformat(),
|
|
}
|
|
|
|
print(f"[*] Scanning {target}...")
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd, capture_output=True, text=True, timeout=self.timeout
|
|
)
|
|
result["status"] = "completed"
|
|
result["end_time"] = datetime.now().isoformat()
|
|
result["stdout"] = proc.stdout[-2000:] if proc.stdout else ""
|
|
result["stderr"] = proc.stderr[-500:] if proc.stderr else ""
|
|
|
|
if xml_output.exists():
|
|
result["findings"] = self.parse_xml(str(xml_output))
|
|
finding_count = len(result["findings"])
|
|
print(f"[+] {target}: {finding_count} findings")
|
|
else:
|
|
print(f"[!] {target}: No XML output generated")
|
|
result["status"] = "no_output"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
result["status"] = "timeout"
|
|
print(f"[-] {target}: Scan timed out after {self.timeout}s")
|
|
except FileNotFoundError:
|
|
result["status"] = "error"
|
|
result["error"] = "nikto not found in PATH"
|
|
print("[-] Error: nikto not found. Install with: apt install nikto")
|
|
except Exception as e:
|
|
result["status"] = "error"
|
|
result["error"] = str(e)
|
|
print(f"[-] {target}: Error - {e}")
|
|
|
|
self.results.append(result)
|
|
return result
|
|
|
|
def scan_targets(self, targets: list, max_parallel: int = 3, **kwargs) -> list:
|
|
"""Scan multiple targets with optional parallelism."""
|
|
self.results = []
|
|
|
|
with ThreadPoolExecutor(max_workers=max_parallel) as executor:
|
|
futures = {
|
|
executor.submit(self.scan_target, target, **kwargs): target
|
|
for target in targets
|
|
}
|
|
for future in as_completed(futures):
|
|
try:
|
|
future.result()
|
|
except Exception as e:
|
|
target = futures[future]
|
|
print(f"[-] {target}: Scan failed - {e}")
|
|
|
|
return self.results
|
|
|
|
@staticmethod
|
|
def parse_xml(xml_path: str) -> list:
|
|
"""Parse Nikto XML output into structured findings."""
|
|
findings = []
|
|
try:
|
|
tree = ET.parse(xml_path)
|
|
root = tree.getroot()
|
|
|
|
for scan_details in root.findall(".//scandetails"):
|
|
target_ip = scan_details.get("targetip", "")
|
|
target_host = scan_details.get("targethostname", "")
|
|
target_port = scan_details.get("targetport", "")
|
|
target_banner = scan_details.get("targetbanner", "")
|
|
|
|
for item in scan_details.findall("item"):
|
|
finding = {
|
|
"target_ip": target_ip,
|
|
"target_host": target_host,
|
|
"target_port": target_port,
|
|
"server_banner": target_banner,
|
|
"nikto_id": item.get("id", ""),
|
|
"osvdb_id": item.get("osvdbid", ""),
|
|
"osvdb_link": item.get("osvdblink", ""),
|
|
"method": item.get("method", "GET"),
|
|
"uri": item.findtext("uri", ""),
|
|
"description": item.findtext("description", ""),
|
|
"name_link": item.findtext("namelink", ""),
|
|
"ip_link": item.findtext("iplink", ""),
|
|
}
|
|
|
|
# Classify severity based on description keywords
|
|
desc_lower = finding["description"].lower()
|
|
if any(w in desc_lower for w in ["remote code", "command execution", "backdoor", "rce"]):
|
|
finding["severity"] = "Critical"
|
|
elif any(w in desc_lower for w in ["sql injection", "xss", "cross-site", "file inclusion"]):
|
|
finding["severity"] = "High"
|
|
elif any(w in desc_lower for w in ["directory listing", "information disclosure", "version"]):
|
|
finding["severity"] = "Medium"
|
|
elif any(w in desc_lower for w in ["header", "cookie", "x-frame"]):
|
|
finding["severity"] = "Low"
|
|
else:
|
|
finding["severity"] = "Info"
|
|
|
|
findings.append(finding)
|
|
|
|
except Exception as e:
|
|
print(f"[!] XML parse error for {xml_path}: {e}")
|
|
|
|
return findings
|
|
|
|
def generate_report(self, output_path: str):
|
|
"""Generate consolidated HTML report from all scan results."""
|
|
all_findings = []
|
|
for result in self.results:
|
|
for finding in result.get("findings", []):
|
|
finding["scan_target"] = result["target"]
|
|
finding["scan_status"] = result["status"]
|
|
all_findings.append(finding)
|
|
|
|
if not all_findings:
|
|
print("[-] No findings to report")
|
|
return
|
|
|
|
df = pd.DataFrame(all_findings)
|
|
|
|
# Severity counts
|
|
sev_counts = df["severity"].value_counts().to_dict()
|
|
|
|
# Target summary
|
|
target_summary = (df.groupby(["scan_target", "target_port"])
|
|
.agg(findings=("nikto_id", "count"),
|
|
critical=("severity", lambda x: (x == "Critical").sum()),
|
|
high=("severity", lambda x: (x == "High").sum()))
|
|
.reset_index())
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Nikto Scan Report - {datetime.now().strftime('%Y-%m-%d')}</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
|
|
.header {{ background: #16213e; color: white; padding: 20px; border-radius: 8px; }}
|
|
.cards {{ display: flex; gap: 15px; margin: 20px 0; }}
|
|
.card {{ background: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
|
|
.card h3 {{ margin: 0; font-size: 2em; }}
|
|
table {{ width: 100%; border-collapse: collapse; background: white; margin: 15px 0;
|
|
border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
|
|
th {{ background: #2c3e50; color: white; padding: 10px; text-align: left; }}
|
|
td {{ padding: 8px 10px; border-bottom: 1px solid #eee; font-size: 0.9em; }}
|
|
.sev-critical {{ color: #e74c3c; font-weight: bold; }}
|
|
.sev-high {{ color: #e67e22; font-weight: bold; }}
|
|
.sev-medium {{ color: #f39c12; }}
|
|
.sev-low {{ color: #3498db; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Nikto Web Scan Report</h1>
|
|
<p>Targets: {len(self.results)} | Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
|
|
</div>
|
|
|
|
<div class="cards">
|
|
<div class="card" style="border-top:4px solid #e74c3c"><h3>{sev_counts.get('Critical', 0)}</h3><p>Critical</p></div>
|
|
<div class="card" style="border-top:4px solid #e67e22"><h3>{sev_counts.get('High', 0)}</h3><p>High</p></div>
|
|
<div class="card" style="border-top:4px solid #f39c12"><h3>{sev_counts.get('Medium', 0)}</h3><p>Medium</p></div>
|
|
<div class="card" style="border-top:4px solid #3498db"><h3>{sev_counts.get('Low', 0)}</h3><p>Low</p></div>
|
|
</div>
|
|
|
|
<h2>Target Summary</h2>
|
|
<table>
|
|
<tr><th>Target</th><th>Port</th><th>Total</th><th>Critical</th><th>High</th></tr>
|
|
{''.join(f"<tr><td>{r.scan_target}</td><td>{r.target_port}</td><td>{r.findings}</td><td>{r.critical}</td><td>{r.high}</td></tr>" for r in target_summary.itertuples())}
|
|
</table>
|
|
|
|
<h2>All Findings</h2>
|
|
<table>
|
|
<tr><th>Target</th><th>Severity</th><th>URI</th><th>Description</th><th>OSVDB</th></tr>
|
|
{''.join(f'<tr><td>{r.scan_target}</td><td class="sev-{r.severity.lower()}">{r.severity}</td><td>{r.uri}</td><td>{r.description[:150]}</td><td>{r.osvdb_id}</td></tr>' for r in df.sort_values("severity").itertuples())}
|
|
</table>
|
|
</body>
|
|
</html>"""
|
|
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(html)
|
|
print(f"[+] Report saved to: {output_path}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Nikto Web Scanning Automation")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
scan_p = subparsers.add_parser("scan", help="Scan web targets with Nikto")
|
|
scan_p.add_argument("--targets", required=True, help="File with target URLs")
|
|
scan_p.add_argument("--output-dir", default="./nikto_results")
|
|
scan_p.add_argument("--report", default=None, help="HTML report output path")
|
|
scan_p.add_argument("--tuning", default="123456789abc", help="Nikto tuning options")
|
|
scan_p.add_argument("--parallel", type=int, default=3, help="Max parallel scans")
|
|
scan_p.add_argument("--timeout", type=int, default=600, help="Per-target timeout")
|
|
scan_p.add_argument("--pause", type=int, default=1, help="Pause between requests")
|
|
|
|
parse_p = subparsers.add_parser("parse", help="Parse existing Nikto XML results")
|
|
parse_p.add_argument("--xml-dir", required=True, help="Directory with Nikto XML files")
|
|
parse_p.add_argument("--report", required=True, help="HTML report output path")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "scan":
|
|
with open(args.targets) as f:
|
|
targets = [line.strip() for line in f if line.strip() and not line.startswith("#")]
|
|
|
|
scanner = NiktoScanner(args.output_dir, args.timeout)
|
|
scanner.scan_targets(targets, max_parallel=args.parallel,
|
|
tuning=args.tuning, pause=args.pause)
|
|
|
|
report_path = args.report or os.path.join(args.output_dir, "nikto_report.html")
|
|
scanner.generate_report(report_path)
|
|
|
|
elif args.command == "parse":
|
|
scanner = NiktoScanner()
|
|
xml_dir = Path(args.xml_dir)
|
|
|
|
for xml_file in xml_dir.glob("*.xml"):
|
|
findings = scanner.parse_xml(str(xml_file))
|
|
scanner.results.append({
|
|
"target": xml_file.stem,
|
|
"status": "parsed",
|
|
"findings": findings,
|
|
})
|
|
|
|
scanner.generate_report(args.report)
|
|
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|