#!/usr/bin/env bash # ============================================================================= # # websec-audit.sh — Professional Web Security Audit Framework # # Author : davidalvarezp (https://davidalvarezp.com) # Version : 1.0.1 # License : MIT # GitHub : https://github.com/davidalvarezp/websec-audit # # LEGAL NOTICE: # This tool is intended for authorized security assessments only. # Unauthorized use against systems you do not own or have explicit # written permission to test is illegal and unethical. # The author assumes no liability for misuse of this software. # # ============================================================================= set -euo pipefail IFS=$'\n\t' # ───────────────────────────────────────────────────────────────────────────── # CONSTANTS & METADATA # ───────────────────────────────────────────────────────────────────────────── readonly TOOL_NAME="websec-audit" readonly TOOL_VERSION="1.0.1" readonly TOOL_AUTHOR="davidalvarezp" readonly TOOL_URL="https://github.com/davidalvarezp/websec-audit" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly TIMESTAMP="$(date +%Y%m%d_%H%M%S)" readonly DATE_HUMAN="$(date '+%Y-%m-%d %H:%M:%S')" readonly AUDIT_PID=$$ # ───────────────────────────────────────────────────────────────────────────── # ANSI COLOR PALETTE # ───────────────────────────────────────────────────────────────────────────── readonly C_RED='\033[0;31m' readonly C_GREEN='\033[0;32m' readonly C_YELLOW='\033[1;33m' readonly C_BLUE='\033[0;34m' readonly C_CYAN='\033[0;36m' readonly C_MAGENTA='\033[0;35m' readonly C_BOLD='\033[1m' readonly C_DIM='\033[2m' readonly C_RESET='\033[0m' # ───────────────────────────────────────────────────────────────────────────── # RUNTIME VARIABLES (set via parse_args, never hardcode below) # ───────────────────────────────────────────────────────────────────────────── TARGET="" TARGET_DOMAIN="" TARGET_IP="" TARGET_SCHEME="" OUTPUT_DIR="" LOG_FILE="" REPORT_HTML="" REPORT_JSON="" REPORT_TXT="" FINDINGS_JSONL="" # Module toggles (1 = enabled) MOD_RECON=1 MOD_PORTSCAN=1 MOD_FINGERPRINT=1 MOD_SSL=1 MOD_HEADERS=1 MOD_DIRBRUTE=1 MOD_NIKTO=1 MOD_SQLI=1 MOD_XSS=1 MOD_CMS=1 MOD_CORS=1 MOD_REDIRECT=1 MOD_SSRF=1 MOD_SUBTAKEOVER=1 MOD_NUCLEI=1 # Scan options OPT_THREADS=10 OPT_TIMEOUT=10 OPT_DEPTH=3 OPT_PORTS="top-1000" # top-100 | top-1000 | full OPT_AGGRESSIVE=0 OPT_STEALTH=0 OPT_PROXY="" OPT_VERBOSE=0 OPT_NO_COLOR=0 OPT_NO_BANNER=0 OPT_OUTPUT_ONLY="" # json | html | txt (empty = all) # Wordlists (can be overridden via --wl-* flags) WL_DIRS_SMALL="/usr/share/wordlists/dirb/common.txt" WL_DIRS_BIG="/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt" WL_DNS="/usr/share/wordlists/dnsmap.txt" # Finding counters TOTAL_FINDINGS=0 COUNT_CRITICAL=0 COUNT_HIGH=0 COUNT_MEDIUM=0 COUNT_LOW=0 COUNT_INFO=0 # Timer AUDIT_START_TIME=0 # ───────────────────────────────────────────────────────────────────────────── # BANNER # ───────────────────────────────────────────────────────────────────────────── print_banner() { [[ $OPT_NO_BANNER -eq 1 ]] && return echo -e "${C_CYAN}${C_BOLD}" cat << 'BANNER' ╦ ╦┌─┐┌┐ ╔═╗┌─┐┌─┐ ╔═╗┬ ┬┌┬┐┬┌┬┐ ║║║├┤ ├┴┐╚═╗├┤ │ ╠═╣│ │ │││ │ ╚╩╝└─┘└─┘╚═╝└─┘└─┘ ╩ ╩└─┘─┴┘┴ ┴ BANNER echo -e "${C_RESET}" printf " %s Professional Web Security Audit Framework\n" "${C_BOLD}" printf " Version %-8s Author: %s%s\n" "${TOOL_VERSION}" "${TOOL_AUTHOR}" "${C_RESET}" printf " %s%s%s\n\n" "${C_DIM}" "${TOOL_URL}" "${C_RESET}" echo -e " ${C_DIM}─────────────────────────────────────────────────${C_RESET}" echo "" } # ───────────────────────────────────────────────────────────────────────────── # HELP # ───────────────────────────────────────────────────────────────────────────── print_usage() { cat << USAGE ${C_BOLD}USAGE${C_RESET} $(basename "$0") -t [options] ${C_BOLD}REQUIRED${C_RESET} -t, --target Target URL or IP address Examples: https://target.com | 192.168.1.1 ${C_BOLD}OUTPUT${C_RESET} -o, --output Output directory (default: ./results__) --format Report format: json | html | txt | all (default: all) ${C_BOLD}SCAN OPTIONS${C_RESET} -T, --threads Concurrent threads (default: 10) -p, --ports Port profile: top-100 | top-1000 | full (default: top-1000) --timeout Connection timeout in seconds (default: 10) --depth Crawl depth (default: 3) --proxy HTTP/HTTPS proxy (e.g. http://127.0.0.1:8080) --aggressive Aggressive mode — deeper scans, more noise --stealth Stealth mode — slower, lower detection footprint ${C_BOLD}MODULE CONTROL${C_RESET} --skip-recon Skip reconnaissance (WHOIS, DNS, subdomains) --skip-portscan Skip port scanning (nmap) --skip-fingerprint Skip web fingerprinting --skip-ssl Skip SSL/TLS analysis --skip-headers Skip HTTP security headers check --skip-dirbrute Skip directory/file brute-forcing --skip-nikto Skip Nikto web scanner --skip-sqli Skip SQL injection tests (sqlmap) --skip-xss Skip XSS tests (dalfox) --skip-cms Skip CMS detection and scanning --skip-cors Skip CORS misconfiguration tests --skip-redirect Skip open redirect tests --skip-ssrf Skip SSRF tests --skip-subtakeover Skip subdomain takeover checks --skip-nuclei Skip Nuclei template scanning ${C_BOLD}WORDLISTS${C_RESET} --wl-dirs-small Small wordlist for directory brute-force --wl-dirs-big Large wordlist for directory brute-force --wl-dns Wordlist for DNS subdomain enumeration ${C_BOLD}MISC${C_RESET} -v, --verbose Verbose output --no-color Disable ANSI color output --no-banner Suppress banner -h, --help Show this help message -V, --version Show version information ${C_BOLD}EXAMPLES${C_RESET} $(basename "$0") -t https://target.com $(basename "$0") -t https://target.com --aggressive -T 20 -o /tmp/audit $(basename "$0") -t https://target.com --stealth --proxy http://127.0.0.1:8080 $(basename "$0") -t https://target.com --skip-nikto --skip-sqli --format json $(basename "$0") -t https://target.com --ports full --depth 5 -v USAGE } # ───────────────────────────────────────────────────────────────────────────── # LOGGING ENGINE # ───────────────────────────────────────────────────────────────────────────── _ts() { date '+%H:%M:%S'; } log_info() { local msg="[$(_ts)] [INFO] $1" [[ $OPT_NO_COLOR -eq 0 ]] && echo -e "${C_BLUE}${msg}${C_RESET}" || echo "$msg" echo "$msg" >> "$LOG_FILE" } log_ok() { local msg="[$(_ts)] [OK] $1" [[ $OPT_NO_COLOR -eq 0 ]] && echo -e "${C_GREEN}${msg}${C_RESET}" || echo "$msg" echo "$msg" >> "$LOG_FILE" } log_warn() { local msg="[$(_ts)] [WARN] $1" [[ $OPT_NO_COLOR -eq 0 ]] && echo -e "${C_YELLOW}${msg}${C_RESET}" || echo "$msg" echo "$msg" >> "$LOG_FILE" } log_error() { local msg="[$(_ts)] [ERROR] $1" [[ $OPT_NO_COLOR -eq 0 ]] && echo -e "${C_RED}${msg}${C_RESET}" || echo "$msg" echo "$msg" >> "$LOG_FILE" } log_verbose() { [[ $OPT_VERBOSE -eq 0 ]] && return local msg="[$(_ts)] [VERBOSE] $1" echo -e "${C_DIM}${msg}${C_RESET}" echo "$msg" >> "$LOG_FILE" } log_finding() { local severity="$1"; shift local msg="[$(_ts)] [${severity}]" # pad severity label case "${#severity}" in 4) msg="[$(_ts)] [${severity}] $1" ;; 3) msg="[$(_ts)] [${severity}] $1" ;; 6) msg="[$(_ts)] [${severity}] $1" ;; 8) msg="[$(_ts)] [${severity}] $1" ;; *) msg="[$(_ts)] [${severity}] $1" ;; esac local color="$C_RESET" case "$severity" in CRITICAL) color="${C_RED}${C_BOLD}" ;; HIGH) color="${C_MAGENTA}${C_BOLD}" ;; MEDIUM) color="$C_YELLOW" ;; LOW) color="$C_CYAN" ;; INFO) color="$C_DIM" ;; esac [[ $OPT_NO_COLOR -eq 0 ]] && echo -e "${color}${msg}${C_RESET}" || echo "$msg" echo "$msg" >> "$LOG_FILE" } log_section() { local title="$1" local line="══════════════════════════════════════════════════════════════" local inner=" ▸ ${title}" echo "" | tee -a "$LOG_FILE" if [[ $OPT_NO_COLOR -eq 0 ]]; then echo -e "${C_CYAN}${C_BOLD}${line}${C_RESET}" | tee -a "$LOG_FILE" echo -e "${C_CYAN}${C_BOLD}${inner}${C_RESET}" | tee -a "$LOG_FILE" echo -e "${C_CYAN}${C_BOLD}${line}${C_RESET}" | tee -a "$LOG_FILE" else echo "$line" | tee -a "$LOG_FILE" echo "$inner" | tee -a "$LOG_FILE" echo "$line" | tee -a "$LOG_FILE" fi echo "" | tee -a "$LOG_FILE" } log_subsection() { local title="$1" local line=" ────────────────────────────────────────" echo "" | tee -a "$LOG_FILE" if [[ $OPT_NO_COLOR -eq 0 ]]; then echo -e "${C_BLUE}${C_BOLD}${line}${C_RESET}" | tee -a "$LOG_FILE" echo -e "${C_BLUE}${C_BOLD} ● ${title}${C_RESET}" | tee -a "$LOG_FILE" echo -e "${C_BLUE}${C_BOLD}${line}${C_RESET}" | tee -a "$LOG_FILE" else echo "$line" | tee -a "$LOG_FILE" echo " [+] $title" | tee -a "$LOG_FILE" echo "$line" | tee -a "$LOG_FILE" fi echo "" | tee -a "$LOG_FILE" } # ───────────────────────────────────────────────────────────────────────────── # FINDING REGISTRY # ───────────────────────────────────────────────────────────────────────────── # add_finding <DESCRIPTION> [EVIDENCE] [RECOMMENDATION] add_finding() { local severity="${1:-INFO}" local module="${2:-UNKNOWN}" local title="${3:-Untitled finding}" local description="${4:-}" local evidence="${5:-}" local recommendation="${6:-}" # Increment counters TOTAL_FINDINGS=$((TOTAL_FINDINGS + 1)) case "$severity" in CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL + 1)) ;; HIGH) COUNT_HIGH=$((COUNT_HIGH + 1)) ;; MEDIUM) COUNT_MEDIUM=$((COUNT_MEDIUM + 1)) ;; LOW) COUNT_LOW=$((COUNT_LOW + 1)) ;; INFO) COUNT_INFO=$((COUNT_INFO + 1)) ;; esac # Console output log_finding "$severity" "[${module}] ${title}" [[ -n "$description" ]] && printf " ↳ %s\n" "$description" | tee -a "$LOG_FILE" [[ -n "$evidence" ]] && printf " ↳ Evidence: %s\n" "$evidence" | tee -a "$LOG_FILE" [[ -n "$recommendation" ]] && printf " ↳ Fix: %s\n" "$recommendation" | tee -a "$LOG_FILE" # Escape for JSON local j_title j_desc j_evid j_rec j_title=$(printf '%s' "$title" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ / /g') j_desc=$(printf '%s' "$description" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ / /g') j_evid=$(printf '%s' "$evidence" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ / /g') j_rec=$(printf '%s' "$recommendation" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ / /g') printf '{"id":%d,"severity":"%s","module":"%s","title":"%s","description":"%s","evidence":"%s","recommendation":"%s","timestamp":"%s"}\n' \ "$TOTAL_FINDINGS" "$severity" "$module" \ "$j_title" "$j_desc" "$j_evid" "$j_rec" \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ >> "$FINDINGS_JSONL" } # ───────────────────────────────────────────────────────────────────────────── # UTILITY FUNCTIONS # ───────────────────────────────────────────────────────────────────────────── has_tool() { command -v "$1" &>/dev/null; } require_tool(){ has_tool "$1" && return 0; log_warn "Tool '$1' not found — skipping related checks."; return 1; } strip_scheme() { echo "$1" | sed -E 's|^https?://||' | cut -d'/' -f1; } get_scheme() { echo "$1" | grep -oP '^https?'; } elapsed_secs() { echo $(( $(date +%s) - AUDIT_START_TIME )); } is_https() { [[ "$TARGET_SCHEME" == "https" ]]; } url_encode() { python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$1" 2>/dev/null || \ printf '%s' "$1" | od -An -tx1 | tr ' ' '%' | tr -d '\n' } resolve_ip() { host "$1" 2>/dev/null | grep "has address" | head -1 | awk '{print $NF}' || \ dig +short A "$1" 2>/dev/null | head -1 || echo "" } # curl wrapper with audit-wide settings _curl() { local args=( -sk --max-time "$OPT_TIMEOUT" -A "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0" --retry 2 --retry-delay 1 ) [[ -n "$OPT_PROXY" ]] && args+=(-x "$OPT_PROXY") curl "${args[@]}" "$@" } # Run a command with timeout, capture output to file, return exit code run_tool() { local label="$1" outfile="$2"; shift 2 log_verbose "Running: $*" timeout "${OPT_TIMEOUT}s" "$@" > "$outfile" 2>&1 local rc=$? [[ $rc -eq 0 ]] && log_verbose "Completed: $label" || log_verbose "Timeout/error ($rc): $label" return $rc } init_output_dir() { local safe_domain safe_domain=$(echo "$TARGET_DOMAIN" | tr '/:.' '_' | tr -cd '[:alnum:]_-') if [[ -z "$OUTPUT_DIR" ]]; then OUTPUT_DIR="${SCRIPT_DIR}/results_${safe_domain}_${TIMESTAMP}" fi mkdir -p "${OUTPUT_DIR}"/{logs,recon,portscan,ssl,headers,dirs,vulns,cms,misc,reports} LOG_FILE="${OUTPUT_DIR}/logs/audit_${TIMESTAMP}.log" REPORT_HTML="${OUTPUT_DIR}/reports/report_${TIMESTAMP}.html" REPORT_JSON="${OUTPUT_DIR}/reports/report_${TIMESTAMP}.json" REPORT_TXT="${OUTPUT_DIR}/reports/report_${TIMESTAMP}.txt" FINDINGS_JSONL="${OUTPUT_DIR}/logs/findings.jsonl" : > "$LOG_FILE" : > "$FINDINGS_JSONL" } # ───────────────────────────────────────────────────────────────────────────── # ARGUMENT PARSER # ───────────────────────────────────────────────────────────────────────────── parse_args() { [[ $# -eq 0 ]] && { print_banner; print_usage; exit 1; } while [[ $# -gt 0 ]]; do case "$1" in -t|--target) TARGET="$2"; shift 2 ;; -o|--output) OUTPUT_DIR="$2"; shift 2 ;; -T|--threads) OPT_THREADS="$2"; shift 2 ;; -p|--ports) OPT_PORTS="$2"; shift 2 ;; --format) OPT_OUTPUT_ONLY="$2"; shift 2 ;; --timeout) OPT_TIMEOUT="$2"; shift 2 ;; --depth) OPT_DEPTH="$2"; shift 2 ;; --proxy) OPT_PROXY="$2"; shift 2 ;; --aggressive) OPT_AGGRESSIVE=1; shift ;; --stealth) OPT_STEALTH=1; shift ;; --wl-dirs-small) WL_DIRS_SMALL="$2"; shift 2 ;; --wl-dirs-big) WL_DIRS_BIG="$2"; shift 2 ;; --wl-dns) WL_DNS="$2"; shift 2 ;; --skip-recon) MOD_RECON=0; shift ;; --skip-portscan) MOD_PORTSCAN=0; shift ;; --skip-fingerprint) MOD_FINGERPRINT=0; shift ;; --skip-ssl) MOD_SSL=0; shift ;; --skip-headers) MOD_HEADERS=0; shift ;; --skip-dirbrute) MOD_DIRBRUTE=0; shift ;; --skip-nikto) MOD_NIKTO=0; shift ;; --skip-sqli) MOD_SQLI=0; shift ;; --skip-xss) MOD_XSS=0; shift ;; --skip-cms) MOD_CMS=0; shift ;; --skip-cors) MOD_CORS=0; shift ;; --skip-redirect) MOD_REDIRECT=0; shift ;; --skip-ssrf) MOD_SSRF=0; shift ;; --skip-subtakeover) MOD_SUBTAKEOVER=0; shift ;; --skip-nuclei) MOD_NUCLEI=0; shift ;; -v|--verbose) OPT_VERBOSE=1; shift ;; --no-color) OPT_NO_COLOR=1; shift ;; --no-banner) OPT_NO_BANNER=1; shift ;; -V|--version) echo "${TOOL_NAME} v${TOOL_VERSION} — ${TOOL_AUTHOR}" exit 0 ;; -h|--help) print_banner; print_usage; exit 0 ;; *) echo "Unknown option: $1" >&2; print_usage; exit 1 ;; esac done # Validate target [[ -z "$TARGET" ]] && { echo -e "${C_RED}[!] Error: --target is required.${C_RESET}"; exit 1; } # Normalise target [[ "$TARGET" =~ ^https?:// ]] || TARGET="https://${TARGET}" TARGET_SCHEME="$(get_scheme "$TARGET")" TARGET_DOMAIN="$(strip_scheme "$TARGET")" TARGET_IP="$(resolve_ip "$TARGET_DOMAIN")" # Validate conflicting modes if [[ $OPT_AGGRESSIVE -eq 1 && $OPT_STEALTH -eq 1 ]]; then echo -e "${C_YELLOW}[!] --aggressive and --stealth are mutually exclusive. Using --aggressive.${C_RESET}" OPT_STEALTH=0 fi init_output_dir } # ───────────────────────────────────────────────────────────────────────────── # DEPENDENCY CHECK # ───────────────────────────────────────────────────────────────────────────── module_check_deps() { log_section "DEPENDENCY CHECK" local required=(curl nmap) local optional=( nikto sqlmap whatweb wafw00f gobuster ffuf dirb wpscan droopescan sslscan "testssl.sh" dalfox subjack nuclei subfinder amass dnsrecon host whois dig jq python3 ) local missing_req=() for t in "${required[@]}"; do if has_tool "$t"; then log_ok " [required] $t" else missing_req+=("$t") log_error " [required] $t — NOT FOUND" fi done for t in "${optional[@]}"; do if has_tool "$t"; then log_ok " [optional] $t" else log_warn " [optional] $t — not found (reduced coverage)" fi done if [[ ${#missing_req[@]} -gt 0 ]]; then echo "" log_error "Missing required tools: ${missing_req[*]}" echo -e " Install with: ${C_CYAN}sudo apt-get install -y ${missing_req[*]}${C_RESET}" exit 1 fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 00 — TARGET INFORMATION # ───────────────────────────────────────────────────────────────────────────── module_target_info() { log_section "MODULE 00 — TARGET INFORMATION" log_info "Target URL : $TARGET" log_info "Target Domain : $TARGET_DOMAIN" log_info "Resolved IP : ${TARGET_IP:-<unresolved>}" log_info "Scheme : $TARGET_SCHEME" log_info "Output Dir : $OUTPUT_DIR" log_info "Timestamp : $TIMESTAMP" log_info "Mode : $( [[ $OPT_AGGRESSIVE -eq 1 ]] && echo AGGRESSIVE; \ [[ $OPT_STEALTH -eq 1 ]] && echo STEALTH; \ [[ $OPT_AGGRESSIVE -eq 0 && $OPT_STEALTH -eq 0 ]] && echo NORMAL )" [[ -n "$OPT_PROXY" ]] && log_info "Proxy : $OPT_PROXY" add_finding "INFO" "INIT" "Audit started against ${TARGET}" \ "Resolved IP: ${TARGET_IP:-<unresolved>} | Mode: $( [[ $OPT_AGGRESSIVE -eq 1 ]] && echo AGGRESSIVE || \ [[ $OPT_STEALTH -eq 1 ]] && echo STEALTH || echo NORMAL)" "" "" } # ───────────────────────────────────────────────────────────────────────────── # MODULE 01 — RECONNAISSANCE # ───────────────────────────────────────────────────────────────────────────── module_recon() { [[ $MOD_RECON -eq 0 ]] && return log_section "MODULE 01 — RECONNAISSANCE" local recon_dir="${OUTPUT_DIR}/recon" # ── WHOIS ────────────────────────────────────────────────────────────────── log_subsection "WHOIS Lookup" if has_tool whois; then whois "$TARGET_DOMAIN" > "${recon_dir}/whois.txt" 2>&1 || true log_ok "WHOIS saved → ${recon_dir}/whois.txt" # Extract key registrant info local registrar expiry registrar=$(grep -iE "^registrar:" "${recon_dir}/whois.txt" | head -1 | sed 's/^[^:]*: *//' || true) expiry=$(grep -iE "expir" "${recon_dir}/whois.txt" | grep -oP '\d{4}-\d{2}-\d{2}' | head -1 || true) [[ -n "$registrar" ]] && log_info "Registrar : $registrar" [[ -n "$expiry" ]] && log_info "Expiry : $expiry" add_finding "INFO" "RECON" "WHOIS data collected for ${TARGET_DOMAIN}" \ "Registrar: ${registrar:-N/A} | Expiry: ${expiry:-N/A}" "" "" else log_warn "whois not available" fi # ── DNS RECORDS ──────────────────────────────────────────────────────────── log_subsection "DNS Record Enumeration" if has_tool dig; then local dns_file="${recon_dir}/dns_records.txt" { for rtype in A AAAA MX TXT NS SOA CNAME CAA; do printf "=== %s ===\n" "$rtype" dig +short "$rtype" "$TARGET_DOMAIN" 2>/dev/null || true echo "" done printf "=== DMARC ===\n" dig +short TXT "_dmarc.${TARGET_DOMAIN}" 2>/dev/null || true printf "\n=== SPF (raw TXT) ===\n" dig +short TXT "$TARGET_DOMAIN" 2>/dev/null | grep -i "v=spf" || true } > "$dns_file" log_ok "DNS records saved → $dns_file" # SPF / DMARC analysis local spf_record dmarc_record spf_record=$(dig +short TXT "$TARGET_DOMAIN" 2>/dev/null | grep -i "v=spf1" || true) dmarc_record=$(dig +short TXT "_dmarc.${TARGET_DOMAIN}" 2>/dev/null | head -1 || true) if [[ -z "$spf_record" ]]; then add_finding "MEDIUM" "RECON" "Missing SPF record on ${TARGET_DOMAIN}" \ "No SPF TXT record found. Domain may be used for email spoofing." "" \ "Add a TXT record: v=spf1 include:<provider> -all" else log_ok "SPF record present: ${spf_record:0:80}" # Check for +all (dangerous) if echo "$spf_record" | grep -q "+all"; then add_finding "HIGH" "RECON" "SPF record uses +all (permissive)" \ "Any IP is allowed to send email on behalf of this domain." "$spf_record" \ "Change +all to -all or ~all to restrict sending sources." fi fi if [[ -z "$dmarc_record" ]]; then add_finding "MEDIUM" "RECON" "Missing DMARC record on ${TARGET_DOMAIN}" \ "No DMARC policy found. Email spoofing protection is absent." "" \ "Add TXT record at _dmarc.${TARGET_DOMAIN}: v=DMARC1; p=reject; rua=mailto:dmarc@${TARGET_DOMAIN}" else log_ok "DMARC record present: ${dmarc_record:0:80}" if echo "$dmarc_record" | grep -qi "p=none"; then add_finding "LOW" "RECON" "DMARC policy set to p=none (monitoring only)" \ "DMARC is in monitor mode and does not enforce rejection." "$dmarc_record" \ "Upgrade DMARC policy to p=quarantine or p=reject." fi fi fi # ── DNS ZONE TRANSFER ────────────────────────────────────────────────────── log_subsection "DNS Zone Transfer (AXFR)" local ns_servers ns_servers=$(dig +short NS "$TARGET_DOMAIN" 2>/dev/null | head -5 || true) local axfr_file="${recon_dir}/axfr.txt" local axfr_found=0 if [[ -n "$ns_servers" ]]; then while IFS= read -r ns; do ns="${ns%.}" log_verbose "Attempting AXFR from $ns" if dig AXFR "$TARGET_DOMAIN" "@${ns}" 2>/dev/null | \ grep -v "^;;" | grep -v "^$" > "${axfr_file}.tmp" 2>&1; then local lines; lines=$(wc -l < "${axfr_file}.tmp" 2>/dev/null || echo 0) if [[ "$lines" -gt 5 ]]; then cat "${axfr_file}.tmp" >> "$axfr_file" axfr_found=1 add_finding "CRITICAL" "RECON" "DNS Zone Transfer (AXFR) is permitted" \ "Name server $ns allows AXFR — full DNS zone disclosed." \ "$(head -5 "$axfr_file")" \ "Restrict AXFR to authorized secondary name servers only." fi fi rm -f "${axfr_file}.tmp" done <<< "$ns_servers" [[ $axfr_found -eq 0 ]] && log_ok "AXFR not permitted (correct)" fi # ── SUBDOMAIN ENUMERATION ────────────────────────────────────────────────── log_subsection "Subdomain Enumeration" local subs_file="${recon_dir}/subdomains.txt" : > "$subs_file" if has_tool subfinder; then log_info "Running subfinder..." timeout 120s subfinder -d "$TARGET_DOMAIN" -silent \ -o "${recon_dir}/subfinder.txt" 2>/dev/null || true [[ -f "${recon_dir}/subfinder.txt" ]] && \ cat "${recon_dir}/subfinder.txt" >> "$subs_file" fi if has_tool amass; then log_info "Running amass (passive)..." timeout 180s amass enum -passive -d "$TARGET_DOMAIN" \ -o "${recon_dir}/amass.txt" 2>/dev/null || true [[ -f "${recon_dir}/amass.txt" ]] && \ cat "${recon_dir}/amass.txt" >> "$subs_file" fi if has_tool dnsrecon; then log_info "Running dnsrecon..." timeout 120s dnsrecon -d "$TARGET_DOMAIN" -t std \ -j "${recon_dir}/dnsrecon.json" 2>/dev/null || true fi # Brute-force fallback if no enumeration tool found if ! has_tool subfinder && ! has_tool amass && [[ -f "$WL_DNS" ]]; then log_info "Brute-forcing subdomains via DNS (wordlist: $WL_DNS)..." local bf_count=0 while IFS= read -r word && [[ $bf_count -lt 500 ]]; do [[ -z "$word" || "$word" == \#* ]] && continue local fqdn="${word}.${TARGET_DOMAIN}" if dig +short A "$fqdn" 2>/dev/null | grep -qP '^\d+\.\d+\.\d+\.\d+'; then echo "$fqdn" >> "$subs_file" log_verbose "Found: $fqdn" fi bf_count=$((bf_count + 1)) done < "$WL_DNS" fi # Deduplicate if [[ -s "$subs_file" ]]; then sort -u "$subs_file" -o "$subs_file" local sub_count; sub_count=$(wc -l < "$subs_file") log_ok "Discovered $sub_count unique subdomains → $subs_file" add_finding "INFO" "RECON" "Subdomain enumeration: $sub_count hosts discovered" \ "$(head -10 "$subs_file" | tr '\n' ' ')" "$subs_file" "" else log_info "No additional subdomains discovered" fi # ── GOOGLE DORKS (generated, not executed) ───────────────────────────────── log_subsection "Google Dork List" local dorks_file="${recon_dir}/google_dorks.txt" cat > "$dorks_file" << DORKS # Google Dorks — ${TARGET_DOMAIN} # Generated by ${TOOL_NAME} v${TOOL_VERSION} on ${DATE_HUMAN} # Run these manually in Google / Bing / DuckDuckGo ## Information disclosure site:${TARGET_DOMAIN} site:${TARGET_DOMAIN} filetype:pdf OR filetype:doc OR filetype:xls OR filetype:xlsx OR filetype:csv site:${TARGET_DOMAIN} ext:php inurl:id= site:${TARGET_DOMAIN} ext:asp OR ext:aspx site:${TARGET_DOMAIN} "index of /" site:${TARGET_DOMAIN} "index of /admin" site:${TARGET_DOMAIN} "index of /backup" site:${TARGET_DOMAIN} intext:"sql syntax" OR intext:"mysql_fetch_array" OR intext:"ORA-" ## Admin/login panels site:${TARGET_DOMAIN} inurl:admin OR inurl:administrator site:${TARGET_DOMAIN} inurl:login OR inurl:signin OR inurl:auth site:${TARGET_DOMAIN} inurl:phpmyadmin OR inurl:adminer site:${TARGET_DOMAIN} inurl:wp-admin OR inurl:wp-login.php ## Credentials & secrets site:${TARGET_DOMAIN} intext:"password" filetype:log site:${TARGET_DOMAIN} intext:"DB_PASSWORD" OR intext:"MYSQL_ROOT_PASSWORD" site:${TARGET_DOMAIN} intext:"api_key" OR intext:"api_secret" OR intext:"client_secret" "${TARGET_DOMAIN}" site:pastebin.com "${TARGET_DOMAIN}" site:github.com password OR secret OR token OR credential ## Config files site:${TARGET_DOMAIN} filetype:env OR filetype:conf OR filetype:cfg OR filetype:ini site:${TARGET_DOMAIN} filetype:bak OR filetype:backup OR filetype:old OR filetype:orig ## Exposed panels & services site:${TARGET_DOMAIN} inurl:swagger OR inurl:api-docs OR inurl:openapi site:${TARGET_DOMAIN} inurl:console OR inurl:debug OR inurl:phpinfo DORKS log_ok "Google Dorks generated → $dorks_file" } # ───────────────────────────────────────────────────────────────────────────── # MODULE 02 — PORT SCANNING # ───────────────────────────────────────────────────────────────────────────── module_portscan() { [[ $MOD_PORTSCAN -eq 0 ]] && return log_section "MODULE 02 — PORT SCANNING" require_tool nmap || return local scan_target="${TARGET_IP:-$TARGET_DOMAIN}" local nmap_base="${OUTPUT_DIR}/portscan/nmap" # Build nmap flags local ports_arg case "$OPT_PORTS" in top-100) ports_arg="--top-ports 100" ;; full) ports_arg="-p-" ;; *) ports_arg="--top-ports 1000" ;; esac local nmap_flags="-sV -sC --open" if [[ $OPT_AGGRESSIVE -eq 1 ]]; then nmap_flags="$nmap_flags -A -O --script=vuln,auth,default,discovery" elif [[ $OPT_STEALTH -eq 1 ]]; then nmap_flags="$nmap_flags -sS -T2 -f --data-length 32 --randomize-hosts" else nmap_flags="$nmap_flags -T4" fi [[ -n "$OPT_PROXY" ]] && nmap_flags="$nmap_flags --proxies $OPT_PROXY" log_info "Target : $scan_target" log_info "Profile: $OPT_PORTS" log_info "Flags : $nmap_flags" echo "" # shellcheck disable=SC2086 nmap $nmap_flags $ports_arg \ -oN "${nmap_base}.txt" \ -oX "${nmap_base}.xml" \ -oG "${nmap_base}.gnmap" \ "$scan_target" 2>&1 | tee -a "$LOG_FILE" || true [[ ! -f "${nmap_base}.txt" ]] && { log_error "nmap failed to produce output"; return; } log_ok "nmap completed → ${nmap_base}.txt" # Parse open ports and flag dangerous ones local open_ports open_ports=$(grep -P "^\d+/tcp\s+open" "${nmap_base}.txt" 2>/dev/null || true) if [[ -z "$open_ports" ]]; then log_info "No open ports found in scan range" return fi log_info "Open ports:" echo "$open_ports" | tee -a "$LOG_FILE" echo "" add_finding "INFO" "PORTSCAN" "Port scan results for $scan_target" \ "$(echo "$open_ports" | wc -l) open port(s) found." \ "$(echo "$open_ports" | head -5 | tr '\n' ' ')" "" # Risk-based analysis of open ports declare -A port_risks=( [21]="CRITICAL|FTP (port 21) — plaintext credentials|Close FTP; use SFTP (port 22) instead." [23]="CRITICAL|Telnet (port 23) — unencrypted remote shell|Disable Telnet; use SSH." [25]="MEDIUM|SMTP (port 25) exposed publicly|Restrict if not a mail relay." [110]="MEDIUM|POP3 (port 110) — plaintext email retrieval|Use POP3S (port 995)." [143]="MEDIUM|IMAP (port 143) — plaintext email access|Use IMAPS (port 993)." [389]="MEDIUM|LDAP (port 389) — unencrypted directory|Use LDAPS (port 636)." [445]="HIGH|SMB (port 445) exposed — risk of EternalBlue/ransomware|Block SMB at firewall; do not expose to internet." [1433]="HIGH|MSSQL (port 1433) exposed — database accessible|Block with firewall; allow local access only." [1521]="HIGH|Oracle DB (port 1521) exposed|Block with firewall; allow local access only." [2375]="CRITICAL|Docker API (port 2375) — unauthenticated exposure|Enable TLS on Docker socket; never expose to internet." [2376]="MEDIUM|Docker TLS API (port 2376) exposed|Restrict to authorized hosts only." [3306]="HIGH|MySQL (port 3306) exposed to internet|Block with firewall; bind to 127.0.0.1." [3389]="HIGH|RDP (port 3389) exposed — brute-force target|Use VPN gateway; disable direct RDP access." [5432]="HIGH|PostgreSQL (port 5432) exposed|Block with firewall; bind to 127.0.0.1." [5900]="HIGH|VNC (port 5900) exposed|Use VPN; require strong authentication." [6379]="HIGH|Redis (port 6379) — often unauthenticated|Require auth; bind to 127.0.0.1." [8080]="LOW|HTTP alternate port (8080) open|Verify intentional exposure; check for admin panels." [8443]="LOW|HTTPS alternate port (8443) open|Verify intentional exposure." [9200]="HIGH|Elasticsearch (port 9200) — often unauthenticated|Enable auth (X-Pack); do not expose publicly." [9300]="HIGH|Elasticsearch transport (port 9300) exposed|Block with firewall." [11211]="HIGH|Memcached (port 11211) — no auth by default|Block with firewall; bind to localhost." [27017]="HIGH|MongoDB (port 27017) — often unauthenticated|Enable auth; bind to localhost." ) echo "$open_ports" | while IFS= read -r line; do local port; port=$(echo "$line" | awk '{print $1}' | cut -d/ -f1) local risk="${port_risks[$port]:-}" if [[ -n "$risk" ]]; then IFS='|' read -r sev title rec <<< "$risk" add_finding "$sev" "PORTSCAN" "$title" \ "Service exposed: $line" "$line" "$rec" fi done } # ───────────────────────────────────────────────────────────────────────────── # MODULE 03 — WEB FINGERPRINTING # ───────────────────────────────────────────────────────────────────────────── module_fingerprint() { [[ $MOD_FINGERPRINT -eq 0 ]] && return log_section "MODULE 03 — WEB FINGERPRINTING" local recon_dir="${OUTPUT_DIR}/recon" # ── WhatWeb ──────────────────────────────────────────────────────────────── log_subsection "Technology Detection" if has_tool whatweb; then local aggr_level=1 [[ $OPT_AGGRESSIVE -eq 1 ]] && aggr_level=3 whatweb -a $aggr_level "$TARGET" \ --log-json="${recon_dir}/whatweb.json" \ --log-brief="${recon_dir}/whatweb_brief.txt" \ 2>/dev/null | tee -a "$LOG_FILE" || true log_ok "WhatWeb → ${recon_dir}/whatweb.json" else # Fallback: header-based detection log_info "whatweb not available — performing header-based detection" local resp resp=$(_curl -I "$TARGET" 2>/dev/null || true) echo "$resp" > "${recon_dir}/basic_headers_raw.txt" local techs=("WordPress" "Drupal" "Joomla" "Magento" "Laravel" "Django" "Rails" "Express" "nginx" "Apache" "IIS" "LiteSpeed" "PHP" "ASP.NET" "ColdFusion" "Tomcat" "Jetty" "Cloudflare") for tech in "${techs[@]}"; do if echo "$resp" | grep -qi "$tech"; then log_info "Technology detected: $tech" add_finding "INFO" "FINGERPRINT" "Technology detected: $tech" \ "Identified via response headers." "" "" fi done fi # ── WAF Detection ────────────────────────────────────────────────────────── log_subsection "WAF Detection" if has_tool wafw00f; then wafw00f "$TARGET" 2>/dev/null | tee -a "$LOG_FILE" | \ tee "${recon_dir}/waf_detection.txt" > /dev/null || true if grep -qi "is behind" "${recon_dir}/waf_detection.txt" 2>/dev/null; then local waf_name waf_name=$(grep -i "is behind" "${recon_dir}/waf_detection.txt" | head -1 | sed 's/.*behind //') log_info "WAF detected: $waf_name" add_finding "INFO" "FINGERPRINT" "WAF detected: $waf_name" \ "The target appears to be protected by a Web Application Firewall." "$waf_name" "" else add_finding "LOW" "FINGERPRINT" "No WAF detected" \ "No known WAF signatures were identified in server responses." "" \ "Consider deploying a WAF (Cloudflare, ModSecurity, AWS WAF, etc.)" fi else log_warn "wafw00f not available — WAF detection skipped" fi # ── Version-leaking headers ──────────────────────────────────────────────── log_subsection "Server Version Disclosure" local headers headers=$(_curl -I "$TARGET" 2>/dev/null || true) echo "$headers" > "${OUTPUT_DIR}/headers/initial_response.txt" local leaking_headers=("Server" "X-Powered-By" "X-AspNet-Version" "X-AspNetMvc-Version" "X-Generator" "X-Drupal-Cache" "X-WordPress-Cache" "Via") for h in "${leaking_headers[@]}"; do local val val=$(echo "$headers" | grep -i "^${h}:" | head -1 || true) if [[ -n "$val" ]]; then log_warn "Version-leaking header: $val" add_finding "LOW" "FINGERPRINT" "Version disclosure via '$h' header" \ "The server reveals technology/version info in response headers." \ "$val" \ "Remove or neutralise the '$h' header in your web server configuration." fi done } # ───────────────────────────────────────────────────────────────────────────── # MODULE 04 — SSL/TLS ANALYSIS # ───────────────────────────────────────────────────────────────────────────── module_ssl() { [[ $MOD_SSL -eq 0 ]] && return if ! is_https; then log_info "[SSL] Target is not HTTPS — skipping SSL/TLS analysis" add_finding "HIGH" "SSL" "Target is served over HTTP (no TLS)" \ "All traffic between client and server is transmitted in plaintext." "" \ "Deploy a TLS certificate (Let's Encrypt is free). Redirect all HTTP to HTTPS." return fi log_section "MODULE 04 — SSL/TLS ANALYSIS" local ssl_dir="${OUTPUT_DIR}/ssl" # ── testssl.sh (primary) ─────────────────────────────────────────────────── if has_tool "testssl.sh"; then log_info "Running testssl.sh (full analysis)..." timeout 600s testssl.sh \ --quiet \ --severity LOW \ --jsonfile "${ssl_dir}/testssl.json" \ --logfile "${ssl_dir}/testssl.log" \ "$TARGET_DOMAIN" 2>/dev/null || true log_ok "testssl.sh → ${ssl_dir}/testssl.json" # Parse findings from JSON if has_tool jq && [[ -f "${ssl_dir}/testssl.json" ]]; then while IFS= read -r entry; do local sev finding_id finding_text sev=$(echo "$entry" | jq -r '.severity // ""' 2>/dev/null) finding_id=$(echo "$entry" | jq -r '.id // ""' 2>/dev/null) finding_text=$(echo "$entry"| jq -r '.finding // ""' 2>/dev/null) [[ -z "$sev" || -z "$finding_id" ]] && continue case "$sev" in CRITICAL|HIGH|MEDIUM|LOW) add_finding "$sev" "SSL" "testssl: [${finding_id}] ${finding_text:0:120}" \ "" "" "Refer to testssl documentation for ${finding_id}" ;; esac done < <(jq -c '.[] | select(.severity != "OK" and .severity != "INFO" and .severity != "DEBUG" and .severity != "")' \ "${ssl_dir}/testssl.json" 2>/dev/null || true) fi # ── sslscan (secondary) ──────────────────────────────────────────────────── elif has_tool sslscan; then log_info "Running sslscan..." sslscan --no-colour "${TARGET_DOMAIN}:443" > "${ssl_dir}/sslscan.txt" 2>&1 || true log_ok "sslscan → ${ssl_dir}/sslscan.txt" local scan_output scan_output=$(cat "${ssl_dir}/sslscan.txt") # Deprecated protocols for proto in "SSLv2" "SSLv3" "TLSv1.0" "TLSv1.1"; do if echo "$scan_output" | grep -qP "^\s*${proto}\s+enabled"; then add_finding "HIGH" "SSL" "Insecure protocol enabled: $proto" \ "$proto is deprecated and vulnerable to known attacks." "$proto" \ "Disable $proto. Allow TLS 1.2 and TLS 1.3 only." fi done # Weak cipher suites if echo "$scan_output" | grep -qiE "RC4|NULL|EXPORT|anon|DES\b|3DES"; then add_finding "HIGH" "SSL" "Weak cipher suites detected" \ "RC4, NULL, EXPORT or DES ciphers are enabled." \ "$(echo "$scan_output" | grep -iE 'RC4|NULL|EXPORT|anon|DES' | head -3)" \ "Disable weak ciphers. Prefer ECDHE+AESGCM and ChaCha20-Poly1305." fi # Self-signed or untrusted if echo "$scan_output" | grep -qi "self-signed\|not trusted"; then add_finding "HIGH" "SSL" "Untrusted or self-signed certificate" \ "The certificate is not issued by a trusted CA." "" \ "Use a certificate from a trusted CA (Let's Encrypt, DigiCert, etc.)" fi # ── openssl fallback ─────────────────────────────────────────────────────── else log_info "testssl.sh/sslscan not available — using openssl fallback" local openssl_out="${ssl_dir}/openssl_info.txt" { echo "=== Certificate Info ===" echo | timeout "$OPT_TIMEOUT" openssl s_client \ -connect "${TARGET_DOMAIN}:443" \ -servername "$TARGET_DOMAIN" 2>/dev/null \ | openssl x509 -noout -text 2>/dev/null || true echo "" echo "=== Protocol Support ===" for proto in ssl2 ssl3 tls1 tls1_1 tls1_2 tls1_3; do if echo | timeout 5s openssl s_client \ -"$proto" -connect "${TARGET_DOMAIN}:443" \ -servername "$TARGET_DOMAIN" 2>/dev/null \ | grep -q "CONNECTED"; then echo "${proto}: ENABLED" else echo "${proto}: disabled" fi done } > "$openssl_out" 2>&1 # Flag insecure protocols found grep "ENABLED" "$openssl_out" 2>/dev/null | while IFS= read -r line; do local proto; proto=$(echo "$line" | cut -d: -f1) case "$proto" in ssl2|ssl3|tls1|tls1_1) add_finding "HIGH" "SSL" "Insecure protocol enabled: $proto" \ "$proto is deprecated and vulnerable." "$proto" \ "Disable $proto. Use TLS 1.2+ only." ;; esac done log_ok "OpenSSL analysis → $openssl_out" fi # ── Certificate expiry check ─────────────────────────────────────────────── log_subsection "Certificate Expiry Check" local expiry_raw expiry_raw=$(echo | timeout "$OPT_TIMEOUT" openssl s_client \ -connect "${TARGET_DOMAIN}:443" \ -servername "$TARGET_DOMAIN" 2>/dev/null \ | openssl x509 -noout -enddate 2>/dev/null \ | cut -d= -f2 || true) if [[ -n "$expiry_raw" ]]; then local exp_epoch now_epoch days_left exp_epoch=$(date -d "$expiry_raw" +%s 2>/dev/null || echo 0) now_epoch=$(date +%s) days_left=$(( (exp_epoch - now_epoch) / 86400 )) if [[ "$days_left" -lt 0 ]]; then add_finding "CRITICAL" "SSL" "Certificate has EXPIRED" \ "Certificate expired $((days_left * -1)) days ago." "$expiry_raw" \ "Renew the TLS certificate immediately." elif [[ "$days_left" -lt 14 ]]; then add_finding "CRITICAL" "SSL" "Certificate expires in $days_left days" \ "Imminent certificate expiry will cause service disruption." "$expiry_raw" \ "Renew the certificate immediately." elif [[ "$days_left" -lt 30 ]]; then add_finding "HIGH" "SSL" "Certificate expires in $days_left days" \ "Certificate is close to expiry." "$expiry_raw" \ "Renew the certificate urgently." elif [[ "$days_left" -lt 90 ]]; then add_finding "MEDIUM" "SSL" "Certificate expires in $days_left days" \ "" "$expiry_raw" "Plan certificate renewal." else log_ok "Certificate valid — $days_left days remaining (${expiry_raw})" fi fi # ── HSTS ────────────────────────────────────────────────────────────────── log_subsection "HSTS (HTTP Strict Transport Security)" local hsts_header hsts_header=$(_curl -I "$TARGET" 2>/dev/null | grep -i "^strict-transport-security:" || true) if [[ -z "$hsts_header" ]]; then add_finding "MEDIUM" "SSL" "HSTS header not configured" \ "Strict-Transport-Security is absent — browsers may access the site over HTTP." "" \ "Add: Strict-Transport-Security: max-age=63072000; includeSubDomains; preload" else local maxage maxage=$(echo "$hsts_header" | grep -oP 'max-age=\K\d+' || echo 0) if [[ "$maxage" -lt 15552000 ]]; then add_finding "LOW" "SSL" "HSTS max-age is too short ($maxage seconds)" \ "Recommended minimum is 6 months (15552000 seconds)." "$hsts_header" \ "Increase HSTS max-age to at least 15552000." else log_ok "HSTS configured: $hsts_header" fi fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 05 — HTTP SECURITY HEADERS # ───────────────────────────────────────────────────────────────────────────── module_headers() { [[ $MOD_HEADERS -eq 0 ]] && return log_section "MODULE 05 — HTTP SECURITY HEADERS" local hdrs_dir="${OUTPUT_DIR}/headers" local hdr_file="${hdrs_dir}/response_headers.txt" _curl -I -L --max-redirs 5 "$TARGET" 2>/dev/null > "$hdr_file" || true log_info "Response headers captured → $hdr_file" cat "$hdr_file" | tee -a "$LOG_FILE" echo "" # ── Security headers that MUST be present ────────────────────────────────── declare -A REQUIRED_HEADERS=( ["content-security-policy"]="MEDIUM|Content-Security-Policy (CSP) missing|Implement a strict CSP to mitigate XSS and data injection attacks." ["x-frame-options"]="MEDIUM|X-Frame-Options missing — clickjacking risk|Add: X-Frame-Options: SAMEORIGIN" ["x-content-type-options"]="LOW|X-Content-Type-Options missing — MIME sniffing risk|Add: X-Content-Type-Options: nosniff" ["referrer-policy"]="LOW|Referrer-Policy missing|Add: Referrer-Policy: strict-origin-when-cross-origin" ["permissions-policy"]="LOW|Permissions-Policy missing|Add: Permissions-Policy: geolocation=(), microphone=(), camera=()" ["cross-origin-opener-policy"]="LOW|Cross-Origin-Opener-Policy (COOP) missing|Add: Cross-Origin-Opener-Policy: same-origin" ["cross-origin-resource-policy"]="LOW|Cross-Origin-Resource-Policy (CORP) missing|Add: Cross-Origin-Resource-Policy: same-origin" ) for header in "${!REQUIRED_HEADERS[@]}"; do IFS='|' read -r sev title rec <<< "${REQUIRED_HEADERS[$header]}" if ! grep -qi "^${header}:" "$hdr_file" 2>/dev/null; then add_finding "$sev" "HEADERS" "$title" \ "The response is missing the '${header}' security header." "" "$rec" else log_ok "Present: $(grep -i "^${header}:" "$hdr_file" | head -1)" # CSP audit — flag dangerous directives if [[ "$header" == "content-security-policy" ]]; then local csp_val csp_val=$(grep -i "^content-security-policy:" "$hdr_file" | head -1) if echo "$csp_val" | grep -qi "unsafe-inline\|unsafe-eval"; then add_finding "MEDIUM" "HEADERS" "CSP contains 'unsafe-inline' or 'unsafe-eval'" \ "Overly permissive CSP directive undermines XSS protection." "$csp_val" \ "Remove 'unsafe-inline'/'unsafe-eval'. Use nonces or hashes instead." fi if echo "$csp_val" | grep -qi "default-src \*\|script-src \*"; then add_finding "HIGH" "HEADERS" "CSP uses wildcard (*) for script-src or default-src" \ "A wildcard CSP directive provides no protection against XSS." "$csp_val" \ "Specify explicit allowed origins in Content-Security-Policy." fi fi fi done # ── Headers that should NOT be present ───────────────────────────────────── log_subsection "Sensitive Header Exposure" local forbidden_headers=("Server" "X-Powered-By" "X-AspNet-Version" "X-AspNetMvc-Version" "X-Generator" "X-CF-Powered-By") for h in "${forbidden_headers[@]}"; do local val; val=$(grep -i "^${h}:" "$hdr_file" 2>/dev/null | head -1 || true) if [[ -n "$val" ]]; then add_finding "LOW" "HEADERS" "Informative header exposed: $h" \ "Server reveals technology details via '$h' header." "$val" \ "Remove or anonymise the '$h' header in your server configuration." fi done # ── Cookie flags ──────────────────────────────────────────────────────────── log_subsection "Cookie Security Flags" local cookies cookies=$(_curl -I "$TARGET" 2>/dev/null | grep -i "^set-cookie:" || true) if [[ -n "$cookies" ]]; then echo "$cookies" | while IFS= read -r cookie; do local cookie_name; cookie_name=$(echo "$cookie" | grep -oP 'Set-Cookie:\s*\K[^=]+' || echo "?") if ! echo "$cookie" | grep -qi "httponly"; then add_finding "MEDIUM" "HEADERS" "Cookie '$cookie_name' missing HttpOnly flag" \ "Cookie is accessible via JavaScript — XSS can steal session tokens." \ "$cookie" "Add 'HttpOnly' flag to all session cookies." fi if ! echo "$cookie" | grep -qi "secure"; then add_finding "MEDIUM" "HEADERS" "Cookie '$cookie_name' missing Secure flag" \ "Cookie may be transmitted over unencrypted HTTP connections." \ "$cookie" "Add 'Secure' flag to all session cookies." fi if ! echo "$cookie" | grep -qi "samesite"; then add_finding "LOW" "HEADERS" "Cookie '$cookie_name' missing SameSite attribute" \ "Absence of SameSite exposes the cookie to CSRF risks." \ "$cookie" "Add 'SameSite=Strict' or 'SameSite=Lax' to session cookies." fi if echo "$cookie" | grep -qiP "samesite=none(?!.*secure)" ; then add_finding "MEDIUM" "HEADERS" "Cookie '$cookie_name' uses SameSite=None without Secure" \ "SameSite=None requires the Secure flag." \ "$cookie" "Add 'Secure' flag when using 'SameSite=None'." fi done else log_info "No Set-Cookie headers found in initial response" fi # ── HTTP → HTTPS redirect ─────────────────────────────────────────────────── log_subsection "HTTP to HTTPS Redirect" local http_url="${TARGET/https:\/\//http://}" local redirect_code redirect_code=$(_curl -sI -o /dev/null -w "%{http_code}" \ --max-redirs 0 "$http_url" 2>/dev/null || echo "000") if echo "$redirect_code" | grep -qP "^30[1-8]$"; then log_ok "HTTP → HTTPS redirect in place (HTTP $redirect_code)" else add_finding "MEDIUM" "HEADERS" "HTTP does not redirect to HTTPS (HTTP $redirect_code)" \ "Requests over HTTP are not automatically upgraded to HTTPS." "$http_url → $redirect_code" \ "Configure a permanent 301 redirect from HTTP to HTTPS." fi # ── Cache-Control on sensitive pages ─────────────────────────────────────── local cache_ctrl cache_ctrl=$(_curl -I "$TARGET" 2>/dev/null | grep -i "^cache-control:" || true) if [[ -z "$cache_ctrl" ]]; then add_finding "LOW" "HEADERS" "Cache-Control header missing" \ "Without Cache-Control, sensitive pages may be cached by intermediaries." "" \ "Add: Cache-Control: no-store, no-cache on authenticated/sensitive pages." fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 06 — DIRECTORY & FILE BRUTE-FORCING # ───────────────────────────────────────────────────────────────────────────── module_dirbrute() { [[ $MOD_DIRBRUTE -eq 0 ]] && return log_section "MODULE 06 — DIRECTORY & FILE ENUMERATION" local dirs_dir="${OUTPUT_DIR}/dirs" local wordlist="$WL_DIRS_SMALL" [[ $OPT_AGGRESSIVE -eq 1 ]] && wordlist="$WL_DIRS_BIG" if [[ ! -f "$wordlist" ]]; then log_warn "Wordlist not found: $wordlist" log_warn "Install wordlists: sudo apt-get install wordlists" log_info "Skipping brute-force, running sensitive file checks only" else # ── gobuster ────────────────────────────────────────────────────────────── if has_tool gobuster; then log_subsection "gobuster (directory)" gobuster dir \ -u "$TARGET" \ -w "$wordlist" \ -t "$OPT_THREADS" \ --timeout "${OPT_TIMEOUT}s" \ -q \ -o "${dirs_dir}/gobuster_dirs.txt" \ $( [[ -n "$OPT_PROXY" ]] && printf -- "--proxy %s" "$OPT_PROXY" ) \ 2>/dev/null || true if [[ -f "${dirs_dir}/gobuster_dirs.txt" ]]; then local found; found=$(wc -l < "${dirs_dir}/gobuster_dirs.txt") log_ok "$found paths found → ${dirs_dir}/gobuster_dirs.txt" [[ $found -gt 0 ]] && add_finding "INFO" "DIRBRUTE" \ "$found directories/files discovered" \ "$(head -10 "${dirs_dir}/gobuster_dirs.txt" | tr '\n' ' ')" \ "${dirs_dir}/gobuster_dirs.txt" "" fi # DNS subdomain brute-force if [[ -f "$WL_DNS" ]]; then log_subsection "gobuster (DNS)" gobuster dns \ -d "$TARGET_DOMAIN" \ -w "$WL_DNS" \ -t "$OPT_THREADS" \ -q \ -o "${dirs_dir}/gobuster_dns.txt" \ 2>/dev/null || true fi # ── ffuf fallback ────────────────────────────────────────────────────────── elif has_tool ffuf; then log_subsection "ffuf" ffuf -u "${TARGET}/FUZZ" \ -w "$wordlist" \ -t "$OPT_THREADS" \ -timeout "$OPT_TIMEOUT" \ -mc 200,201,204,301,302,307,401,403 \ -of json \ -o "${dirs_dir}/ffuf_results.json" \ -s 2>/dev/null || true log_ok "ffuf → ${dirs_dir}/ffuf_results.json" # ── dirb fallback ────────────────────────────────────────────────────────── elif has_tool dirb; then log_subsection "dirb" dirb "$TARGET" "$wordlist" -S -r \ -o "${dirs_dir}/dirb_results.txt" 2>/dev/null || true log_ok "dirb → ${dirs_dir}/dirb_results.txt" else log_warn "No directory brute-force tool found (gobuster/ffuf/dirb)" fi fi # ── Sensitive file/path probing ──────────────────────────────────────────── log_subsection "Sensitive File Probing" declare -A SENSITIVE_PATHS=( # Format: PATH="SEVERITY|short description" ["/.git/HEAD"]="CRITICAL|Git repository exposed — source code may be accessible" ["/.git/config"]="CRITICAL|Git config file exposed" ["/.env"]="CRITICAL|Environment file exposed — may contain credentials" ["/.env.local"]="CRITICAL|Local env file exposed" ["/.env.production"]="CRITICAL|Production env file exposed" ["/wp-config.php"]="CRITICAL|WordPress config file accessible — DB credentials exposed" ["/wp-config.php.bak"]="CRITICAL|WordPress config backup exposed" ["/configuration.php"]="CRITICAL|Joomla config file exposed" ["/config/database.yml"]="CRITICAL|Rails database config exposed" ["/.aws/credentials"]="CRITICAL|AWS credentials file exposed" ["/db.sql"]="CRITICAL|Database dump accessible" ["/dump.sql"]="CRITICAL|SQL dump accessible" ["/backup.sql"]="CRITICAL|SQL backup accessible" ["/backup.zip"]="HIGH|Backup archive accessible" ["/backup.tar.gz"]="HIGH|Backup archive accessible" ["/phpinfo.php"]="HIGH|phpinfo() page accessible — full PHP/server info exposed" ["/info.php"]="HIGH|PHP info page accessible" ["/test.php"]="MEDIUM|Test PHP file accessible" ["/phpmyadmin/"]="HIGH|phpMyAdmin interface exposed" ["/adminer.php"]="HIGH|Adminer database interface exposed" ["/adminer/"]="HIGH|Adminer database interface exposed" ["/admin/"]="MEDIUM|Admin panel accessible" ["/administrator/"]="MEDIUM|Administrator panel accessible" ["/.htaccess"]="MEDIUM|.htaccess file readable — server config exposed" ["/web.config"]="HIGH|web.config exposed — IIS config/credentials may be disclosed" ["/server-status"]="MEDIUM|Apache server-status page accessible" ["/server-info"]="MEDIUM|Apache server-info page accessible" ["/_profiler/"]="MEDIUM|Symfony profiler exposed" ["/_debugbar"]="MEDIUM|DebugBar exposed" ["/api/swagger.json"]="LOW|Swagger API spec accessible" ["/api/openapi.json"]="LOW|OpenAPI spec accessible" ["/swagger-ui.html"]="LOW|Swagger UI exposed" ["/api-docs"]="LOW|API documentation exposed" ["/graphql"]="LOW|GraphQL endpoint accessible" ["/graphiql"]="MEDIUM|GraphiQL IDE exposed" ["/console"]="HIGH|Interactive console exposed" ["/robots.txt"]="INFO|robots.txt accessible — may reveal hidden paths" ["/sitemap.xml"]="INFO|sitemap.xml accessible" ["/.well-known/security.txt"]="INFO|security.txt present — check responsible disclosure policy" ["/crossdomain.xml"]="LOW|crossdomain.xml present" ["/clientaccesspolicy.xml"]="LOW|clientaccesspolicy.xml present" ["/.DS_Store"]="MEDIUM|.DS_Store file exposed — directory structure leaked" ["/package.json"]="MEDIUM|package.json exposed — dependency list revealed" ["/composer.json"]="MEDIUM|composer.json exposed" ["/Dockerfile"]="HIGH|Dockerfile exposed" ["/docker-compose.yml"]="HIGH|docker-compose.yml exposed — may contain credentials" ["/Makefile"]="LOW|Makefile accessible" ["/.travis.yml"]="LOW|CI config exposed" ["/.circleci/config.yml"]="LOW|CircleCI config exposed" ) local sensitive_found="${dirs_dir}/sensitive_paths_found.txt" : > "$sensitive_found" for path in "${!SENSITIVE_PATHS[@]}"; do IFS='|' read -r sev description <<< "${SENSITIVE_PATHS[$path]}" local url="${TARGET}${path}" local status status=$(_curl -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000") if [[ "$status" =~ ^(200|206|301|302)$ ]]; then echo "[HTTP ${status}] ${url}" | tee -a "$sensitive_found" | tee -a "$LOG_FILE" add_finding "$sev" "DIRBRUTE" "Sensitive path accessible: ${path}" \ "$description" "HTTP ${status} — ${url}" \ "Restrict access, remove the file, or block the path at the web server level." fi done if [[ -s "$sensitive_found" ]]; then log_warn "$(wc -l < "$sensitive_found") sensitive path(s) found — review $sensitive_found" else log_ok "No high-priority sensitive files detected" fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 07 — NIKTO # ───────────────────────────────────────────────────────────────────────────── module_nikto() { [[ $MOD_NIKTO -eq 0 ]] && return log_section "MODULE 07 — NIKTO WEB SERVER SCAN" require_tool nikto || return local nikto_base="${OUTPUT_DIR}/vulns/nikto" local nikto_flags="-host $TARGET -nointeractive -maxtime 600" [[ $OPT_AGGRESSIVE -eq 1 ]] && nikto_flags="$nikto_flags -Plugins @@ALL" [[ -n "$OPT_PROXY" ]] && nikto_flags="$nikto_flags -useproxy $OPT_PROXY" log_info "Running Nikto (may take several minutes)..." # shellcheck disable=SC2086 timeout 650s nikto $nikto_flags \ -output "${nikto_base}.txt" -Format txt 2>/dev/null | tee -a "$LOG_FILE" || true # shellcheck disable=SC2086 timeout 650s nikto $nikto_flags \ -output "${nikto_base}.json" -Format json 2>/dev/null || true if [[ ! -f "${nikto_base}.txt" ]]; then log_warn "Nikto did not produce output" return fi log_ok "Nikto → ${nikto_base}.txt" # Parse findings grep -E "^\+ " "${nikto_base}.txt" 2>/dev/null | while IFS= read -r line; do local sev="LOW" echo "$line" | grep -qiE "vuln|exploit|inject|XSS|SQL|CVE|OSVDB-[0-9]{4,}" && sev="HIGH" echo "$line" | grep -qiE "outdated|version|disclose|found|enabled" && sev="MEDIUM" add_finding "$sev" "NIKTO" "${line:2:120}" \ "" "$line" "Investigate Nikto finding and apply the appropriate remediation." done } # ───────────────────────────────────────────────────────────────────────────── # MODULE 08 — SQL INJECTION # ───────────────────────────────────────────────────────────────────────────── module_sqli() { [[ $MOD_SQLI -eq 0 ]] && return log_section "MODULE 08 — SQL INJECTION (sqlmap)" require_tool sqlmap || return local sqli_dir="${OUTPUT_DIR}/vulns/sqlmap" mkdir -p "$sqli_dir" local sqlmap_flags="--batch --random-agent --timeout=$OPT_TIMEOUT --threads=$OPT_THREADS" sqlmap_flags="$sqlmap_flags --level=3 --risk=2" if [[ $OPT_AGGRESSIVE -eq 1 ]]; then sqlmap_flags="$sqlmap_flags --level=5 --risk=3 --forms --crawl=${OPT_DEPTH} --dbs --tamper=space2comment" fi [[ $OPT_STEALTH -eq 1 ]] && sqlmap_flags="$sqlmap_flags --delay=2 --safe-freq=3 --smart" [[ -n "$OPT_PROXY" ]] && sqlmap_flags="$sqlmap_flags --proxy=$OPT_PROXY" log_info "Running sqlmap on $TARGET" log_verbose "Flags: $sqlmap_flags" # shellcheck disable=SC2086 timeout 600s sqlmap -u "$TARGET" $sqlmap_flags \ --output-dir="$sqli_dir" \ 2>/dev/null | tee "${sqli_dir}/sqlmap_console.txt" || true local output_file="${sqli_dir}/sqlmap_console.txt" if grep -qiE "is vulnerable|sqlmap identified|injectable" "$output_file" 2>/dev/null; then add_finding "CRITICAL" "SQLI" "SQL Injection confirmed on ${TARGET}" \ "sqlmap identified injectable parameter(s)." \ "$(grep -iE 'Parameter:|Type:|Title:' "$output_file" | head -5 | tr '\n' ' ')" \ "Use parameterised queries / prepared statements. Never concatenate user input into SQL." else log_ok "No SQL injection detected on the main target URL" add_finding "INFO" "SQLI" "No obvious SQLi on primary URL" \ "Manual testing with specific parameters recommended." "" "" fi log_info "sqlmap results → $sqli_dir" } # ───────────────────────────────────────────────────────────────────────────── # MODULE 09 — CROSS-SITE SCRIPTING (XSS) # ───────────────────────────────────────────────────────────────────────────── module_xss() { [[ $MOD_XSS -eq 0 ]] && return log_section "MODULE 09 — CROSS-SITE SCRIPTING (XSS)" local xss_dir="${OUTPUT_DIR}/vulns/xss" mkdir -p "$xss_dir" # ── dalfox ──────────────────────────────────────────────────────────────── if has_tool dalfox; then log_subsection "dalfox XSS Scanner" local dalfox_flags="--silence --timeout $OPT_TIMEOUT" [[ $OPT_AGGRESSIVE -eq 1 ]] && dalfox_flags="$dalfox_flags --deep-domxss --follow-redirects" [[ -n "$OPT_PROXY" ]] && dalfox_flags="$dalfox_flags --proxy $OPT_PROXY" # shellcheck disable=SC2086 timeout 300s dalfox url "$TARGET" $dalfox_flags \ --output "${xss_dir}/dalfox_results.txt" 2>/dev/null || true if [[ -s "${xss_dir}/dalfox_results.txt" ]] && \ grep -qiE "\[V\]|WEAK|MEDIUM|HIGH|CRITICAL" "${xss_dir}/dalfox_results.txt" 2>/dev/null; then add_finding "HIGH" "XSS" "XSS vulnerability confirmed by dalfox" \ "dalfox identified reflected or DOM-based XSS." \ "$(head -3 "${xss_dir}/dalfox_results.txt")" \ "Escape all HTML output. Implement a strict Content-Security-Policy." fi fi # ── Reflected XSS — manual probe ────────────────────────────────────────── log_subsection "Reflected XSS Probe" local xss_payloads=( "<script>alert(1)</script>" "'><img src=x onerror=alert(1)>" "<svg onload=alert(1)>" "\"><script>alert(1)</script>" "javascript:alert(1)" "';alert(1);//" "<details open ontoggle=alert(1)>" "<iframe srcdoc='<script>alert(1)</script>'>" ) local xss_params=("q" "s" "search" "query" "keyword" "id" "name" "page" "url" "ref" "return" "redirect" "next" "view" "lang") local xss_found="${xss_dir}/reflected_xss.txt" : > "$xss_found" for param in "${xss_params[@]}"; do for payload in "${xss_payloads[@]}"; do local enc; enc=$(url_encode "$payload") local test_url="${TARGET}?${param}=${enc}" local response response=$(_curl "$test_url" 2>/dev/null | head -100 || true) if echo "$response" | grep -qF "$payload"; then echo "[REFLECTED XSS] param=$param payload=$payload" | tee -a "$xss_found" | tee -a "$LOG_FILE" add_finding "HIGH" "XSS" "Reflected XSS in parameter '$param'" \ "Payload is reflected verbatim in the response without encoding." \ "${test_url:0:200}" \ "Encode all user-supplied output. Add Content-Security-Policy." break # One confirmed finding per param is enough fi done done if [[ ! -s "$xss_found" ]]; then log_ok "No reflected XSS detected in common parameters" fi log_info "XSS tests → $xss_dir" } # ───────────────────────────────────────────────────────────────────────────── # MODULE 10 — CMS SCANNING # ───────────────────────────────────────────────────────────────────────────── module_cms() { [[ $MOD_CMS -eq 0 ]] && return log_section "MODULE 10 — CMS DETECTION & SCANNING" local cms_dir="${OUTPUT_DIR}/cms" # Detect CMS local page page=$(_curl "$TARGET" 2>/dev/null || true) local headers headers=$(_curl -I "$TARGET" 2>/dev/null || true) local combined="${page}${headers}" local detected_cms="unknown" if echo "$combined" | grep -qi "wp-content\|wp-includes\|wordpress"; then detected_cms="wordpress" elif echo "$combined" | grep -qi "sites/all/modules\|Drupal.settings\|X-Generator: Drupal"; then detected_cms="drupal" elif echo "$combined" | grep -qi "Joomla\|/media/jui\|generator.*joomla"; then detected_cms="joomla" elif echo "$combined" | grep -qi "Mage.Cookies\|magento\|/skin/frontend"; then detected_cms="magento" fi log_info "Detected CMS: $detected_cms" add_finding "INFO" "CMS" "CMS detected: $detected_cms" "" "" "" # ── WordPress ────────────────────────────────────────────────────────────── if [[ "$detected_cms" == "wordpress" ]]; then log_subsection "WordPress — wpscan" if has_tool wpscan; then local wpscan_flags="--url $TARGET --no-banner" [[ $OPT_AGGRESSIVE -eq 1 ]] && \ wpscan_flags="$wpscan_flags --enumerate ap,at,cb,dbe,u --plugins-detection aggressive" [[ -n "$OPT_PROXY" ]] && wpscan_flags="$wpscan_flags --proxy $OPT_PROXY" # shellcheck disable=SC2086 timeout 600s wpscan $wpscan_flags \ --format json \ --output "${cms_dir}/wpscan_results.json" \ 2>/dev/null | tee "${cms_dir}/wpscan_console.txt" || true log_ok "wpscan → ${cms_dir}/wpscan_results.json" if has_tool jq && [[ -f "${cms_dir}/wpscan_results.json" ]]; then local wp_version wp_version=$(jq -r '.version.number // "unknown"' \ "${cms_dir}/wpscan_results.json" 2>/dev/null || echo "unknown") log_info "WordPress version: $wp_version" local plugin_vulns plugin_vulns=$(jq '[.plugins[]?.vulnerabilities[]?] | length' \ "${cms_dir}/wpscan_results.json" 2>/dev/null || echo 0) [[ "$plugin_vulns" -gt 0 ]] && add_finding "HIGH" "CMS" \ "WordPress: $plugin_vulns plugin vulnerability/vulnerabilities found" \ "wpscan identified vulnerable plugins." \ "See ${cms_dir}/wpscan_results.json" \ "Update all plugins to the latest version immediately." local theme_vulns theme_vulns=$(jq '[.themes[]?.vulnerabilities[]?] | length' \ "${cms_dir}/wpscan_results.json" 2>/dev/null || echo 0) [[ "$theme_vulns" -gt 0 ]] && add_finding "MEDIUM" "CMS" \ "WordPress: $theme_vulns theme vulnerability/vulnerabilities found" \ "" "See ${cms_dir}/wpscan_results.json" \ "Update all themes. Remove inactive/unused themes." local wp_users wp_users=$(jq '.users | length' \ "${cms_dir}/wpscan_results.json" 2>/dev/null || echo 0) [[ "$wp_users" -gt 0 ]] && add_finding "MEDIUM" "CMS" \ "WordPress: $wp_users user(s) enumerable via the API" \ "Username enumeration can aid brute-force attacks." \ "$(jq -r '.users | keys[]' "${cms_dir}/wpscan_results.json" 2>/dev/null | head -5 | tr '\n' ' ')" \ "Add 'remove_action(\"template_redirect\",\"redirect_canonical\")' and restrict REST API user endpoint." fi fi # WordPress-specific path probes declare -A WP_PATHS=( ["/xmlrpc.php"]="MEDIUM|WordPress XML-RPC enabled — brute-force amplification risk|Disable xmlrpc.php if not required." ["/wp-json/wp/v2/users"]="MEDIUM|REST API exposes user list|Restrict REST API user endpoint." ["/wp-content/debug.log"]="HIGH|WordPress debug.log accessible|Delete file; disable WP_DEBUG_LOG in production." ["/?author=1"]="LOW|Author enumeration via ?author= parameter|Add redirect to prevent author slug leakage." ["/wp-login.php"]="INFO|WordPress login page accessible|Enable two-factor authentication; limit login attempts." ) for wpath in "${!WP_PATHS[@]}"; do IFS='|' read -r sev desc rec <<< "${WP_PATHS[$wpath]}" local status status=$(_curl -o /dev/null -w "%{http_code}" "${TARGET}${wpath}" 2>/dev/null || echo "000") [[ "$status" =~ ^(200|301|302)$ ]] && add_finding "$sev" "CMS" "$desc" \ "" "${TARGET}${wpath} (HTTP $status)" "$rec" done # ── Drupal / Joomla ──────────────────────────────────────────────────────── elif [[ "$detected_cms" =~ ^(drupal|joomla)$ ]] && has_tool droopescan; then log_subsection "droopescan — $detected_cms" droopescan scan "$detected_cms" -u "$TARGET" \ --output-format json \ > "${cms_dir}/droopescan_${detected_cms}.json" 2>/dev/null || true log_ok "droopescan → ${cms_dir}/droopescan_${detected_cms}.json" fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 11 — CORS MISCONFIGURATION # ───────────────────────────────────────────────────────────────────────────── module_cors() { [[ $MOD_CORS -eq 0 ]] && return log_section "MODULE 11 — CORS MISCONFIGURATION" local cors_file="${OUTPUT_DIR}/misc/cors_tests.txt" : > "$cors_file" local test_origins=( "https://evil.com" "https://${TARGET_DOMAIN}.evil.com" "https://evil.${TARGET_DOMAIN}" "null" "https://attacker.io" "http://localhost" "https://not${TARGET_DOMAIN}" ) log_info "Testing ${#test_origins[@]} adversarial origins..." for origin in "${test_origins[@]}"; do local response response=$(_curl \ -H "Origin: ${origin}" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Authorization" \ -I "$TARGET" 2>/dev/null || true) local acao acac acam acao=$(echo "$response" | grep -i "access-control-allow-origin:" | head -1 | tr -d '\r' || true) acac=$(echo "$response" | grep -i "access-control-allow-credentials:" | head -1 | tr -d '\r' || true) acam=$(echo "$response" | grep -i "access-control-allow-methods:" | head -1 | tr -d '\r' || true) printf "Origin: %s\n ACAO: %s\n ACAC: %s\n\n" "$origin" "$acao" "$acac" >> "$cors_file" # Origin reflected back if echo "$acao" | grep -qi "$(echo "$origin" | sed 's|https://||')"; then local sev="MEDIUM" echo "$acac" | grep -qi "true" && sev="CRITICAL" add_finding "$sev" "CORS" "CORS: arbitrary origin reflected — Origin: $origin" \ "The server reflects the attacker's origin in Access-Control-Allow-Origin.$( echo "$acac" | grep -qi 'true' && echo ' ACAC: true permits credentialed cross-origin requests.')" \ "ACAO: $acao | ACAC: $acac" \ "Validate origins against an explicit allowlist. Never reflect the Origin header directly." fi # Wildcard if echo "$acao" | grep -qP ":\s*\*\s*$"; then add_finding "MEDIUM" "CORS" "CORS wildcard (*) in Access-Control-Allow-Origin" \ "Any origin can make cross-origin requests to this endpoint." "$acao" \ "Specify allowed origins explicitly. Wildcard is unsafe for authenticated endpoints." fi # null origin accepted if [[ "$origin" == "null" ]] && echo "$acao" | grep -qi "null"; then add_finding "HIGH" "CORS" "CORS: 'null' origin accepted" \ "'null' origin can be triggered from sandboxed iframes — allows cross-origin attacks." \ "$acao" "Never allow 'null' origin in CORS policy." fi done log_info "CORS tests → $cors_file" } # ───────────────────────────────────────────────────────────────────────────── # MODULE 12 — OPEN REDIRECT # ───────────────────────────────────────────────────────────────────────────── module_open_redirect() { [[ $MOD_REDIRECT -eq 0 ]] && return log_section "MODULE 12 — OPEN REDIRECT" local redirect_file="${OUTPUT_DIR}/misc/open_redirect.txt" : > "$redirect_file" local payloads=( "https://evil.com" "//evil.com" "///evil.com" "https:evil.com" "/\\evil.com" "https://evil.com%2F%2E%2E" "%2F%2Fevil.com" "https:///evil.com" "/%5Cevil.com" "https://evil.com@${TARGET_DOMAIN}" ) local params=( "next" "url" "redirect" "redirect_uri" "redirect_url" "return" "return_url" "returnUrl" "returnTo" "go" "goto" "dest" "destination" "target" "forward" "redir" "link" "to" "r" "out" "ref" "continue" "callback" "successUrl" "failureUrl" ) log_info "Testing ${#params[@]} params × ${#payloads[@]} payloads..." local found=0 for param in "${params[@]}"; do for payload in "${payloads[@]}"; do local enc; enc=$(url_encode "$payload") local test_url="${TARGET}?${param}=${enc}" local location location=$(_curl -sI -o /dev/null -w "%{redirect_url}" \ --max-redirs 0 "$test_url" 2>/dev/null || true) if echo "$location" | grep -qi "evil\.com"; then found=$((found + 1)) echo "[OPEN REDIRECT] param=${param} payload=${payload} → ${location}" | \ tee -a "$redirect_file" | tee -a "$LOG_FILE" add_finding "MEDIUM" "REDIRECT" "Open Redirect via parameter '$param'" \ "Unvalidated redirect to external domain." \ "${test_url:0:200} → $location" \ "Validate redirect targets against an allowlist of internal paths/domains." break # One finding per parameter fi done done if [[ $found -eq 0 ]]; then log_ok "No open redirects detected in common parameters" else log_warn "$found open redirect(s) confirmed — see $redirect_file" fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 13 — SERVER-SIDE REQUEST FORGERY (SSRF) # ───────────────────────────────────────────────────────────────────────────── module_ssrf() { [[ $MOD_SSRF -eq 0 ]] && return log_section "MODULE 13 — SERVER-SIDE REQUEST FORGERY (SSRF)" local ssrf_file="${OUTPUT_DIR}/misc/ssrf_tests.txt" : > "$ssrf_file" local payloads=( "http://127.0.0.1/" "http://127.0.0.1:22/" "http://127.0.0.1:8080/" "http://localhost/" "http://[::1]/" "http://0.0.0.0/" "http://2130706433/" # 127.0.0.1 decimal "http://0x7f000001/" # 127.0.0.1 hex "http://169.254.169.254/" # IMDS (AWS/Azure/GCP shared) "http://169.254.169.254/latest/meta-data/" "http://169.254.169.254/latest/meta-data/iam/security-credentials/" "http://metadata.google.internal/" "http://metadata.google.internal/computeMetadata/v1/" "http://169.254.169.254/metadata/v1/" "http://192.168.0.1/" "http://10.0.0.1/" ) local params=( "url" "uri" "src" "href" "path" "dest" "redirect" "image" "img" "load" "fetch" "proxy" "resource" "data" "continue" "file" "page" "feed" "host" "api" "callback" "endpoint" "target" "domain" ) # Cloud SSRF indicator patterns local indicators=("ami-id" "instance-id" "availability-zone" "instance-type" "iam" "security-credentials" "computeMetadata" "root:" "daemon:" "mysql:" "postgres:" # /etc/passwd fragments "127.0.0.1" "localhost" "internal") log_info "Testing ${#params[@]} params × ${#payloads[@]} SSRF payloads..." local found=0 for param in "${params[@]}"; do for payload in "${payloads[@]}"; do local enc; enc=$(url_encode "$payload") local test_url="${TARGET}?${param}=${enc}" local response response=$(_curl "$test_url" 2>/dev/null | head -30 || true) for indicator in "${indicators[@]}"; do if echo "$response" | grep -qi "$indicator"; then found=$((found + 1)) echo "[SSRF CONFIRMED] param=$param payload=$payload indicator=$indicator" | \ tee -a "$ssrf_file" | tee -a "$LOG_FILE" add_finding "CRITICAL" "SSRF" "SSRF confirmed via parameter '$param'" \ "Response contains internal/cloud-metadata content ('$indicator')." \ "${test_url:0:200}" \ "Validate and allowlist outbound URLs. Block IMDS access (e.g. IMDSv2 on AWS). Use egress firewalling." break 2 # Next param fi done done done if [[ $found -eq 0 ]]; then log_ok "No in-band SSRF detected" add_finding "INFO" "SSRF" "No in-band SSRF detected on common parameters" \ "Out-of-band (OOB) SSRF may still exist. Use Burp Collaborator or Interactsh for blind testing." "" "" fi log_info "SSRF tests → $ssrf_file" } # ───────────────────────────────────────────────────────────────────────────── # MODULE 14 — SUBDOMAIN TAKEOVER # ───────────────────────────────────────────────────────────────────────────── module_subtakeover() { [[ $MOD_SUBTAKEOVER -eq 0 ]] && return log_section "MODULE 14 — SUBDOMAIN TAKEOVER" local subs_file="${OUTPUT_DIR}/recon/subdomains.txt" local takeover_file="${OUTPUT_DIR}/misc/subtakeover.txt" : > "$takeover_file" if [[ ! -s "$subs_file" ]]; then log_info "No subdomain list available — run with MOD_RECON enabled first" return fi # ── subjack ──────────────────────────────────────────────────────────────── if has_tool subjack; then log_subsection "subjack" subjack \ -w "$subs_file" \ -t "$OPT_THREADS" \ -timeout "$OPT_TIMEOUT" \ -o "$takeover_file" \ -ssl 2>/dev/null || true if [[ -s "$takeover_file" ]]; then add_finding "HIGH" "SUBTAKEOVER" "Potential subdomain takeover(s) detected by subjack" \ "$(cat "$takeover_file")" "$takeover_file" \ "Remove stale DNS records pointing to deprovisioned external services." fi fi # ── Nuclei takeover templates ────────────────────────────────────────────── if has_tool nuclei && [[ $MOD_NUCLEI -eq 1 ]]; then log_subsection "Nuclei — takeover templates" timeout 300s nuclei \ -l "$subs_file" \ -tags takeover \ -o "${OUTPUT_DIR}/misc/nuclei_takeover.txt" \ -silent 2>/dev/null || true if [[ -s "${OUTPUT_DIR}/misc/nuclei_takeover.txt" ]]; then add_finding "HIGH" "SUBTAKEOVER" "Nuclei: subdomain takeover template matched" \ "$(cat "${OUTPUT_DIR}/misc/nuclei_takeover.txt")" \ "${OUTPUT_DIR}/misc/nuclei_takeover.txt" \ "Remove the stale DNS record immediately." fi fi # ── Manual CNAME → dangling-service check ────────────────────────────────── log_subsection "Dangling CNAME analysis" declare -A TAKEOVER_SERVICES=( ["amazonaws.com"]="AWS S3 / Elastic Beanstalk" ["elasticbeanstalk.com"]="AWS Elastic Beanstalk" ["cloudfront.net"]="AWS CloudFront" ["github.io"]="GitHub Pages" ["heroku.com"]="Heroku" ["herokussl.com"]="Heroku" ["zendesk.com"]="Zendesk" ["freshdesk.com"]="Freshdesk" ["helpscoutdocs.com"]="HelpScout" ["surge.sh"]="Surge.sh" ["netlify.app"]="Netlify" ["render.com"]="Render" ["azurewebsites.net"]="Azure Web Apps" ["azure-api.net"]="Azure API Management" ["myshopify.com"]="Shopify" ["squarespace.com"]="Squarespace" ["tumblr.com"]="Tumblr" ["ghost.io"]="Ghost" ["webflow.io"]="Webflow" ["fly.dev"]="Fly.io" ["pages.dev"]="Cloudflare Pages" ) local sub_count; sub_count=$(wc -l < "$subs_file") log_info "Analysing $sub_count subdomain(s) for dangling CNAMEs..." while IFS= read -r sub; do [[ -z "$sub" ]] && continue local cname cname=$(dig +short CNAME "$sub" 2>/dev/null | sed 's/\.$//' || true) [[ -z "$cname" ]] && continue for svc_domain in "${!TAKEOVER_SERVICES[@]}"; do if echo "$cname" | grep -qi "$svc_domain"; then local http_code http_code=$(_curl -o /dev/null -w "%{http_code}" "https://${sub}" 2>/dev/null || echo "000") if [[ "$http_code" =~ ^(404|410|403|503)$ ]]; then echo "[TAKEOVER RISK] ${sub} → CNAME ${cname} (${TAKEOVER_SERVICES[$svc_domain]}) HTTP ${http_code}" | \ tee -a "$takeover_file" | tee -a "$LOG_FILE" add_finding "HIGH" "SUBTAKEOVER" \ "Dangling CNAME: ${sub} → ${TAKEOVER_SERVICES[$svc_domain]}" \ "CNAME points to $cname but the resource returns HTTP $http_code — takeover may be possible." \ "${sub} → ${cname} (HTTP ${http_code})" \ "Remove the CNAME record for ${sub} or reclaim the resource at ${TAKEOVER_SERVICES[$svc_domain]}." fi break fi done done < "$subs_file" if [[ -s "$takeover_file" ]]; then log_warn "Takeover findings → $takeover_file" else log_ok "No subdomain takeover candidates found" fi } # ───────────────────────────────────────────────────────────────────────────── # MODULE 15 — NUCLEI TEMPLATE SCAN # ───────────────────────────────────────────────────────────────────────────── module_nuclei() { [[ $MOD_NUCLEI -eq 0 ]] && return require_tool nuclei || return log_section "MODULE 15 — NUCLEI TEMPLATE SCAN" local nuclei_dir="${OUTPUT_DIR}/vulns/nuclei" mkdir -p "$nuclei_dir" local severity_filter="medium,high,critical" [[ $OPT_AGGRESSIVE -eq 1 ]] && severity_filter="low,medium,high,critical" local nuclei_flags="-silent -no-interactsh" [[ -n "$OPT_PROXY" ]] && nuclei_flags="$nuclei_flags -proxy $OPT_PROXY" log_info "Running Nuclei (severity: $severity_filter)..." # shellcheck disable=SC2086 timeout 600s nuclei \ -u "$TARGET" \ -severity "$severity_filter" \ -o "${nuclei_dir}/nuclei_results.txt" \ -json -o "${nuclei_dir}/nuclei_results.json" \ $nuclei_flags \ 2>/dev/null || true if [[ -s "${nuclei_dir}/nuclei_results.txt" ]]; then local ncount; ncount=$(wc -l < "${nuclei_dir}/nuclei_results.txt") log_ok "Nuclei: $ncount finding(s) → ${nuclei_dir}/nuclei_results.txt" grep -oP '\[.*?\]\s+\[.*?\]\s+\[.*?\]\s+.*' "${nuclei_dir}/nuclei_results.txt" 2>/dev/null | \ head -50 | while IFS= read -r finding; do local sev="MEDIUM" echo "$finding" | grep -qi "\[critical\]" && sev="CRITICAL" echo "$finding" | grep -qi "\[high\]" && sev="HIGH" echo "$finding" | grep -qi "\[low\]" && sev="LOW" echo "$finding" | grep -qi "\[info\]" && sev="INFO" add_finding "$sev" "NUCLEI" "${finding:0:120}" \ "" "$finding" "Review Nuclei finding and apply corresponding remediation." done else log_ok "No Nuclei findings for severity level: $severity_filter" fi } # ───────────────────────────────────────────────────────────────────────────── # REPORT GENERATOR # ───────────────────────────────────────────────────────────────────────────── generate_reports() { log_section "GENERATING REPORTS" local do_all=1 [[ -n "$OPT_OUTPUT_ONLY" && "$OPT_OUTPUT_ONLY" != "all" ]] && do_all=0 local elapsed; elapsed=$(elapsed_secs) # ── JSON REPORT ───────────────────────────────────────────────────────────── if [[ $do_all -eq 1 || "$OPT_OUTPUT_ONLY" == "json" ]]; then { printf '{\n' printf ' "metadata": {\n' printf ' "tool": "%s",\n' "$TOOL_NAME" printf ' "version": "%s",\n' "$TOOL_VERSION" printf ' "author": "%s",\n' "$TOOL_AUTHOR" printf ' "target": "%s",\n' "$TARGET" printf ' "domain": "%s",\n' "$TARGET_DOMAIN" printf ' "ip": "%s",\n' "${TARGET_IP:-}" printf ' "start_time": "%s",\n' "$DATE_HUMAN" printf ' "duration_secs": %d\n' "$elapsed" printf ' },\n' printf ' "summary": {\n' printf ' "total": %d,\n' "$TOTAL_FINDINGS" printf ' "critical": %d,\n' "$COUNT_CRITICAL" printf ' "high": %d,\n' "$COUNT_HIGH" printf ' "medium": %d,\n' "$COUNT_MEDIUM" printf ' "low": %d,\n' "$COUNT_LOW" printf ' "info": %d\n' "$COUNT_INFO" printf ' },\n' printf ' "findings": [\n' local first=1 while IFS= read -r line; do [[ -z "$line" ]] && continue [[ $first -eq 0 ]] && printf ',\n' printf ' %s' "$line" first=0 done < "$FINDINGS_JSONL" printf '\n ]\n}\n' } > "$REPORT_JSON" log_ok "JSON report → $REPORT_JSON" fi # ── TXT REPORT ────────────────────────────────────────────────────────────── if [[ $do_all -eq 1 || "$OPT_OUTPUT_ONLY" == "txt" ]]; then { local sep="════════════════════════════════════════════════════════════════" echo "$sep" echo " ${TOOL_NAME^^} v${TOOL_VERSION} — Security Audit Report" echo "$sep" printf " Target : %s\n" "$TARGET" printf " IP : %s\n" "${TARGET_IP:-N/A}" printf " Date : %s\n" "$DATE_HUMAN" printf " Duration : %d seconds\n" "$elapsed" printf " Auditor : %s@%s\n" "$(whoami)" "$(hostname)" echo "$sep" echo "" echo " RISK SUMMARY" echo " ─────────────────────────────────" printf " %-10s %d\n" "CRITICAL" "$COUNT_CRITICAL" printf " %-10s %d\n" "HIGH" "$COUNT_HIGH" printf " %-10s %d\n" "MEDIUM" "$COUNT_MEDIUM" printf " %-10s %d\n" "LOW" "$COUNT_LOW" printf " %-10s %d\n" "INFO" "$COUNT_INFO" printf " %-10s %d\n" "TOTAL" "$TOTAL_FINDINGS" echo "" echo "$sep" echo " FINDINGS" echo "$sep" echo "" cat "$LOG_FILE" } > "$REPORT_TXT" log_ok "TXT report → $REPORT_TXT" fi # ── HTML REPORT ───────────────────────────────────────────────────────────── if [[ $do_all -eq 1 || "$OPT_OUTPUT_ONLY" == "html" ]]; then local risk_color="#3fb950" [[ $COUNT_LOW -gt 0 ]] && risk_color="#58a6ff" [[ $COUNT_MEDIUM -gt 0 ]] && risk_color="#d29922" [[ $COUNT_HIGH -gt 0 ]] && risk_color="#f0883e" [[ $COUNT_CRITICAL -gt 0 ]] && risk_color="#f85149" local total_non_info=$(( COUNT_CRITICAL + COUNT_HIGH + COUNT_MEDIUM + COUNT_LOW )) local bar_critical=0 bar_high=0 bar_medium=0 bar_low=0 if [[ $total_non_info -gt 0 ]]; then bar_critical=$(( COUNT_CRITICAL * 100 / total_non_info )) bar_high=$(( COUNT_HIGH * 100 / total_non_info )) bar_medium=$(( COUNT_MEDIUM * 100 / total_non_info )) bar_low=$(( COUNT_LOW * 100 / total_non_info )) fi cat > "$REPORT_HTML" << HTMLSTART <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${TOOL_NAME} — ${TARGET_DOMAIN} — Security Audit Report

