├── 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/HEAD/images/banner.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GamehunterKaan/AutoPWN-Suite/HEAD/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 | ('123', {"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 |
24 |
Reproduce, copy, distribute, resell or otherwise use the Software for any commercial purpose
25 |
Allow any third party to use the Software on behalf of or for the benefit of any third party
26 |
Use the Software in any way which breaches any applicable local, national or international law
27 |
You may not use software for illegal or nefarious purposes.
28 |
use the Software for any purpose that AutoPWN Suite considers is a breach of this EULA agreement
29 |
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 | 
6 | 
7 | [](https://github.com/GamehunterKaan/AutoPWN-Suite/actions/workflows/tests.yml)
8 | 
9 | 
10 | 
11 | 
12 | 
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 | [](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 | [](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="