commit
7f8068227c
|
|
@ -0,0 +1,48 @@
|
|||
# =============================================================================
|
||||
# AngelaMos | 2025
|
||||
# publish-api-rate-limter.yml
|
||||
# =============================================================================
|
||||
|
||||
name: Publish api-rate-limiter to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'PROJECTS/api-rate-limiter/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload api-rate-limiter to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/fastapi-420
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
|
||||
- name: Build package
|
||||
working-directory: PROJECTS/fastapi-420
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: PROJECTS/fastapi-420/dist/
|
||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 CarterPerez-dev | CertGames.com
|
||||
Copyright (c) 2026 CarterPerez-dev | CertGames.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -20,6 +20,4 @@ examples/certs/
|
|||
*.log
|
||||
|
||||
# Private progress tracking
|
||||
PROGRESS.md
|
||||
decisions.md
|
||||
notes.md
|
||||
refer/
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# Makefile
|
||||
# ⒸAngelaMos | 2025
|
||||
# Alias commands to easily run commands (use this or package.json scripts)
|
||||
|
||||
.PHONY: help dev dev-build dev-down dev-logs prod prod-build prod-down prod-logs clean clean-all
|
||||
|
||||
help:
|
||||
@echo "API Security Scanner - Available Commands"
|
||||
@echo "=========================================="
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " make dev - Start development environment"
|
||||
@echo " make dev-build - Build and start development environment"
|
||||
@echo " make dev-down - Stop development environment"
|
||||
@echo " make dev-logs - View development logs (follow mode)"
|
||||
@echo ""
|
||||
@echo "Production:"
|
||||
@echo " make prod - Start production environment (detached)"
|
||||
@echo " make prod-build - Build and start production environment"
|
||||
@echo " make prod-down - Stop production environment"
|
||||
@echo " make prod-logs - View production logs (follow mode)"
|
||||
@echo ""
|
||||
@echo "Cleanup:"
|
||||
@echo " make clean - Stop all containers and remove volumes"
|
||||
@echo " make clean-all - Clean + remove all Docker images/cache"
|
||||
@echo ""
|
||||
|
||||
dev:
|
||||
docker compose -f docker-compose.dev.yml up
|
||||
|
||||
dev-build:
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
|
||||
dev-down:
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
|
||||
dev-logs:
|
||||
docker compose -f docker-compose.dev.yml logs -f
|
||||
|
||||
prod:
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
prod-build:
|
||||
docker compose -f docker-compose.prod.yml up --build -d
|
||||
|
||||
prod-down:
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
|
||||
prod-logs:
|
||||
docker compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
clean:
|
||||
docker compose -f docker-compose.dev.yml down -v
|
||||
docker compose -f docker-compose.prod.yml down -v
|
||||
|
||||
clean-all: clean
|
||||
docker system prune -af --volumes
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
# CertGames Backend Makefile
|
||||
# 2025 | ©AngelaMos
|
||||
|
||||
|
||||
PYTHON := python
|
||||
|
||||
.DEFAULT_GOAL := dev
|
||||
|
||||
MAKEFLAGS += --no-print-directory
|
||||
|
||||
.PHONY: help dev install reinstall check fix mypy clean tree TODO pylint
|
||||
|
||||
|
||||
define ASCII
|
||||
@echo "\033[35m"
|
||||
@echo " ⡋⣡⣴⣶⣶⡀⠄⠄⠙⢿⣿⣿⣿⣿⣿⣴⣿⣿⣿⢃⣤⣄⣀⣥⣿"
|
||||
@echo " ⢸⣇⠻⣿⣿⣿⣧⣀⢀⣠⡌⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⣿⣿"
|
||||
@echo " ⢸⣿⣷⣤⣤⣤⣬⣙⣛⢿⣿⣿⣿⣿⣿⣿⡿⣿⣿⡍⠄⠄⢀⣤⣄⠉"
|
||||
@echo " ⣖⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⢇⣿⣿⡷⠶⠶⢿⣿⣿⠇⢀"
|
||||
@echo " ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣷⣶⣥⣴"
|
||||
@echo " ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿"
|
||||
@echo " ⣦⣌⣛⣻⣿⣿⣧⠙⠛⠛⡭⠅⠒⠦⠭⣭⡻⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠄"
|
||||
@echo " ⣿⣿⣿⣿⣿⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠄⠹⠈⢋⣽⣿⣿⣿⣿⣵⣾"
|
||||
@echo " ⣿⣿⣿⣿⣿⣿⣿⣿⠄⣴⣿⣶⣄⠄⣴⣶⠄⢀⣾⣿⣿⣿⣿⣿⣿⠃⠄⠄"
|
||||
@echo " ⠈⠻⣿⣿⣿⣿⣿⣿⡄⢻⣿⣿⣿⠄⣿⣿⡀⣾⣿⣿⣿⣿⣛⠛⠁"
|
||||
@echo " ⠄⠄⠈⠛⢿⣿⣿⣿⠁⠞⢿⣿⣿⡄⢿⣿⡇⣸⣿⣿⠿⠛⠁⠄"
|
||||
@echo " ⠄⠄⠄⠄⠄⠉⠻⣿⣿⣾⣦⡙⠻⣷⣾⣿⠃⠿⠋⠁⠄"
|
||||
@echo "\033[0m"
|
||||
endef
|
||||
|
||||
|
||||
|
||||
help:
|
||||
@echo "\033[1;91mC\033[1;92me\033[1;93mr\033[1;94mt\033[1;95mG\033[1;96ma\033[1;91mm\033[1;92me\033[1;93ms\033[0m \033[1;94mBackend\033[0m \033[1;95mTesting\033[0m \033[1;96mCommands\033[0m"
|
||||
@echo "\033[1;5;93m==============================================\033[0m"
|
||||
@echo ""
|
||||
@echo "\033[1;4;96mSetup:\033[0m"
|
||||
@echo " \033[92mmake \033[36minstall-dev\033[0m \033[94m- Install development dependencies\033[0m"
|
||||
@echo " \033[92mmake \033[36mreinstall\033[0m \033[94m- Reinstall all dependencies with upgrades\033[0m"
|
||||
@echo ""
|
||||
@echo "\033[1;4;91mSyntax Checking (Ruff):\033[0m"
|
||||
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Check all code for syntax issues\033[0m"
|
||||
@echo "\033[1;4;91mSyntax Fixing (Ruff):\033[0m"
|
||||
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Fix all code of syntax issues\033[0m"
|
||||
@echo ""
|
||||
@echo "\033[1;4;94mType Checking (mypy):\033[0m"
|
||||
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Type check all code\033[0m"
|
||||
@echo "\033[1;4;94mFormat:\033[0m"
|
||||
@echo " \033[92mmake \033[36mqa\033[0m \033[94m- Format code\033[0m"
|
||||
@echo ""
|
||||
@echo "\033[1;4;92mUtilities:\033[0m"
|
||||
@echo " \033[92mmake \033[36mclean\033[0m \033[94m- Remove all cache files\033[0m"
|
||||
@echo " \033[92mmake \033[36mtree\033[0m \033[94m- Display the file tree\033[0m"
|
||||
@echo " \033[92mmake \033[36mTODO\033[0m \033[94m- Find All TODO \033[0m"
|
||||
@echo ""
|
||||
@echo "\033[1;93mCurrent default domain: \033[1;5;95m$(DOMAIN)\033[0m"
|
||||
@echo ""
|
||||
@echo "\033[1;4;93mAvailable domains:\033[0m"
|
||||
@echo " \033[91maccount\033[0m, \033[92mcommunity\033[0m, \033[93mcontent\033[0m, \033[94mfreemium\033[0m, \033[95mgames\033[0m,"
|
||||
@echo " \033[96mmarketing\033[0m, \033[91mprogression\033[0m, \033[92mshop\033[0m, \033[93mtesting\033[0m, \033[94mtools\033[0m"
|
||||
@echo ""
|
||||
@echo "\033[95m ------------------------------"
|
||||
@echo "\033[34m ⣿⣿⣿⡷⠊⡢⡹⣦⡑⢂⢕⢂⢕⢂⢕⢂⠕⠔⠌⠝⠛⠶⠶⢶⣦⣄⢂⢕⢂⢕"
|
||||
@echo "\033[34m ⣿⣿⠏⣠⣾⣦⡐⢌⢿⣷⣦⣅⡑⠕⠡⠐⢿⠿⣛⠟⠛⠛⠛⠛⠡⢷⡈⢂⢕⢂"
|
||||
@echo "\033[34m ⠟⣡⣾⣿⣿⣿⣿⣦⣑⠝⢿⣿⣿⣿⣿⣿⡵⢁⣤⣶⣶⣿⢿⢿⢿⡟⢻⣤⢑⢂"
|
||||
@echo "\033[36m ⣾⣿⣿⡿⢟⣛⣻⣿⣿⣿⣦⣬⣙⣻⣿⣿⣷⣿⣿⢟⢝⢕⢕⢕⢕⢽⣿⣿⣷⣔"
|
||||
@echo "\033[36m ⣿⣿⠵⠚⠉⢀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⢕⢕⢕⢕⢕⢕⣽⣿⣿⣿⣿"
|
||||
@echo "\033[36m ⢷⣂⣠⣴⣾⡿⡿⡻⡻⣿⣿⣴⣿⣿⣿⣿⣿⣿⣷⣵⣵⣵⣷⣿⣿⣿⣿⣿⣿⡿"
|
||||
@echo "\033[36m ⢌⠻⣿⡿⡫⡪⡪⡪⡪⣺⣿⣿⣿⣿⣿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃"
|
||||
@echo "\033[32m ⠣⡁⠹⡪⡪⡪⡪⣪⣾⣿⣿⣿⣿⠋⠐⢉⢍⢄⢌⠻⣿⣿⣿⣿⣿⣿⣿⣿⠏⠈"
|
||||
@echo "\033[32m ⡣⡘⢄⠙⣾⣾⣾⣿⣿⣿⣿⣿⣿⡀⢐⢕⢕⢕⢕⢕⡘⣿⣿⣿⣿⣿⣿⠏⠠⠈"
|
||||
@echo "\033[32m ⠌⢊⢂⢣⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢐⢕⢕⢕⢕⢕⢅⣿⣿⣿⣿⡿⢋⢜⠠⠈"
|
||||
@echo "\033[95m ------------------------------"
|
||||
@echo "\033[0m"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
venv:
|
||||
@echo "Creating and initializing virtual environment in .venv..."
|
||||
@python3 -m venv .venv
|
||||
|
||||
# If you use bash, change @zsh
|
||||
# can also run as just 'make'
|
||||
dev:
|
||||
@zsh -c "source .venv/bin/activate && exec zsh"
|
||||
$(call ASCII)
|
||||
|
||||
# ==================== Setup ====================
|
||||
install:
|
||||
@echo "Installing development dependencies..."
|
||||
pip install -e ".[dev]"
|
||||
|
||||
reinstall:
|
||||
@echo "Reinstalling all dependencies (including upgrades)..."
|
||||
pip install -e ".[dev]" --upgrade --upgrade-strategy eager
|
||||
$(call ASCII)
|
||||
|
||||
|
||||
# ==================== Syntax Checking (Ruff) ====================
|
||||
check:
|
||||
@echo "\033[1;3;4;96m======== Checking All Code ========\033[0m"
|
||||
@ruff check .
|
||||
$(call ASCII)
|
||||
|
||||
|
||||
# ==================== Syntax Checking (Ruff) ====================
|
||||
fix:
|
||||
@echo "\033[1;3;4;96m======== ruff fix All Code ========\033[0m"
|
||||
@ruff check . --fix
|
||||
$(call ASCII)
|
||||
|
||||
|
||||
# ======================= In Depth Linting (pylint)=======================
|
||||
pylint:
|
||||
@echo "\033[1;3;4;96m======== Linting All Code ========\033[0m"
|
||||
@pylint .
|
||||
$(call ASCII)
|
||||
|
||||
# ======================= Type Checking (mypy) =======================
|
||||
mypy:
|
||||
@echo "\033[1;3;4;96m======== Type Checking All Code ========\033[0m"
|
||||
@mypy .
|
||||
$(call ASCII)
|
||||
|
||||
|
||||
# ======================== Yapf Format =============================
|
||||
format:
|
||||
@echo "\033[1;3;4;96m======== Formatting only qa/ ========\033[0m"
|
||||
@yapf -i -r -vv models/ repositories/ schemas/ scanners/ core/ factory/
|
||||
$(call ASCII)
|
||||
|
||||
# ==================== Utilities ====================
|
||||
clean:
|
||||
@echo "\033[1;3;4;96m======== Cleaning Cache Files ========\033[0m"
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name ".coverage" -delete
|
||||
$(call ASCII)
|
||||
|
||||
tree:
|
||||
@echo "\033[1;3;4;96m======== Creating Tree ========\033[0m"
|
||||
@tree -I 'node_modules|.git|*.log|dist|build|*.cache|certgames.egg-info|context|.venv|*.env|qa_context'
|
||||
$(call ASCII)
|
||||
|
||||
TODO:
|
||||
@echo "\033[1;3;4;96m======== Looking for TODO's ========\033[0m"
|
||||
@find . -name "*.py" -print0 | xargs -0 pylint --disable=all --enable=W0511 --msg-template='{path}:{line}:{column}: {msg_id}: {msg} ({symbol})' || true
|
||||
$(call ASCII)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# API Security Scanner Backend Commands
|
||||
# AngelaMos | 2025
|
||||
|
||||
set dotenv-load
|
||||
set export
|
||||
set shell := ["bash", "-uc"]
|
||||
|
||||
# =============================================================================
|
||||
# Setup (Local Development)
|
||||
# =============================================================================
|
||||
|
||||
# Create venv and install all dependencies
|
||||
[group('setup')]
|
||||
setup:
|
||||
@echo "Creating virtual environment with uv..."
|
||||
uv venv
|
||||
uv sync --all-extras
|
||||
@echo "✓ Setup complete"
|
||||
|
||||
# Install dependencies
|
||||
[group('setup')]
|
||||
install:
|
||||
uv sync
|
||||
|
||||
# Reinstall all dependencies with upgrades
|
||||
[group('setup')]
|
||||
reinstall:
|
||||
uv lock --upgrade
|
||||
uv sync --all-extras
|
||||
|
||||
# =============================================================================
|
||||
# Linting & Formatting
|
||||
# =============================================================================
|
||||
|
||||
# Check all code for syntax issues (ruff)
|
||||
[group('lint')]
|
||||
check:
|
||||
@echo "======== Checking All Code ========"
|
||||
uv run ruff check .
|
||||
|
||||
# Fix all code syntax issues (ruff)
|
||||
[group('lint')]
|
||||
fix:
|
||||
@echo "======== Ruff Fix All Code ========"
|
||||
uv run ruff check . --fix
|
||||
|
||||
# In-depth linting (pylint)
|
||||
[group('lint')]
|
||||
pylint:
|
||||
@echo "======== Linting All Code ========"
|
||||
uv run pylint .
|
||||
|
||||
# Type checking (mypy)
|
||||
[group('lint')]
|
||||
mypy:
|
||||
@echo "======== Type Checking All Code ========"
|
||||
uv run mypy .
|
||||
|
||||
# Format code (yapf)
|
||||
[group('lint')]
|
||||
format:
|
||||
@echo "======== Formatting Code ========"
|
||||
uv run yapf -i -r -vv models/ repositories/ schemas/ scanners/ core/ factory/
|
||||
|
||||
# Run all quality checks
|
||||
[group('lint')]
|
||||
qa: check mypy
|
||||
@echo "✓ All quality checks passed"
|
||||
|
||||
# =============================================================================
|
||||
# Utilities
|
||||
# =============================================================================
|
||||
|
||||
# Clean cache files
|
||||
[group('utility')]
|
||||
clean:
|
||||
@echo "======== Cleaning Cache Files ========"
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name ".coverage" -delete
|
||||
@echo "✓ Cleaned"
|
||||
|
||||
# Display file tree
|
||||
[group('utility')]
|
||||
tree:
|
||||
@echo "======== File Tree ========"
|
||||
tree -I 'node_modules|.git|*.log|dist|build|*.cache|*.egg-info|context|.venv|*.env'
|
||||
|
||||
# Find all TODOs
|
||||
[group('utility')]
|
||||
todo:
|
||||
@echo "======== Looking for TODOs ========"
|
||||
find . -name "*.py" -print0 | xargs -0 uv run pylint --disable=all --enable=W0511 --msg-template='{path}:{line}:{column}: {msg_id}: {msg} ({symbol})' || true
|
||||
|
||||
# Lock dependencies
|
||||
[group('utility')]
|
||||
lock:
|
||||
uv lock
|
||||
|
||||
# Update dependencies
|
||||
[group('utility')]
|
||||
update:
|
||||
uv lock --upgrade
|
||||
uv sync --all-extras
|
||||
|
|
@ -50,26 +50,21 @@ dev = [
|
|||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=80.9.0", "wheel>=0.45.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = [
|
||||
"main",
|
||||
"config",
|
||||
"factory"
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = [
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = [
|
||||
"main.py",
|
||||
"config.py",
|
||||
"factory.py",
|
||||
"core",
|
||||
"routes",
|
||||
"models",
|
||||
"schemas",
|
||||
"services",
|
||||
"scanners",
|
||||
"repositories"
|
||||
"repositories",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# ⒸAngelaMos | 2025
|
||||
|
||||
# FastAPI and server
|
||||
fastapi>=0.109.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
python-multipart>=0.0.6
|
||||
|
||||
# Database
|
||||
sqlalchemy>=2.0.25
|
||||
psycopg2-binary>=2.9.9
|
||||
alembic>=1.13.0
|
||||
|
||||
# Security
|
||||
slowapi>=0.1.9
|
||||
python-jose[cryptography]>=3.3.0
|
||||
bcrypt==5.0.0
|
||||
|
||||
# HTTP client for scanners
|
||||
requests>=2.31.0
|
||||
httpx>=0.26.0
|
||||
aiohttp>=3.9.0
|
||||
|
||||
# Settings management
|
||||
pydantic>=2.5.0
|
||||
pydantic-settings>=2.1.0
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Utilities
|
||||
email-validator>=2.1.0
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# API Security Scanner - Development Commands
|
||||
# AngelaMos | 2025
|
||||
|
||||
set dotenv-load
|
||||
set export
|
||||
set shell := ["bash", "-uc"]
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
|
||||
mod backend 'backend/backend.just'
|
||||
|
||||
# Show available commands
|
||||
default:
|
||||
@just --list --unsorted
|
||||
|
||||
# =============================================================================
|
||||
# Development
|
||||
# =============================================================================
|
||||
|
||||
# Start development environment
|
||||
[group('dev')]
|
||||
dev:
|
||||
docker compose -f dev.compose.yml up
|
||||
|
||||
# Build and start development environment
|
||||
[group('dev')]
|
||||
dev-build:
|
||||
docker compose -f dev.compose.yml up --build
|
||||
|
||||
# Stop development environment
|
||||
[group('dev')]
|
||||
dev-down:
|
||||
docker compose -f dev.compose.yml down
|
||||
|
||||
# View development logs (follow mode)
|
||||
[group('dev')]
|
||||
dev-logs *SERVICE:
|
||||
docker compose -f dev.compose.yml logs -f {{SERVICE}}
|
||||
|
||||
# Shell into a dev container
|
||||
[group('dev')]
|
||||
dev-shell service='backend':
|
||||
docker compose -f dev.compose.yml exec -it {{service}} /bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# Production
|
||||
# =============================================================================
|
||||
|
||||
# Start production environment (detached)
|
||||
[group('prod')]
|
||||
prod:
|
||||
docker compose up -d
|
||||
|
||||
# Build and start production environment
|
||||
[group('prod')]
|
||||
prod-build:
|
||||
docker compose up --build -d
|
||||
|
||||
# Stop production environment
|
||||
[group('prod')]
|
||||
prod-down:
|
||||
docker compose down
|
||||
|
||||
# View production logs (follow mode)
|
||||
[group('prod')]
|
||||
prod-logs *SERVICE:
|
||||
docker compose logs -f {{SERVICE}}
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup
|
||||
# =============================================================================
|
||||
|
||||
# Stop all containers and remove volumes
|
||||
[group('cleanup')]
|
||||
clean:
|
||||
docker compose -f dev.compose.yml down -v
|
||||
docker compose down -v
|
||||
|
||||
# Clean + remove all Docker images/cache
|
||||
[group('cleanup')]
|
||||
[confirm("This will remove all Docker images and cache. Continue?")]
|
||||
clean-all: clean
|
||||
docker system prune -af --volumes
|
||||
|
||||
# =============================================================================
|
||||
# Utilities
|
||||
# =============================================================================
|
||||
|
||||
# Show running containers
|
||||
ps:
|
||||
docker compose ps
|
||||
docker compose -f dev.compose.yml ps
|
||||
|
||||
# Execute command in running backend container
|
||||
exec *CMD:
|
||||
docker compose -f dev.compose.yml exec backend {{CMD}}
|
||||
|
||||
# Run one-off command in backend (new container)
|
||||
run *CMD:
|
||||
docker compose -f dev.compose.yml run --rm backend {{CMD}}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
# How The Scanner Is Built
|
||||
|
||||
This doc explains the architecture decisions. Not how to use it, but why it works the way it does.
|
||||
|
||||
## The Layer Cake
|
||||
|
||||
The application follows a standard layered architecture.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Routes (API) │ FastAPI endpoints
|
||||
├─────────────────────────────────────┤
|
||||
│ Services │ Business logic
|
||||
├─────────────────────────────────────┤
|
||||
│ Repositories / Scanners │ Data access / Scanning
|
||||
├─────────────────────────────────────┤
|
||||
│ Models │ SQLAlchemy ORM
|
||||
├─────────────────────────────────────┤
|
||||
│ Database │ PostgreSQL
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Routes** handle HTTP requests, validate input with Pydantic, and return responses. They do not contain business logic.
|
||||
|
||||
**Services** orchestrate operations. ScanService coordinates which scanners to run and saves results. AuthService handles user registration and login.
|
||||
|
||||
**Repositories** abstract database operations. This keeps SQL queries out of services and makes testing easier.
|
||||
|
||||
**Scanners** are the security testing engines. Each scanner inherits from BaseScanner and implements a scan() method.
|
||||
|
||||
## The Scanner Pattern
|
||||
|
||||
All scanners inherit from BaseScanner which provides common functionality.
|
||||
|
||||
```python
|
||||
class BaseScanner(ABC):
|
||||
def __init__(self, target_url, auth_token, max_requests):
|
||||
self.target_url = target_url
|
||||
self.session = self._create_session()
|
||||
# ...
|
||||
|
||||
def make_request(self, method, endpoint, **kwargs):
|
||||
# Rate limiting, retries, timing
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def scan(self) -> TestResultCreate:
|
||||
# Implemented by each scanner
|
||||
pass
|
||||
```
|
||||
|
||||
This design means:
|
||||
- Common HTTP logic lives in one place
|
||||
- Request spacing and retry logic is consistent
|
||||
- Each scanner focuses only on its detection logic
|
||||
- Adding a new scanner is straightforward
|
||||
|
||||
### Request Spacing
|
||||
|
||||
Scanners do not blast requests at targets. Each request is spaced to avoid overwhelming the target or triggering defensive rate limits.
|
||||
|
||||
```python
|
||||
required_delay = 1.0 / (max_requests / window_seconds)
|
||||
```
|
||||
|
||||
With default settings of 100 requests per 60 second window, that is about 600ms between requests. Random jitter is added to avoid predictable patterns.
|
||||
|
||||
This matters because:
|
||||
1. You do not want to DoS your own production systems during testing
|
||||
2. Aggressive scanning triggers alerts and gets you blocked
|
||||
3. Some timing attacks need consistent baseline measurements
|
||||
|
||||
### Retry Logic
|
||||
|
||||
Requests that fail get retried with exponential backoff.
|
||||
|
||||
```python
|
||||
retry_count = 0
|
||||
backoff_factor = 2.0
|
||||
|
||||
while retry_count <= max_retries:
|
||||
response = session.request(method, url)
|
||||
|
||||
if response.status_code == 429:
|
||||
wait_time = int(response.headers.get("Retry-After", default_wait))
|
||||
time.sleep(wait_time)
|
||||
retry_count += 1
|
||||
continue
|
||||
|
||||
if response.status_code >= 500:
|
||||
wait_time = backoff_factor ** retry_count
|
||||
time.sleep(wait_time)
|
||||
retry_count += 1
|
||||
continue
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
429 responses respect the Retry-After header. 5xx errors trigger exponential backoff. This keeps the scanner resilient against temporary failures.
|
||||
|
||||
## Evidence Collection
|
||||
|
||||
Every scan result includes evidence. This is not just for debugging. Proper evidence is required for professional security reports.
|
||||
|
||||
```python
|
||||
evidence = {
|
||||
"status_code": response.status_code,
|
||||
"response_time_ms": elapsed * 1000,
|
||||
"response_length": len(response.text),
|
||||
"headers": self._redact_sensitive_headers(dict(response.headers)),
|
||||
"payload": str(payload),
|
||||
}
|
||||
```
|
||||
|
||||
Sensitive headers are automatically redacted. You do not want authorization tokens showing up in scan reports.
|
||||
|
||||
```python
|
||||
sensitive_headers = [
|
||||
"authorization",
|
||||
"cookie",
|
||||
"x-api-key",
|
||||
"x-auth-token",
|
||||
]
|
||||
```
|
||||
|
||||
## The Service Layer
|
||||
|
||||
ScanService coordinates scans. It maps test types to scanner classes and handles the workflow.
|
||||
|
||||
```python
|
||||
scanner_mapping = {
|
||||
TestType.RATE_LIMIT: RateLimitScanner,
|
||||
TestType.AUTH: AuthScanner,
|
||||
TestType.SQLI: SQLiScanner,
|
||||
TestType.IDOR: IDORScanner,
|
||||
}
|
||||
|
||||
for test_type in scan_request.tests_to_run:
|
||||
scanner_class = scanner_mapping.get(test_type)
|
||||
scanner = scanner_class(target_url, auth_token, max_requests)
|
||||
result = scanner.scan()
|
||||
results.append(result)
|
||||
```
|
||||
|
||||
If a scanner throws an exception, the service catches it and creates an error result. One failing scanner does not kill the entire scan.
|
||||
|
||||
```python
|
||||
except Exception as e:
|
||||
results.append(
|
||||
TestResultCreate(
|
||||
test_name=test_type,
|
||||
status="error",
|
||||
details=f"Scanner error: {str(e)}",
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Database Design
|
||||
|
||||
Three main tables.
|
||||
|
||||
**users**: Account information. Passwords are bcrypt hashed.
|
||||
|
||||
**scans**: Metadata about each scan (who ran it, when, target URL).
|
||||
|
||||
**test_results**: Individual test outcomes linked to scans.
|
||||
|
||||
```
|
||||
users (1) ──── (*) scans (1) ──── (*) test_results
|
||||
```
|
||||
|
||||
Cascade deletes are configured so deleting a user removes their scans, and deleting a scan removes its test results.
|
||||
|
||||
## Rate Limiting The Scanner API
|
||||
|
||||
The scanner API itself is rate limited using slowapi.
|
||||
|
||||
```python
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
@router.post("/")
|
||||
@limiter.limit("5/minute")
|
||||
async def create_scan(...):
|
||||
pass
|
||||
```
|
||||
|
||||
Running security scans is expensive. Without rate limiting, someone could hammer your scanner with requests and either run up your bills or use it to attack third parties.
|
||||
|
||||
The scan endpoint has stricter limits than read endpoints. Creating a scan triggers potentially hundreds of requests to the target.
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
JWT tokens with bcrypt password hashing.
|
||||
|
||||
```
|
||||
1. User registers with email/password
|
||||
2. Password is hashed with bcrypt (cost factor 12)
|
||||
3. User logs in with credentials
|
||||
4. Server validates password against hash
|
||||
5. Server issues JWT with user ID and expiration
|
||||
6. Client sends JWT in Authorization header
|
||||
7. Server validates JWT on each request
|
||||
```
|
||||
|
||||
The JWT contains minimal claims. Just enough to identify the user.
|
||||
|
||||
```python
|
||||
{
|
||||
"sub": "user_id",
|
||||
"exp": expiration_timestamp
|
||||
}
|
||||
```
|
||||
|
||||
No roles or permissions in the token. Those are checked against the database on each request. This means you can revoke permissions instantly without waiting for token expiration.
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
The application uses HTTPException for expected errors and lets unexpected errors bubble up.
|
||||
|
||||
```python
|
||||
if not scan:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scan not found",
|
||||
)
|
||||
|
||||
if scan.user_id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to access this scan",
|
||||
)
|
||||
```
|
||||
|
||||
FastAPI catches HTTPException and returns proper JSON error responses. Unhandled exceptions return 500 with minimal details (no stack traces in production).
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are loaded from environment variables with pydantic_settings.
|
||||
|
||||
```python
|
||||
class Settings(BaseSettings):
|
||||
BACKEND_HOST: str = "0.0.0.0"
|
||||
BACKEND_PORT: int = 8000
|
||||
DEBUG: bool = False
|
||||
DATABASE_URL: str
|
||||
JWT_SECRET_KEY: str
|
||||
# ...
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
```
|
||||
|
||||
Defaults exist for development convenience but secrets like JWT_SECRET_KEY have no default. The application fails fast if required settings are missing.
|
||||
|
||||
## Why This Structure
|
||||
|
||||
The layered approach might seem like overkill for a scanner, but it pays off.
|
||||
|
||||
**Testing**: Each layer can be tested independently. Mock the repository to test services. Mock services to test routes.
|
||||
|
||||
**Extensibility**: Adding a new scanner means creating one file that inherits from BaseScanner. No changes to services or routes needed.
|
||||
|
||||
**Maintainability**: Database logic stays in repositories. HTTP logic stays in routes. Business logic stays in services. Changes are localized.
|
||||
|
||||
**Security**: Separation of concerns makes security review easier. Auth checks happen in one place. Input validation happens in one place.
|
||||
|
||||
## Adding A New Scanner
|
||||
|
||||
To add a new vulnerability scanner:
|
||||
|
||||
1. Create a new file in `scanners/`
|
||||
2. Inherit from BaseScanner
|
||||
3. Implement the scan() method
|
||||
4. Add payloads to payloads.py if needed
|
||||
5. Add the test type to core/enums.py
|
||||
6. Add mapping in ScanService
|
||||
|
||||
```python
|
||||
class XSSScanner(BaseScanner):
|
||||
def scan(self) -> TestResultCreate:
|
||||
# Detection logic here
|
||||
pass
|
||||
```
|
||||
|
||||
The scanner pattern handles HTTP requests, retries, and evidence collection. You focus on the detection logic.
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
# How Security Scanning Actually Works
|
||||
|
||||
This doc covers the techniques behind API security scanning. How detection works, what makes a good payload, and the tradeoffs between thoroughness and noise.
|
||||
|
||||
## The Scanning Process
|
||||
|
||||
Every scanner follows the same general flow.
|
||||
|
||||
```
|
||||
1. Establish baseline (normal behavior)
|
||||
2. Send test payloads
|
||||
3. Observe differences from baseline
|
||||
4. Classify findings
|
||||
5. Collect evidence
|
||||
```
|
||||
|
||||
The key insight: vulnerability detection is about finding anomalies. Normal requests produce normal responses. Malicious requests produce different responses. The difference reveals the vulnerability.
|
||||
|
||||
## Baseline Timing
|
||||
|
||||
Time based attacks need accurate baselines. Network latency varies. Server response time varies. You need to distinguish between normal variance and injected delays.
|
||||
|
||||
```python
|
||||
def get_baseline_timing(endpoint, samples=5):
|
||||
times = []
|
||||
for _ in range(samples):
|
||||
response = make_request("GET", endpoint)
|
||||
times.append(response.elapsed)
|
||||
time.sleep(0.5)
|
||||
|
||||
return mean(times), stdev(times)
|
||||
```
|
||||
|
||||
Taking multiple samples and calculating standard deviation lets you set a proper threshold.
|
||||
|
||||
```python
|
||||
threshold = baseline_mean + (3 * baseline_stdev)
|
||||
```
|
||||
|
||||
If your baseline is 200ms with a standard deviation of 50ms, your threshold is 350ms. A response taking 5.2 seconds when you injected a 5 second delay is clearly the attack working, not network issues.
|
||||
|
||||
## Payload Design
|
||||
|
||||
Good payloads are specific. They test one thing and produce a clear signal.
|
||||
|
||||
### SQL Injection Payloads
|
||||
|
||||
Basic auth bypass payloads:
|
||||
```
|
||||
' OR '1'='1
|
||||
' OR 1=1--
|
||||
admin'--
|
||||
```
|
||||
|
||||
These work against string fields in WHERE clauses. The quote breaks out of the value, the OR makes the condition always true, and the comment (-- or #) ignores the rest of the query.
|
||||
|
||||
Time delay payloads vary by database:
|
||||
```
|
||||
'; WAITFOR DELAY '0:0:5'-- (MSSQL)
|
||||
'; SELECT SLEEP(5)-- (MySQL)
|
||||
'; pg_sleep(5)-- (PostgreSQL)
|
||||
```
|
||||
|
||||
Each database has different syntax for delays. The scanner tries multiple variants to identify which database is in use.
|
||||
|
||||
Union payloads for data extraction:
|
||||
```
|
||||
' UNION SELECT NULL--
|
||||
' UNION SELECT NULL,NULL--
|
||||
' UNION SELECT username,password FROM users--
|
||||
```
|
||||
|
||||
The number of NULLs must match the column count of the original query. Scanners typically try 1, 2, 3, etc. until one works.
|
||||
|
||||
### Authentication Payloads
|
||||
|
||||
JWT none algorithm variants:
|
||||
```python
|
||||
["none", "None", "NONE", "nOnE", "NoNe"]
|
||||
```
|
||||
|
||||
Case variations catch libraries that only check for lowercase "none" but accept mixed case.
|
||||
|
||||
Invalid tokens to test error handling:
|
||||
```python
|
||||
["", "invalid", "null", "undefined", "' OR '1'='1"]
|
||||
```
|
||||
|
||||
If any of these get a 200 response, the token validation is broken.
|
||||
|
||||
### Rate Limit Bypass Headers
|
||||
|
||||
```python
|
||||
[
|
||||
{"X-Forwarded-For": "127.0.0.1"},
|
||||
{"X-Real-IP": "10.0.0.1"},
|
||||
{"X-Originating-IP": "192.168.1.1"},
|
||||
{"X-Client-IP": "8.8.8.8"},
|
||||
{"CF-Connecting-IP": "1.1.1.1"},
|
||||
{"True-Client-IP": "172.16.0.1"},
|
||||
]
|
||||
```
|
||||
|
||||
Each header is tested with rotating IP values. If requests with different spoofed IPs all succeed while requests without the header get blocked, the rate limiter trusts that header.
|
||||
|
||||
## Response Analysis
|
||||
|
||||
Different vulnerabilities produce different signals.
|
||||
|
||||
### Error Messages
|
||||
|
||||
SQL injection often produces database errors.
|
||||
|
||||
```python
|
||||
error_signatures = {
|
||||
"mysql": ["sql syntax", "mysql_fetch", "warning: mysql"],
|
||||
"postgres": ["pg_query", "pg_exec", "pgsql"],
|
||||
"mssql": ["odbc sql server", "sqlexception"],
|
||||
"oracle": ["ora-", "pl/sql"],
|
||||
}
|
||||
|
||||
for db_type, signatures in error_signatures.items():
|
||||
for signature in signatures:
|
||||
if signature in response.text.lower():
|
||||
return {"vulnerable": True, "database_type": db_type}
|
||||
```
|
||||
|
||||
The error message also reveals the database type, which informs which payloads to try next.
|
||||
|
||||
### Response Length Differences
|
||||
|
||||
Boolean based blind SQLi detection relies on response length.
|
||||
|
||||
```python
|
||||
true_response = make_request("GET", "/?id=1 AND 1=1")
|
||||
false_response = make_request("GET", "/?id=1 AND 1=2")
|
||||
|
||||
length_diff = abs(len(true_response.text) - len(false_response.text))
|
||||
|
||||
if length_diff > 100:
|
||||
return {"vulnerable": True, "confidence": "HIGH" if length_diff > 500 else "MEDIUM"}
|
||||
```
|
||||
|
||||
A large difference means the database is evaluating the boolean condition. The query structure is being modified by the input.
|
||||
|
||||
### Status Codes
|
||||
|
||||
Status codes reveal a lot.
|
||||
|
||||
| Code | Meaning for Testing |
|
||||
|------|---------------------|
|
||||
| 200 | Request succeeded, possibly vulnerable |
|
||||
| 401 | Authentication required (expected for auth tests) |
|
||||
| 403 | Forbidden (authorization working) |
|
||||
| 404 | Resource not found (might be good for IDOR) |
|
||||
| 429 | Rate limited (rate limiter working) |
|
||||
| 500 | Server error (might indicate crash from payload) |
|
||||
|
||||
A 200 on an endpoint that should require auth is a vulnerability. A 429 when testing rate limits means they work. A 500 from a SQL payload might indicate injection but also might just be a crash.
|
||||
|
||||
### Timing
|
||||
|
||||
Timing analysis requires statistical rigor.
|
||||
|
||||
```python
|
||||
baseline_mean, baseline_stdev = get_baseline_timing("/")
|
||||
delay_seconds = 5
|
||||
expected_delay = baseline_mean + delay_seconds
|
||||
|
||||
for payload in time_based_payloads:
|
||||
times = []
|
||||
for _ in range(3): # Multiple samples per payload
|
||||
response = make_request("GET", f"/?id={payload}")
|
||||
times.append(response.elapsed)
|
||||
time.sleep(1)
|
||||
|
||||
avg_time = mean(times)
|
||||
|
||||
if avg_time >= expected_delay - 1: # Allow 1 second tolerance
|
||||
return {"vulnerable": True}
|
||||
```
|
||||
|
||||
Three samples per payload reduces false positives from network hiccups. The tolerance accounts for timing imprecision.
|
||||
|
||||
## False Positives and Confidence
|
||||
|
||||
Not every anomaly is a vulnerability. Scanners need to distinguish real findings from noise.
|
||||
|
||||
### High Confidence Indicators
|
||||
|
||||
- Database error strings in response (SQLi)
|
||||
- Token with "none" algorithm accepted (JWT)
|
||||
- 5+ second response when 5 second delay injected (Time based SQLi)
|
||||
- Different IDs accessible with same auth token (IDOR)
|
||||
|
||||
### Lower Confidence Indicators
|
||||
|
||||
- Response length differences (might be caching)
|
||||
- Single timing anomaly (network variance)
|
||||
- 500 errors (might be unrelated crash)
|
||||
|
||||
The scanner assigns confidence levels based on how definitive the evidence is.
|
||||
|
||||
```python
|
||||
if response_time >= expected_delay:
|
||||
confidence = "HIGH"
|
||||
elif response_time >= expected_delay * 0.8:
|
||||
confidence = "MEDIUM"
|
||||
```
|
||||
|
||||
## Scanning Safely
|
||||
|
||||
Security scanning can break things. These practices minimize damage.
|
||||
|
||||
### Request Spacing
|
||||
|
||||
Never send requests as fast as possible.
|
||||
|
||||
```python
|
||||
required_delay = 1.0 / (max_requests / window_seconds)
|
||||
time.sleep(required_delay + random_jitter)
|
||||
```
|
||||
|
||||
This protects:
|
||||
- Target servers from being overwhelmed
|
||||
- Your scanner from being blocked
|
||||
- Timing measurements from being skewed
|
||||
|
||||
### Read Only Operations
|
||||
|
||||
The scanner uses GET requests where possible and avoids destructive payloads.
|
||||
|
||||
```python
|
||||
# Testing payloads
|
||||
"' OR '1'='1" # Yes: tests injection without modification
|
||||
"'; DROP TABLE--" # Included but dangerous on real data
|
||||
```
|
||||
|
||||
Even though the payloads include statements like DROP TABLE, the scanner is testing for the vulnerability, not exploiting it. If the database is vulnerable, the SELECT from the OR clause would work anyway.
|
||||
|
||||
### Authentication Scope
|
||||
|
||||
When testing authenticated endpoints, the scanner uses the provided token consistently.
|
||||
|
||||
```python
|
||||
if self.auth_token:
|
||||
session.headers.update({"Authorization": f"Bearer {self.auth_token}"})
|
||||
```
|
||||
|
||||
This means:
|
||||
- Tests stay within authorized scope
|
||||
- IDOR tests check if you can access other users' data, not if you can bypass auth entirely
|
||||
- Results are relevant to your access level
|
||||
|
||||
### Error Recovery
|
||||
|
||||
Failed requests do not crash the scan.
|
||||
|
||||
```python
|
||||
try:
|
||||
result = scanner.scan()
|
||||
except Exception as e:
|
||||
result = TestResultCreate(
|
||||
status="error",
|
||||
details=f"Scanner error: {str(e)}",
|
||||
)
|
||||
```
|
||||
|
||||
Network timeouts, connection resets, and unexpected responses are handled. The scan continues with remaining tests.
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
Scanner output needs human interpretation.
|
||||
|
||||
### Vulnerable Status
|
||||
|
||||
The scanner found evidence of a vulnerability. Review the evidence to confirm:
|
||||
- Is the payload visible in the evidence?
|
||||
- Does the detection method match the vulnerability type?
|
||||
- Could this be a false positive?
|
||||
|
||||
### Safe Status
|
||||
|
||||
No vulnerability detected with the payloads tested. This does not mean the target is secure. It means these specific tests passed.
|
||||
|
||||
```
|
||||
"No SQL injection vulnerabilities detected"
|
||||
|
||||
What this means: Standard SQLi payloads did not trigger errors or timing anomalies
|
||||
What this does NOT mean: The application is immune to SQL injection
|
||||
```
|
||||
|
||||
Different payloads, different endpoints, or different parameters might reveal vulnerabilities the scan missed.
|
||||
|
||||
### Error Status
|
||||
|
||||
The test could not complete. Check the error message:
|
||||
- Connection refused: Target is down or blocking your IP
|
||||
- Timeout: Target is slow or has aggressive rate limiting
|
||||
- SSL error: Certificate issues
|
||||
|
||||
Errors mean you need to investigate before trusting the result.
|
||||
|
||||
## Limitations
|
||||
|
||||
No scanner finds everything. Understanding limitations helps you use results appropriately.
|
||||
|
||||
**Coverage**: The scanner tests a predefined set of payloads and techniques. Novel attack vectors are not covered.
|
||||
|
||||
**Depth**: Automated scanning is shallow compared to manual testing. Complex logic flaws require human analysis.
|
||||
|
||||
**Context**: The scanner does not understand your business logic. It cannot tell if a vulnerability matters for your specific application.
|
||||
|
||||
**Evasion**: Sophisticated applications might detect and block scanning patterns. Low and slow scanning helps but is not foolproof.
|
||||
|
||||
**False Negatives**: Passing all tests does not mean the application is secure. It means these specific tests passed against these specific endpoints with these specific inputs.
|
||||
|
||||
Use automated scanning as one layer of security testing, not the only layer. Combine with code review, manual penetration testing, and ongoing monitoring.
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
# API Vulnerabilities This Scanner Actually Detects
|
||||
|
||||
This scanner tests for four categories of vulnerabilities. Each one maps to real OWASP API Security Top 10 entries. Here is what they are, how attackers exploit them, and why they matter.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Vulnerability | OWASP ID | Severity | What Happens |
|
||||
|---------------|----------|----------|--------------|
|
||||
| IDOR/BOLA | API1:2023 | High | Attacker accesses other users data |
|
||||
| Broken Auth | API2:2023 | Critical | Attacker bypasses login entirely |
|
||||
| SQL Injection | API8:2023 | Critical | Attacker reads/modifies your database |
|
||||
| Missing Rate Limits | API4:2023 | High | Attacker brute forces or DoS attacks |
|
||||
|
||||
## IDOR/BOLA (Broken Object Level Authorization)
|
||||
|
||||
OWASP ranks this number one for a reason. It is the most common API vulnerability in the wild.
|
||||
|
||||
The problem: your API checks if a user is logged in, but not if they own the resource they are requesting.
|
||||
|
||||
```
|
||||
GET /api/users/42/orders
|
||||
|
||||
User 42: sees their orders (correct)
|
||||
User 99: also sees user 42's orders (vulnerable)
|
||||
```
|
||||
|
||||
The scanner tests this by:
|
||||
1. Extracting IDs from API responses (both numeric and UUIDs)
|
||||
2. Trying to access resources with manipulated IDs
|
||||
3. Checking if sequential IDs are predictable
|
||||
|
||||
Real world example: In 2019, First American Financial exposed 885 million records because their document URLs used sequential IDs with no authorization check. Change the number in the URL, see someone else's mortgage documents.
|
||||
|
||||
### Why UUIDs Do Not Fix This
|
||||
|
||||
Some developers think switching from numeric IDs to UUIDs solves IDOR. It does not.
|
||||
|
||||
UUIDs make enumeration harder, not impossible. If an attacker finds one UUID (from a shared link, email, logs, or browser history), they can still access that resource without authorization.
|
||||
|
||||
```
|
||||
GET /api/documents/550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
The fix is always authorization checks. Verify the requesting user has permission to access the specific resource, regardless of ID format.
|
||||
|
||||
## Broken Authentication (API2:2023)
|
||||
|
||||
Authentication vulnerabilities let attackers log in as other users or bypass authentication entirely.
|
||||
|
||||
### Missing Authentication
|
||||
|
||||
The most obvious case: endpoints that should require login but do not.
|
||||
|
||||
```python
|
||||
# Vulnerable: no auth required
|
||||
@app.get("/api/admin/users")
|
||||
def get_all_users():
|
||||
return db.query(User).all()
|
||||
```
|
||||
|
||||
The scanner tests this by making requests without any authentication headers and checking if it gets a 200 response instead of 401.
|
||||
|
||||
### JWT Vulnerabilities
|
||||
|
||||
JWTs have specific implementation bugs that keep appearing.
|
||||
|
||||
**The None Algorithm Attack**
|
||||
|
||||
JWTs have three parts: header, payload, signature. The header specifies which algorithm verifies the signature. Some libraries accept "none" as a valid algorithm, meaning no signature required.
|
||||
|
||||
```
|
||||
Original header: {"alg": "HS256", "typ": "JWT"}
|
||||
Malicious header: {"alg": "none", "typ": "JWT"}
|
||||
```
|
||||
|
||||
If the server accepts this, an attacker can forge any token they want. The scanner tests multiple case variations because some libraries only check for lowercase "none".
|
||||
|
||||
```python
|
||||
# These all bypass poorly implemented JWT validation
|
||||
"none", "None", "NONE", "nOnE"
|
||||
```
|
||||
|
||||
**Signature Removal**
|
||||
|
||||
Related to the none algorithm: some implementations check if the algorithm is "none" but still accept tokens with empty signatures.
|
||||
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.
|
||||
^ empty signature
|
||||
```
|
||||
|
||||
The scanner removes signatures from valid tokens and tests if the server still accepts them.
|
||||
|
||||
### Invalid Token Handling
|
||||
|
||||
Good APIs reject malformed tokens. Bad APIs might accept them or crash.
|
||||
|
||||
The scanner sends garbage tokens and checks for 200 responses:
|
||||
- Empty strings
|
||||
- Random characters
|
||||
- SQL injection payloads in tokens
|
||||
- Path traversal strings
|
||||
|
||||
If any of these return 200, something is wrong with your token validation.
|
||||
|
||||
## SQL Injection (API8:2023)
|
||||
|
||||
SQL injection has been around for 25 years and still makes the top 10 because developers keep building queries with string concatenation.
|
||||
|
||||
### Error Based Detection
|
||||
|
||||
The easiest SQLi to find. Send a malformed query, get a database error in the response.
|
||||
|
||||
```
|
||||
GET /api/users?id=' OR '1'='1
|
||||
|
||||
Response: "You have an error in your SQL syntax near..."
|
||||
```
|
||||
|
||||
The scanner looks for error signatures from MySQL, PostgreSQL, MSSQL, and Oracle. Each database has distinctive error messages.
|
||||
|
||||
| Database | Error Contains |
|
||||
|----------|----------------|
|
||||
| MySQL | "mysql_fetch", "sql syntax", "warning: mysql" |
|
||||
| PostgreSQL | "pg_query", "pg_exec", "pgsql" |
|
||||
| MSSQL | "odbc sql server", "sqlexception" |
|
||||
| Oracle | "ora-", "pl/sql" |
|
||||
|
||||
Finding these errors means two things: the input reaches the database unsanitized, and error messages are exposed to users. Both are bad.
|
||||
|
||||
### Boolean Based Blind
|
||||
|
||||
When errors are hidden, you can still detect SQLi by comparing responses.
|
||||
|
||||
```
|
||||
GET /api/users?id=1 AND 1=1 (true condition)
|
||||
GET /api/users?id=1 AND 1=2 (false condition)
|
||||
```
|
||||
|
||||
If the true condition returns normal results and the false condition returns empty or different results, the input is being evaluated as SQL.
|
||||
|
||||
The scanner measures response length differences. A significant difference (over 100 bytes typically) between true and false conditions indicates a vulnerability.
|
||||
|
||||
### Time Based Blind
|
||||
|
||||
The hardest to detect but most reliable for confirming SQLi.
|
||||
|
||||
```
|
||||
GET /api/users?id=1; WAITFOR DELAY '0:0:5'-- (MSSQL)
|
||||
GET /api/users?id=1'; SELECT SLEEP(5)-- (MySQL)
|
||||
GET /api/users?id=1'; pg_sleep(5)-- (PostgreSQL)
|
||||
```
|
||||
|
||||
If the response takes 5 seconds longer than normal, the database executed the delay. This works even when the application shows no visible difference in output.
|
||||
|
||||
The scanner:
|
||||
1. Establishes baseline response times with multiple samples
|
||||
2. Calculates standard deviation to account for network variance
|
||||
3. Sends delay payloads and measures actual response time
|
||||
4. Compares against expected delay time (baseline + injected delay)
|
||||
|
||||
False positives are rare with time based testing because network latency does not consistently add exactly 5 seconds.
|
||||
|
||||
## Rate Limiting (API4:2023)
|
||||
|
||||
APIs without rate limiting are vulnerable to brute force attacks, credential stuffing, and denial of service.
|
||||
|
||||
### Detection
|
||||
|
||||
The scanner makes multiple requests and looks for:
|
||||
1. Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, etc.)
|
||||
2. 429 Too Many Requests responses
|
||||
3. Retry-After headers
|
||||
|
||||
Finding rate limit headers but never hitting 429 means the limits exist in name only. The scanner flags this as "headers only" which is a medium severity issue.
|
||||
|
||||
### Bypass Testing
|
||||
|
||||
Rate limiters often have implementation bugs.
|
||||
|
||||
**IP Header Spoofing**
|
||||
|
||||
Many rate limiters use client IP for tracking. If they trust X-Forwarded-For or similar headers, attackers can bypass limits by rotating fake IPs.
|
||||
|
||||
```
|
||||
X-Forwarded-For: 10.0.0.1
|
||||
X-Forwarded-For: 10.0.0.2
|
||||
X-Forwarded-For: 10.0.0.3
|
||||
```
|
||||
|
||||
The scanner tests X-Forwarded-For, X-Real-IP, X-Client-IP, X-Originating-IP, CF-Connecting-IP, and True-Client-IP. If any of these bypass rate limits, attackers can make unlimited requests.
|
||||
|
||||
**Endpoint Variations**
|
||||
|
||||
Some rate limiters are case sensitive or miss URL variations.
|
||||
|
||||
```
|
||||
/api/login (rate limited)
|
||||
/API/LOGIN (not rate limited?)
|
||||
/api/login/ (not rate limited?)
|
||||
/api/./login (not rate limited?)
|
||||
```
|
||||
|
||||
The scanner tests path variations including case changes, trailing slashes, and encoded characters.
|
||||
|
||||
## What The Severity Ratings Mean
|
||||
|
||||
The scanner uses five severity levels.
|
||||
|
||||
**Critical**: Immediate risk of data breach or system compromise. SQL injection and JWT bypass fall here. Fix before anything else.
|
||||
|
||||
**High**: Serious risk that needs prompt attention. IDOR and missing rate limits on auth endpoints. Attackers will find and exploit these.
|
||||
|
||||
**Medium**: Moderate risk. Rate limit headers without enforcement, or predictable ID patterns. Still needs fixing, but less urgent.
|
||||
|
||||
**Low**: Minor issues. Might indicate poor practices but no immediate exploit path.
|
||||
|
||||
**Info**: Informational findings. The test passed or found something worth noting.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- OWASP API Security Top 10 2023: https://owasp.org/API-Security/
|
||||
- PortSwigger Web Security Academy: https://portswigger.net/web-security
|
||||
- HackTricks API Pentesting: https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/api-pentesting.html
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# CarterPerez-dev | 2025
|
||||
# Makefile
|
||||
|
||||
.PHONY: help venv setup install install-dev test lint format clean all
|
||||
|
||||
help:
|
||||
@echo "Keylogger - Makefile Commands"
|
||||
@echo "=========================================="
|
||||
@echo ""
|
||||
@echo "Setup Commands:"
|
||||
@echo " make setup - Create venv and install dependencies"
|
||||
@echo " make install - Install project dependencies only"
|
||||
@echo " make install-dev - Install with dev tools (linters, formatters)"
|
||||
@echo ""
|
||||
@echo "Testing & Quality:"
|
||||
@echo " make test - Run test suite"
|
||||
@echo " make lint - Run all linting checks (ruff, pylint, mypy)"
|
||||
@echo " make format - Format code with yapf"
|
||||
@echo ""
|
||||
@echo "Utility:"
|
||||
@echo " make clean - Remove venv and cache files"
|
||||
@echo " make all - Setup, test, and lint everything"
|
||||
@echo ""
|
||||
|
||||
venv:
|
||||
@echo "Creating virtual environment..."
|
||||
python -m venv venv
|
||||
@echo "Virtual environment created at ./venv"
|
||||
@echo ""
|
||||
@echo "Activate with:"
|
||||
@echo " source venv/bin/activate (Linux/macOS)"
|
||||
@echo " venv\\Scripts\\activate (Windows)"
|
||||
|
||||
setup: venv
|
||||
@echo ""
|
||||
@echo "Installing dependencies..."
|
||||
./venv/bin/pip install --upgrade pip
|
||||
./venv/bin/pip install -e ".[dev]"
|
||||
@echo "✓ Setup complete!"
|
||||
@echo ""
|
||||
@echo "Run tests with: make test"
|
||||
|
||||
install:
|
||||
@echo "Installing main dependencies..."
|
||||
pip install -e .
|
||||
@echo "Dependencies installed"
|
||||
|
||||
install-dev:
|
||||
@echo "Installing with dev dependencies..."
|
||||
pip install -e ".[dev]"
|
||||
@echo "Dev dependencies installed"
|
||||
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@echo ""
|
||||
python test_keylogger.py
|
||||
|
||||
lint:
|
||||
@echo "Running linting checks..."
|
||||
@echo ""
|
||||
@echo "=== Ruff ==="
|
||||
ruff check keylogger.py
|
||||
@echo ""
|
||||
@echo "=== Pylint ==="
|
||||
pylint keylogger.py
|
||||
@echo ""
|
||||
@echo "=== Mypy ==="
|
||||
mypy keylogger.py
|
||||
@echo ""
|
||||
@echo "All linting checks passed!"
|
||||
|
||||
format:
|
||||
@echo "Formatting code with yapf..."
|
||||
yapf -i keylogger.py test_keylogger.py
|
||||
@echo "Code formatted"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up..."
|
||||
rm -rf venv
|
||||
rm -rf __pycache__
|
||||
rm -rf *.pyc
|
||||
rm -rf .mypy_cache
|
||||
rm -rf .ruff_cache
|
||||
rm -rf .pytest_cache
|
||||
rm -rf *.egg-info
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
@echo "Cleaned"
|
||||
|
||||
# Do everything: setup, test, lint
|
||||
all: setup test lint
|
||||
@echo ""
|
||||
@echo "=========================================="
|
||||
@echo "Everything complete!"
|
||||
@echo "=========================================="
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
# CarterPerez-dev | 2025
|
||||
# Keylogger Development Commands
|
||||
|
||||
set dotenv-load
|
||||
set export
|
||||
set shell := ["bash", "-uc"]
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
|
||||
# Show available commands
|
||||
default:
|
||||
@just --list --unsorted
|
||||
|
||||
# =============================================================================
|
||||
# Setup Commands
|
||||
# =============================================================================
|
||||
|
||||
# Create virtual environment and install all dependencies
|
||||
[group('setup')]
|
||||
setup:
|
||||
@echo "Creating virtual environment with uv..."
|
||||
uv venv
|
||||
@echo ""
|
||||
@echo "Installing dependencies..."
|
||||
uv sync --all-extras
|
||||
@echo ""
|
||||
@echo "✓ Setup complete!"
|
||||
@echo ""
|
||||
@echo "Activate with:"
|
||||
@echo " source .venv/bin/activate (Linux/macOS)"
|
||||
@echo " .venv\\Scripts\\activate (Windows)"
|
||||
@echo ""
|
||||
@echo "Run tests with: just test"
|
||||
|
||||
# Install main dependencies only
|
||||
[group('setup')]
|
||||
install:
|
||||
@echo "Installing main dependencies..."
|
||||
uv sync
|
||||
@echo "Dependencies installed"
|
||||
|
||||
# Install with dev dependencies
|
||||
[group('setup')]
|
||||
install-dev:
|
||||
@echo "Installing with dev dependencies..."
|
||||
uv sync --all-extras
|
||||
@echo "Dev dependencies installed"
|
||||
|
||||
# =============================================================================
|
||||
# Testing & Quality
|
||||
# =============================================================================
|
||||
|
||||
# Run test suite
|
||||
[group('test')]
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@echo ""
|
||||
uv run python test_keylogger.py
|
||||
|
||||
# Run all linting checks (ruff, pylint, mypy)
|
||||
[group('test')]
|
||||
lint:
|
||||
@echo "Running linting checks..."
|
||||
@echo ""
|
||||
@echo "=== Ruff ==="
|
||||
uv run ruff check keylogger.py
|
||||
@echo ""
|
||||
@echo "=== Pylint ==="
|
||||
uv run pylint keylogger.py
|
||||
@echo ""
|
||||
@echo "=== Mypy ==="
|
||||
uv run mypy keylogger.py
|
||||
@echo ""
|
||||
@echo "All linting checks passed!"
|
||||
|
||||
# Format code with yapf
|
||||
[group('test')]
|
||||
format:
|
||||
@echo "Formatting code with yapf..."
|
||||
uv run yapf -i keylogger.py test_keylogger.py
|
||||
@echo "Code formatted"
|
||||
|
||||
# Run ruff check and fix
|
||||
[group('test')]
|
||||
fix:
|
||||
@echo "Running ruff fix..."
|
||||
uv run ruff check keylogger.py --fix
|
||||
@echo "Ruff fixes applied"
|
||||
|
||||
# =============================================================================
|
||||
# Utilities
|
||||
# =============================================================================
|
||||
|
||||
# Remove virtual environment and cache files
|
||||
[group('utility')]
|
||||
clean:
|
||||
@echo "Cleaning up..."
|
||||
rm -rf .venv
|
||||
rm -rf __pycache__
|
||||
rm -rf *.pyc
|
||||
rm -rf .mypy_cache
|
||||
rm -rf .ruff_cache
|
||||
rm -rf .pytest_cache
|
||||
rm -rf *.egg-info
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
@echo "Cleaned"
|
||||
|
||||
# Lock dependencies
|
||||
[group('utility')]
|
||||
lock:
|
||||
uv lock
|
||||
|
||||
# Update all dependencies
|
||||
[group('utility')]
|
||||
update:
|
||||
uv lock --upgrade
|
||||
uv sync --all-extras
|
||||
|
||||
# Run the keylogger (use with caution - for testing only)
|
||||
[group('utility')]
|
||||
[confirm("This will start the keylogger. Continue?")]
|
||||
run:
|
||||
uv run python keylogger.py
|
||||
|
||||
# =============================================================================
|
||||
# CI / Full Pipeline
|
||||
# =============================================================================
|
||||
|
||||
# Setup, test, and lint everything
|
||||
[group('ci')]
|
||||
all: setup test lint
|
||||
@echo ""
|
||||
@echo "=========================================="
|
||||
@echo "Everything complete!"
|
||||
@echo "=========================================="
|
||||
|
||||
# CI pipeline (tests and linting only, no setup)
|
||||
[group('ci')]
|
||||
ci: lint test
|
||||
@echo "✓ CI checks passed"
|
||||
|
|
@ -32,12 +32,11 @@ dev = [
|
|||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=80.9.0", "wheel>=0.45.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["keylogger"]
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["keylogger.py"]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
# CarterPerez-dev | 2025
|
||||
# Keylogger requirements.txt
|
||||
|
||||
|
||||
pynput==1.8.1
|
||||
requests==2.32.5
|
||||
|
||||
|
||||
# Platform specific dependencies (optional but recommended)
|
||||
# Windows:
|
||||
# pywin32==311
|
||||
# psutil==7.1.3
|
||||
|
||||
# macOS:
|
||||
# pyobjc-framework-Cocoa==12.0
|
||||
|
||||
# Optional recommened linting
|
||||
# ruff==0.14.4
|
||||
# mypy==1.18.2
|
||||
# pylint==4.0.2
|
||||
|
||||
# Optional Formatter
|
||||
# yapf==0.43.0
|
||||
Loading…
Reference in New Issue