🔐 ${TOOL_NAME} — Security Audit Report

${TARGET}  |  ${DATE_HUMAN}  |  ${TOOL_NAME} v${TOOL_VERSION} by ${TOOL_AUTHOR}
$( [[ $COUNT_CRITICAL -gt 0 ]] && echo "CRITICAL RISK" || \ [[ $COUNT_HIGH -gt 0 ]] && echo "HIGH RISK" || \ [[ $COUNT_MEDIUM -gt 0 ]] && echo "MEDIUM RISK" || \ [[ $COUNT_LOW -gt 0 ]] && echo "LOW RISK" || echo "INFO ONLY" )
${COUNT_CRITICAL}
Critical
${COUNT_HIGH}
High
${COUNT_MEDIUM}
Medium
${COUNT_LOW}
Low
${COUNT_INFO}
Info

Scan Metadata

Target ${TARGET} Domain ${TARGET_DOMAIN} IP ${TARGET_IP:-N/A} Date ${DATE_HUMAN} Duration ${elapsed}s Auditor $(whoami)@$(hostname) Mode $( [[ $OPT_AGGRESSIVE -eq 1 ]] && echo AGGRESSIVE || \ [[ $OPT_STEALTH -eq 1 ]] && echo STEALTH || echo NORMAL) Tool ${TOOL_NAME} v${TOOL_VERSION}

