Closes the four remaining items from the post-Codex hardening list. #1 Rate-limit unlock + change-password endpoints (1.0.0-33) * Generalised the existing login limiter into a reusable `_RateLimiter` class in app/auth.py. Atomic check-then-increment in synchronous code so a parallel asyncio burst can't slip past the threshold. * `unlock_limiter` (5 attempts in 10 min → 10 min lockout) gates POST /api/v1/drives/{id}/unlock per-drive AND per-source-IP. * `pwchange_limiter` (5 in 10 min → 15 min lockout) gates POST /api/v1/auth/change-password per-user AND per-IP. * Both clear on successful operation. The login limiter keeps its existing `register_login_attempt` / `clear_login_failures` facade names so external callers don't change. #3 mypy in security-scan (1.0.0-33) * Added a 4th tool to the daily scan + forge workflow. Runs in a throwaway python:3.12-slim container against the deploy dir, exit code is informational only (NOT included in the `TOTAL_EXIT` failure sum). Findings land in ~/security-scans/scan-YYYY-MM-DD/mypy.txt for ratchet-down work over time. * Forge job uses `continue-on-error: true` so it doesn't fail the workflow until the type-debt baseline is annotated down. #4 Lifecycle test coverage (1.0.0-33) * New tests/test_lifecycle.py with 15 cases: - TestCommonHelpers (7 tests): _start_stage, _finish_stage success/failure/error-preservation, _recalculate_progress weighted math, _is_cancelled, _append_stage_log. - TestStartCancelJob (4 tests): start_job inserts queued row + correct stage list, duplicate-active rejection, cancel marks state, cancel returns False on terminal-state jobs. - TestRateLimiter (4 tests): under-threshold ok, trips at threshold, clear removes both counter + lockout, separate keys don't interfere. * Total goes from 44 to 59 tests; closes the orchestration-path coverage gap Codex flagged. #2 Partial routes.py split (1.0.0-34) * routes.py → routes/ package. Same staged-extraction pattern as the burnin.py split. * routes/auth.py — login/logout/setup/change-password (170 LoC). * routes/system.py — /health, /ws/terminal, /api/v1/updates/check (136 LoC). * routes/_helpers.py — shared utilities used by both extracted modules and the still-monolithic remainder: client_ip, operator_for, is_stale, stale_context, secret_status, SECRET_FIELDS (97 LoC). * routes/__init__.py shrank from 1568 LoC to 1261. Future slices can extract drives, burnin, history, settings the same way. * GOTCHA recorded in commit body: `from app import auth` at the top of __init__.py binds `auth` as an attribute on the package namespace, so `from . import auth as _auth_routes` finds the OUTER module and yields `app.auth` instead of the submodule. Fix is `import app.routes.auth as _auth_routes` (absolute). This bit me once at deploy time; container failed to start with `module 'app.auth' has no attribute 'router'`. Verification: 59/59 tests pass (44 existing + 15 new); container boots clean at 1.0.0-34; /health 200 with all checks green; security scan still clean (mypy informational findings ignored from totals). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.4 KiB
YAML
76 lines
2.4 KiB
YAML
name: Security scan
|
|
|
|
# Runs on every push to main, every PR, and nightly at 07:00 UTC (~03:00 EDT).
|
|
# Three jobs run in parallel — failure of any one fails the workflow,
|
|
# making findings visible in the forge UI.
|
|
#
|
|
# Tools:
|
|
# pip-audit — known CVEs in pinned dependencies (PyPI advisory DB)
|
|
# bandit — Python static security analysis (subprocess, eval, etc.)
|
|
# gitleaks — secrets in git history (full repo scan)
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
schedule:
|
|
- cron: "0 7 * * *"
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
|
|
pip-audit:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
- name: Install pip-audit
|
|
run: pip install --upgrade pip-audit
|
|
- name: Audit requirements.txt
|
|
run: pip-audit --requirement requirements.txt --strict --format=columns
|
|
|
|
bandit:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
- name: Install bandit
|
|
run: pip install --upgrade bandit
|
|
- name: Static security analysis
|
|
# B608: SQL string construction. All dynamic SQL in this repo uses
|
|
# bound parameters for data; the dynamic part is structural
|
|
# (column lists / IN-clause '?,?,?' placeholders). Reviewed.
|
|
run: bandit -r app -ll -ii --skip B608 -x app/__pycache__,tests
|
|
|
|
gitleaks:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Install gitleaks
|
|
run: |
|
|
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \
|
|
| tar -xz gitleaks
|
|
chmod +x gitleaks
|
|
- name: Scan git history for secrets
|
|
run: ./gitleaks detect --source . --no-banner --redact --verbose
|
|
|
|
mypy:
|
|
runs-on: ubuntu-latest
|
|
# Informational — does not fail the workflow. Use `continue-on-error`
|
|
# so the build stays green while we work down the type-debt baseline.
|
|
continue-on-error: true
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
- name: Install mypy
|
|
run: pip install --upgrade mypy
|
|
- name: Type check
|
|
run: mypy --ignore-missing-imports --no-strict-optional app
|