fix ruff and mypy lint, add pre commit so it doesnt happen again

This commit is contained in:
CarterPerez-dev 2025-11-12 15:30:47 -05:00
parent 0c4a22748c
commit e7a4ea6f92
43 changed files with 591 additions and 1217 deletions

View File

@ -12,24 +12,24 @@ jobs:
lint: lint:
name: Run Linters name: Run Linters
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: write pull-requests: write
contents: read contents: read
defaults: defaults:
run: run:
working-directory: PROJECTS/api-security-scanner/backend working-directory: PROJECTS/api-security-scanner/backend
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: '3.12'
- name: Cache pip dependencies - name: Cache pip dependencies
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -37,12 +37,12 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('PROJECTS/api-security-scanner/backend/pyproject.toml') }} key: ${{ runner.os }}-pip-${{ hashFiles('PROJECTS/api-security-scanner/backend/pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -e ".[dev]" pip install -e ".[dev]"
- name: Run pylint - name: Run pylint
id: pylint id: pylint
run: | run: |
@ -56,7 +56,7 @@ jobs:
fi fi
cat pylint-output.txt cat pylint-output.txt
continue-on-error: true continue-on-error: true
- name: Run ruff - name: Run ruff
id: ruff id: ruff
run: | run: |
@ -70,7 +70,7 @@ jobs:
fi fi
cat ruff-output.txt cat ruff-output.txt
continue-on-error: true continue-on-error: true
- name: Run mypy - name: Run mypy
id: mypy id: mypy
run: | run: |
@ -84,7 +84,7 @@ jobs:
fi fi
cat mypy-output.txt cat mypy-output.txt
continue-on-error: true continue-on-error: true
- name: Create Lint Summary - name: Create Lint Summary
id: create_summary id: create_summary
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
@ -92,7 +92,7 @@ jobs:
{ {
echo '## 🔍 Lint & Type Check Results' echo '## 🔍 Lint & Type Check Results'
echo '' echo ''
# Pylint Status # Pylint Status
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]]; then if [[ "${{ env.PYLINT_PASSED }}" == "true" ]]; then
echo '### ✅ Pylint: **Passed**' echo '### ✅ Pylint: **Passed**'
@ -107,7 +107,7 @@ jobs:
echo '</details>' echo '</details>'
fi fi
echo '' echo ''
# Ruff Status # Ruff Status
if [[ "${{ env.RUFF_PASSED }}" == "true" ]]; then if [[ "${{ env.RUFF_PASSED }}" == "true" ]]; then
echo '### ✅ Ruff: **Passed**' echo '### ✅ Ruff: **Passed**'
@ -122,7 +122,7 @@ jobs:
echo '</details>' echo '</details>'
fi fi
echo '' echo ''
# Mypy Status # Mypy Status
if [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then if [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
echo '### ✅ Mypy: **Passed**' echo '### ✅ Mypy: **Passed**'
@ -137,7 +137,7 @@ jobs:
echo '</details>' echo '</details>'
fi fi
echo '' echo ''
# Overall Summary # Overall Summary
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]] && [[ "${{ env.RUFF_PASSED }}" == "true" ]] && [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then if [[ "${{ env.PYLINT_PASSED }}" == "true" ]] && [[ "${{ env.RUFF_PASSED }}" == "true" ]] && [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
echo '---' echo '---'
@ -149,7 +149,7 @@ jobs:
echo '' echo ''
echo '<!-- lint-check-comment-marker -->' echo '<!-- lint-check-comment-marker -->'
} > ../lint-report.md } > ../lint-report.md
- name: Post PR Comment - name: Post PR Comment
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4 uses: peter-evans/create-or-update-comment@v4

62
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,62 @@
repos:
# Python Backend Checks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
name: ruff check (backend)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/api-security-scanner/backend/
exclude: ^PROJECTS/api-security-scanner/backend/(\.venv|__pycache__|\.pytest_cache)/
- id: ruff-format
name: ruff format (backend)
files: ^PROJECTS/api-security-scanner/backend/
exclude: ^PROJECTS/api-security-scanner/backend/(\.venv|__pycache__|\.pytest_cache)/
- repo: local
hooks:
- id: mypy
name: mypy type check (backend)
entry: bash -c 'cd PROJECTS/api-security-scanner/backend && mypy .'
language: system
types: [python]
files: ^PROJECTS/api-security-scanner/backend/
pass_filenames: false
- id: pylint
name: pylint check (backend)
entry: bash -c 'cd PROJECTS/api-security-scanner/backend && pylint **/*.py'
language: system
types: [python]
files: ^PROJECTS/api-security-scanner/backend/
pass_filenames: false
# Frontend TypeScript/ESLint Checks
- id: eslint
name: eslint check (frontend)
entry: bash -c 'cd PROJECTS/api-security-scanner/frontend && npm run lint:eslint'
language: system
types_or: [ts, tsx, javascript, jsx]
files: ^PROJECTS/api-security-scanner/frontend/src/
pass_filenames: false
- id: typescript
name: TypeScript type check (frontend)
entry: bash -c 'cd PROJECTS/api-security-scanner/frontend && npm run lint:types'
language: system
types_or: [ts, tsx]
files: ^PROJECTS/api-security-scanner/frontend/src/
pass_filenames: false
# General File Checks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-merge-conflict
- id: check-added-large-files
args: ['--maxkb=1000']

View File

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

View File

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

View File

@ -1,129 +0,0 @@
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

View File

@ -1,5 +1,5 @@
# ⒸAngelaMos | 2025 # ⒸAngelaMos | 2025
# Gitignore - This is where you list files/folders that # Gitignore - This is where you list files/folders that
# should not be publicly commited to the remote repository # should not be publicly commited to the remote repository
# Python # Python

View File

@ -41,17 +41,17 @@ help:
@echo "\033[1;4;91mSyntax Checking (Ruff):\033[0m" @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[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[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 " \033[92mmake \033[36mqa\033[0m \033[94m- Fix all code of syntax issues\033[0m"
@echo "" @echo ""
@echo "\033[1;4;94mType Checking (mypy):\033[0m" @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[92mmake \033[36mqa\033[0m \033[94m- Type check all code\033[0m"
@echo "\033[1;4;94mFormat:\033[0m" @echo "\033[1;4;94mFormat:\033[0m"
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Format code\033[0m" @echo " \033[92mmake \033[36mqa\033[0m \033[94m- Format code\033[0m"
@echo "" @echo ""
@echo "\033[1;4;92mUtilities:\033[0m" @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[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[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 " \033[92mmake \033[36mTODO\033[0m \033[94m- Find All TODO \033[0m"
@echo "" @echo ""
@echo "\033[1;93mCurrent default domain: \033[1;5;95m$(DOMAIN)\033[0m" @echo "\033[1;93mCurrent default domain: \033[1;5;95m$(DOMAIN)\033[0m"
@echo "" @echo ""
@ -59,7 +59,7 @@ help:
@echo " \033[91maccount\033[0m, \033[92mcommunity\033[0m, \033[93mcontent\033[0m, \033[94mfreemium\033[0m, \033[95mgames\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 " \033[96mmarketing\033[0m, \033[91mprogression\033[0m, \033[92mshop\033[0m, \033[93mtesting\033[0m, \033[94mtools\033[0m"
@echo "" @echo ""
@echo "\033[95m ------------------------------" @echo "\033[95m ------------------------------"
@echo "\033[34m ⣿⣿⣿⡷⠊⡢⡹⣦⡑⢂⢕⢂⢕⢂⢕⢂⠕⠔⠌⠝⠛⠶⠶⢶⣦⣄⢂⢕⢂⢕" @echo "\033[34m ⣿⣿⣿⡷⠊⡢⡹⣦⡑⢂⢕⢂⢕⢂⢕⢂⠕⠔⠌⠝⠛⠶⠶⢶⣦⣄⢂⢕⢂⢕"
@echo "\033[34m ⣿⣿⠏⣠⣾⣦⡐⢌⢿⣷⣦⣅⡑⠕⠡⠐⢿⠿⣛⠟⠛⠛⠛⠛⠡⢷⡈⢂⢕⢂" @echo "\033[34m ⣿⣿⠏⣠⣾⣦⡐⢌⢿⣷⣦⣅⡑⠕⠡⠐⢿⠿⣛⠟⠛⠛⠛⠛⠡⢷⡈⢂⢕⢂"
@echo "\033[34m ⠟⣡⣾⣿⣿⣿⣿⣦⣑⠝⢿⣿⣿⣿⣿⣿⡵⢁⣤⣶⣶⣿⢿⢿⢿⡟⢻⣤⢑⢂" @echo "\033[34m ⠟⣡⣾⣿⣿⣿⣿⣦⣑⠝⢿⣿⣿⣿⣿⣿⡵⢁⣤⣶⣶⣿⢿⢿⢿⡟⢻⣤⢑⢂"
@ -70,7 +70,7 @@ help:
@echo "\033[32m ⠣⡁⠹⡪⡪⡪⡪⣪⣾⣿⣿⣿⣿⠋⠐⢉⢍⢄⢌⠻⣿⣿⣿⣿⣿⣿⣿⣿⠏⠈" @echo "\033[32m ⠣⡁⠹⡪⡪⡪⡪⣪⣾⣿⣿⣿⣿⠋⠐⢉⢍⢄⢌⠻⣿⣿⣿⣿⣿⣿⣿⣿⠏⠈"
@echo "\033[32m ⡣⡘⢄⠙⣾⣾⣾⣿⣿⣿⣿⣿⣿⡀⢐⢕⢕⢕⢕⢕⡘⣿⣿⣿⣿⣿⣿⠏⠠⠈" @echo "\033[32m ⡣⡘⢄⠙⣾⣾⣾⣿⣿⣿⣿⣿⣿⡀⢐⢕⢕⢕⢕⢕⡘⣿⣿⣿⣿⣿⣿⠏⠠⠈"
@echo "\033[32m ⠌⢊⢂⢣⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢐⢕⢕⢕⢕⢕⢅⣿⣿⣿⣿⡿⢋⢜⠠⠈" @echo "\033[32m ⠌⢊⢂⢣⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢐⢕⢕⢕⢕⢕⢅⣿⣿⣿⣿⡿⢋⢜⠠⠈"
@echo "\033[95m ------------------------------" @echo "\033[95m ------------------------------"
@echo "\033[0m" @echo "\033[0m"
@ -104,20 +104,20 @@ check:
@ruff check . @ruff check .
$(call ASCII) $(call ASCII)
# ==================== Syntax Checking (Ruff) ==================== # ==================== Syntax Checking (Ruff) ====================
fix: fix:
@echo "\033[1;3;4;96m======== ruff fix All Code ========\033[0m" @echo "\033[1;3;4;96m======== ruff fix All Code ========\033[0m"
@ruff check . --fix @ruff check . --fix
$(call ASCII) $(call ASCII)
# ======================= In Depth Linting (pylint)======================= # ======================= In Depth Linting (pylint)=======================
pylint: pylint:
@echo "\033[1;3;4;96m======== Linting All Code ========\033[0m" @echo "\033[1;3;4;96m======== Linting All Code ========\033[0m"
@pylint . @pylint .
$(call ASCII) $(call ASCII)
# ======================= Type Checking (mypy) ======================= # ======================= Type Checking (mypy) =======================
mypy: mypy:
@echo "\033[1;3;4;96m======== Type Checking All Code ========\033[0m" @echo "\033[1;3;4;96m======== Type Checking All Code ========\033[0m"
@ -146,9 +146,8 @@ tree:
@echo "\033[1;3;4;96m======== Creating Tree ========\033[0m" @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' @tree -I 'node_modules|.git|*.log|dist|build|*.cache|certgames.egg-info|context|.venv|*.env|qa_context'
$(call ASCII) $(call ASCII)
TODO: TODO:
@echo "\033[1;3;4;96m======== Looking for TODO's ========\033[0m" @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 @find . -name "*.py" -print0 | xargs -0 pylint --disable=all --enable=W0511 --msg-template='{path}:{line}:{column}: {msg_id}: {msg} ({symbol})' || true
$(call ASCII) $(call ASCII)

View File

@ -1,5 +1,5 @@
""" """
AngelaMos | CarterPerez-dev AngelaMos | CarterPerez-dev
CertGames.com | 2025 CertGames.com | 2025
---- ----
API Security Scanner API Security Scanner
@ -15,6 +15,6 @@ API Security Scanner
""" """

View File

@ -16,9 +16,7 @@ class Settings(BaseSettings):
""" """
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file = "../.env", env_file="../.env", env_file_encoding="utf-8", case_sensitive=True
env_file_encoding = "utf-8",
case_sensitive = True
) )
# Application metadata # Application metadata
@ -86,9 +84,7 @@ class Settings(BaseSettings):
""" """
Convert comma separated CORS origins string to list Convert comma separated CORS origins string to list
""" """
return [ return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
origin.strip() for origin in self.CORS_ORIGINS.split(",")
]
@lru_cache @lru_cache

View File

@ -13,16 +13,12 @@ from config import settings
# Database engine # Database engine
engine = create_engine( engine = create_engine(
settings.DATABASE_URL, settings.DATABASE_URL,
pool_pre_ping = True, pool_pre_ping=True,
echo = settings.DEBUG, echo=settings.DEBUG,
) )
# Session factory # Session factory
SessionLocal = sessionmaker( SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
autocommit = False,
autoflush = False,
bind = engine
)
# Base class # Base class
Base = declarative_base() Base = declarative_base()

View File

@ -34,25 +34,25 @@ async def get_current_user(
if email is None: if email is None:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail = "Invalid authentication credentials", detail="Invalid authentication credentials",
headers = {"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
user = UserRepository.get_by_email(db, email) user = UserRepository.get_by_email(db, email)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail = "User not found", detail="User not found",
headers = {"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return UserResponse.model_validate(user) return UserResponse.model_validate(user)
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail = "Invalid authentication credentials", detail="Invalid authentication credentials",
headers = {"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) from None ) from None

View File

@ -9,6 +9,7 @@ class ScanStatus(str, Enum):
""" """
Enum for scan result status Enum for scan result status
""" """
VULNERABLE = "vulnerable" VULNERABLE = "vulnerable"
SAFE = "safe" SAFE = "safe"
ERROR = "error" ERROR = "error"
@ -18,6 +19,7 @@ class Severity(str, Enum):
""" """
Enum for vulnerability severity levels Enum for vulnerability severity levels
""" """
CRITICAL = "critical" CRITICAL = "critical"
HIGH = "high" HIGH = "high"
MEDIUM = "medium" MEDIUM = "medium"
@ -29,6 +31,7 @@ class TestType(str, Enum):
""" """
Enum for available security test types Enum for available security test types
""" """
RATE_LIMIT = "rate_limit" RATE_LIMIT = "rate_limit"
AUTH = "auth" AUTH = "auth"
SQLI = "sqli" SQLI = "sqli"

View File

@ -16,29 +16,22 @@ def hash_password(password: str) -> str:
""" """
Hash a plain text password using bcrypt Hash a plain text password using bcrypt
""" """
password_bytes = password.encode('utf-8') password_bytes = password.encode("utf-8")
salt = bcrypt.gensalt() salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt) hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8') return hashed.decode("utf-8")
def verify_password( def verify_password(plain_password: str, hashed_password: str) -> bool:
plain_password: str,
hashed_password: str
) -> bool:
""" """
Verify a plain text password against a hashed password Verify a plain text password against a hashed password
""" """
password_bytes = plain_password.encode('utf-8') password_bytes = plain_password.encode("utf-8")
hashed_bytes = hashed_password.encode('utf-8') hashed_bytes = hashed_password.encode("utf-8")
return bcrypt.checkpw(password_bytes, hashed_bytes) return bcrypt.checkpw(password_bytes, hashed_bytes)
def create_access_token( def create_access_token(data: dict[str, str], expires_delta: timedelta | None = None) -> str:
data: dict[str,
str],
expires_delta: timedelta | None = None
) -> str:
""" """
Create a JWT access token Create a JWT access token
""" """
@ -47,16 +40,10 @@ def create_access_token(
if expires_delta: if expires_delta:
expire = datetime.utcnow() + expires_delta expire = datetime.utcnow() + expires_delta
else: else:
expire = datetime.utcnow() + timedelta( expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode( encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
to_encode,
settings.SECRET_KEY,
algorithm = settings.ALGORITHM
)
return encoded_jwt return encoded_jwt
@ -65,11 +52,7 @@ def decode_token(token: str) -> dict[str, str]:
Decode and verify a JWT token Decode and verify a JWT token
""" """
try: try:
payload = jwt.decode( payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
token,
settings.SECRET_KEY,
algorithms = [settings.ALGORITHM]
)
return payload return payload
except JWTError as e: except JWTError as e:
raise ValueError(f"Invalid token: {str(e)}") from e raise ValueError(f"Invalid token: {str(e)}") from e

View File

@ -22,31 +22,28 @@ def create_app() -> FastAPI:
""" """
Application factory function Application factory function
""" """
Base.metadata.create_all(bind = engine) Base.metadata.create_all(bind=engine)
app = FastAPI( app = FastAPI(
title = settings.APP_NAME, title=settings.APP_NAME,
version = settings.VERSION, version=settings.VERSION,
openapi_version = "3.1.0", openapi_version="3.1.0",
docs_url = "/docs", docs_url="/docs",
redoc_url = "/redoc", redoc_url="/redoc",
openapi_url = "/openapi.json", openapi_url="/openapi.json",
debug = settings.DEBUG, debug=settings.DEBUG,
) )
limiter = Limiter(key_func = get_remote_address) limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter app.state.limiter = limiter
app.add_exception_handler( app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
RateLimitExceeded,
_rate_limit_exceeded_handler
)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins = settings.cors_origins_list, allow_origins=settings.cors_origins_list,
allow_credentials = True, allow_credentials=True,
allow_methods = ["*"], allow_methods=["*"],
allow_headers = ["*"], allow_headers=["*"],
) )
_register_routes(app) _register_routes(app)
@ -58,6 +55,7 @@ def _register_routes(app: FastAPI) -> None:
""" """
Register all application routes Register all application routes
""" """
@app.get("/") @app.get("/")
def root() -> dict[str, str]: def root() -> dict[str, str]:
""" """

View File

@ -1,6 +1,6 @@
""" """
CertGames.com | 2025 CertGames.com | 2025
AngelaMos | CarterPerez-dev AngelaMos | CarterPerez-dev
---- ----
API Security Scanner FastAPI entry point API Security Scanner FastAPI entry point
""" """

View File

@ -1,6 +1,6 @@
""" """
AngelaMos | 2025 AngelaMos | 2025
Base model class Base model class
Common fields and methods for all models Common fields and methods for all models
""" """
@ -21,22 +21,15 @@ class BaseModel(Base):
Abstract base model with common fields and methods Abstract base model with common fields and methods
All models inherit from this class All models inherit from this class
""" """
__abstract__ = True __abstract__ = True
id = Column( id = Column(Integer, primary_key=True, index=True, autoincrement=True)
Integer, created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
primary_key = True,
index = True,
autoincrement = True
)
created_at = Column(
DateTime(timezone = True),
default = lambda: datetime.now(UTC)
)
updated_at = Column( updated_at = Column(
DateTime(timezone = True), DateTime(timezone=True),
default = lambda: datetime.now(UTC), default=lambda: datetime.now(UTC),
onupdate = lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC),
) )
@declared_attr @declared_attr
@ -53,11 +46,7 @@ class BaseModel(Base):
Returns: Returns:
dict: Dictionary representation of the model dict: Dictionary representation of the model
""" """
return { return {column.name: getattr(self, column.name) for column in self.__table__.columns}
column.name: getattr(self,
column.name)
for column in self.__table__.columns
}
def update(self, **kwargs: Any) -> None: def update(self, **kwargs: Any) -> None:
""" """

View File

@ -24,30 +24,30 @@ class Scan(BaseModel):
""" """
Stores metadata about scans performed on target URLs Stores metadata about scans performed on target URLs
""" """
__tablename__ = "scans" __tablename__ = "scans"
user_id = Column( user_id = Column(
Integer, Integer,
ForeignKey("users.id", ForeignKey("users.id", ondelete="CASCADE"),
ondelete = "CASCADE"), nullable=False,
nullable = False, index=True,
index = True,
) )
target_url = Column( target_url = Column(
String(settings.URL_MAX_LENGTH), String(settings.URL_MAX_LENGTH),
nullable = False, nullable=False,
) )
scan_date = Column( scan_date = Column(
DateTime(timezone = True), DateTime(timezone=True),
default = lambda: datetime.now(UTC), default=lambda: datetime.now(UTC),
nullable = False, nullable=False,
) )
user = relationship("User", backref = "scans") user = relationship("User", backref="scans")
test_results = relationship( test_results = relationship(
"TestResult", "TestResult",
back_populates = "scan", back_populates="scan",
cascade = "all, delete-orphan", cascade="all, delete-orphan",
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@ -64,10 +64,7 @@ class Scan(BaseModel):
Returns: Returns:
bool: True if any test result is vulnerable bool: True if any test result is vulnerable
""" """
return any( return any(result.status == "vulnerable" for result in self.test_results)
result.status == "vulnerable"
for result in self.test_results
)
@property @property
def vulnerability_count(self) -> int: def vulnerability_count(self) -> int:
@ -77,7 +74,4 @@ class Scan(BaseModel):
Returns: Returns:
int: Number of vulnerable test results int: Number of vulnerable test results
""" """
return sum( return sum(1 for result in self.test_results if result.status == "vulnerable")
1 for result in self.test_results
if result.status == "vulnerable"
)

View File

@ -8,7 +8,7 @@ from sqlalchemy import (
Enum, Enum,
Integer, Integer,
Text, Text,
ForeignKey, ForeignKey,
) )
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSON
@ -25,39 +25,35 @@ class TestResult(BaseModel):
""" """
Stores individual test results for each security scan Stores individual test results for each security scan
""" """
__tablename__ = "test_results" __tablename__ = "test_results"
scan_id = Column( scan_id = Column(
Integer, Integer,
ForeignKey("scans.id", ForeignKey("scans.id", ondelete="CASCADE"),
ondelete = "CASCADE"), nullable=False,
nullable = False, index=True,
index = True,
) )
test_name = Column( test_name = Column(
Enum(TestType), Enum(TestType),
nullable = False, nullable=False,
index = True, index=True,
) )
status = Column( status = Column(
Enum(ScanStatus), Enum(ScanStatus),
nullable = False, nullable=False,
index = True, index=True,
) )
severity = Column( severity = Column(
Enum(Severity), Enum(Severity),
nullable = False, nullable=False,
index = True, index=True,
)
details = Column(Text, nullable = False)
evidence_json = Column(JSON, nullable = False, default = dict)
recommendations_json = Column(
JSON,
nullable = False,
default = list
) )
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") scan = relationship("Scan", back_populates="test_results")
def __repr__(self) -> str: def __repr__(self) -> str:
""" """

View File

@ -17,16 +17,17 @@ class User(BaseModel):
""" """
Stores authentication credentials and user information Stores authentication credentials and user information
""" """
__tablename__ = "users" __tablename__ = "users"
email = Column( email = Column(
String(settings.EMAIL_MAX_LENGTH), String(settings.EMAIL_MAX_LENGTH),
unique = True, unique=True,
nullable = False, nullable=False,
index = True, index=True,
) )
hashed_password = Column(String, nullable = False) hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default = True, nullable = False) is_active = Column(Boolean, default=True, nullable=False)
def __repr__(self) -> str: def __repr__(self) -> str:
""" """

View File

@ -56,19 +56,19 @@ build-backend = "setuptools.build_meta"
[tool.setuptools] [tool.setuptools]
py-modules = [ py-modules = [
"main", "main",
"config", "config",
"factory" "factory"
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = [ include = [
"core", "core",
"routes", "routes",
"models", "models",
"schemas", "schemas",
"services", "services",
"scanners", "scanners",
"repositories" "repositories"
] ]
@ -203,21 +203,21 @@ load-plugins = [
persistent = true persistent = true
suggestion-mode = true suggestion-mode = true
init-hook = "import sys; import os; sys.path.insert(0, os.getcwd())" init-hook = "import sys; import os; sys.path.insert(0, os.getcwd())"
ignore = [ ignore = [
"venv", "venv",
".venv", ".venv",
"__pycache__", "__pycache__",
"build", "build",
"dist", "dist",
".git", ".git",
".pytest_cache", ".pytest_cache",
".mypy_cache", ".mypy_cache",
".ruff_cache", ".ruff_cache",
] ]
ignore-paths = [ ignore-paths = [
"^venv/.*", "^venv/.*",
"^.venv/.*", "^.venv/.*",
"^build/.*", "^build/.*",
"^dist/.*", "^dist/.*",
] ]
[tool.pylint.type-check] [tool.pylint.type-check]
@ -268,7 +268,7 @@ max-statements = 40
[tool.bandit] [tool.bandit]
exclude_dirs = [ exclude_dirs = [
".venv", ".venv",
"venv", "venv",
] ]
skips = ["B104"] skips = ["B104"]

View File

@ -19,13 +19,9 @@ class ScanRepository:
""" """
Repository for Scan database operations Repository for Scan database operations
""" """
@staticmethod @staticmethod
def create_scan( def create_scan(db: Session, user_id: int, target_url: str, commit: bool = True) -> Scan:
db: Session,
user_id: int,
target_url: str,
commit: bool = True
) -> Scan:
""" """
Create a new scan Create a new scan
@ -39,9 +35,9 @@ class ScanRepository:
Scan: Created scan instance Scan: Created scan instance
""" """
scan = Scan( scan = Scan(
user_id = user_id, user_id=user_id,
target_url = target_url, target_url=target_url,
scan_date = datetime.now(UTC), scan_date=datetime.now(UTC),
) )
db.add(scan) db.add(scan)
if commit: if commit:
@ -62,17 +58,15 @@ class ScanRepository:
Scan | None: Scan instance or None if not found Scan | None: Scan instance or None if not found
""" """
return ( return (
db.query(Scan).options( db.query(Scan)
joinedload(Scan.test_results) .options(joinedload(Scan.test_results))
).filter(Scan.id == scan_id).first() .filter(Scan.id == scan_id)
.first()
) )
@staticmethod @staticmethod
def get_by_user( def get_by_user(
db: Session, db: Session, user_id: int, skip: int = 0, limit: int | None = None
user_id: int,
skip: int = 0,
limit: int | None = None
) -> list[Scan]: ) -> list[Scan]:
""" """
Get all scans for a user with pagination. Get all scans for a user with pagination.
@ -90,16 +84,17 @@ class ScanRepository:
limit = settings.DEFAULT_PAGINATION_LIMIT limit = settings.DEFAULT_PAGINATION_LIMIT
return ( return (
db.query(Scan).options( db.query(Scan)
joinedload(Scan.test_results) .options(joinedload(Scan.test_results))
).filter(Scan.user_id == user_id).order_by( .filter(Scan.user_id == user_id)
Scan.scan_date.desc() .order_by(Scan.scan_date.desc())
).offset(skip).limit(limit).all() .offset(skip)
.limit(limit)
.all()
) )
@staticmethod @staticmethod
def get_recent(db: Session, def get_recent(db: Session, limit: int | None = None) -> list[Scan]:
limit: int | None = None) -> list[Scan]:
""" """
Get most recent scans across all users. Get most recent scans across all users.
@ -114,17 +109,15 @@ class ScanRepository:
limit = settings.DEFAULT_PAGINATION_LIMIT limit = settings.DEFAULT_PAGINATION_LIMIT
return ( return (
db.query(Scan).options( db.query(Scan)
joinedload(Scan.test_results) .options(joinedload(Scan.test_results))
).order_by(Scan.scan_date.desc()).limit(limit).all() .order_by(Scan.scan_date.desc())
.limit(limit)
.all()
) )
@staticmethod @staticmethod
def delete( def delete(db: Session, scan_id: int, commit: bool = True) -> bool:
db: Session,
scan_id: int,
commit: bool = True
) -> bool:
""" """
Delete a scan (cascades to test results). Delete a scan (cascades to test results).

View File

@ -21,6 +21,7 @@ class TestResultRepository:
""" """
Repository for TestResult database operations Repository for TestResult database operations
""" """
@staticmethod @staticmethod
def create_test_result( def create_test_result(
db: Session, db: Session,
@ -30,8 +31,7 @@ class TestResultRepository:
status: ScanStatus, status: ScanStatus,
severity: Severity, severity: Severity,
details: str, details: str,
evidence_json: dict[str, evidence_json: dict[str, Any],
Any],
recommendations_json: list[str], recommendations_json: list[str],
commit: bool = True, commit: bool = True,
) -> TestResult: ) -> TestResult:
@ -53,13 +53,13 @@ class TestResultRepository:
TestResult: Created test result instance TestResult: Created test result instance
""" """
test_result = TestResult( test_result = TestResult(
scan_id = scan_id, scan_id=scan_id,
test_name = test_name, test_name=test_name,
status = status, status=status,
severity = severity, severity=severity,
details = details, details=details,
evidence_json = evidence_json, evidence_json=evidence_json,
recommendations_json = recommendations_json, recommendations_json=recommendations_json,
) )
db.add(test_result) db.add(test_result)
if commit: if commit:
@ -69,9 +69,7 @@ class TestResultRepository:
@staticmethod @staticmethod
def bulk_create( def bulk_create(
db: Session, db: Session, test_results: list[TestResult], commit: bool = True
test_results: list[TestResult],
commit: bool = True
) -> list[TestResult]: ) -> list[TestResult]:
""" """
Create multiple test results in bulk Create multiple test results in bulk
@ -104,15 +102,14 @@ class TestResultRepository:
list[TestResult]: List of test results for the scan list[TestResult]: List of test results for the scan
""" """
return ( return (
db.query(TestResult).filter( db.query(TestResult)
TestResult.scan_id == scan_id .filter(TestResult.scan_id == scan_id)
).order_by(TestResult.created_at.asc()).all() .order_by(TestResult.created_at.asc())
.all()
) )
@staticmethod @staticmethod
def get_by_status(db: Session, def get_by_status(db: Session, scan_id: int, status: ScanStatus) -> list[TestResult]:
scan_id: int,
status: ScanStatus) -> list[TestResult]:
""" """
Get test results by status for a scan Get test results by status for a scan
@ -125,15 +122,13 @@ class TestResultRepository:
list[TestResult]: Filtered test results list[TestResult]: Filtered test results
""" """
return ( return (
db.query(TestResult).filter( db.query(TestResult)
TestResult.scan_id == scan_id, .filter(TestResult.scan_id == scan_id, TestResult.status == status)
TestResult.status == status .all()
).all()
) )
@staticmethod @staticmethod
def get_vulnerabilities(db: Session, def get_vulnerabilities(db: Session, scan_id: int) -> list[TestResult]:
scan_id: int) -> list[TestResult]:
""" """
Get only vulnerable test results for a scan Get only vulnerable test results for a scan
@ -144,18 +139,10 @@ class TestResultRepository:
Returns: Returns:
list[TestResult]: Vulnerable test results only list[TestResult]: Vulnerable test results only
""" """
return TestResultRepository.get_by_status( return TestResultRepository.get_by_status(db, scan_id, ScanStatus.VULNERABLE)
db,
scan_id,
ScanStatus.VULNERABLE
)
@staticmethod @staticmethod
def delete_by_scan( def delete_by_scan(db: Session, scan_id: int, commit: bool = True) -> int:
db: Session,
scan_id: int,
commit: bool = True
) -> int:
""" """
Delete all test results for a scan Delete all test results for a scan
@ -167,11 +154,7 @@ class TestResultRepository:
Returns: Returns:
int: Number of test results deleted int: Number of test results deleted
""" """
count = ( count = db.query(TestResult).filter(TestResult.scan_id == scan_id).delete()
db.query(TestResult).filter(
TestResult.scan_id == scan_id
).delete()
)
if commit: if commit:
db.commit() db.commit()
return count return count

View File

@ -15,6 +15,7 @@ class UserRepository:
""" """
Repository for User database operations Repository for User database operations
""" """
@staticmethod @staticmethod
def get_by_id(db: Session, user_id: int) -> User | None: def get_by_id(db: Session, user_id: int) -> User | None:
""" """
@ -45,10 +46,7 @@ class UserRepository:
@staticmethod @staticmethod
def create_user( def create_user(
db: Session, db: Session, email: str, hashed_password: str, commit: bool = True
email: str,
hashed_password: str,
commit: bool = True
) -> User: ) -> User:
""" """
Create a new user Create a new user
@ -62,7 +60,7 @@ class UserRepository:
Returns: Returns:
User: Created user instance User: Created user instance
""" """
user = User(email = email, hashed_password = hashed_password) user = User(email=email, hashed_password=hashed_password)
db.add(user) db.add(user)
if commit: if commit:
db.commit() db.commit()
@ -70,11 +68,7 @@ class UserRepository:
return user return user
@staticmethod @staticmethod
def get_all_active( def get_all_active(db: Session, skip: int = 0, limit: int | None = None) -> list[User]:
db: Session,
skip: int = 0,
limit: int | None = None
) -> list[User]:
""" """
Get all active users with pagination Get all active users with pagination
@ -89,17 +83,11 @@ class UserRepository:
if limit is None: if limit is None:
limit = settings.DEFAULT_PAGINATION_LIMIT limit = settings.DEFAULT_PAGINATION_LIMIT
return ( return db.query(User).filter(User.is_active).offset(skip).limit(limit).all()
db.query(User).filter(User.is_active
).offset(skip).limit(limit).all()
)
@staticmethod @staticmethod
def update_active_status( def update_active_status(
db: Session, db: Session, user_id: int, is_active: bool, commit: bool = True
user_id: int,
is_active: bool,
commit: bool = True
) -> User | None: ) -> User | None:
""" """
Update user active status Update user active status
@ -122,11 +110,7 @@ class UserRepository:
return user return user
@staticmethod @staticmethod
def delete( def delete(db: Session, user_id: int, commit: bool = True) -> bool:
db: Session,
user_id: int,
commit: bool = True
) -> bool:
""" """
Delete a user Delete a user

View File

@ -24,14 +24,14 @@ from schemas.user_schemas import (
from services.auth_service import AuthService from services.auth_service import AuthService
router = APIRouter(prefix = "/auth", tags = ["authentication"]) router = APIRouter(prefix="/auth", tags=["authentication"])
limiter = Limiter(key_func = get_remote_address) limiter = Limiter(key_func=get_remote_address)
@router.post( @router.post(
"/register", "/register",
response_model = UserResponse, response_model=UserResponse,
status_code = status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
) )
@limiter.limit(settings.API_RATE_LIMIT_REGISTER) @limiter.limit(settings.API_RATE_LIMIT_REGISTER)
async def register( async def register(
@ -47,8 +47,8 @@ async def register(
@router.post( @router.post(
"/login", "/login",
response_model = TokenResponse, response_model=TokenResponse,
status_code = status.HTTP_200_OK, status_code=status.HTTP_200_OK,
) )
@limiter.limit(settings.API_RATE_LIMIT_LOGIN) @limiter.limit(settings.API_RATE_LIMIT_LOGIN)
async def login( async def login(

View File

@ -24,14 +24,14 @@ from schemas.user_schemas import UserResponse
from services.scan_service import ScanService from services.scan_service import ScanService
router = APIRouter(prefix = "/scans", tags = ["scans"]) router = APIRouter(prefix="/scans", tags=["scans"])
limiter = Limiter(key_func = get_remote_address) limiter = Limiter(key_func=get_remote_address)
@router.post( @router.post(
"/", "/",
response_model = ScanResponse, response_model=ScanResponse,
status_code = status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
) )
@limiter.limit(settings.API_RATE_LIMIT_SCAN) @limiter.limit(settings.API_RATE_LIMIT_SCAN)
async def create_scan( async def create_scan(
@ -48,8 +48,8 @@ async def create_scan(
@router.get( @router.get(
"/", "/",
response_model = list[ScanResponse], response_model=list[ScanResponse],
status_code = status.HTTP_200_OK, status_code=status.HTTP_200_OK,
) )
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) @limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def get_user_scans( async def get_user_scans(
@ -62,18 +62,13 @@ async def get_user_scans(
""" """
Get all scans for the authenticated user Get all scans for the authenticated user
""" """
return ScanService.get_user_scans( return ScanService.get_user_scans(db, current_user.id, skip, limit)
db,
current_user.id,
skip,
limit
)
@router.get( @router.get(
"/{scan_id}", "/{scan_id}",
response_model = ScanResponse, response_model=ScanResponse,
status_code = status.HTTP_200_OK, status_code=status.HTTP_200_OK,
) )
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) @limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def get_scan( async def get_scan(
@ -90,7 +85,7 @@ async def get_scan(
@router.delete( @router.delete(
"/{scan_id}", "/{scan_id}",
status_code = status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
) )
@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) @limiter.limit(settings.API_RATE_LIMIT_DEFAULT)
async def delete_scan( async def delete_scan(

View File

@ -35,6 +35,7 @@ class AuthScanner(BaseScanner):
Maps to OWASP API Security Top 10 2023: API2:2023 Maps to OWASP API Security Top 10 2023: API2:2023
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute authentication tests Execute authentication tests
@ -45,10 +46,10 @@ class AuthScanner(BaseScanner):
missing_auth_test = self._test_missing_authentication() missing_auth_test = self._test_missing_authentication()
if missing_auth_test["vulnerable"]: if missing_auth_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = "Endpoint accessible without authentication", details="Endpoint accessible without authentication",
evidence = missing_auth_test, evidence=missing_auth_test,
severity = Severity.HIGH, severity=Severity.HIGH,
recommendations = [ recommendations=[
"Require authentication for all sensitive endpoints", "Require authentication for all sensitive endpoints",
"Implement proper authentication middleware", "Implement proper authentication middleware",
"Return 401 Unauthorized for missing/invalid credentials", "Return 401 Unauthorized for missing/invalid credentials",
@ -59,11 +60,10 @@ class AuthScanner(BaseScanner):
jwt_test = self._test_jwt_vulnerabilities() jwt_test = self._test_jwt_vulnerabilities()
if jwt_test["vulnerable"]: if jwt_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details=f"JWT vulnerability: {jwt_test['vulnerability_type']}",
f"JWT vulnerability: {jwt_test['vulnerability_type']}", evidence=jwt_test,
evidence = jwt_test, severity=Severity.CRITICAL,
severity = Severity.CRITICAL, recommendations=jwt_test.get(
recommendations = jwt_test.get(
"recommendations", "recommendations",
[ [
"Properly validate JWT signatures", "Properly validate JWT signatures",
@ -77,10 +77,10 @@ class AuthScanner(BaseScanner):
invalid_token_test = self._test_invalid_token_handling() invalid_token_test = self._test_invalid_token_handling()
if invalid_token_test["vulnerable"]: if invalid_token_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = "Invalid tokens accepted by endpoint", details="Invalid tokens accepted by endpoint",
evidence = invalid_token_test, evidence=invalid_token_test,
severity = Severity.HIGH, severity=Severity.HIGH,
recommendations = [ recommendations=[
"Reject invalid/malformed tokens with 401 status", "Reject invalid/malformed tokens with 401 status",
"Validate token format, signature, and expiration", "Validate token format, signature, and expiration",
"Log authentication failures for monitoring", "Log authentication failures for monitoring",
@ -88,15 +88,15 @@ class AuthScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name = TestType.AUTH, test_name=TestType.AUTH,
status = ScanStatus.SAFE, status=ScanStatus.SAFE,
severity = Severity.INFO, severity=Severity.INFO,
details = "Authentication properly implemented", details="Authentication properly implemented",
evidence_json = { evidence_json={
"missing_auth_test": missing_auth_test, "missing_auth_test": missing_auth_test,
"invalid_token_test": invalid_token_test, "invalid_token_test": invalid_token_test,
}, },
recommendations_json = [ recommendations_json=[
"Authentication is properly configured", "Authentication is properly configured",
"Consider implementing additional security measures (2FA, refresh tokens)", "Consider implementing additional security measures (2FA, refresh tokens)",
], ],
@ -114,8 +114,7 @@ class AuthScanner(BaseScanner):
session_without_auth = self.session.__class__() session_without_auth = self.session.__class__()
session_without_auth.headers.update( session_without_auth.headers.update(
{ {
"User-Agent": "User-Agent": f"{settings.APP_NAME}/{settings.VERSION}",
f"{settings.APP_NAME}/{settings.VERSION}",
"Accept": "application/json", "Accept": "application/json",
} }
) )
@ -123,29 +122,22 @@ class AuthScanner(BaseScanner):
try: try:
response = session_without_auth.get( response = session_without_auth.get(
self.target_url, self.target_url,
timeout = settings.SCANNER_CONNECTION_TIMEOUT, timeout=settings.SCANNER_CONNECTION_TIMEOUT,
) )
if response.status_code == 200: if response.status_code == 200:
return { return {
"vulnerable": "vulnerable": True,
True, "status_code": response.status_code,
"status_code": "response_length": len(response.text),
response.status_code, "description": "Endpoint accessible without authentication",
"response_length":
len(response.text),
"description":
"Endpoint accessible without authentication",
} }
if response.status_code in (401, 403): if response.status_code in (401, 403):
return { return {
"vulnerable": "vulnerable": False,
False, "status_code": response.status_code,
"status_code": "description": "Endpoint properly requires authentication",
response.status_code,
"description":
"Endpoint properly requires authentication",
} }
return { return {
@ -158,8 +150,7 @@ class AuthScanner(BaseScanner):
return { return {
"vulnerable": False, "vulnerable": False,
"error": str(e), "error": str(e),
"description": "description": "Error testing authentication requirement",
"Error testing authentication requirement",
} }
def _test_jwt_vulnerabilities(self) -> dict[str, Any]: def _test_jwt_vulnerabilities(self) -> dict[str, Any]:
@ -190,9 +181,7 @@ class AuthScanner(BaseScanner):
return { return {
"vulnerable": False, "vulnerable": False,
"tests_performed": "tests_performed": ["none_algorithm", "signature_removal"],
["none_algorithm",
"signature_removal"],
"description": "No JWT vulnerabilities detected", "description": "No JWT vulnerabilities detected",
} }
@ -212,10 +201,7 @@ class AuthScanner(BaseScanner):
for variant in none_variants: for variant in none_variants:
malicious_header = self._base64url_encode( malicious_header = self._base64url_encode(
json.dumps({ json.dumps({"alg": variant, "typ": "JWT"})
"alg": variant,
"typ": "JWT"
})
) )
malicious_token = f"{malicious_header}.{payload}." malicious_token = f"{malicious_header}.{payload}."
@ -223,21 +209,15 @@ class AuthScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
"/", "/",
headers = { headers={"Authorization": f"Bearer {malicious_token}"},
"Authorization": f"Bearer {malicious_token}"
},
) )
if response.status_code == 200: if response.status_code == 200:
return { return {
"vulnerable": "vulnerable": True,
True, "vulnerability_type": "JWT None Algorithm",
"vulnerability_type": "algorithm_variant": variant,
"JWT None Algorithm", "status_code": response.status_code,
"algorithm_variant":
variant,
"status_code":
response.status_code,
"recommendations": [ "recommendations": [
"Reject tokens with 'none' algorithm (all case variations)", "Reject tokens with 'none' algorithm (all case variations)",
"Explicitly verify signature before accepting tokens", "Explicitly verify signature before accepting tokens",
@ -272,19 +252,14 @@ class AuthScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
"/", "/",
headers = { headers={"Authorization": f"Bearer {malicious_token}"},
"Authorization": f"Bearer {malicious_token}"
},
) )
if response.status_code == 200: if response.status_code == 200:
return { return {
"vulnerable": "vulnerable": True,
True, "vulnerability_type": "JWT Signature Not Verified",
"vulnerability_type": "status_code": response.status_code,
"JWT Signature Not Verified",
"status_code":
response.status_code,
"recommendations": [ "recommendations": [
"Require valid signature on all JWT tokens", "Require valid signature on all JWT tokens",
"Reject tokens with missing or invalid signatures", "Reject tokens with missing or invalid signatures",
@ -320,15 +295,13 @@ class AuthScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
"/", "/",
headers = { headers={"Authorization": f"Bearer {invalid_token}"},
"Authorization": f"Bearer {invalid_token}"
},
) )
if response.status_code == 200: if response.status_code == 200:
accepted_invalid.append( accepted_invalid.append(
{ {
"token": invalid_token[: 50], "token": invalid_token[:50],
"status_code": response.status_code, "status_code": response.status_code,
} }
) )
@ -382,8 +355,7 @@ class AuthScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, evidence: dict[str, Any],
Any],
severity: Severity = Severity.HIGH, severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -400,10 +372,10 @@ class AuthScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name = TestType.AUTH, test_name=TestType.AUTH,
status = ScanStatus.VULNERABLE, status=ScanStatus.VULNERABLE,
severity = severity, severity=severity,
details = details, details=details,
evidence_json = evidence, evidence_json=evidence,
recommendations_json = recommendations or [], recommendations_json=recommendations or [],
) )

View File

@ -25,6 +25,7 @@ class BaseScanner(ABC):
Provides common HTTP functionality, request spacing, retry logic, Provides common HTTP functionality, request spacing, retry logic,
and evidence collection. Specific scanners inherit and implement scan(). and evidence collection. Specific scanners inherit and implement scan().
""" """
def __init__( def __init__(
self, self,
target_url: str, target_url: str,
@ -57,23 +58,17 @@ class BaseScanner(ABC):
session.headers.update( session.headers.update(
{ {
"User-Agent": "User-Agent": f"{settings.APP_NAME}/{settings.VERSION}",
f"{settings.APP_NAME}/{settings.VERSION}",
"Accept": "application/json", "Accept": "application/json",
} }
) )
if self.auth_token: if self.auth_token:
session.headers.update( session.headers.update({"Authorization": f"Bearer {self.auth_token}"})
{"Authorization": f"Bearer {self.auth_token}"}
)
return session return session
def _wait_before_request( def _wait_before_request(self, jitter_ms: int | None = None) -> None:
self,
jitter_ms: int | None = None
) -> None:
""" """
Implement request spacing to avoid overwhelming target Implement request spacing to avoid overwhelming target
@ -86,10 +81,7 @@ class BaseScanner(ABC):
if jitter_ms is None: if jitter_ms is None:
jitter_ms = settings.DEFAULT_JITTER_MS jitter_ms = settings.DEFAULT_JITTER_MS
required_delay = 1.0 / ( required_delay = 1.0 / (self.max_requests / settings.SCANNER_RATE_LIMIT_WINDOW_SECONDS)
self.max_requests /
settings.SCANNER_RATE_LIMIT_WINDOW_SECONDS
)
jitter = random.uniform(0, jitter_ms / 1000.0) jitter = random.uniform(0, jitter_ms / 1000.0)
elapsed = time.time() - self.last_request_time elapsed = time.time() - self.last_request_time
@ -130,31 +122,24 @@ class BaseScanner(ABC):
retry_count = 0 retry_count = 0
backoff_factor = 2.0 backoff_factor = 2.0
kwargs.setdefault( kwargs.setdefault("timeout", settings.SCANNER_CONNECTION_TIMEOUT)
"timeout",
settings.SCANNER_CONNECTION_TIMEOUT
)
while retry_count <= settings.DEFAULT_RETRY_COUNT: while retry_count <= settings.DEFAULT_RETRY_COUNT:
try: try:
start_time = time.time() start_time = time.time()
response = self.session.request(method, url, **kwargs) response = self.session.request(method, url, **kwargs)
setattr( setattr(response, "request_time", time.time() - start_time)
response,
"request_time",
time.time() - start_time
)
self.request_count += 1 self.request_count += 1
if response.status_code == 429: if response.status_code == 429:
retry_after = response.headers.get( retry_after = response.headers.get(
"Retry-After", "Retry-After", str(settings.DEFAULT_RETRY_WAIT_SECONDS)
str(settings.DEFAULT_RETRY_WAIT_SECONDS)
) )
wait_time = ( wait_time = (
int(retry_after) if retry_after.isdigit() else int(retry_after)
settings.DEFAULT_RETRY_WAIT_SECONDS if retry_after.isdigit()
else settings.DEFAULT_RETRY_WAIT_SECONDS
) )
time.sleep(wait_time) time.sleep(wait_time)
retry_count += 1 retry_count += 1
@ -179,11 +164,8 @@ class BaseScanner(ABC):
return response return response
def get_baseline_timing( def get_baseline_timing(
self, self, endpoint: str, samples: int | None = None
endpoint: str, ) -> tuple[float, float]:
samples: int | None = None
) -> tuple[float,
float]:
""" """
Establish baseline response time for an endpoint Establish baseline response time for an endpoint
@ -214,8 +196,7 @@ class BaseScanner(ABC):
response: requests.Response, response: requests.Response,
payload: Any | None = None, payload: Any | None = None,
**additional_data: Any, **additional_data: Any,
) -> dict[str, ) -> dict[str, Any]:
Any]:
""" """
Collect evidence from test execution with sensitive data redaction Collect evidence from test execution with sensitive data redaction
@ -228,17 +209,10 @@ class BaseScanner(ABC):
dict[str, Any]: Evidence dictionary dict[str, Any]: Evidence dictionary
""" """
evidence = { evidence = {
"status_code": "status_code": response.status_code,
response.status_code, "response_time_ms": round(getattr(response, "request_time", 0.0) * 1000, 2),
"response_time_ms": "response_length": len(response.text),
round(getattr(response, "headers": self._redact_sensitive_headers(dict(response.headers)),
"request_time",
0.0) * 1000,
2),
"response_length":
len(response.text),
"headers":
self._redact_sensitive_headers(dict(response.headers)),
} }
if payload is not None: if payload is not None:
@ -248,10 +222,7 @@ class BaseScanner(ABC):
return evidence return evidence
def _redact_sensitive_headers(self, def _redact_sensitive_headers(self, headers: dict[str, str]) -> dict[str, str]:
headers: dict[str,
str]) -> dict[str,
str]:
""" """
Redact sensitive header values for evidence collection Redact sensitive header values for evidence collection

View File

@ -30,6 +30,7 @@ class IDORScanner(BaseScanner):
Maps to OWASP API Security Top 10 2023: API1:2023 Maps to OWASP API Security Top 10 2023: API1:2023
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute IDOR/BOLA tests Execute IDOR/BOLA tests
@ -41,11 +42,10 @@ class IDORScanner(BaseScanner):
if id_enumeration_test["vulnerable"]: if id_enumeration_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details=f"IDOR vulnerability detected: {id_enumeration_test['vulnerability_type']}",
f"IDOR vulnerability detected: {id_enumeration_test['vulnerability_type']}", evidence=id_enumeration_test,
evidence = id_enumeration_test, severity=Severity.HIGH,
severity = Severity.HIGH, recommendations=[
recommendations = [
"Implement proper authorization checks for all object access", "Implement proper authorization checks for all object access",
"Verify user owns/has permission to access requested resource", "Verify user owns/has permission to access requested resource",
"Use UUIDs instead of sequential IDs (but still check authorization)", "Use UUIDs instead of sequential IDs (but still check authorization)",
@ -58,11 +58,10 @@ class IDORScanner(BaseScanner):
if predictable_id_test["vulnerable"]: if predictable_id_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details="Predictable ID patterns detected enabling enumeration",
"Predictable ID patterns detected enabling enumeration", evidence=predictable_id_test,
evidence = predictable_id_test, severity=Severity.MEDIUM,
severity = Severity.MEDIUM, recommendations=[
recommendations = [
"Use non-sequential, non-predictable identifiers (UUIDs)", "Use non-sequential, non-predictable identifiers (UUIDs)",
"Implement rate limiting on ID-based endpoints", "Implement rate limiting on ID-based endpoints",
"Add authorization checks regardless of ID format", "Add authorization checks regardless of ID format",
@ -70,15 +69,15 @@ class IDORScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name = TestType.IDOR, test_name=TestType.IDOR,
status = ScanStatus.SAFE, status=ScanStatus.SAFE,
severity = Severity.INFO, severity=Severity.INFO,
details = "No IDOR/BOLA vulnerabilities detected", details="No IDOR/BOLA vulnerabilities detected",
evidence_json = { evidence_json={
"id_enumeration_test": id_enumeration_test, "id_enumeration_test": id_enumeration_test,
"predictable_id_test": predictable_id_test, "predictable_id_test": predictable_id_test,
}, },
recommendations_json = [ recommendations_json=[
"Authorization checks appear to be in place", "Authorization checks appear to be in place",
"Continue monitoring for authorization bypasses", "Continue monitoring for authorization bypasses",
], ],
@ -102,9 +101,7 @@ class IDORScanner(BaseScanner):
"description": "No IDs found in endpoint responses", "description": "No IDs found in endpoint responses",
} }
numeric_test = self._test_numeric_id_manipulation( numeric_test = self._test_numeric_id_manipulation(extracted_ids)
extracted_ids
)
if numeric_test["vulnerable"]: if numeric_test["vulnerable"]:
return numeric_test return numeric_test
@ -135,32 +132,22 @@ class IDORScanner(BaseScanner):
response_text = response.text 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}' 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( uuids = re.findall(uuid_pattern, response_text, re.IGNORECASE)
uuid_pattern,
response_text,
re.IGNORECASE
)
numeric_id_pattern = r'"id"\s*:\s*(\d+)' numeric_id_pattern = r'"id"\s*:\s*(\d+)'
numeric_ids = re.findall( numeric_ids = re.findall(numeric_id_pattern, response_text)
numeric_id_pattern,
response_text
)
ids = [] ids = []
ids.extend(uuids[: 3]) ids.extend(uuids[:3])
ids.extend([int(nid) for nid in numeric_ids[: 3]]) ids.extend([int(nid) for nid in numeric_ids[:3]])
return ids return ids
except Exception: except Exception:
return [] return []
def _test_numeric_id_manipulation(self, def _test_numeric_id_manipulation(self, extracted_ids: list[Any]) -> dict[str, Any]:
extracted_ids: list[Any]
) -> dict[str,
Any]:
""" """
Test numeric ID manipulation for IDOR Test numeric ID manipulation for IDOR
@ -170,10 +157,7 @@ class IDORScanner(BaseScanner):
Returns: Returns:
dict[str, Any]: Numeric ID manipulation test results dict[str, Any]: Numeric ID manipulation test results
""" """
numeric_ids = [ numeric_ids = [id_val for id_val in extracted_ids if isinstance(id_val, int)]
id_val for id_val in extracted_ids
if isinstance(id_val, int)
]
if not numeric_ids: if not numeric_ids:
return { return {
@ -220,10 +204,7 @@ class IDORScanner(BaseScanner):
"numeric_ids_tested": len(test_ids), "numeric_ids_tested": len(test_ids),
} }
def _test_string_id_manipulation(self, def _test_string_id_manipulation(self, extracted_ids: list[Any]) -> dict[str, Any]:
extracted_ids: list[Any]
) -> dict[str,
Any]:
""" """
Test string/UUID ID manipulation for IDOR Test string/UUID ID manipulation for IDOR
@ -233,10 +214,7 @@ class IDORScanner(BaseScanner):
Returns: Returns:
dict[str, Any]: String ID manipulation test results dict[str, Any]: String ID manipulation test results
""" """
string_ids = [ string_ids = [id_val for id_val in extracted_ids if isinstance(id_val, str)]
id_val for id_val in extracted_ids
if isinstance(id_val, str)
]
if not string_ids: if not string_ids:
return { return {
@ -305,7 +283,7 @@ class IDORScanner(BaseScanner):
"vulnerable": True, "vulnerable": True,
"pattern_type": "Sequential IDs", "pattern_type": "Sequential IDs",
"id_difference": diff1, "id_difference": diff1,
"example_ids": numeric_ids1[: 3], "example_ids": numeric_ids1[:3],
} }
return { return {
@ -323,8 +301,7 @@ class IDORScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, evidence: dict[str, Any],
Any],
severity: Severity = Severity.HIGH, severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -341,10 +318,10 @@ class IDORScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name = TestType.IDOR, test_name=TestType.IDOR,
status = ScanStatus.VULNERABLE, status=ScanStatus.VULNERABLE,
severity = severity, severity=severity,
details = details, details=details,
evidence_json = evidence, evidence_json=evidence,
recommendations_json = recommendations or [], recommendations_json=recommendations or [],
) )

View File

@ -8,6 +8,7 @@ class SQLiPayloads:
""" """
SQL Injection test payloads covering various database types and techniques SQL Injection test payloads covering various database types and techniques
""" """
ERROR_SIGNATURES = { ERROR_SIGNATURES = {
"mysql": [ "mysql": [
"sql syntax", "sql syntax",
@ -130,10 +131,13 @@ class SQLiPayloads:
list[str]: All SQLi test payloads list[str]: All SQLi test payloads
""" """
return ( return (
cls.BASIC_AUTHENTICATION_BYPASS + cls.UNION_BASED + cls.BASIC_AUTHENTICATION_BYPASS
cls.TIME_BASED_BLIND + cls.BOOLEAN_BASED_BLIND + + cls.UNION_BASED
cls.ERROR_BASED + cls.STACKED_QUERIES + + cls.TIME_BASED_BLIND
cls.COMMENT_VARIATIONS + cls.BOOLEAN_BASED_BLIND
+ cls.ERROR_BASED
+ cls.STACKED_QUERIES
+ cls.COMMENT_VARIATIONS
) )
@classmethod @classmethod
@ -221,6 +225,7 @@ class IDORPayloads:
""" """
Insecure Direct Object Reference (IDOR) test patterns Insecure Direct Object Reference (IDOR) test patterns
""" """
NUMERIC_ID_MANIPULATIONS = [ NUMERIC_ID_MANIPULATIONS = [
0, 0,
-1, -1,
@ -273,13 +278,11 @@ class RateLimitBypassPayloads:
""" """
Rate limiting bypass techniques and patterns Rate limiting bypass techniques and patterns
""" """
HEADER_PATTERNS = { HEADER_PATTERNS = {
"limit": "limit": r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit",
r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit", "remaining": r"x-ratelimit-remaining|x-rate-limit-remaining|ratelimit-remaining",
"remaining": "reset": r"x-ratelimit-reset|x-rate-limit-reset|ratelimit-reset",
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", "retry_after": r"retry-after",
} }
@ -297,30 +300,14 @@ class RateLimitBypassPayloads:
] ]
HEADER_SPOOFING = [ HEADER_SPOOFING = [
{ {"X-Forwarded-For": "127.0.0.1"},
"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-Forwarded-For": "8.8.8.8" {"X-Remote-IP": "127.0.0.1"},
}, {"X-Client-IP": "127.0.0.1"},
{ {"CF-Connecting-IP": "127.0.0.1"},
"X-Real-IP": "127.0.0.1" {"True-Client-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 = [ USER_AGENT_ROTATION = [
@ -413,9 +400,9 @@ class XSSPayloads:
ATTRIBUTE_BREAKING = [ ATTRIBUTE_BREAKING = [
"' onmouseover='alert(\"XSS\")'", "' onmouseover='alert(\"XSS\")'",
"\" onmouseover=\"alert('XSS')\"", '" onmouseover="alert(\'XSS\')"',
"' onclick='alert(\"XSS\")' '", "' onclick='alert(\"XSS\")' '",
"\" autofocus onfocus=\"alert('XSS')\"", '" autofocus onfocus="alert(\'XSS\')"',
"'/><script>alert('XSS')</script>", "'/><script>alert('XSS')</script>",
"\"/><script>alert('XSS')</script>", "\"/><script>alert('XSS')</script>",
] ]
@ -427,7 +414,7 @@ class XSSPayloads:
"<script>alert('XSS')<!--", "<script>alert('XSS')<!--",
"<<script>alert('XSS')</script>", "<<script>alert('XSS')</script>",
"<script\x20type='text/javascript'>alert('XSS')</script>", "<script\x20type='text/javascript'>alert('XSS')</script>",
"<script\x0D\x0A>alert('XSS')</script>", "<script\x0d\x0a>alert('XSS')</script>",
] ]
POLYGLOT_XSS = [ POLYGLOT_XSS = [
@ -446,10 +433,14 @@ class XSSPayloads:
list[str]: All XSS test payloads list[str]: All XSS test payloads
""" """
return ( return (
cls.BASIC_XSS + cls.EVENT_HANDLER_XSS + cls.SVG_XSS + cls.BASIC_XSS
cls.IFRAME_XSS + cls.ENCODED_XSS + + cls.EVENT_HANDLER_XSS
cls.ATTRIBUTE_BREAKING + cls.FILTER_BYPASS + + cls.SVG_XSS
cls.POLYGLOT_XSS + cls.IFRAME_XSS
+ cls.ENCODED_XSS
+ cls.ATTRIBUTE_BREAKING
+ cls.FILTER_BYPASS
+ cls.POLYGLOT_XSS
) )
@classmethod @classmethod

View File

@ -26,6 +26,7 @@ class RateLimitScanner(BaseScanner):
""" """
Rate limiting and bypass vulnerabilities tests Rate limiting and bypass vulnerabilities tests
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute rate limiting tests Execute rate limiting tests
@ -37,10 +38,9 @@ class RateLimitScanner(BaseScanner):
if not rate_limit_info["rate_limit_detected"]: if not rate_limit_info["rate_limit_detected"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details="No rate limiting detected on target endpoint",
"No rate limiting detected on target endpoint", evidence=rate_limit_info,
evidence = rate_limit_info, recommendations=[
recommendations = [
"Implement rate limiting to prevent abuse and DoS attacks", "Implement rate limiting to prevent abuse and DoS attacks",
"Use standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining)", "Use standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining)",
"Return 429 Too Many Requests when limits are exceeded", "Return 429 Too Many Requests when limits are exceeded",
@ -50,11 +50,10 @@ class RateLimitScanner(BaseScanner):
if rate_limit_info["enforcement_status"] == "HEADERS_ONLY": if rate_limit_info["enforcement_status"] == "HEADERS_ONLY":
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details="Rate limit headers present but not enforced",
"Rate limit headers present but not enforced", evidence=rate_limit_info,
evidence = rate_limit_info, severity=Severity.MEDIUM,
severity = Severity.MEDIUM, recommendations=[
recommendations = [
"Enforce rate limits with 429 responses when thresholds are exceeded", "Enforce rate limits with 429 responses when thresholds are exceeded",
"Rate limit headers without enforcement provide false security", "Rate limit headers without enforcement provide false security",
], ],
@ -64,14 +63,13 @@ class RateLimitScanner(BaseScanner):
if bypass_results["bypass_successful"]: if bypass_results["bypass_successful"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details=f"Rate limiting bypassed using: {bypass_results['bypass_method']}",
f"Rate limiting bypassed using: {bypass_results['bypass_method']}", evidence={
evidence = {
"rate_limit_info": rate_limit_info, "rate_limit_info": rate_limit_info,
"bypass_details": bypass_results, "bypass_details": bypass_results,
}, },
severity = Severity.HIGH, severity=Severity.HIGH,
recommendations = [ recommendations=[
f"Fix bypass vulnerability: {bypass_results['bypass_method']}", f"Fix bypass vulnerability: {bypass_results['bypass_method']}",
"Do not trust client-provided IP headers (X-Forwarded-For, X-Real-IP)", "Do not trust client-provided IP headers (X-Forwarded-For, X-Real-IP)",
"Implement rate limiting at multiple layers (IP, user, API key)", "Implement rate limiting at multiple layers (IP, user, API key)",
@ -80,25 +78,21 @@ class RateLimitScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name = TestType.RATE_LIMIT, test_name=TestType.RATE_LIMIT,
status = ScanStatus.SAFE, status=ScanStatus.SAFE,
severity = Severity.INFO, severity=Severity.INFO,
details = details="Rate limiting properly implemented and enforced",
"Rate limiting properly implemented and enforced", evidence_json={
evidence_json = {
"rate_limit_info": rate_limit_info, "rate_limit_info": rate_limit_info,
"bypass_attempts": bypass_results, "bypass_attempts": bypass_results,
}, },
recommendations_json = [ recommendations_json=[
"Rate limiting is properly configured", "Rate limiting is properly configured",
"Continue monitoring for new bypass techniques", "Continue monitoring for new bypass techniques",
], ],
) )
def _detect_rate_limiting(self, def _detect_rate_limiting(self, test_request_count: int = 20) -> dict[str, Any]:
test_request_count: int = 20
) -> dict[str,
Any]:
""" """
Detect rate limiting by analyzing headers and response patterns Detect rate limiting by analyzing headers and response patterns
@ -110,8 +104,7 @@ class RateLimitScanner(BaseScanner):
Returns: Returns:
dict[str, Any]: Rate limiting detection results dict[str, Any]: Rate limiting detection results
""" """
rate_limit_patterns = RateLimitBypassPayloads.get_header_patterns( rate_limit_patterns = RateLimitBypassPayloads.get_header_patterns()
)
results = { results = {
"rate_limit_detected": False, "rate_limit_detected": False,
@ -127,35 +120,23 @@ class RateLimitScanner(BaseScanner):
try: try:
response = self.make_request("GET", "/") response = self.make_request("GET", "/")
headers_lower = { headers_lower = {k.lower(): v for k, v in response.headers.items()}
k.lower(): v
for k, v in response.headers.items()
}
for header_type, pattern in rate_limit_patterns.items(): for header_type, pattern in rate_limit_patterns.items():
for header_name, header_value in headers_lower.items(): for header_name, header_value in headers_lower.items():
if re.search(pattern, if re.search(pattern, header_name, re.IGNORECASE):
header_name, results["rate_limit_headers"][header_type] = {
re.IGNORECASE): "header_name": header_name,
results["rate_limit_headers"][ "value": header_value,
header_type] = { }
"header_name": header_name,
"value": header_value,
}
results["rate_limit_detected"] = True results["rate_limit_detected"] = True
results["request_results"].append( results["request_results"].append(
{ {
"attempt": "attempt": attempt,
attempt, "status_code": response.status_code,
"status_code": "response_time_ms": round(
response.status_code, getattr(response, "request_time", 0.0) * 1000, 2
"response_time_ms":
round(
getattr(response,
"request_time",
0.0) * 1000,
2
), ),
} }
) )
@ -173,22 +154,15 @@ class RateLimitScanner(BaseScanner):
time.sleep(0.1) time.sleep(0.1)
except Exception as e: except Exception as e:
results["request_results"].append( results["request_results"].append({"attempt": attempt, "error": str(e)})
{
"attempt": attempt,
"error": str(e)
}
)
break break
if results["rate_limit_detected"]: if results["rate_limit_detected"]:
if "limit" in results["rate_limit_headers"]: if "limit" in results["rate_limit_headers"]:
results["limit_threshold"] = results[ results["limit_threshold"] = results["rate_limit_headers"]["limit"]["value"]
"rate_limit_headers"]["limit"]["value"]
if "reset" in results["rate_limit_headers"]: if "reset" in results["rate_limit_headers"]:
results["reset_window"] = results["rate_limit_headers" results["reset_window"] = results["rate_limit_headers"]["reset"]["value"]
]["reset"]["value"]
if not results["enforcement_status"]: if not results["enforcement_status"]:
results["enforcement_status"] = "HEADERS_ONLY" results["enforcement_status"] = "HEADERS_ONLY"
@ -235,9 +209,7 @@ class RateLimitScanner(BaseScanner):
return results return results
def _test_ip_header_bypass(self, def _test_ip_header_bypass(self, test_count: int = 15) -> dict[str, Any]:
test_count: int = 15) -> dict[str,
Any]:
""" """
Test if rate limiting can be bypassed with IP spoofing headers Test if rate limiting can be bypassed with IP spoofing headers
@ -261,11 +233,7 @@ class RateLimitScanner(BaseScanner):
test_headers = {header_name: fake_ip} test_headers = {header_name: fake_ip}
try: try:
response = self.make_request( response = self.make_request("GET", "/", headers=test_headers)
"GET",
"/",
headers = test_headers
)
if response.status_code != 429: if response.status_code != 429:
success_count += 1 success_count += 1
@ -285,8 +253,7 @@ class RateLimitScanner(BaseScanner):
return { return {
"bypass_successful": False, "bypass_successful": False,
"headers_tested": "headers_tested": [list(h.keys())[0] for h in bypass_headers],
[list(h.keys())[0] for h in bypass_headers],
} }
def _test_endpoint_variation_bypass(self) -> dict[str, Any]: def _test_endpoint_variation_bypass(self) -> dict[str, Any]:
@ -328,8 +295,7 @@ class RateLimitScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, evidence: dict[str, Any],
Any],
severity: Severity = Severity.HIGH, severity: Severity = Severity.HIGH,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -346,10 +312,10 @@ class RateLimitScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name = TestType.RATE_LIMIT, test_name=TestType.RATE_LIMIT,
status = ScanStatus.VULNERABLE, status=ScanStatus.VULNERABLE,
severity = severity, severity=severity,
details = details, details=details,
evidence_json = evidence, evidence_json=evidence,
recommendations_json = recommendations or [], recommendations_json=recommendations or [],
) )

View File

@ -31,6 +31,7 @@ class SQLiScanner(BaseScanner):
Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle
""" """
def scan(self) -> TestResultCreate: def scan(self) -> TestResultCreate:
""" """
Execute SQL injection tests Execute SQL injection tests
@ -41,11 +42,10 @@ class SQLiScanner(BaseScanner):
error_based_test = self._test_error_based_sqli() error_based_test = self._test_error_based_sqli()
if error_based_test["vulnerable"]: if error_based_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details=f"Error-based SQL injection detected: {error_based_test['database_type']}",
f"Error-based SQL injection detected: {error_based_test['database_type']}", evidence=error_based_test,
evidence = error_based_test, severity=Severity.CRITICAL,
severity = Severity.CRITICAL, recommendations=[
recommendations = [
"Use parameterized queries (prepared statements)", "Use parameterized queries (prepared statements)",
"Never concatenate user input into SQL queries", "Never concatenate user input into SQL queries",
"Implement input validation and sanitization", "Implement input validation and sanitization",
@ -57,10 +57,10 @@ class SQLiScanner(BaseScanner):
boolean_based_test = self._test_boolean_based_sqli() boolean_based_test = self._test_boolean_based_sqli()
if boolean_based_test["vulnerable"]: if boolean_based_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = "Boolean-based blind SQL injection detected", details="Boolean-based blind SQL injection detected",
evidence = boolean_based_test, evidence=boolean_based_test,
severity = Severity.CRITICAL, severity=Severity.CRITICAL,
recommendations = [ recommendations=[
"Use parameterized queries for all database operations", "Use parameterized queries for all database operations",
"Implement proper input validation", "Implement proper input validation",
"Avoid exposing different responses for true/false conditions", "Avoid exposing different responses for true/false conditions",
@ -70,11 +70,10 @@ class SQLiScanner(BaseScanner):
time_based_test = self._test_time_based_sqli() time_based_test = self._test_time_based_sqli()
if time_based_test["vulnerable"]: if time_based_test["vulnerable"]:
return self._create_vulnerable_result( return self._create_vulnerable_result(
details = details=f"Time-based blind SQL injection detected: {time_based_test['database_type']}",
f"Time-based blind SQL injection detected: {time_based_test['database_type']}", evidence=time_based_test,
evidence = time_based_test, severity=Severity.CRITICAL,
severity = Severity.CRITICAL, recommendations=[
recommendations = [
"Use parameterized queries exclusively", "Use parameterized queries exclusively",
"Implement strict input validation", "Implement strict input validation",
"Monitor for unusual response time patterns", "Monitor for unusual response time patterns",
@ -82,16 +81,16 @@ class SQLiScanner(BaseScanner):
) )
return TestResultCreate( return TestResultCreate(
test_name = TestType.SQLI, test_name=TestType.SQLI,
status = ScanStatus.SAFE, status=ScanStatus.SAFE,
severity = Severity.INFO, severity=Severity.INFO,
details = "No SQL injection vulnerabilities detected", details="No SQL injection vulnerabilities detected",
evidence_json = { evidence_json={
"error_based_test": error_based_test, "error_based_test": error_based_test,
"boolean_based_test": boolean_based_test, "boolean_based_test": boolean_based_test,
"time_based_test": time_based_test, "time_based_test": time_based_test,
}, },
recommendations_json = [ recommendations_json=[
"Continue using parameterized queries", "Continue using parameterized queries",
"Regularly update security testing", "Regularly update security testing",
], ],
@ -125,8 +124,7 @@ class SQLiScanner(BaseScanner):
"payload": payload, "payload": payload,
"status_code": response.status_code, "status_code": response.status_code,
"error_signature": signature, "error_signature": signature,
"response_excerpt": "response_excerpt": response.text[:500],
response.text[: 500],
} }
except Exception: except Exception:
@ -161,12 +159,12 @@ class SQLiScanner(BaseScanner):
boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND
true_payloads = [ true_payloads = [
p for p in boolean_payloads p for p in boolean_payloads if "AND '1'='1" in p or "AND 1=1" in p
if "AND '1'='1" in p or "AND 1=1" in p
] ]
false_payloads = [ false_payloads = [
p for p in boolean_payloads if "AND '1'='2" in p p
or "AND 1=2" in p or "AND 1=0" in 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 = [] true_lengths = []
@ -186,18 +184,12 @@ class SQLiScanner(BaseScanner):
if length_diff > 100 and avg_true != avg_false: if length_diff > 100 and avg_true != avg_false:
return { return {
"vulnerable": "vulnerable": True,
True, "baseline_length": baseline_length,
"baseline_length": "true_condition_avg_length": avg_true,
baseline_length, "false_condition_avg_length": avg_false,
"true_condition_avg_length": "length_difference": length_diff,
avg_true, "confidence": "HIGH" if length_diff > 500 else "MEDIUM",
"false_condition_avg_length":
avg_false,
"length_difference":
length_diff,
"confidence":
"HIGH" if length_diff > 500 else "MEDIUM",
} }
return { return {
@ -213,9 +205,7 @@ class SQLiScanner(BaseScanner):
"description": "Error testing boolean-based SQLi", "description": "Error testing boolean-based SQLi",
} }
def _test_time_based_sqli(self, def _test_time_based_sqli(self, delay_seconds: int = 5) -> dict[str, Any]:
delay_seconds: int = 5) -> dict[str,
Any]:
""" """
Test for time based blind SQL injection Test for time based blind SQL injection
@ -237,14 +227,9 @@ class SQLiScanner(BaseScanner):
all_time_payloads = SQLiPayloads.TIME_BASED_BLIND all_time_payloads = SQLiPayloads.TIME_BASED_BLIND
delay_payloads = { delay_payloads = {
"mysql": "mysql": [p for p in all_time_payloads if "SLEEP" in p],
[p for p in all_time_payloads if "SLEEP" in p], "postgres": [p for p in all_time_payloads if "pg_sleep" in p],
"postgres": [ "mssql": [p for p in all_time_payloads if "WAITFOR" in p],
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 db_type, payloads in delay_payloads.items():
@ -256,13 +241,9 @@ class SQLiScanner(BaseScanner):
response = self.make_request( response = self.make_request(
"GET", "GET",
f"/?id={payload}", f"/?id={payload}",
timeout = delay_seconds + 10, timeout=delay_seconds + 10,
)
elapsed = getattr(
response,
"request_time",
0.0
) )
elapsed = getattr(response, "request_time", 0.0)
delay_times.append(elapsed) delay_times.append(elapsed)
except Exception: except Exception:
@ -273,28 +254,17 @@ class SQLiScanner(BaseScanner):
avg_delay = statistics.mean(delay_times) avg_delay = statistics.mean(delay_times)
if avg_delay >= expected_delay_time - 1: if avg_delay >= expected_delay_time - 1:
confidence = ( confidence = "HIGH" if avg_delay >= expected_delay_time else "MEDIUM"
"HIGH" if avg_delay >= expected_delay_time
else "MEDIUM"
)
return { return {
"vulnerable": "vulnerable": True,
True, "database_type": db_type,
"database_type": "payload": payload,
db_type, "baseline_time": f"{baseline_mean:.3f}s",
"payload": "response_time": f"{avg_delay:.3f}s",
payload, "expected_delay": f"{expected_delay_time:.3f}s",
"baseline_time": "confidence": confidence,
f"{baseline_mean:.3f}s", "individual_times": [f"{t:.3f}s" for t in delay_times],
"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 { return {
@ -314,8 +284,7 @@ class SQLiScanner(BaseScanner):
def _create_vulnerable_result( def _create_vulnerable_result(
self, self,
details: str, details: str,
evidence: dict[str, evidence: dict[str, Any],
Any],
severity: Severity = Severity.CRITICAL, severity: Severity = Severity.CRITICAL,
recommendations: list[str] | None = None, recommendations: list[str] | None = None,
) -> TestResultCreate: ) -> TestResultCreate:
@ -332,10 +301,10 @@ class SQLiScanner(BaseScanner):
TestResultCreate: Vulnerable result TestResultCreate: Vulnerable result
""" """
return TestResultCreate( return TestResultCreate(
test_name = TestType.SQLI, test_name=TestType.SQLI,
status = ScanStatus.VULNERABLE, status=ScanStatus.VULNERABLE,
severity = severity, severity=severity,
details = details, details=details,
evidence_json = evidence, evidence_json=evidence,
recommendations_json = recommendations or [], recommendations_json=recommendations or [],
) )

View File

@ -22,13 +22,14 @@ class ScanRequest(BaseModel):
""" """
Schema for creating a new security scan Schema for creating a new security scan
""" """
target_url: HttpUrl = Field(max_length = settings.URL_MAX_LENGTH)
target_url: HttpUrl = Field(max_length=settings.URL_MAX_LENGTH)
auth_token: str | None = None auth_token: str | None = None
tests_to_run: list[TestType] = Field(min_length = 1) tests_to_run: list[TestType] = Field(min_length=1)
max_requests: int = Field( max_requests: int = Field(
default = settings.DEFAULT_MAX_REQUESTS, default=settings.DEFAULT_MAX_REQUESTS,
ge = 1, ge=1,
le = settings.SCANNER_MAX_CONCURRENT_REQUESTS, le=settings.SCANNER_MAX_CONCURRENT_REQUESTS,
) )
@ -36,7 +37,8 @@ class ScanResponse(BaseModel):
""" """
Schema for scan data in API responses Schema for scan data in API responses
""" """
model_config = ConfigDict(from_attributes = True)
model_config = ConfigDict(from_attributes=True)
id: int id: int
user_id: int user_id: int
@ -57,6 +59,4 @@ class ScanResponse(BaseModel):
""" """
Number of vulnerabilities found Number of vulnerabilities found
""" """
return sum( return sum(1 for r in self.test_results if r.status == "vulnerable")
1 for r in self.test_results if r.status == "vulnerable"
)

View File

@ -22,19 +22,21 @@ class TestResultCreate(BaseModel):
""" """
Schema for creating a new test result (used by scanners) Schema for creating a new test result (used by scanners)
""" """
test_name: TestType test_name: TestType
status: ScanStatus status: ScanStatus
severity: Severity severity: Severity
details: str details: str
evidence_json: dict[str, Any] = Field(default_factory = dict) evidence_json: dict[str, Any] = Field(default_factory=dict)
recommendations_json: list[str] = Field(default_factory = list) recommendations_json: list[str] = Field(default_factory=list)
class TestResultResponse(BaseModel): class TestResultResponse(BaseModel):
""" """
Schema for individual test result in API responses Schema for individual test result in API responses
""" """
model_config = ConfigDict(from_attributes = True)
model_config = ConfigDict(from_attributes=True)
id: int id: int
scan_id: int scan_id: int

View File

@ -8,13 +8,7 @@ from __future__ import annotations
import re import re
from datetime import datetime from datetime import datetime
from pydantic import ( from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
BaseModel,
ConfigDict,
EmailStr,
Field,
field_validator
)
from config import settings from config import settings
@ -22,24 +16,25 @@ class UserCreate(BaseModel):
""" """
Schema for user registration request. Schema for user registration request.
""" """
email: EmailStr email: EmailStr
password: str = Field( password: str = Field(
min_length = settings.PASSWORD_MIN_LENGTH, min_length=settings.PASSWORD_MIN_LENGTH,
max_length = settings.PASSWORD_MAX_LENGTH, max_length=settings.PASSWORD_MAX_LENGTH,
) )
@field_validator('password') @field_validator("password")
@classmethod @classmethod
def validate_password_strength(cls, v: str) -> str: def validate_password_strength(cls, v: str) -> str:
""" """
Validate password meets security requirements Validate password meets security requirements
""" """
if not re.search(r'[A-Z]', v): if not re.search(r"[A-Z]", v):
raise ValueError('Password must contain at least one uppercase letter') raise ValueError("Password must contain at least one uppercase letter")
if not re.search(r'[a-z]', v): if not re.search(r"[a-z]", v):
raise ValueError('Password must contain at least one lowercase letter') raise ValueError("Password must contain at least one lowercase letter")
if not re.search(r'[0-9]', v): if not re.search(r"[0-9]", v):
raise ValueError('Password must contain at least one number') raise ValueError("Password must contain at least one number")
return v return v
@ -47,6 +42,7 @@ class UserLogin(BaseModel):
""" """
Schema for user login request. Schema for user login request.
""" """
email: EmailStr email: EmailStr
password: str password: str
@ -56,7 +52,8 @@ class UserResponse(BaseModel):
Schema for user data in API responses. Schema for user data in API responses.
Excludes sensitive fields like hashed_password. Excludes sensitive fields like hashed_password.
""" """
model_config = ConfigDict(from_attributes = True)
model_config = ConfigDict(from_attributes=True)
id: int id: int
email: str email: str
@ -68,5 +65,6 @@ class TokenResponse(BaseModel):
""" """
Schema for JWT token response. Schema for JWT token response.
""" """
access_token: str access_token: str
token_type: str = "bearer" token_type: str = "bearer"

View File

@ -28,11 +28,9 @@ class AuthService:
""" """
User registration, login, and token generation User registration, login, and token generation
""" """
@staticmethod @staticmethod
def register_user( def register_user(db: Session, user_data: UserCreate) -> UserResponse:
db: Session,
user_data: UserCreate
) -> UserResponse:
""" """
Register a new user Register a new user
@ -43,31 +41,25 @@ class AuthService:
Returns: Returns:
UserResponse: Created user data UserResponse: Created user data
""" """
existing_user = UserRepository.get_by_email( existing_user = UserRepository.get_by_email(db, user_data.email)
db,
user_data.email
)
if existing_user: if existing_user:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail = "Email already registered", detail="Email already registered",
) )
hashed_password = hash_password(user_data.password) hashed_password = hash_password(user_data.password)
user = UserRepository.create_user( user = UserRepository.create_user(
db = db, db=db,
email = user_data.email, email=user_data.email,
hashed_password = hashed_password, hashed_password=hashed_password,
) )
return UserResponse.model_validate(user) return UserResponse.model_validate(user)
@staticmethod @staticmethod
def login_user( def login_user(db: Session, login_data: UserLogin) -> TokenResponse:
db: Session,
login_data: UserLogin
) -> TokenResponse:
""" """
Authenticate user and generate access token Authenticate user and generate access token
@ -82,40 +74,31 @@ class AuthService:
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail = "Invalid email or password", detail="Invalid email or password",
) )
if not verify_password(login_data.password, if not verify_password(login_data.password, user.hashed_password):
user.hashed_password):
raise HTTPException( raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail = "Invalid email or password", detail="Invalid email or password",
) )
if not user.is_active: if not user.is_active:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail = "Account is inactive", detail="Account is inactive",
) )
access_token = create_access_token( access_token = create_access_token(
data = {"sub": user.email}, data={"sub": user.email},
expires_delta = timedelta( expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
),
) )
return TokenResponse( return TokenResponse(access_token=access_token, token_type="bearer")
access_token = access_token,
token_type = "bearer"
)
@staticmethod @staticmethod
def get_user_by_email( def get_user_by_email(db: Session, email: str) -> UserResponse | None:
db: Session,
email: str
) -> UserResponse | None:
""" """
Get user by email address Get user by email address

View File

@ -5,8 +5,6 @@ Coordinates scanners and saves results
from __future__ import annotations from __future__ import annotations
from typing import Type
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from fastapi import HTTPException, status from fastapi import HTTPException, status
@ -27,12 +25,9 @@ class ScanService:
""" """
Orchestrates security scanning workflow Orchestrates security scanning workflow
""" """
@staticmethod @staticmethod
def run_scan( def run_scan(db: Session, user_id: int, scan_request: ScanRequest) -> ScanResponse:
db: Session,
user_id: int,
scan_request: ScanRequest
) -> ScanResponse:
""" """
Execute security scan with selected tests Execute security scan with selected tests
@ -45,12 +40,12 @@ class ScanService:
ScanResponse: Scan results with all test outcomes ScanResponse: Scan results with all test outcomes
""" """
scan = ScanRepository.create_scan( scan = ScanRepository.create_scan(
db = db, db=db,
user_id = user_id, user_id=user_id,
target_url = str(scan_request.target_url), target_url=str(scan_request.target_url),
) )
scanner_mapping: dict[TestType, Type[BaseScanner]] = { scanner_mapping: dict[TestType, type[BaseScanner]] = {
TestType.RATE_LIMIT: RateLimitScanner, TestType.RATE_LIMIT: RateLimitScanner,
TestType.AUTH: AuthScanner, TestType.AUTH: AuthScanner,
TestType.SQLI: SQLiScanner, TestType.SQLI: SQLiScanner,
@ -60,16 +55,16 @@ class ScanService:
results: list[TestResultCreate] = [] results: list[TestResultCreate] = []
for test_type in scan_request.tests_to_run: for test_type in scan_request.tests_to_run:
scanner_class: Type[BaseScanner] | None = scanner_mapping.get(test_type) scanner_class: type[BaseScanner] | None = scanner_mapping.get(test_type)
if not scanner_class: if not scanner_class:
continue continue
try: try:
scanner = scanner_class( scanner = scanner_class(
target_url = str(scan_request.target_url), target_url=str(scan_request.target_url),
auth_token = scan_request.auth_token, auth_token=scan_request.auth_token,
max_requests = scan_request.max_requests, max_requests=scan_request.max_requests,
) )
result = scanner.scan() result = scanner.scan()
@ -78,12 +73,12 @@ class ScanService:
except Exception as e: except Exception as e:
results.append( results.append(
TestResultCreate( TestResultCreate(
test_name = test_type, test_name=test_type,
status = "error", status="error",
severity = "info", severity="info",
details = f"Scanner error: {str(e)}", details=f"Scanner error: {str(e)}",
evidence_json = {"error": str(e)}, evidence_json={"error": str(e)},
recommendations_json = [ recommendations_json=[
"Check target URL is accessible", "Check target URL is accessible",
"Verify authentication token if provided", "Verify authentication token if provided",
], ],
@ -92,14 +87,14 @@ class ScanService:
for result in results: for result in results:
TestResultRepository.create_test_result( TestResultRepository.create_test_result(
db = db, db=db,
scan_id = scan.id, scan_id=scan.id,
test_name = result.test_name, test_name=result.test_name,
status = result.status, status=result.status,
severity = result.severity, severity=result.severity,
details = result.details, details=result.details,
evidence_json = result.evidence_json, evidence_json=result.evidence_json,
recommendations_json = result.recommendations_json, recommendations_json=result.recommendations_json,
) )
db.refresh(scan) db.refresh(scan)
@ -107,11 +102,7 @@ class ScanService:
return ScanResponse.model_validate(scan) return ScanResponse.model_validate(scan)
@staticmethod @staticmethod
def get_scan_by_id( def get_scan_by_id(db: Session, scan_id: int, user_id: int) -> ScanResponse:
db: Session,
scan_id: int,
user_id: int
) -> ScanResponse:
""" """
Get scan by ID with authorization check Get scan by ID with authorization check
@ -127,24 +118,21 @@ class ScanService:
if not scan: if not scan:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail = "Scan not found", detail="Scan not found",
) )
if scan.user_id != user_id: if scan.user_id != user_id:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail = "Not authorized to access this scan", detail="Not authorized to access this scan",
) )
return ScanResponse.model_validate(scan) return ScanResponse.model_validate(scan)
@staticmethod @staticmethod
def get_user_scans( def get_user_scans(
db: Session, db: Session, user_id: int, skip: int = 0, limit: int | None = None
user_id: int,
skip: int = 0,
limit: int | None = None
) -> list[ScanResponse]: ) -> list[ScanResponse]:
""" """
Get all scans for a user with pagination Get all scans for a user with pagination
@ -158,12 +146,7 @@ class ScanService:
Returns: Returns:
list[ScanResponse]: List of user's scans list[ScanResponse]: List of user's scans
""" """
scans = ScanRepository.get_by_user( scans = ScanRepository.get_by_user(db=db, user_id=user_id, skip=skip, limit=limit)
db = db,
user_id = user_id,
skip = skip,
limit = limit
)
return [ScanResponse.model_validate(scan) for scan in scans] return [ScanResponse.model_validate(scan) for scan in scans]
@ -184,14 +167,14 @@ class ScanService:
if not scan: if not scan:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail = "Scan not found", detail="Scan not found",
) )
if scan.user_id != user_id: if scan.user_id != user_id:
raise HTTPException( raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail = "Not authorized to delete this scan", detail="Not authorized to delete this scan",
) )
return ScanRepository.delete(db, scan_id) return ScanRepository.delete(db, scan_id)

View File

@ -20,7 +20,7 @@
{ {
"files": "*.tsx", "files": "*.tsx",
"options": { "options": {
"printWidth": 77, "printWidth": 77,
"jsxSingleQuote": false, "jsxSingleQuote": false,
"bracketSameLine": false "bracketSameLine": false
} }
@ -28,7 +28,7 @@
{ {
"files": "*.ts", "files": "*.ts",
"options": { "options": {
"printWidth": 77, "printWidth": 77,
"parser": "typescript" "parser": "typescript"
} }
}, },

View File

@ -17,7 +17,7 @@ export default tseslint.config(
}, },
js.configs.recommended, js.configs.recommended,
...tseslint.configs.strictTypeChecked, ...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked, ...tseslint.configs.stylisticTypeChecked,
@ -49,10 +49,10 @@ export default tseslint.config(
rules: { rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }], '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }],
'@typescript-eslint/explicit-function-return-type': ['error', { '@typescript-eslint/explicit-function-return-type': ['error', {
allowExpressions: true, allowExpressions: true,
allowTypedFunctionExpressions: true, allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: true, allowHigherOrderFunctions: true,
allowDirectConstAssertionInArrowFunctions: true, allowDirectConstAssertionInArrowFunctions: true,
allowedNames: ['Component'] allowedNames: ['Component']
}], }],
@ -60,9 +60,9 @@ export default tseslint.config(
'@typescript-eslint/no-non-null-assertion': 'error', '@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/array-type': ['error', { default: 'array' }], '@typescript-eslint/array-type': ['error', { default: 'array' }],
'@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-confusing-void-expression': 'off', '@typescript-eslint/no-confusing-void-expression': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off', '@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/strict-boolean-expressions': ['error', { '@typescript-eslint/strict-boolean-expressions': ['error', {
allowString: false, allowString: false,
allowNumber: false, allowNumber: false,

View File

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

View File

@ -15,7 +15,7 @@
"isolatedModules": true, "isolatedModules": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"]
}, },
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,

View File

@ -28,6 +28,6 @@
"react", "react",
"docker" "docker"
], ],
"author": "CarerPerez-dev - also replace my github url username with yours", "author": "CarerPerez-dev - also replace my github url username with yours",
"license": "MIT" "license": "MIT"
} }

View File

@ -3,7 +3,7 @@ CarterPerez-dev | 2025
This keylogger demonstrates: This keylogger demonstrates:
keyboard event capture, log management, and remote delivery keyboard event capture, log management, and remote delivery
Unauthorized use of keyloggers is illegal. Unauthorized use of keyloggers is illegal.
Only use on systems you own or have permission to monitor Only use on systems you own or have permission to monitor
""" """
@ -11,11 +11,11 @@ import sys
import logging import logging
import platform import platform
from enum import ( from enum import (
Enum, Enum,
auto, auto,
) )
from threading import ( from threading import (
Event, Event,
Lock, Lock,
) )
from pathlib import Path from pathlib import Path

View File

@ -822,5 +822,3 @@ A collection of tools, courses, frameworks, and educational resources for cyber
- Use practice exams to measure progress - Use practice exams to measure progress
- Join communities for support and networking - Join communities for support and networking
- Stay current with security news and trends - Stay current with security news and trends