#!/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""" Nikto Scan Report - {datetime.now().strftime('%Y-%m-%d')}

Nikto Web Scan Report

Targets: {len(self.results)} | Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}

{sev_counts.get('Critical', 0)}

Critical

{sev_counts.get('High', 0)}

High

{sev_counts.get('Medium', 0)}

Medium

{sev_counts.get('Low', 0)}

Low

Target Summary

{''.join(f"" for r in target_summary.itertuples())}
TargetPortTotalCriticalHigh
{r.scan_target}{r.target_port}{r.findings}{r.critical}{r.high}

All Findings

{''.join(f'' for r in df.sort_values("severity").itertuples())}
TargetSeverityURIDescriptionOSVDB
{r.scan_target}{r.severity}{r.uri}{r.description[:150]}{r.osvdb_id}
""" 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()