Commit Graph

5 Commits

Author SHA1 Message Date
knacky
dd5c508b04 fix(backend): JSON error envelope for every HTTPException + strict_slashes=False
Some checks failed
ci / backend (lint + typecheck + unit tests) (push) Failing after 0s
ci / frontend (lint + typecheck + build + unit tests) (push) Failing after 1s
Two issues spotted by ux-frontend consuming docs/api.md against the actual
code path:

1. `flask.abort(...)` returned the Werkzeug HTML error page for 400/403/404/
   422/etc. — only the 401 paths going through `api_error()` and the
   Flask-Login `unauthorized_handler` honoured the `{error, message}`
   envelope the contract promised. The frontend's `ApiClientError.body`
   parser was forced to fall back to a raw string, and the 422 case
   could not surface Pydantic per-field errors.

   Fix: register `@app.errorhandler(HTTPException)` that serialises every
   `HTTPException` to the same JSON envelope. 422s gain a `details: [...]`
   field holding the Pydantic `errors()` list (`loc` / `msg` / `type`),
   matching the shape now documented in `docs/api.md`.

   A `_HTTP_ERROR_CODES` map maps statuses to stable snake_case codes
   (`bad_request`, `not_found`, `method_not_allowed`,
   `validation_error`, `forbidden`, `internal_error`, ...). Unknown
   statuses fall back to `http_error`.

   `description` is `cast(object, ...)` because the Werkzeug stub pins it
   to `str | None` while `flask.abort(..., description=<list>)` is the
   officially supported way to smuggle a Pydantic errors list to the
   handler.

2. `@bp.get("")` on the engagements blueprint produced `/api/v1/engagements`
   (no slash). Hitting it with a trailing slash issued a 308 redirect,
   and some browsers drop the session cookie across that hop.

   Fix: `app.url_map.strict_slashes = False`. Both forms now match the
   same handler without redirect.

5 new integration tests cover the new envelope shape (422 with details,
unknown 404, malformed-JSON 400) and the dual-slash matching. `docs/api.md`
rewritten to reflect the table of stable codes, the `details` shape, and
the no-trailing-slash convention. `CHANGELOG.md` gains a follow-up entry.

Verification: ruff check / mypy --strict / pytest tests/unit all green
(61 unit + 5 new integration).
2026-05-23 04:33:23 +02:00
knacky
e1b381af4d test(backend): cover auth schemas + login/engagement E2E
Unit:
- test_auth_schemas: LoginRequest validation (min/max bounds, extra-fields
  policy) + serialize_current_user round-trip (RT lead permission set,
  RT operator subset, display_name None pass-through).

Integration (testcontainers Postgres, marked `integration`):
- test_login_then_create_and_list_engagement: full sprint-1 user journey —
  /me → 401, POST /login → 200, /me → 200, POST /engagements → 201,
  GET /engagements lists the new row, POST /logout → 204, /me → 401.
- test_login_rejects_bad_credentials: wrong password AND unknown user
  return the exact same 401 invalid_credentials envelope (no enumeration
  leak).
- test_logout_without_session_returns_401: /logout on anonymous returns
  the uniform not_authenticated envelope.

Unit total: 61 passed in 0.50s. Integration tests skip locally when
testcontainers is absent.
2026-05-23 04:21:55 +02:00
knacky
e77ca906d4 docs(backend): track sprint-0 follow-ups + flag integration migration gap
- `tasks/todo.md`: B0.5 description updated (re2 hard dep, no fallback);
  add a "Backend follow-ups (sprint 1+)" section with M1-M7 + N1-N6 from
  the code-review verdict.
- `CHANGELOG.md`: backend skeleton bullets refreshed (no re fallback,
  streaming blob store, audit + scope on CRUD, 56 unit tests); new
  "Code-review remediation" subsection lists B1 / MA1-MA6 / N4 / N6 / M8
  with one-line rationale each.
- `tests/integration/conftest.py`: leave `db.create_all()` in place but
  add an inline TODO (N6) pointing at the Alembic switchover that will
  exercise the F11 seed + audit-log role grants in CI.
2026-05-22 05:25:04 +02:00
knacky
adab8a58e7 chore(backend): mypy strict clean + ruff format pass
Pre-merge sanity per devops checklist (ruff format --check, mypy --strict).

Type fixes:
- ORM models: `Mapped[dict]` → `Mapped[dict[str, Any]]` (audit, scenario, run,
  report, ttp, detection.artifact_files_json). Equivalent on Pydantic DTOs
  (TtpBase.params_schema_json, ScenarioStepBase.params_override_json).
- Rename `TtpRead.current_version` → `TtpRead.version` to mirror the ORM
  column (which itself was renamed in D-009 cleanup).
- Flask blueprints: add `-> ResponseReturnValue` to every view, plus typed
  UUID params on `_validate_step_consistency`.
- `templating/filters.py`: rewrite the conditional re2 import so mypy can
  narrow the union (`ModuleType | None`); the runtime branch on `_re2 is not
  None` removes the unused-ignore that was triggered by warn_unused_ignores.
- `pyproject.toml`: add `flask_login.*` and `pythonjsonlogger.*` to the
  `[[tool.mypy.overrides]]` `ignore_missing_imports` list (both ship without
  typed marker).
- Misc: drop stale `# type: ignore` comments (`app.py:36`,
  `rbac/decorators.py:35`) flagged by `warn_unused_ignores`. Keep
  `logging.JsonFormatter` ignore because the symbol exists at runtime but is
  not re-exported through the typed surface.

Formatting:
- `ruff format` applied (15 files normalized; line-length unchanged at 100).

Verification on this commit:
- `ruff check`  → All checks passed.
- `ruff format --check` → 68 files already formatted.
- `mypy --strict src` → Success: no issues found in 54 source files.
- `pytest tests/unit` → 49 passed.
2026-05-22 05:10:51 +02:00
knacky
5d9415bb9f test(backend): add pytest baseline (B0.8)
Unit (SQLite, pure logic):
- test_templating.py: Jinja2 sandbox, regex_extract, strict-undefined,
  sandbox blocks attribute-access escape, output blob 10 MB cap.
- test_password.py: bcrypt hash + verify, empty / malformed handling.
- test_soc_token.py: 256-bit url-safe token + bcrypt verification.
- test_rbac_matrix.py: F11 invariants (lead ⊇ operator, SOC restricted
  to detection + report-read, audit_read & ttp_promote lead-only).
- test_connector_factory.py: register / build / double-register-rejected,
  TaskStatus terminal helper, Mythic mapping vs empty Home mapping.
- test_audit_hash.py: SHA-256 chain helper is deterministic and reacts
  to prev_hash / metadata changes.

Integration scaffold (testcontainers Postgres):
- tests/integration/conftest.py spins up postgres:16-alpine, monkeypatches
  MIMIC_DATABASE_URL, creates a Flask app + db.create_all.
- test_healthz.py: end-to-end smoke through the Flask test client.

38 unit tests pass; ruff clean.
2026-05-21 20:36:03 +02:00