Merge pull request #4 from CarterPerez-dev/chore/prettier-and-workflows

move workflow files - add prettier formatting and format
This commit is contained in:
Carter Perez 2025-11-12 15:39:32 -05:00 committed by GitHub
commit 104c4fa07a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 646 additions and 375 deletions

View File

@ -4,11 +4,11 @@ on:
pull_request:
branches: [ '*' ]
paths:
- 'frontend/**/*.ts'
- 'frontend/**/*.tsx'
- 'frontend/eslint.config.js'
- 'frontend/tsconfig*.json'
- 'frontend/package.json'
- 'PROJECTS/api-security-scanner/frontend/**/*.ts'
- 'PROJECTS/api-security-scanner/frontend/**/*.tsx'
- 'PROJECTS/api-security-scanner/frontend/eslint.config.js'
- 'PROJECTS/api-security-scanner/frontend/tsconfig*.json'
- 'PROJECTS/api-security-scanner/frontend/package.json'
- '.github/workflows/eslint-check.yml'
jobs:
@ -22,7 +22,7 @@ jobs:
defaults:
run:
working-directory: frontend
working-directory: PROJECTS/api-security-scanner/frontend
steps:
- name: Checkout code
@ -33,7 +33,7 @@ jobs:
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
cache-dependency-path: PROJECTS/api-security-scanner/frontend/package-lock.json
- name: Install dependencies
run: npm install
@ -106,7 +106,7 @@ jobs:
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body-path: frontend/eslint-report.md
body-path: PROJECTS/api-security-scanner/frontend/eslint-report.md
edit-mode: replace
- name: Exit with proper code

View File

