nuclei_scanner.py•3.13 kB
# nuclei_scanner.py
import logging
import subprocess
import os
import shlex
from datetime import datetime
from .utils import parse_json_file  # Relative import
NUCLEI_TIMEOUT_SECONDS = 900  # 15 minutes default
def run_nuclei(target_url: str, output_dir="results", timeout=NUCLEI_TIMEOUT_SECONDS, severity="low,medium,high,critical"):
    """Runs the Nuclei security scanner against a target URL or IP."""
    if not target_url:
        logging.error("Nuclei target URL/IP is required")
        return []
    logging.info(f"Starting Nuclei scan for target: {target_url}")
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_filename = f"nuclei_output_{timestamp}.json"
    output_filepath = os.path.join(output_dir, output_filename)
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    # Configure nuclei command with common best practices
    command = [
        "nuclei", 
        "-target", target_url,
        "-json", 
        "-o", output_filepath,
        "-severity", severity,
        "-silent"
    ]
    logging.debug(f"Executing Nuclei command: {' '.join(shlex.quote(cmd) for cmd in command)}")
    try:
        result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=False)
        logging.info("Nuclei process finished.")
        logging.debug(f"Nuclei stdout:\n{result.stdout}")
        if result.returncode != 0:
            logging.warning(f"Nuclei exited with non-zero status code: {result.returncode}")
            return [f"Nuclei exited with non-zero status code: {result.returncode}"]
        # Parse the JSON output file
        findings = parse_json_file(output_filepath)
        if findings:
            logging.info(f"Successfully parsed {len(findings)} findings from Nuclei output.")
            # Add tool name for context
            for finding in findings:
                finding['tool'] = 'Nuclei'
                # Standardize some fields to match our expected format
                if 'info' in finding:
                    finding['severity'] = finding.get('info', {}).get('severity')
                    finding['message'] = finding.get('info', {}).get('name')
                    finding['description'] = finding.get('info', {}).get('description')
                    finding['matched_at'] = finding.get('matched-at', '')
            return findings
        else:
            logging.warning(f"Could not parse findings from Nuclei output file: {output_filepath}")
            return [f"Could not parse findings from Nuclei output file: {output_filepath}"]
    except subprocess.TimeoutExpired:
        logging.error(f"Nuclei scan timed out after {timeout} seconds.")
        return [f"Nuclei scan timed out after {timeout} seconds."]
    except FileNotFoundError:
        logging.error("Nuclei command not found. Is Nuclei installed and in PATH?")
        return ["Nuclei command not found. Is Nuclei installed and in PATH?"]
    except Exception as e:
        logging.error(f"An unexpected error occurred while running Nuclei: {e}")
        return [f"An unexpected error occurred while running Nuclei: {e}"]