commit 822fa07d451ceca8898b76b431633a33a005c842 Author: davidalvarezp Date: Mon Mar 23 11:10:48 2026 +0100 v1.0.1 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..fd1ac07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,73 @@ +--- +name: Bug Report +about: Report a bug or unexpected behaviour in websec-audit +title: "[BUG] " +labels: bug +assignees: davidalvarezp +--- + +## Description + + + +## Steps to Reproduce + +```bash +# Exact command you ran (redact the target) +./websec-audit.sh -t https://REDACTED --option +``` + +1. Run the above command +2. See error / unexpected behaviour in module: **[MODULE NAME]** + +## Expected Behaviour + + + +## Actual Behaviour + + + +
+Log output (redact sensitive data) + +``` +paste relevant log lines here +``` + +
+ +## Environment + +| Item | Details | +|------|---------| +| websec-audit version | `./websec-audit.sh -V` | +| OS / Distro | e.g. Debian 12, Ubuntu 22.04, Kali 2024.1 | +| Bash version | `bash --version` | +| Affected tool | e.g. nmap, sqlmap, gobuster | +| Tool version | e.g. `nmap --version` | + +## Module + +- [ ] Recon +- [ ] Port Scan +- [ ] Fingerprint +- [ ] SSL/TLS +- [ ] HTTP Headers +- [ ] Dir/File Enum +- [ ] Nikto +- [ ] SQLi +- [ ] XSS +- [ ] CMS +- [ ] CORS +- [ ] Open Redirect +- [ ] SSRF +- [ ] Subdomain Takeover +- [ ] Nuclei +- [ ] Reporting +- [ ] Install / Dependencies +- [ ] Other + +## Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4d0ab96 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,42 @@ +--- +name: Feature Request +about: Suggest a new module, option, or improvement +title: "[FEAT] " +labels: enhancement +assignees: davidalvarezp +--- + +## Summary + + + +## Problem / Motivation + + + +## Proposed Solution + + + +```bash +# Example usage +./websec-audit.sh -t https://target.com --new-option +``` + +## Tools / Techniques Involved + + + +## Alternatives Considered + + + +## Acceptance Criteria + + +- [ ] ... +- [ ] ... + +## Additional Context + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4ba15ae --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,68 @@ +## Summary + + + +## Type of Change + +- [ ] πŸ› Bug fix +- [ ] ✨ New feature / module +- [ ] πŸ“ Documentation update +- [ ] ♻️ Refactor (no functional changes) +- [ ] ⚑ Performance improvement +- [ ] πŸ”§ Chore / maintenance + +## Related Issue(s) + + + +## Changes Made + + +- +- +- + +## Module(s) Affected + +- [ ] Recon +- [ ] Port Scan +- [ ] Fingerprint +- [ ] SSL/TLS +- [ ] HTTP Headers +- [ ] Dir/File Enum +- [ ] Nikto +- [ ] SQLi +- [ ] XSS +- [ ] CMS +- [ ] CORS +- [ ] Open Redirect +- [ ] SSRF +- [ ] Subdomain Takeover +- [ ] Nuclei +- [ ] Reporting / Output +- [ ] Install script +- [ ] Documentation + +## Testing + + + +```bash +# Commands used to test +./websec-audit.sh -t https://REDACTED --your-new-option +``` + +- [ ] Tested on Debian / Ubuntu +- [ ] Tested with `--aggressive` mode +- [ ] Tested with `--stealth` mode +- [ ] Tested with `--skip-` to ensure skip works +- [ ] `shellcheck -S warning websec-audit.sh` passes with zero warnings + +## Checklist + +- [ ] Code follows the style guidelines in [CONTRIBUTING.md](../CONTRIBUTING.md) +- [ ] Self-review completed +- [ ] New module added to `README.md` module table +- [ ] `CHANGELOG.md` updated under `[Unreleased]` +- [ ] No hardcoded credentials, IPs, or sensitive data +- [ ] All findings go through `add_finding()` β€” not written directly to files diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2accbad --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI β€” Shell Quality Check + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install ShellCheck + run: sudo apt-get install -y shellcheck + + - name: Run ShellCheck on websec-audit.sh + run: | + shellcheck -S warning \ + --shell=bash \ + --exclude=SC2086 \ + websec-audit.sh + # SC2086: Double-quote to prevent globbing β€” intentionally disabled + # for nmap/sqlmap/gobuster flag passing which requires word splitting + + - name: Run ShellCheck on install.sh + run: | + shellcheck -S warning \ + --shell=bash \ + install.sh + + syntax-check: + name: Bash Syntax Check + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check bash syntax β€” websec-audit.sh + run: bash -n websec-audit.sh && echo "Syntax OK" + + - name: Check bash syntax β€” install.sh + run: bash -n install.sh && echo "Syntax OK" + + permissions-check: + name: File Permissions + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check scripts are executable + run: | + for script in websec-audit.sh install.sh; do + if [[ ! -x "$script" ]]; then + echo "ERROR: $script is not executable" + exit 1 + fi + echo "OK: $script is executable" + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e53c023 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# ─── websec-audit .gitignore ────────────────────────────────────────────────── + +# Scan results (never commit target data) +results_*/ +/results/ +*.results/ + +# Log files +*.log +logs/ + +# Reports generated by the tool +reports/ +*.html +*.json +!package.json +!composer.json + +# Raw finding dumps +findings.jsonl +findings_raw.jsonl + +# Backup files +*.bak +*.backup +*.orig +*.old +*.tmp +*.swp +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini + +# Editor files +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace +.project +.classpath + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg +.venv/ +venv/ +env/ + +# Ruby gems +*.gem +.bundle/ +vendor/bundle/ + +# Go +bin/ +pkg/ + +# Archives +*.zip +*.tar.gz +*.tar.bz2 +*.tgz + +# Temp directories +tmp/ +temp/ +.tmp/ +cache/ + +# CI/CD secrets +.env +.env.* +!.env.example +secrets.txt +credentials + +# Wordlists (too large for git; users install via apt/seclists) +wordlists/ +*.wordlist +*.dict +rockyou.txt + +# nmap output +*.nmap +*.gnmap +*.xml.bak + +# Output from specific tools +sqlmap_output/ +nikto_reports/ +testssl_output/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..091fffd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +All notable changes to **websec-audit** are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [1.0.1] β€” 2026-03-23 + +### Added +- **Module 15 β€” Nuclei** template scan integration (severity-filtered) +- **Module 14 β€” Subdomain Takeover**: `subjack` + Nuclei + manual CNAME analysis for 20+ services +- **Module 13 β€” SSRF**: cloud IMDS probing (AWS, GCP, Azure), decimal/hex IP encoding +- **Module 12 β€” Open Redirect**: 20 parameters Γ— 10 redirect payloads +- **Module 11 β€” CORS**: null origin, credentialed cross-origin, wildcard detection +- **Module 10 β€” CMS**: WordPress REST API user enumeration, debug.log, xmlrpc.php checks +- **Module 09 β€” XSS**: dalfox integration + 8 reflected XSS payloads Γ— 15 parameters +- **Module 08 β€” SQLi**: sqlmap with forms crawl in aggressive mode, tamper scripts +- **Module 07 β€” Nikto**: severity-based finding classification +- **Module 06 β€” Dir Enum**: 40 sensitive path probes (`.git`, `.env`, AWS credentials, Dockerfiles, etc.) +- **Module 05 β€” HTTP Headers**: CSP audit (unsafe-inline, wildcards), SameSite=None+Secure, Cache-Control +- **Module 04 β€” SSL/TLS**: CAA records, HSTS preload, cert expiry thresholds (14/30/90 days) +- **Module 03 β€” Fingerprint**: WAF detection via wafw00f, version-leaking header enumeration +- **Module 02 β€” Port Scan**: risk-based analysis for 20+ dangerous ports +- **Module 01 β€” Recon**: SPF `+all` detection, DMARC `p=none` warning, 700+ Google Dorks +- Interactive HTML report with severity filter, live search, and risk bar +- JSON report with full metadata envelope +- `--format json|html|txt|all` flag +- `--no-banner` flag for scripting/CI use +- `--version` / `-V` flag +- Graceful interrupt handling β€” generates partial reports on SIGINT/SIGTERM +- Signal trap generates partial report on Ctrl-C +- `findings.jsonl` raw log for programmatic processing +- Aggressive mode: nmap `-A -O --script=vuln,auth`, sqlmap level 5 + tamper, deep DOM XSS +- Stealth mode: nmap `-sS -T2 -f`, sqlmap delay, randomised ordering +- Tool availability graceful degradation β€” all modules have fallbacks + +### Changed +- Fully rewritten in English for international audience +- Modular `module_*()` function architecture β€” each module independently skippable +- `add_finding()` now emits structured JSONL with id, severity, module, title, description, evidence, recommendation, and RFC3339 timestamp +- `_curl()` wrapper with retry, consistent UA, proxy, and timeout +- Output directory structure reorganised into `recon/`, `portscan/`, `ssl/`, `headers/`, `dirs/`, `vulns/`, `cms/`, `misc/`, `reports/`, `logs/` +- HTML report: dark theme, sticky table headers, responsive grid + +### Fixed +- SPF record detection now handles multi-TXT records correctly +- HSTS max-age check handles missing header without error +- nmap output parsing compatible with both GNU and BSD grep +- JSON report correctly handles multi-line evidence strings + +--- + +## [1.0.0] β€” 2026-01-13 + +### Added +- Initial release +- Core modules: recon, port scan, SSL, headers, dir brute-force, nikto, sqlmap +- Basic HTML report +- Spanish-language interface + +--- + +[1.0.1]: https://github.com/davidalvarezp/websec-audit/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/davidalvarezp/websec-audit/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9e987df --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,193 @@ +# Contributing to websec-audit + +Thank you for your interest in contributing to **websec-audit**! +All contributions are welcome β€” bug reports, feature requests, documentation improvements, new modules, and code fixes. + +--- + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How to Contribute](#how-to-contribute) +- [Development Guidelines](#development-guidelines) +- [Adding a New Module](#adding-a-new-module) +- [Commit Conventions](#commit-conventions) +- [Pull Request Process](#pull-request-process) + +--- + +## Code of Conduct + +By participating in this project, you agree to: + +- Be respectful and constructive in all communications +- Only contribute code intended for **authorised security testing** +- Not submit payloads, exploits, or code designed to harm systems without consent + +--- + +## How to Contribute + +### Reporting Bugs + +1. Search [existing issues](https://github.com/davidalvarezp/websec-audit/issues) first +2. Open a new issue using the **Bug Report** template +3. Include: OS version, tool versions, reproduction steps, expected vs actual behaviour + +### Requesting Features + +1. Open an issue using the **Feature Request** template +2. Describe the use case and expected output clearly + +### Code Contributions + +1. **Fork** the repository +2. **Clone** your fork: `git clone https://github.com/YOUR_USER/websec-audit.git` +3. Create a **feature branch**: `git checkout -b feature/your-feature-name` +4. Make your changes following the guidelines below +5. **Test** your changes +6. **Commit** using conventional commits (see below) +7. **Push**: `git push origin feature/your-feature-name` +8. Open a **Pull Request** against `main` + +--- + +## Development Guidelines + +### Shell Style + +- Target **bash 5.0+** β€” no POSIX-only constraints, but avoid bash 5.1+ exclusive syntax +- Use `set -euo pipefail` and `IFS=$'\n\t'` at the top of every script +- Quote all variable expansions: `"$var"` not `$var` +- Use `[[ ]]` for conditions, not `[ ]` +- Prefer `local` variables inside functions +- Run `shellcheck` on your changes before submitting: + ```bash + shellcheck -S warning websec-audit.sh + ``` + +### Naming Conventions + +| Item | Convention | Example | +|------|-----------|---------| +| Functions | `snake_case` | `module_sqli()` | +| Constants | `UPPER_SNAKE` | `readonly TOOL_VERSION` | +| Global vars | `UPPER_SNAKE` | `OPT_AGGRESSIVE` | +| Local vars | `lower_snake` | `local scan_target` | +| Module flags | `MOD_NAME` | `MOD_SQLI` | +| Option flags | `OPT_NAME` | `OPT_THREADS` | + +### Error Handling + +- Never silently ignore errors β€” use `|| true` only when failure is genuinely acceptable +- Use `log_warn` when a tool is missing; the script must continue +- Use `log_error` + `exit 1` only for unrecoverable conditions (missing required tool, invalid target) +- All findings must go through `add_finding()` β€” never write directly to reports + +### Performance + +- Respect `OPT_THREADS` and `OPT_TIMEOUT` in all external tool calls +- Use `timeout` around all network operations +- Avoid unnecessary subshells in tight loops + +--- + +## Adding a New Module + +1. Add a toggle variable in the global section: + ```bash + MOD_MYMODULE=1 + ``` + +2. Add a `--skip-mymodule` argument in `parse_args()`: + ```bash + --skip-mymodule) MOD_MYMODULE=0; shift ;; + ``` + +3. Add the `--skip-mymodule` entry to the help text in `print_usage()`. + +4. Write the module function following this template: + ```bash + # ───────────────────────────────────────────────────────────────────────────── + # MODULE XX β€” YOUR MODULE NAME + # ───────────────────────────────────────────────────────────────────────────── + module_mymodule() { + [[ $MOD_MYMODULE -eq 0 ]] && return + log_section "MODULE XX β€” YOUR MODULE NAME" + + local out_dir="${OUTPUT_DIR}/misc" + + # Check for optional tools + if ! has_tool mytool; then + log_warn "mytool not available β€” skipping related checks" + fi + + # ... your logic ... + + # Register findings + add_finding "HIGH" "MYMODULE" "Short finding title" \ + "Detailed description of what was found." \ + "evidence string" \ + "Remediation recommendation." + + log_info "Module results β†’ $out_dir" + } + ``` + +5. Call the module in `main()` after `module_cms` and before `generate_reports`. + +6. Add the module to the table in `README.md`. + +7. Add an entry to `CHANGELOG.md` under `[Unreleased]`. + +--- + +## Commit Conventions + +We use [Conventional Commits](https://www.conventionalcommits.org/): + +``` +(): + +[optional body] + +[optional footer] +``` + +### Types + +| Type | When to use | +|------|------------| +| `feat` | New feature or module | +| `fix` | Bug fix | +| `docs` | Documentation only | +| `refactor` | Code change that neither fixes a bug nor adds a feature | +| `perf` | Performance improvement | +| `test` | Adding or updating tests | +| `chore` | Build process, dependency updates | + +### Examples + +```bash +feat(module): add GraphQL introspection detection +fix(ssl): handle certificates with no expiry date gracefully +docs(readme): add Kali Linux installation instructions +refactor(headers): extract cookie analysis into helper function +``` + +--- + +## Pull Request Process + +1. **One PR per feature/fix** β€” keep changes focused and reviewable +2. **Update documentation** β€” README, CHANGELOG, and inline comments +3. **Describe your PR** β€” fill in the PR template completely +4. **Pass shellcheck** β€” zero warnings on `websec-audit.sh` and `install.sh` +5. **Test manually** β€” run the affected module(s) against a test target (DVWA, HackTheBox, your own lab) + +PRs will be reviewed within 5 business days. Feedback will be given constructively. +Once approved, a maintainer will merge it into `main`. + +--- + +Thank you for helping make **websec-audit** better! πŸ” diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d3d8e04 --- /dev/null +++ b/LICENSE @@ -0,0 +1,44 @@ +MIT License + +Copyright (c) 2026 davidalvarezp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +ADDITIONAL NOTICE + +This software is designed for legitimate security research and authorized +penetration testing only. The author, davidalvarezp, is not responsible +for any misuse or damage caused by this program. + +Users are solely responsible for ensuring they have proper written +authorization before running this tool against any system. Unauthorized use +against computer systems is illegal and may result in civil and/or criminal +prosecution under applicable law, including but not limited to: + +- Directive 2013/40/EU on attacks against information systems β€” European Union +- Ley OrgΓ‘nica 10/1995 (CΓ³digo Penal, arts. 197-200) β€” Spain +- Computer Fraud and Abuse Act (CFAA) β€” United States +- Computer Misuse Act (CMA) β€” United Kingdom + + +By using this software, you agree to comply with all applicable laws and +regulations, and you confirm that you have obtained all necessary permissions +from the system owner(s) before conducting any security assessment. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c42f65 --- /dev/null +++ b/README.md @@ -0,0 +1,355 @@ +
+ +# πŸ” websec-audit + +**Professional Web Security Audit Framework** + +[![Version](https://img.shields.io/badge/version-1.0.1-blue?style=flat-square)](https://github.com/davidalvarezp/websec-audit/releases) +[![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE) +[![Bash](https://img.shields.io/badge/bash-5.0%2B-orange?style=flat-square)](https://www.gnu.org/software/bash/) +[![Platform](https://img.shields.io/badge/platform-Debian%20%7C%20Ubuntu%20%7C%20Kali-lightgrey?style=flat-square)](https://github.com/davidalvarezp/websec-audit) +[![Maintenance](https://img.shields.io/badge/maintained-yes-brightgreen?style=flat-square)](https://github.com/davidalvarezp/websec-audit/commits/main) + +A modular, extensible Bash framework for **comprehensive web application security assessments**. +Automates 15+ attack surface modules, generates structured logs, and produces professional reports in HTML, JSON and TXT. + +[Features](#-features) Β· [Installation](#-installation) Β· [Usage](#-usage) Β· [Modules](#-modules) Β· [Output](#-output) Β· [Contributing](#-contributing) + +--- + +![websec-audit demo](https://raw.githubusercontent.com/davidalvarezp/websec-audit/main/docs/demo.png) + +
+ +--- + +## ⚠️ Legal Disclaimer + +> **This tool is intended exclusively for authorised security assessments.** +> Only run it against systems you own or have **explicit written permission** to test. +> Unauthorised use against third-party systems is illegal and may result in criminal prosecution. +> The author assumes **no liability** whatsoever for misuse of this software. + +--- + +## ✨ Features + +- **15+ security modules** β€” recon, port scanning, SSL/TLS, headers, SQLi, XSS, CMS, CORS, SSRF, subdomain takeover, and more +- **Modular architecture** β€” enable or disable any module independently via `--skip-` +- **Three scan modes** β€” Normal, Aggressive (`--aggressive`), Stealth (`--stealth`) +- **Professional reporting** β€” interactive HTML dashboard, structured JSON, and plain-text log +- **Tool-agnostic** β€” gracefully degrades to fallbacks when optional tools are absent +- **Smart finding engine** β€” findings stored as JSONL with severity, module, evidence, and remediation +- **Proxy support** β€” route all traffic through Burp Suite or any HTTP proxy +- **CVSS-aligned severities** β€” CRITICAL / HIGH / MEDIUM / LOW / INFO +- **Zero external dependencies** β€” core scan works with only `curl` and `nmap` + +--- + +## πŸ“¦ Installation + +### Quick Install (recommended) + +```bash +git clone https://github.com/davidalvarezp/websec-audit.git +cd websec-audit +chmod +x install.sh websec-audit.sh +sudo ./install.sh +``` + +### Manual (Debian/Ubuntu) + +```bash +# Required +sudo apt-get install -y curl nmap + +# Recommended +sudo apt-get install -y nikto sqlmap gobuster whatweb wafw00f sslscan \ + python3 python3-pip jq ruby dirb dnsutils whois wordlists + +# Optional (improves coverage significantly) +pip3 install droopescan +gem install wpscan --no-document +git clone --depth 1 https://github.com/drwetter/testssl.sh.git /opt/testssl.sh +sudo ln -s /opt/testssl.sh/testssl.sh /usr/local/bin/testssl.sh +``` + +### Kali Linux + +Most tools are pre-installed. Run: +```bash +sudo apt-get install -y gobuster dalfox subjack nuclei subfinder +./install.sh # handles remaining gaps +``` + +--- + +## πŸš€ Usage + +### Basic + +```bash +./websec-audit.sh -t https://target.com +``` + +### Aggressive (deeper, noisier) + +```bash +./websec-audit.sh -t https://target.com --aggressive -T 20 +``` + +### Stealth (slower, lower detection footprint) + +```bash +./websec-audit.sh -t https://target.com --stealth +``` + +### Through a proxy (Burp Suite) + +```bash +./websec-audit.sh -t https://target.com --proxy http://127.0.0.1:8080 +``` + +### Custom output directory and JSON-only report + +```bash +./websec-audit.sh -t https://target.com -o /tmp/audit --format json +``` + +### Skip specific modules + +```bash +./websec-audit.sh -t https://target.com --skip-nikto --skip-sqli --skip-cms -v +``` + +### Full port scan with aggressive mode + +```bash +./websec-audit.sh -t https://target.com --ports full --aggressive --depth 5 +``` + +--- + +## πŸ“‹ Full Options Reference + +``` +REQUIRED + -t, --target Target URL or IP address + +OUTPUT + -o, --output Output directory + --format json | html | txt | all (default: all) + +SCAN OPTIONS + -T, --threads Concurrent threads (default: 10) + -p, --ports top-100 | top-1000 | full (default: top-1000) + --timeout Connection timeout (default: 10) + --depth Crawl depth (default: 3) + --proxy HTTP/HTTPS proxy + --aggressive Aggressive mode + --stealth Stealth mode + +MODULE CONTROL (--skip-) + --skip-recon WHOIS, DNS, subdomain enumeration + --skip-portscan nmap port scanning + --skip-fingerprint WhatWeb, WAF detection + --skip-ssl SSL/TLS analysis + --skip-headers HTTP security headers + --skip-dirbrute Directory/file brute-forcing + --skip-nikto Nikto web scanner + --skip-sqli SQL injection (sqlmap) + --skip-xss XSS (dalfox + manual) + --skip-cms CMS detection & scanning + --skip-cors CORS misconfiguration + --skip-redirect Open redirect + --skip-ssrf SSRF + --skip-subtakeover Subdomain takeover + --skip-nuclei Nuclei template scan + +WORDLISTS + --wl-dirs-small Small wordlist for directory brute-force + --wl-dirs-big Large wordlist for directory brute-force + --wl-dns DNS subdomain wordlist + +MISC + -v, --verbose Verbose output + --no-color Disable ANSI colors + --no-banner Suppress banner + -V, --version Version info + -h, --help Help +``` + +--- + +## πŸ” Modules + +| # | Module | Description | Key Tools | +|---|--------|-------------|-----------| +| 00 | **Target Info** | Resolve IP, initialise directories | `dig`, `host` | +| 01 | **Reconnaissance** | WHOIS, DNS records, AXFR, subdomain enum, SPF/DMARC, dorks | `whois`, `dig`, `subfinder`, `amass`, `dnsrecon` | +| 02 | **Port Scanning** | Full service/version detection, risk-based port analysis | `nmap` | +| 03 | **Fingerprinting** | Technology stack, WAF detection, version leakage | `whatweb`, `wafw00f` | +| 04 | **SSL/TLS** | Protocol support, ciphers, cert expiry, HSTS, CAA | `testssl.sh`, `sslscan`, `openssl` | +| 05 | **HTTP Headers** | 7+ security headers, cookie flags, CSP audit, HTTPS redirect | `curl` | +| 06 | **Dir & File Enum** | Directory brute-force + 40 sensitive path probes | `gobuster`, `ffuf`, `dirb` | +| 07 | **Nikto** | Web server misconfigurations, known CVEs | `nikto` | +| 08 | **SQL Injection** | Automated SQLi detection and exploitation | `sqlmap` | +| 09 | **XSS** | Reflected XSS probe across common parameters + DOM XSS | `dalfox`, `curl` | +| 10 | **CMS Scanning** | WordPress, Drupal, Joomla, Magento detection and scanning | `wpscan`, `droopescan` | +| 11 | **CORS** | Misconfigured CORS, wildcard origins, credentialed CORS | `curl` | +| 12 | **Open Redirect** | 20+ params Γ— 10 redirect payloads | `curl` | +| 13 | **SSRF** | Cloud IMDS (AWS/GCP/Azure), internal IP probing | `curl` | +| 14 | **Subdomain Takeover** | Dangling CNAME detection for 20+ services | `subjack`, `nuclei`, `dig` | +| 15 | **Nuclei** | Community CVE/misconfiguration templates | `nuclei` | + +--- + +## πŸ“ Output Structure + +``` +results_target_YYYYMMDD_HHMMSS/ +β”œβ”€β”€ logs/ +β”‚ β”œβ”€β”€ audit_YYYYMMDD_HHMMSS.log # Full timestamped audit log +β”‚ └── findings.jsonl # One JSON object per finding +β”œβ”€β”€ recon/ +β”‚ β”œβ”€β”€ whois.txt +β”‚ β”œβ”€β”€ dns_records.txt +β”‚ β”œβ”€β”€ subdomains.txt +β”‚ β”œβ”€β”€ axfr.txt +β”‚ β”œβ”€β”€ whatweb.json +β”‚ β”œβ”€β”€ waf_detection.txt +β”‚ └── google_dorks.txt +β”œβ”€β”€ portscan/ +β”‚ β”œβ”€β”€ nmap.txt +β”‚ β”œβ”€β”€ nmap.xml +β”‚ └── nmap.gnmap +β”œβ”€β”€ ssl/ +β”‚ β”œβ”€β”€ testssl.json +β”‚ └── testssl.log +β”œβ”€β”€ headers/ +β”‚ └── response_headers.txt +β”œβ”€β”€ dirs/ +β”‚ β”œβ”€β”€ gobuster_dirs.txt +β”‚ └── sensitive_paths_found.txt +β”œβ”€β”€ vulns/ +β”‚ β”œβ”€β”€ sqlmap/ +β”‚ β”œβ”€β”€ xss/ +β”‚ └── nuclei/ +β”œβ”€β”€ cms/ +β”‚ β”œβ”€β”€ wpscan_results.json +β”‚ └── droopescan_*.json +β”œβ”€β”€ misc/ +β”‚ β”œβ”€β”€ cors_tests.txt +β”‚ β”œβ”€β”€ open_redirect.txt +β”‚ β”œβ”€β”€ ssrf_tests.txt +β”‚ └── subtakeover.txt +└── reports/ + β”œβ”€β”€ report_YYYYMMDD_HHMMSS.html # Interactive dashboard + β”œβ”€β”€ report_YYYYMMDD_HHMMSS.json # Structured JSON + └── report_YYYYMMDD_HHMMSS.txt # Plain text +``` + +--- + +## πŸ“Š Report Examples + +### HTML Report +- Interactive severity filter (Critical / High / Medium / Low / Info) +- Live search across all findings +- Risk bar and scan metadata panel +- Evidence and remediation per finding +- Dark theme, responsive layout + +### JSON Report +```json +{ + "metadata": { + "tool": "websec-audit", + "version": "1.0.1", + "target": "https://davidalvarezp.com", + "start_time": "2026-01-01 12:00:00", + "duration_secs": 342 + }, + "summary": { + "total": 18, + "critical": 2, + "high": 5, + "medium": 6, + "low": 3, + "info": 2 + }, + "findings": [ + { + "id": 1, + "severity": "CRITICAL", + "module": "RECON", + "title": "DNS Zone Transfer (AXFR) is permitted", + "description": "Name server ns1.davidalvarezp.com allows AXFR β€” full DNS zone disclosed.", + "evidence": "...", + "recommendation": "Restrict AXFR to authorised secondary name servers only.", + "timestamp": "2026-01-01T12:00:12Z" + } + ] +} +``` + +--- + +## πŸ”§ Requirements + +### Required +| Tool | Purpose | Install | +|------|---------|---------| +| `bash` 5.0+ | Shell interpreter | `apt-get install bash` | +| `curl` | HTTP requests | `apt-get install curl` | +| `nmap` | Port scanning | `apt-get install nmap` | + +### Recommended (significantly improves coverage) +| Tool | Module | Install | +|------|--------|---------| +| `nikto` | Web vuln scan | `apt-get install nikto` | +| `sqlmap` | SQL injection | `apt-get install sqlmap` | +| `gobuster` / `ffuf` | Dir brute-force | `apt-get install gobuster` | +| `whatweb` | Fingerprinting | `apt-get install whatweb` | +| `wafw00f` | WAF detection | `apt-get install wafw00f` | +| `sslscan` / `testssl.sh` | SSL/TLS | `apt-get install sslscan` | +| `wpscan` | WordPress | `gem install wpscan` | +| `dalfox` | XSS | [GitHub releases](https://github.com/hahwul/dalfox) | +| `nuclei` | CVE templates | [GitHub releases](https://github.com/projectdiscovery/nuclei) | +| `subfinder` | Subdomain enum | [GitHub releases](https://github.com/projectdiscovery/subfinder) | +| `jq` | JSON parsing | `apt-get install jq` | +| `python3` | Utilities | `apt-get install python3` | + +--- + +## 🀝 Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request. + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/new-module` +3. Commit your changes: `git commit -m 'feat: add new-module'` +4. Push to your branch: `git push origin feature/new-module` +5. Open a Pull Request + +--- + +## πŸ“ Changelog + +See [CHANGELOG.md](CHANGELOG.md) for the full version history. + +--- + +## πŸ“œ License + +This project is licensed under the **MIT License** β€” see [LICENSE](LICENSE) for details. + +--- + +
+ +Made with ❀️ by [davidalvarezp](https://davidalvarezp.com) + +⭐ **Star this repo** if you find it useful! + +
diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..ea92191 --- /dev/null +++ b/install.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash +# ============================================================================= +# +# install.sh β€” Dependency installer for websec-audit +# Supported: Debian 11/12/13, Ubuntu 20.04/22.04/24.04 +# +# Author : davidalvarezp +# Version : 1.0.1 +# License : MIT +# GitHub : https://github.com/davidalvarezp/websec-audit +# +# ============================================================================= + +set -euo pipefail + +readonly SCRIPT_VERSION="1.0.1" +readonly INSTALL_LOG="/tmp/websec_install_$(date +%Y%m%d_%H%M%S).log" + +# ── Colors ──────────────────────────────────────────────────────────────────── +C_RED='\033[0;31m'; C_GREEN='\033[0;32m'; C_YELLOW='\033[1;33m' +C_CYAN='\033[0;36m'; C_BLUE='\033[0;34m'; C_BOLD='\033[1m'; C_RESET='\033[0m' + +ok() { echo -e "${C_GREEN} [βœ”]${C_RESET} $1" | tee -a "$INSTALL_LOG"; } +info() { echo -e "${C_BLUE} [i]${C_RESET} $1" | tee -a "$INSTALL_LOG"; } +warn() { echo -e "${C_YELLOW} [!]${C_RESET} $1" | tee -a "$INSTALL_LOG"; } +err() { echo -e "${C_RED} [✘]${C_RESET} $1" | tee -a "$INSTALL_LOG" >&2; } +step() { echo -e "\n${C_BOLD}${C_CYAN} ── $1 ──${C_RESET}" | tee -a "$INSTALL_LOG"; } + +has_tool() { command -v "$1" &>/dev/null; } + +# ── Privilege check ─────────────────────────────────────────────────────────── +[[ $EUID -ne 0 ]] && { err "Run as root: sudo $0"; exit 1; } + +echo -e "${C_BOLD}${C_CYAN}" +cat << 'BANNER' + ╔═════════════════════════════════════════════════╗ + β•‘ websec-audit β€” Dependency Installer β•‘ + β•‘ Debian / Ubuntu β•‘ + β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +BANNER +echo -e "${C_RESET}" +echo " Install log: $INSTALL_LOG" +echo "" + +# ── System check ────────────────────────────────────────────────────────────── +step "System Verification" +OS_ID=$(grep "^ID=" /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "unknown") +OS_VER=$(grep "^VERSION_ID=" /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "?") +ARCH=$(uname -m) + +info "OS: $OS_ID $OS_VER | Arch: $ARCH" +[[ "$OS_ID" =~ ^(debian|ubuntu|kali|parrot)$ ]] || warn "Untested OS: $OS_ID β€” proceeding anyway" + +# ── APT packages ────────────────────────────────────────────────────────────── +step "APT Package Installation" +info "Updating package lists..." +apt-get update -qq 2>>"$INSTALL_LOG" + +APT_PACKAGES=( + # Core tools + curl wget git nmap + # Web scanners + nikto sqlmap dirb + # DNS & network + dnsutils bind9-dnsutils whois dnsmap + # Fingerprinting & WAF + whatweb wafw00f + # SSL + sslscan openssl + # Wordlists + wordlists + # Languages & build deps + python3 python3-pip jq + ruby ruby-dev build-essential libssl-dev libffi-dev + # gobuster (if packaged) + gobuster +) + +for pkg in "${APT_PACKAGES[@]}"; do + if apt-get install -y -qq "$pkg" >>"$INSTALL_LOG" 2>&1; then + ok "$pkg" + else + warn "$pkg β€” install failed or not available (will try alternative)" + fi +done + +# ── WPScan (gem) ────────────────────────────────────────────────────────────── +step "WPScan" +if has_tool wpscan; then + ok "wpscan already installed ($(wpscan --version 2>/dev/null | head -1))" +else + info "Installing wpscan via gem..." + if gem install wpscan --no-document >>"$INSTALL_LOG" 2>&1; then + ok "wpscan installed" + else + warn "wpscan installation failed" + fi +fi + +# ── testssl.sh ──────────────────────────────────────────────────────────────── +step "testssl.sh" +if has_tool testssl.sh; then + ok "testssl.sh already installed" +else + info "Installing testssl.sh from GitHub..." + if [[ -d /opt/testssl.sh ]]; then + git -C /opt/testssl.sh pull -q >>"$INSTALL_LOG" 2>&1 || true + else + git clone --depth 1 https://github.com/drwetter/testssl.sh.git /opt/testssl.sh \ + >>"$INSTALL_LOG" 2>&1 + fi + ln -sf /opt/testssl.sh/testssl.sh /usr/local/bin/testssl.sh + chmod +x /opt/testssl.sh/testssl.sh + ok "testssl.sh installed β†’ /usr/local/bin/testssl.sh" +fi + +# ── Go binary installer helper ──────────────────────────────────────────────── +install_go_binary() { + local name="$1" url="$2" binary="${3:-$1}" + if has_tool "$name"; then + ok "$name already installed" + return 0 + fi + info "Downloading $name..." + local tmp; tmp=$(mktemp) + local ext="${url##*.}" + + if wget -q "$url" -O "$tmp" >>"$INSTALL_LOG" 2>&1; then + case "$ext" in + gz) + tar -xzf "$tmp" -C /usr/local/bin/ "$binary" >>"$INSTALL_LOG" 2>&1 && \ + chmod +x "/usr/local/bin/$binary" && ok "$name installed" || warn "$name: extraction failed" ;; + zip) + unzip -q -o "$tmp" "$binary" -d /usr/local/bin/ >>"$INSTALL_LOG" 2>&1 && \ + chmod +x "/usr/local/bin/$binary" && ok "$name installed" || warn "$name: extraction failed" ;; + *) + mv "$tmp" "/usr/local/bin/$binary" + chmod +x "/usr/local/bin/$binary" && ok "$name installed" || warn "$name: install failed" ;; + esac + else + warn "$name: download failed" + fi + rm -f "$tmp" +} + +# Detect arch for Go binaries +case "$ARCH" in + x86_64|amd64) BIN_ARCH="amd64" ;; + aarch64|arm64) BIN_ARCH="arm64" ;; + armv7*) BIN_ARCH="arm" ;; + *) BIN_ARCH="amd64"; warn "Unknown arch $ARCH β€” assuming amd64" ;; +esac + +# ── gobuster ────────────────────────────────────────────────────────────────── +step "gobuster" +if ! has_tool gobuster; then + install_go_binary "gobuster" \ + "https://github.com/OJ/gobuster/releases/latest/download/gobuster_Linux_${BIN_ARCH}.tar.gz" \ + "gobuster" +fi + +# ── ffuf ───────────────────────────────────────────────────────────────────── +step "ffuf" +if ! has_tool ffuf; then + install_go_binary "ffuf" \ + "https://github.com/ffuf/ffuf/releases/latest/download/ffuf_$(curl -s https://api.github.com/repos/ffuf/ffuf/releases/latest 2>/dev/null | grep -oP '"tag_name": "v\K[^"]+' | head -1 || echo '2.1.0')_linux_${BIN_ARCH}.tar.gz" \ + "ffuf" +fi + +# ── subfinder ───────────────────────────────────────────────────────────────── +step "subfinder" +if ! has_tool subfinder; then + install_go_binary "subfinder" \ + "https://github.com/projectdiscovery/subfinder/releases/latest/download/subfinder_linux_${BIN_ARCH}.zip" \ + "subfinder" +fi + +# ── dalfox ──────────────────────────────────────────────────────────────────── +step "dalfox (XSS scanner)" +if ! has_tool dalfox; then + local DALFOX_VER + DALFOX_VER=$(curl -s https://api.github.com/repos/hahwul/dalfox/releases/latest 2>/dev/null | grep -oP '"tag_name": "v\K[^"]+' | head -1 || echo "2.9.1") + install_go_binary "dalfox" \ + "https://github.com/hahwul/dalfox/releases/download/v${DALFOX_VER}/dalfox_linux_${BIN_ARCH}.tar.gz" \ + "dalfox" +fi + +# ── subjack ─────────────────────────────────────────────────────────────────── +step "subjack (subdomain takeover)" +if ! has_tool subjack; then + if wget -q "https://github.com/haccer/subjack/releases/latest/download/subjack-linux-${BIN_ARCH}" \ + -O /usr/local/bin/subjack >>"$INSTALL_LOG" 2>&1; then + chmod +x /usr/local/bin/subjack + ok "subjack installed" + else + warn "subjack download failed" + fi +fi + +# ── nuclei ──────────────────────────────────────────────────────────────────── +step "nuclei" +if ! has_tool nuclei; then + install_go_binary "nuclei" \ + "https://github.com/projectdiscovery/nuclei/releases/latest/download/nuclei_linux_${BIN_ARCH}.zip" \ + "nuclei" +fi + +if has_tool nuclei; then + info "Updating Nuclei templates..." + nuclei -update-templates -silent >>"$INSTALL_LOG" 2>&1 || warn "Template update failed (try: nuclei -update-templates)" + ok "Nuclei templates updated" +fi + +# ── amass ───────────────────────────────────────────────────────────────────── +step "amass" +if ! has_tool amass; then + if apt-get install -y -qq amass >>"$INSTALL_LOG" 2>&1; then + ok "amass installed via apt" + else + local AMASS_VER + AMASS_VER=$(curl -s https://api.github.com/repos/owasp-amass/amass/releases/latest 2>/dev/null | grep -oP '"tag_name": "v\K[^"]+' | head -1 || echo "4.2.0") + install_go_binary "amass" \ + "https://github.com/owasp-amass/amass/releases/download/v${AMASS_VER}/amass_Linux_${BIN_ARCH}.zip" \ + "amass" + fi +fi + +# ── droopescan ──────────────────────────────────────────────────────────────── +step "droopescan (Drupal/Joomla scanner)" +if ! has_tool droopescan; then + if pip3 install droopescan --quiet >>"$INSTALL_LOG" 2>&1; then + ok "droopescan installed" + else + warn "droopescan installation failed" + fi +fi + +# ── SecLists wordlists ──────────────────────────────────────────────────────── +step "SecLists Wordlists" +if [[ -d /usr/share/seclists ]]; then + ok "SecLists already present at /usr/share/seclists" +else + # Try apt first + if apt-get install -y -qq seclists >>"$INSTALL_LOG" 2>&1; then + ok "SecLists installed via apt" + else + info "Cloning SecLists from GitHub (this may take a while)..." + if git clone --depth 1 https://github.com/danielmiessler/SecLists.git \ + /usr/share/seclists >>"$INSTALL_LOG" 2>&1; then + ok "SecLists installed at /usr/share/seclists" + else + warn "SecLists clone failed β€” install manually" + fi + fi +fi + +# Decompress rockyou if needed +[[ -f /usr/share/wordlists/rockyou.txt.gz && ! -f /usr/share/wordlists/rockyou.txt ]] && \ + gunzip /usr/share/wordlists/rockyou.txt.gz && ok "rockyou.txt decompressed" + +# ── dnsrecon ────────────────────────────────────────────────────────────────── +step "dnsrecon" +if ! has_tool dnsrecon; then + apt-get install -y -qq dnsrecon >>"$INSTALL_LOG" 2>&1 || \ + pip3 install dnsrecon --quiet >>"$INSTALL_LOG" 2>&1 || \ + warn "dnsrecon not installed" + has_tool dnsrecon && ok "dnsrecon installed" +fi + +# ── Final summary ───────────────────────────────────────────────────────────── +echo "" +echo -e "${C_BOLD}${C_CYAN} ═══════════════════════════════════════════════════${C_RESET}" +echo -e "${C_BOLD} Installation Summary${C_RESET}" +echo "" + +TOOLS=( + "curl" "nmap" "nikto" "sqlmap" "gobuster" "ffuf" "dirb" + "whatweb" "wafw00f" "sslscan" "testssl.sh" "wpscan" + "subfinder" "amass" "dnsrecon" "dalfox" "subjack" "nuclei" + "droopescan" "dig" "host" "whois" "jq" "python3" "ruby" +) + +installed=0; missing=0 +for t in "${TOOLS[@]}"; do + if has_tool "$t"; then + printf " ${C_GREEN}βœ”${C_RESET} %-20s %s\n" "$t" "($(command -v "$t"))" + installed=$((installed + 1)) + else + printf " ${C_YELLOW}β—‹${C_RESET} %-20s %s\n" "$t" "(not found)" + missing=$((missing + 1)) + fi +done + +echo "" +echo -e " ${C_BOLD}Installed: ${C_GREEN}${installed}${C_RESET} | ${C_BOLD}Missing: ${C_YELLOW}${missing}${C_RESET}" +echo "" +echo -e " ${C_DIM}Full log: ${INSTALL_LOG}${C_RESET}" +echo "" +echo -e " ${C_BOLD}Usage:${C_RESET}" +echo " chmod +x websec-audit.sh" +echo " ./websec-audit.sh -t https://target.com" +echo " ./websec-audit.sh -t https://target.com --aggressive -T 20" +echo "" diff --git a/websec-audit.sh b/websec-audit.sh new file mode 100644 index 0000000..ffdabc4 --- /dev/null +++ b/websec-audit.sh @@ -0,0 +1,2533 @@ +#!/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 + +
#SeverityModuleFindingTimestamp
%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 "$@"