@ -12,37 +12,37 @@ jobs:
lint:
name: Run Linters
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
defaults:
run:
working-directory: backend
working-directory: PROJECTS/api-security-scanner/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') }}
key: ${{ runner.os }}-pip-${{ hashFiles('PROJECTS/api-security-scanner/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: |
@ -56,7 +56,7 @@ jobs:
fi
cat pylint-output.txt
continue-on-error: true
- name: Run ruff
id: ruff
run: |
@ -70,7 +70,7 @@ jobs:
fi
cat ruff-output.txt
continue-on-error: true
- name: Run mypy
id: mypy
run: |
@ -84,7 +84,7 @@ jobs:
fi
cat mypy-output.txt
continue-on-error: true
- name: Create Lint Summary
id: create_summary
if: github.event_name == 'pull_request'
@ -92,7 +92,7 @@ jobs:
{
echo '## 🔍 Lint & Type Check Results'
echo ''
# Pylint Status
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]]; then
echo '### ✅ Pylint: **Passed**'
@ -107,7 +107,7 @@ jobs:
echo '</details>'
fi
echo ''
# Ruff Status
if [[ "${{ env.RUFF_PASSED }}" == "true" ]]; then
echo '### ✅ Ruff: **Passed**'
@ -122,7 +122,7 @@ jobs:
echo '</details>'
fi
echo ''
# Mypy Status
if [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
echo '### ✅ Mypy: **Passed**'
@ -137,7 +137,7 @@ jobs:
echo '</details>'
fi
echo ''
# Overall Summary
if [[ "${{ env.PYLINT_PASSED }}" == "true" ]] && [[ "${{ env.RUFF_PASSED }}" == "true" ]] && [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then
echo '---'
@ -149,11 +149,11 @@ jobs:
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
body-path: PROJECTS/api-security-scanner/lint-report.md
comment-marker: lint-check-comment-marker

View File

@ -4,10 +4,10 @@ on:
pull_request:
branches: [ '*' ]
paths:
- 'frontend/**/*.ts'
- 'frontend/**/*.tsx'
- 'frontend/tsconfig*.json'
- 'frontend/package.json'
- 'PROJECTS/api-security-scanner/frontend/**/*.ts'
- 'PROJECTS/api-security-scanner/frontend/**/*.tsx'
- 'PROJECTS/api-security-scanner/frontend/tsconfig*.json'
- 'PROJECTS/api-security-scanner/frontend/package.json'
- '.github/workflows/typescript-check.yml'
jobs:
@ -21,7 +21,7 @@ jobs:
defaults:
run:
working-directory: frontend
working-directory: PROJECTS/api-security-scanner/frontend
steps:
- name: Checkout code
@ -32,7 +32,7 @@ jobs:
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
cache-dependency-path: PROJECTS/api-security-scanner/frontend/package-lock.json
- name: Install dependencies
run: |
@ -115,7 +115,7 @@ jobs:
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body-path: frontend/typescript-report.md
body-path: PROJECTS/api-security-scanner/frontend/typescript-report.md
edit-mode: replace
- name: Exit with proper code

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

@ -0,0 +1,65 @@
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)/
- repo: local
hooks:
- id: yapf
name: yapf format (backend)
entry: bash -c 'cd PROJECTS/api-security-scanner/backend && yapf -i -r -vv models/ repositories/ schemas/ scanners/ core/ factory/'
language: system
types: [python]
files: ^PROJECTS/api-security-scanner/backend/
pass_filenames: false
- 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,5 +1,5 @@
# Ⓒ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
# Python

View File

@ -41,17 +41,17 @@ help:
@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 " \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 " \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 " \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 ""
@ -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[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[95m ------------------------------"
@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[95m ------------------------------"
@echo "\033[95m ------------------------------"
@echo "\033[0m"
@ -104,20 +104,20 @@ check:
@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"
@ -146,9 +146,8 @@ tree:
@echo "\033[1;3;4;96m======== Creating Tree ========\033[0m"
@tree -I 'node_modules|.git|*.log|dist|build|*.cache|certgames.egg-info|context|.venv|*.env|qa_context'
$(call ASCII)
TODO:
@echo "\033[1;3;4;96m======== Looking for TODO's ========\033[0m"
@find . -name "*.py" -print0 | xargs -0 pylint --disable=all --enable=W0511 --msg-template='{path}:{line}:{column}: {msg_id}: {msg} ({symbol})' || true
$(call ASCII)

View File

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

View File

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

View File

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

View File

@ -16,10 +16,10 @@ def hash_password(password: str) -> str:
"""
Hash a plain text password using bcrypt
"""
password_bytes = password.encode('utf-8')
password_bytes = password.encode("utf-8")
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
return hashed.decode("utf-8")
def verify_password(
@ -29,8 +29,8 @@ def verify_password(
"""
Verify a plain text password against a hashed password
"""
password_bytes = plain_password.encode('utf-8')
hashed_bytes = hashed_password.encode('utf-8')
password_bytes = plain_password.encode("utf-8")
hashed_bytes = hashed_password.encode("utf-8")
return bcrypt.checkpw(password_bytes, hashed_bytes)

View File

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

View File

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

View File

@ -1,6 +1,6 @@
"""
AngelaMos | 2025
Base model class
Base model class
Common fields and methods for all models
"""
@ -21,6 +21,7 @@ class BaseModel(Base):
Abstract base model with common fields and methods
All models inherit from this class
"""
__abstract__ = True
id = Column(

View File

@ -24,6 +24,7 @@ class Scan(BaseModel):
"""
Stores metadata about scans performed on target URLs
"""
__tablename__ = "scans"
user_id = Column(

View File

@ -8,7 +8,7 @@ from sqlalchemy import (
Enum,
Integer,
Text,
ForeignKey,
ForeignKey,
)
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSON
@ -25,6 +25,7 @@ class TestResult(BaseModel):
"""
Stores individual test results for each security scan
"""
__tablename__ = "test_results"
scan_id = Column(

View File

@ -17,6 +17,7 @@ class User(BaseModel):
"""
Stores authentication credentials and user information
"""
__tablename__ = "users"
email = Column(

View File

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

View File

@ -167,11 +167,9 @@ class TestResultRepository:
Returns:
int: Number of test results deleted
"""
count = (
db.query(TestResult).filter(
TestResult.scan_id == scan_id
).delete()
)
count = db.query(TestResult).filter(
TestResult.scan_id == scan_id
).delete()
if commit:
db.commit()
return count

View File

@ -89,10 +89,8 @@ class UserRepository:
if limit is None:
limit = settings.DEFAULT_PAGINATION_LIMIT
return (
db.query(User).filter(User.is_active
).offset(skip).limit(limit).all()
)
return db.query(User).filter(User.is_active
).offset(skip).limit(limit).all()
@staticmethod
def update_active_status(

View File

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

View File

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

View File

@ -135,7 +135,7 @@ class IDORScanner(BaseScanner):
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(
uuid_pattern,
response_text,

View File

@ -8,6 +8,7 @@ class SQLiPayloads:
"""
SQL Injection test payloads covering various database types and techniques
"""
ERROR_SIGNATURES = {
"mysql": [
"sql syntax",
@ -221,6 +222,7 @@ class IDORPayloads:
"""
Insecure Direct Object Reference (IDOR) test patterns
"""
NUMERIC_ID_MANIPULATIONS = [
0,
-1,
@ -273,6 +275,7 @@ class RateLimitBypassPayloads:
"""
Rate limiting bypass techniques and patterns
"""
HEADER_PATTERNS = {
"limit":
r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit",
@ -413,9 +416,9 @@ class XSSPayloads:
ATTRIBUTE_BREAKING = [
"' onmouseover='alert(\"XSS\")'",
"\" onmouseover=\"alert('XSS')\"",
'" onmouseover="alert(\'XSS\')"',
"' onclick='alert(\"XSS\")' '",
"\" autofocus onfocus=\"alert('XSS')\"",
'" autofocus onfocus="alert(\'XSS\')"',
"'/><script>alert('XSS')</script>",
"\"/><script>alert('XSS')</script>",
]
@ -427,7 +430,7 @@ class XSSPayloads:
"<script>alert('XSS')<!--",
"<<script>alert('XSS')</script>",
"<script\x20type='text/javascript'>alert('XSS')</script>",
"<script\x0D\x0A>alert('XSS')</script>",
"<script\x0d\x0a>alert('XSS')</script>",
]
POLYGLOT_XSS = [

View File

@ -273,10 +273,7 @@ class SQLiScanner(BaseScanner):
avg_delay = statistics.mean(delay_times)
if avg_delay >= expected_delay_time - 1:
confidence = (
"HIGH" if avg_delay >= expected_delay_time
else "MEDIUM"
)
confidence = "HIGH" if avg_delay >= expected_delay_time else "MEDIUM"
return {
"vulnerable":

View File

@ -22,6 +22,7 @@ class ScanRequest(BaseModel):
"""
Schema for creating a new security scan
"""
target_url: HttpUrl = Field(max_length = settings.URL_MAX_LENGTH)
auth_token: str | None = None
tests_to_run: list[TestType] = Field(min_length = 1)
@ -36,6 +37,7 @@ class ScanResponse(BaseModel):
"""
Schema for scan data in API responses
"""
model_config = ConfigDict(from_attributes = True)
id: int

View File

@ -22,6 +22,7 @@ class TestResultCreate(BaseModel):
"""
Schema for creating a new test result (used by scanners)
"""
test_name: TestType
status: ScanStatus
severity: Severity
@ -34,6 +35,7 @@ class TestResultResponse(BaseModel):
"""
Schema for individual test result in API responses
"""
model_config = ConfigDict(from_attributes = True)
id: int

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
# Dependencies
node_modules/
# Build outputs
dist/
build/
# Logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Coverage directory used by tools like istanbul
coverage/
# Other
.DS_Store
.env*
.vscode/
.idea/

View File

@ -0,0 +1,76 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"printWidth": 77,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "always",
"htmlWhitespaceSensitivity": "strict",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto",
"singleAttributePerLine": true,
"overrides": [
{
"files": "*.tsx",
"options": {
"printWidth": 77,
"jsxSingleQuote": false,
"bracketSameLine": false
}
},
{
"files": "*.ts",
"options": {
"printWidth": 77,
"parser": "typescript"
}
},
{
"files": ["*.json", "*.jsonc"],
"options": {
"printWidth": 80,
"trailingComma": "none"
}
},
{
"files": "*.scss",
"options": {
"singleQuote": false,
"printWidth": 100
}
},
{
"files": "*.md",
"options": {
"proseWrap": "always",
"printWidth": 80
}
},
{
"files": ["*.yml", "*.yaml"],
"options": {
"singleQuote": false,
"bracketSpacing": true
}
},
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
},
{
"files": "package.json",
"options": {
"printWidth": 1000
}
}
]
}

View File

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

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

@ -13,7 +13,9 @@ import { router } from '@/router';
import { useAuthStore } from '@/store/authStore';
const AuthInitializer = (): null => {
const loadUserFromStorage = useAuthStore((state) => state.loadUserFromStorage);
const loadUserFromStorage = useAuthStore(
(state) => state.loadUserFromStorage,
);
useEffect(() => {
loadUserFromStorage();
@ -27,7 +29,12 @@ function App(): React.ReactElement {
<QueryClientProvider client={queryClient}>
<AuthInitializer />
<RouterProvider router={router} />
<Toaster position="top-right" closeButton duration={4500} theme="dark" />
<Toaster
position="top-right"
closeButton
duration={4500}
theme="dark"
/>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);

View File

@ -21,9 +21,10 @@ export const LoginForm = (): React.ReactElement => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [errors, setErrors] = useState<{ email?: string; password?: string }>(
{},
);
const [errors, setErrors] = useState<{
email?: string;
password?: string;
}>({});
const { mutate: login, isPending, error } = useLogin();
@ -49,14 +50,19 @@ export const LoginForm = (): React.ReactElement => {
}
};
const validateField = (field: 'email' | 'password', value: string): void => {
const validateField = (
field: 'email' | 'password',
value: string,
): void => {
const result = loginSchema.safeParse({
email: field === 'email' ? value : email,
password: field === 'password' ? value : password,
});
if (!result.success) {
const fieldError = result.error.issues.find((err) => err.path[0] === field);
const fieldError = result.error.issues.find(
(err) => err.path[0] === field,
);
if (fieldError !== null && fieldError !== undefined) {
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
} else {
@ -120,7 +126,10 @@ export const LoginForm = (): React.ReactElement => {
};
return (
<form className="auth-form" onSubmit={handleSubmit}>
<form
className="auth-form"
onSubmit={handleSubmit}
>
<div className="auth-form__header">
<h1 className="auth-form__title">Welcome Back</h1>
<p className="auth-form__subtitle">Sign in to your account</p>
@ -153,18 +162,29 @@ export const LoginForm = (): React.ReactElement => {
</div>
{error !== null && error !== undefined && isAxiosError(error) ? (
<div className="auth-form__error-message" role="alert">
{(error.response?.data as { detail?: string } | undefined)?.detail ?? 'Login failed. Please try again.'}
<div
className="auth-form__error-message"
role="alert"
>
{(error.response?.data as { detail?: string } | undefined)
?.detail ?? 'Login failed. Please try again.'}
</div>
) : null}
<Button type="submit" isLoading={isPending} disabled={isPending}>
<Button
type="submit"
isLoading={isPending}
disabled={isPending}
>
Sign In
</Button>
<p className="auth-form__link">
Don&apos;t have an account?{' '}
<Link to="/register" className="auth-form__link-text">
<Link
to="/register"
className="auth-form__link-text"
>
Sign up
</Link>
</p>

View File

@ -15,7 +15,9 @@ import './AuthForm.css';
export const RegisterForm = (): React.ReactElement => {
const registerFormState = useUIStore((state) => state.registerForm);
const setRegisterFormField = useUIStore((state) => state.setRegisterFormField);
const setRegisterFormField = useUIStore(
(state) => state.setRegisterFormField,
);
const clearRegisterForm = useUIStore((state) => state.clearRegisterForm);
const clearExpiredData = useUIStore((state) => state.clearExpiredData);
@ -61,7 +63,10 @@ export const RegisterForm = (): React.ReactElement => {
const handleConfirmPasswordChange = (value: string): void => {
setConfirmPassword(value);
setRegisterFormField('confirmPassword', value);
if (errors.confirmPassword !== null && errors.confirmPassword !== undefined) {
if (
errors.confirmPassword !== null &&
errors.confirmPassword !== undefined
) {
validateField('confirmPassword', value);
}
};
@ -77,7 +82,9 @@ export const RegisterForm = (): React.ReactElement => {
});
if (!result.success) {
const fieldError = result.error.issues.find((err) => err.path[0] === field);
const fieldError = result.error.issues.find(
(err) => err.path[0] === field,
);
if (fieldError !== null && fieldError !== undefined) {
setErrors((prev) => ({ ...prev, [field]: fieldError.message }));
} else {
@ -94,7 +101,9 @@ export const RegisterForm = (): React.ReactElement => {
}
};
const handleBlur = (field: 'email' | 'password' | 'confirmPassword'): void => {
const handleBlur = (
field: 'email' | 'password' | 'confirmPassword',
): void => {
let value: string;
if (field === 'email') {
value = email;
@ -153,10 +162,15 @@ export const RegisterForm = (): React.ReactElement => {
};
return (
<form className="auth-form" onSubmit={handleSubmit}>
<form
className="auth-form"
onSubmit={handleSubmit}
>
<div className="auth-form__header">
<h1 className="auth-form__title">Create Account</h1>
<p className="auth-form__subtitle">Get started with API Security Scanner</p>
<p className="auth-form__subtitle">
Get started with API Security Scanner
</p>
</div>
<div className="auth-form__fields">
@ -198,18 +212,29 @@ export const RegisterForm = (): React.ReactElement => {
</div>
{error !== null && error !== undefined && isAxiosError(error) ? (
<div className="auth-form__error-message" role="alert">
{(error.response?.data as { detail?: string } | undefined)?.detail ?? 'Registration failed. Please try again.'}
<div
className="auth-form__error-message"
role="alert"
>
{(error.response?.data as { detail?: string } | undefined)
?.detail ?? 'Registration failed. Please try again.'}
</div>
) : null}
<Button type="submit" isLoading={isPending} disabled={isPending}>
<Button
type="submit"
isLoading={isPending}
disabled={isPending}
>
Create Account
</Button>
<p className="auth-form__link">
Already have an account?{' '}
<Link to="/login" className="auth-form__link-text">
<Link
to="/login"
className="auth-form__link-text"
>
Sign in
</Link>
</p>

View File

@ -13,12 +13,16 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, id, ...props }, ref) => {
const inputId = id ?? `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
const inputId =
id ?? `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
const errorId = `${inputId}-error`;
return (
<div className="input-wrapper">
<label htmlFor={inputId} className="input-label">
<label
htmlFor={inputId}
className="input-label"
>
{label}
</label>
<input
@ -26,11 +30,17 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
id={inputId}
className={`input ${error !== null && error !== undefined ? 'input--error' : ''}`}
aria-invalid={error !== null && error !== undefined ? true : false}
aria-describedby={error !== null && error !== undefined ? errorId : undefined}
aria-describedby={
error !== null && error !== undefined ? errorId : undefined
}
{...props}
/>
{error !== null && error !== undefined ? (
<p id={errorId} className="input-error" role="alert">
<p
id={errorId}
className="input-error"
role="alert"
>
{error}
</p>
) : null}

View File

@ -14,18 +14,26 @@ export const LoadingOverlay = ({
tests,
}: LoadingOverlayProps): React.ReactElement => {
return (
<div className="loading-overlay" role="dialog" aria-label="Scan in progress">
<div
className="loading-overlay"
role="dialog"
aria-label="Scan in progress"
>
<div className="loading-overlay__content">
<div className="loading-overlay__spinner">
<div className="spinner"></div>
</div>
<h2 className="loading-overlay__title">Running Security Scan</h2>
<p className="loading-overlay__subtitle">
Testing {tests.length} {tests.length === 1 ? 'vulnerability' : 'vulnerabilities'}
Testing {tests.length}{' '}
{tests.length === 1 ? 'vulnerability' : 'vulnerabilities'}
</p>
<div className="loading-overlay__tests">
{tests.map((test) => (
<div key={test} className="loading-overlay__test">
<div
key={test}
className="loading-overlay__test"
>
{TEST_TYPE_LABELS[test]}
</div>
))}

View File

@ -33,7 +33,12 @@ export const ProtectedRoute = ({
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
return (
<Navigate
to="/login"
replace
/>
);
}
return children as React.ReactElement;

View File

@ -138,68 +138,82 @@ export const NewScanForm = (): React.ReactElement => {
return (
<>
{isPending ? <LoadingOverlay tests={selectedTests} /> : null}
<form className="scan-form" onSubmit={handleSubmit}>
<div className="scan-form__fields">
<Input
label="Target URL"
type="url"
value={targetUrl}
onChange={(e) => handleTargetUrlChange(e.target.value)}
error={errors.targetUrl}
placeholder="https://api.example.com/endpoint"
required
/>
<form
className="scan-form"
onSubmit={handleSubmit}
>
<div className="scan-form__fields">
<Input
label="Target URL"
type="url"
value={targetUrl}
onChange={(e) => handleTargetUrlChange(e.target.value)}
error={errors.targetUrl}
placeholder="https://api.example.com/endpoint"
required
/>
<Input
label="Auth Token (Optional)"
type="text"
value={authToken}
onChange={(e) => handleAuthTokenChange(e.target.value)}
error={errors.authToken}
placeholder="Bearer token or API key"
/>
<Input
label="Auth Token (Optional)"
type="text"
value={authToken}
onChange={(e) => handleAuthTokenChange(e.target.value)}
error={errors.authToken}
placeholder="Bearer token or API key"
/>
<div className="scan-form__field">
<label className="scan-form__label">
Select Tests
{errors.testsToRun !== null && errors.testsToRun !== undefined ? (
<span className="scan-form__error" role="alert">
{errors.testsToRun}
</span>
) : null}
</label>
<div className="scan-form__checkboxes">
{Object.values(SCAN_TEST_TYPES).map((test) => (
<label key={test} className="scan-form__checkbox-label">
<input
type="checkbox"
checked={selectedTests.includes(test)}
onChange={() => handleTestToggle(test)}
className="scan-form__checkbox"
/>
<span>{TEST_TYPE_LABELS[test]}</span>
</label>
))}
<div className="scan-form__field">
<label className="scan-form__label">
Select Tests
{errors.testsToRun !== null &&
errors.testsToRun !== undefined ? (
<span
className="scan-form__error"
role="alert"
>
{errors.testsToRun}
</span>
) : null}
</label>
<div className="scan-form__checkboxes">
{Object.values(SCAN_TEST_TYPES).map((test) => (
<label
key={test}
className="scan-form__checkbox-label"
>
<input
type="checkbox"
checked={selectedTests.includes(test)}
onChange={() => handleTestToggle(test)}
className="scan-form__checkbox"
/>
<span>{TEST_TYPE_LABELS[test]}</span>
</label>
))}
</div>
</div>
<Input
label="Max Requests"
type="number"
value={maxRequests}
onChange={(e) => handleMaxRequestsChange(e.target.value)}
error={errors.maxRequests}
placeholder="50"
min="1"
max="50"
required
/>
</div>
<Input
label="Max Requests"
type="number"
value={maxRequests}
onChange={(e) => handleMaxRequestsChange(e.target.value)}
error={errors.maxRequests}
placeholder="50"
min="1"
max="50"
required
/>
</div>
<Button type="submit" isLoading={isPending} disabled={isPending}>
{isPending ? 'Running Scan...' : 'Start Scan'}
</Button>
</form>
<Button
type="submit"
isLoading={isPending}
disabled={isPending}
>
{isPending ? 'Running Scan...' : 'Start Scan'}
</Button>
</form>
</>
);
};

View File

@ -65,7 +65,10 @@ export const ScansList = (): React.ReactElement => {
const scanDate = formatDate(scan.scan_date);
return (
<div key={scan.id} className="scans-list__row">
<div
key={scan.id}
className="scans-list__row"
>
<div className="scans-list__cell">
<span className="scans-list__url">{scan.target_url}</span>
</div>

View File

@ -59,10 +59,15 @@ export const TestResultCard = ({
{result.recommendations_json.length > 0 ? (
<div className="test-result-card__section">
<h4 className="test-result-card__section-title">Recommendations</h4>
<h4 className="test-result-card__section-title">
Recommendations
</h4>
<ul className="test-result-card__recommendations">
{result.recommendations_json.map((rec) => (
<li key={`${result.id.toString()}-rec-${rec}`} className="test-result-card__recommendation">
<li
key={`${result.id.toString()}-rec-${rec}`}
className="test-result-card__recommendation"
>
{rec}
</li>
))}
@ -78,9 +83,7 @@ export const TestResultCard = ({
className="test-result-card__evidence-toggle"
aria-expanded={showEvidence}
>
<span>
{showEvidence ? '▼' : '▶'} Technical Evidence
</span>
<span>{showEvidence ? '▼' : '▶'} Technical Evidence</span>
</button>
{showEvidence ? (
<pre className="test-result-card__evidence">

View File

@ -21,7 +21,8 @@ export const AUTH_ENDPOINTS = {
LOGIN: '/auth/login',
} as const;
export type AuthEndpoint = (typeof AUTH_ENDPOINTS)[keyof typeof AUTH_ENDPOINTS];
export type AuthEndpoint =
(typeof AUTH_ENDPOINTS)[keyof typeof AUTH_ENDPOINTS];
/**
* Auth Error Messages

View File

@ -3,10 +3,7 @@
// ©AngelaMos | 2025
// ===========================
import {
useMutation,
type UseMutationResult,
} from '@tanstack/react-query';
import { useMutation, type UseMutationResult } from '@tanstack/react-query';
import { isAxiosError, type AxiosError } from 'axios';
import { toast } from 'sonner';
import { useNavigate } from 'react-router-dom';
@ -29,12 +26,23 @@ import { useAuthStore } from '@/store/authStore';
const createAuthErrorHandler = (context: string) => {
return (error: unknown): void => {
if (isAxiosError(error) && error.response?.data !== null && error.response?.data !== undefined) {
if (
isAxiosError(error) &&
error.response?.data !== null &&
error.response?.data !== undefined
) {
const errorData: unknown = error.response.data;
if (typeof errorData === 'object' && errorData !== null && 'detail' in errorData) {
if (
typeof errorData === 'object' &&
errorData !== null &&
'detail' in errorData
) {
const apiError = errorData as { detail: unknown };
if (typeof apiError.detail === 'string' && apiError.detail.length > 0) {
if (
typeof apiError.detail === 'string' &&
apiError.detail.length > 0
) {
toast.error(apiError.detail);
return;
}

View File

@ -43,7 +43,9 @@ axiosInstance.interceptors.response.use(
(error: AxiosError) => {
if (error.response?.status === 401) {
const requestUrl = error.config?.url ?? '';
const isAuthEndpoint = requestUrl.includes('/auth/login') || requestUrl.includes('/auth/register');
const isAuthEndpoint =
requestUrl.includes('/auth/login') ||
requestUrl.includes('/auth/register');
if (!isAuthEndpoint) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);

View File

@ -8,7 +8,11 @@ import { toast } from 'sonner';
export const createApiErrorHandler = (context: string) => {
return (error: unknown): void => {
if (isAxiosError(error) && error.response?.data !== null && error.response?.data !== undefined) {
if (
isAxiosError(error) &&
error.response?.data !== null &&
error.response?.data !== undefined
) {
const errorData: unknown = error.response.data;
if (
@ -24,7 +28,9 @@ export const createApiErrorHandler = (context: string) => {
}
const fallbackMessage =
error instanceof Error ? error.message : `Operation failed: ${context}`;
error instanceof Error
? error.message
: `Operation failed: ${context}`;
toast.error(fallbackMessage);
};
};

View File

@ -33,7 +33,10 @@ export const DashboardPage = (): React.ReactElement => {
</div>
<div className="dashboard__header-actions">
<span className="dashboard__user-email">{user?.email}</span>
<Button onClick={handleLogout} variant="secondary">
<Button
onClick={handleLogout}
variant="secondary"
>
Logout
</Button>
</div>

View File

@ -27,7 +27,10 @@ export const ScanResultsPage = (): React.ReactElement => {
return (
<div className="scan-results__error">
<p>Failed to load scan results. Please try again.</p>
<Link to="/" className="scan-results__back-link">
<Link
to="/"
className="scan-results__back-link"
>
Back to Dashboard
</Link>
</div>
@ -38,7 +41,10 @@ export const ScanResultsPage = (): React.ReactElement => {
return (
<div className="scan-results__error">
<p>Scan not found.</p>
<Link to="/" className="scan-results__back-link">
<Link
to="/"
className="scan-results__back-link"
>
Back to Dashboard
</Link>
</div>
@ -55,7 +61,10 @@ export const ScanResultsPage = (): React.ReactElement => {
<div className="scan-results">
<div className="scan-results__container">
<header className="scan-results__header">
<Link to="/" className="scan-results__back-link">
<Link
to="/"
className="scan-results__back-link"
>
Back to Dashboard
</Link>
<h1 className="scan-results__title">Scan Results</h1>
@ -77,7 +86,9 @@ export const ScanResultsPage = (): React.ReactElement => {
</span>
</div>
<div className="scan-results__meta-item">
<span className="scan-results__meta-label">Vulnerabilities:</span>
<span className="scan-results__meta-label">
Vulnerabilities:
</span>
<span
className={`scan-results__vuln-count ${
vulnerableCount > 0
@ -93,7 +104,10 @@ export const ScanResultsPage = (): React.ReactElement => {
<div className="scan-results__tests">
{scan.test_results.map((result) => (
<TestResultCard key={result.id} result={result} />
<TestResultCard
key={result.id}
result={result}
/>
))}
</div>
</div>

View File

@ -37,6 +37,11 @@ export const router = createBrowserRouter([
},
{
path: '*',
element: <Navigate to="/" replace />,
element: (
<Navigate
to="/"
replace
/>
),
},
]);

View File

@ -41,9 +41,18 @@ interface UIState {
}
interface UIActions {
setLoginFormField: (field: keyof Omit<LoginFormState, 'expiresAt'>, value: string) => void;
setRegisterFormField: (field: keyof Omit<RegisterFormState, 'expiresAt'>, value: string) => void;
setScanFormField: (field: keyof Omit<ScanFormState, 'expiresAt'>, value: string | ScanTestType[]) => void;
setLoginFormField: (
field: keyof Omit<LoginFormState, 'expiresAt'>,
value: string,
) => void;
setRegisterFormField: (
field: keyof Omit<RegisterFormState, 'expiresAt'>,
value: string,
) => void;
setScanFormField: (
field: keyof Omit<ScanFormState, 'expiresAt'>,
value: string | ScanTestType[],
) => void;
toggleTestExpanded: (testId: number) => void;
clearLoginForm: () => void;
clearRegisterForm: () => void;
@ -113,7 +122,8 @@ export const useUIStore = create<UIStore>()(
toggleTestExpanded: (testId): void => {
set((state) => {
const currentValue = state.testResults.expandedTests[testId] ?? false;
const currentValue =
state.testResults.expandedTests[testId] ?? false;
state.testResults.expandedTests[testId] = !currentValue;
});
},

View File

@ -14,11 +14,7 @@ import type {
ScanStatus,
Severity,
} from './scan.types';
import {
SCAN_TEST_TYPES,
SCAN_STATUS,
SEVERITY,
} from '@/config/constants';
import { SCAN_TEST_TYPES, SCAN_STATUS, SEVERITY } from '@/config/constants';
export const isValidLoginResponse = (
data: unknown,
@ -89,7 +85,9 @@ const isValidTestResult = (data: unknown): data is TestResult => {
typeof obj.evidence_json === 'object' &&
obj.evidence_json !== null &&
Array.isArray(obj.recommendations_json) &&
obj.recommendations_json.every((rec: unknown) => typeof rec === 'string') &&
obj.recommendations_json.every(
(rec: unknown) => typeof rec === 'string',
) &&
typeof obj.created_at === 'string'
);
};

View File

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

View File

@ -28,6 +28,6 @@
"react",
"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"
}

View File

@ -3,7 +3,7 @@ CarterPerez-dev | 2025
This keylogger demonstrates:
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
"""
@ -11,11 +11,11 @@ import sys
import logging
import platform
from enum import (
Enum,
Enum,
auto,
)
from threading import (
Event,
Event,
Lock,
)
from pathlib import Path

View File

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