Modules Executed

Recon $( [[ $MOD_RECON -eq 1 ]] && echo "✔ enabled" || echo "— skipped") Port Scan $( [[ $MOD_PORTSCAN -eq 1 ]] && echo "✔ enabled" || echo "— skipped") Fingerprint $( [[ $MOD_FINGERPRINT -eq 1 ]] && echo "✔ enabled" || echo "— skipped") SSL/TLS $( [[ $MOD_SSL -eq 1 ]] && echo "✔ enabled" || echo "— skipped") Headers $( [[ $MOD_HEADERS -eq 1 ]] && echo "✔ enabled" || echo "— skipped") Dir Brute $( [[ $MOD_DIRBRUTE -eq 1 ]] && echo "✔ enabled" || echo "— skipped") SQLi / XSS $( [[ $MOD_SQLI -eq 1 ]] && echo "✔" || echo "—") / $( [[ $MOD_XSS -eq 1 ]] && echo "✔" || echo "—") CMS / Nuclei $( [[ $MOD_CMS -eq 1 ]] && echo "✔" || echo "—") / $( [[ $MOD_NUCLEI -eq 1 ]] && echo "✔" || echo "—")

Security Findings (${TOTAL_FINDINGS})

HTMLSTART # Generate table rows from JSONL local row=0 while IFS= read -r entry; do [[ -z "$entry" ]] && continue row=$((row + 1)) local sev mod title desc evid rec ts if has_tool jq; then sev=$(echo "$entry" | jq -r '.severity // "INFO"' 2>/dev/null) mod=$(echo "$entry" | jq -r '.module // ""' 2>/dev/null) title=$(echo "$entry"| jq -r '.title // ""' 2>/dev/null) desc=$(echo "$entry" | jq -r '.description // ""' 2>/dev/null) evid=$(echo "$entry" | jq -r '.evidence // ""' 2>/dev/null) rec=$(echo "$entry" | jq -r '.recommendation // ""' 2>/dev/null) ts=$(echo "$entry" | jq -r '.timestamp // ""' 2>/dev/null) else sev=$(echo "$entry" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('severity','INFO'))" 2>/dev/null || echo "INFO") mod=$(echo "$entry" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('module',''))" 2>/dev/null || echo "") title=$(echo "$entry"| python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('title',''))" 2>/dev/null || echo "") desc=$(echo "$entry" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('description',''))" 2>/dev/null || echo "") evid=$(echo "$entry" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('evidence',''))" 2>/dev/null || echo "") rec=$(echo "$entry" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('recommendation',''))" 2>/dev/null || echo "") ts=$(echo "$entry" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('timestamp',''))" 2>/dev/null || echo "") fi # HTML-escape special chars title=$(echo "$title" | sed 's//\>/g') desc=$(echo "$desc" | sed 's//\>/g') evid=$(echo "$evid" | sed 's//\>/g') rec=$(echo "$rec" | sed 's//\>/g') printf '\n' \ "$sev" "$(echo "$sev $mod $title $desc" | tr '[:upper:]' '[:lower:]')" >> "$REPORT_HTML" printf ' \n' "$row" >> "$REPORT_HTML" printf ' \n' "$sev" "$sev" >> "$REPORT_HTML" printf ' \n' "$mod" >> "$REPORT_HTML" { printf ' \n' printf ' \n' "$ts" printf '\n' } >> "$REPORT_HTML" done < "$FINDINGS_JSONL" cat >> "$REPORT_HTML" << HTMLEND
# Severity Module Finding Timestamp
%d%s%s\n' printf ' %s\n' "$title" [[ -n "$desc" ]] && printf '
%s\n' "$desc" [[ -n "$evid" ]] && printf '
%s
\n' "$evid" [[ -n "$rec" ]] && printf '
%s
\n' "$rec" printf '
%s
Generated by ${TOOL_NAME} v${TOOL_VERSION} — ${TOOL_URL} — For authorized security assessments only
HTMLEND log_ok "HTML report → $REPORT_HTML" fi } # ───────────────────────────────────────────────────────────────────────────── # FINAL SUMMARY # ───────────────────────────────────────────────────────────────────────────── print_summary() { log_section "AUDIT COMPLETE" local elapsed; elapsed=$(elapsed_secs) local risk="LOW" [[ $COUNT_MEDIUM -gt 0 ]] && risk="MEDIUM" [[ $COUNT_HIGH -gt 0 ]] && risk="HIGH" [[ $COUNT_CRITICAL -gt 0 ]] && risk="CRITICAL" local risk_color="$C_CYAN" case "$risk" in CRITICAL) risk_color="$C_RED" ;; HIGH) risk_color="$C_MAGENTA" ;; MEDIUM) risk_color="$C_YELLOW" ;; esac echo "" echo -e "${C_BOLD}" printf " ┌───────────────────────────────────────────────────┐\n" printf " │ %-49s│\n" "TARGET : $TARGET" printf " │ %-49s│\n" "IP : ${TARGET_IP:-N/A}" printf " │ ${risk_color}%-49s${C_RESET}${C_BOLD}│\n" "RISK : $risk" printf " ├───────────────────────────────────────────────────┤\n" printf " │ ${C_RED}%-49s${C_RESET}${C_BOLD}│\n" "CRITICAL : $COUNT_CRITICAL" printf " │ ${C_MAGENTA}%-49s${C_RESET}${C_BOLD}│\n" "HIGH : $COUNT_HIGH" printf " │ ${C_YELLOW}%-49s${C_RESET}${C_BOLD}│\n" "MEDIUM : $COUNT_MEDIUM" printf " │ ${C_CYAN}%-49s${C_RESET}${C_BOLD}│\n" "LOW : $COUNT_LOW" printf " │ ${C_DIM}%-49s${C_RESET}${C_BOLD}│\n" "INFO : $COUNT_INFO" printf " ├───────────────────────────────────────────────────┤\n" printf " │ %-49s│\n" "TOTAL : $TOTAL_FINDINGS finding(s)" printf " │ %-49s│\n" "DURATION : ${elapsed}s" printf " └───────────────────────────────────────────────────┘\n" echo -e "${C_RESET}" echo "" log_ok "Output directory : $OUTPUT_DIR" log_ok "HTML report : $REPORT_HTML" log_ok "JSON report : $REPORT_JSON" log_ok "TXT report : $REPORT_TXT" log_ok "Audit log : $LOG_FILE" echo "" } # ───────────────────────────────────────────────────────────────────────────── # SIGNAL HANDLING & CLEANUP # ───────────────────────────────────────────────────────────────────────────── handle_interrupt() { echo "" log_warn "Audit interrupted by user (SIGINT/SIGTERM)" [[ -f "$LOG_FILE" ]] && echo "[INTERRUPTED] PID=$AUDIT_PID" >> "$LOG_FILE" # Try to generate partial reports generate_reports 2>/dev/null || true print_summary 2>/dev/null || true exit 130 } trap 'handle_interrupt' INT TERM # ───────────────────────────────────────────────────────────────────────────── # ENTRY POINT # ───────────────────────────────────────────────────────────────────────────── main() { AUDIT_START_TIME=$(date +%s) parse_args "$@" print_banner # Legal notice echo -e "${C_RED}${C_BOLD}" printf " ╔══════════════════════════════════════════════════════╗\n" printf " ║ ⚠ LEGAL NOTICE ║\n" printf " ║ This tool may only be used against systems you ║\n" printf " ║ own or have explicit written authorisation to ║\n" printf " ║ test. Unauthorised use is illegal. ║\n" printf " ╚══════════════════════════════════════════════════════╝\n" echo -e "${C_RESET}" read -r -t 5 -p " Press ENTER to continue (auto-continues in 5s)..." 2>/dev/null || true echo "" echo "" log_info "Starting ${TOOL_NAME} v${TOOL_VERSION} | PID: ${AUDIT_PID}" module_check_deps module_target_info module_recon module_portscan module_fingerprint module_ssl module_headers module_dirbrute module_nikto module_sqli module_xss module_cms module_cors module_open_redirect module_ssrf module_subtakeover module_nuclei generate_reports print_summary } main "$@"