Merge pull request #4 from CarterPerez-dev/chore/prettier-and-workflows
move workflow files - add prettier formatting and format
This commit is contained in:
commit
104c4fa07a
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
ⒸAngelaMos | CarterPerez-dev
|
||||
ⒸAngelaMos | CarterPerez-dev
|
||||
ⒸCertGames.com | 2025
|
||||
----
|
||||
API Security Scanner
|
||||
|
|
@ -15,6 +15,6 @@ API Security Scanner
|
|||
⣿⣿⣿⣿⣿⣿⣿⣿⠄⣴⣿⣶⣄⠄⣴⣶⠄⢀⣾⣿⣿⣿⣿⣿⣿⠃⠄⠄
|
||||
⠈⠻⣿⣿⣿⣿⣿⣿⡄⢻⣿⣿⣿⠄⣿⣿⡀⣾⣿⣿⣿⣿⣛⠛⠁
|
||||
⠄⠄⠈⠛⢿⣿⣿⣿⠁⠞⢿⣿⣿⡄⢿⣿⡇⣸⣿⣿⠿⠛⠁⠄
|
||||
⠄⠄⠄⠄⠄⠉⠻⣿⣿⣾⣦⡙⠻⣷⣾⣿⠃⠿⠋⠁⠄
|
||||
⠄⠄⠄⠄⠄⠉⠻⣿⣿⣾⣦⡙⠻⣷⣾⣿⠃⠿⠋⠁⠄
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
ⒸCertGames.com | 2025
|
||||
ⒸAngelaMos | CarterPerez-dev
|
||||
ⒸAngelaMos | CarterPerez-dev
|
||||
----
|
||||
API Security Scanner FastAPI entry point
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class Scan(BaseModel):
|
|||
"""
|
||||
Stores metadata about scans performed on target URLs
|
||||
"""
|
||||
|
||||
__tablename__ = "scans"
|
||||
|
||||
user_id = Column(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class User(BaseModel):
|
|||
"""
|
||||
Stores authentication credentials and user information
|
||||
"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
email = Column(
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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't have an account?{' '}
|
||||
<Link to="/register" className="auth-form__link-text">
|
||||
<Link
|
||||
to="/register"
|
||||
className="auth-form__link-text"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ export const ProtectedRoute = ({
|
|||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
return (
|
||||
<Navigate
|
||||
to="/login"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children as React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ export const router = createBrowserRouter([
|
|||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/" replace />,
|
||||
element: (
|
||||
<Navigate
|
||||
to="/"
|
||||
replace
|
||||
/>
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue