Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ Important paths:
- `backend/alembic/versions/` contains database migrations.
- `backend/tests/` contains the backend test suite.

The application exposes process-only `/health/live` and database-backed `/health/ready`. HTTP responses include a privacy-safe request ID and optional bounded correlation ID. Backend middleware records normalized route, method, status family, and duration through the structured/redacted logging and in-process metrics foundation. Optional Prometheus text export at `/internal/metrics` is disabled by default and token protected when enabled. These are vendor-neutral operational foundations, not distributed tracing, a durable audit ledger, or a production hosting architecture; see [the observability policy](docs/observability/logging-policy.md) and [runbook](docs/operations/observability-runbook.md).
The application exposes process-only `/health/live` and database-backed `/health/ready`. HTTP responses include a privacy-safe request ID and optional bounded correlation ID. Backend middleware records normalized route, method, status family, and duration through the structured/redacted logging and in-process metrics foundation. Optional Prometheus text export at `/internal/metrics` is disabled by default and token protected when enabled. These are vendor-neutral operational foundations, not distributed tracing or a production hosting architecture; see [the observability policy](docs/observability/logging-policy.md) and [runbook](docs/operations/observability-runbook.md).

High-impact supported mutations additionally write a transaction-bound `audit_events` record with allowlisted metadata and minimized before/after state. Platform and organization review APIs apply explicit scopes; per-scope SHA-256 chains support integrity verification after backup/restore. This is application-level tamper evidence, not externally anchored tamper-proof storage. See [the audit-event policy](docs/audit/audit-event-policy.md).

Backend authorization uses explicit platform/organization role allowlists plus reusable organization, section, course-content, forum-ownership, and learner-progress scope checks. Sensitive routes use a configurable process-local fixed-window limiter; it is suitable only for the checked-in single-Uvicorn-process topology and must be replaced by shared storage before horizontal scaling. The canonical policy and limitations are in [docs/security/role-authorization-policy.md](docs/security/role-authorization-policy.md) and [docs/security/rate-limiting-policy.md](docs/security/rate-limiting-policy.md).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ For a deeper overview, see [ARCHITECTURE.md](ARCHITECTURE.md).

Platform maturity evidence, including the measured frontend bundle work and the security, observability, operations, dependency, and backend-capability baselines, is indexed in [docs/platform-maturity](docs/platform-maturity/phase-7-baseline.md). These documents are readiness inputs; they do not claim that EchoEd 1.0 is production-ready.

Phase 10 adds vendor-neutral structured logging, request correlation, liveness/readiness, protected optional metrics export, safe frontend support references, and operational guidance. Configuration and endpoint policy are documented under [docs/observability](docs/observability/logging-policy.md); this is not a commercial monitoring integration or durable audit system.
Phase 10 adds vendor-neutral structured logging, request correlation, liveness/readiness, protected optional metrics export, safe frontend support references, and operational guidance. Configuration and endpoint policy are documented under [docs/observability](docs/observability/logging-policy.md). High-impact administrative mutations are separately captured under [the audit-event policy](docs/audit/audit-event-policy.md).

Phase 8 security-hardening evidence and operator-facing limitations are indexed in [docs/security/phase-8-security-baseline.md](docs/security/phase-8-security-baseline.md). Configure rate limits with the documented `RATE_LIMIT_<GROUP>_LIMIT` and `RATE_LIMIT_<GROUP>_WINDOW_SECONDS` variables; the current store is process-local and forwarded client-IP headers are intentionally ignored.

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Security reports may cover:

The focused [Phase 7 security baseline](docs/platform-maturity/security-baseline.md) records the prior evidence. The [Phase 8 security baseline](docs/security/phase-8-security-baseline.md), [threat model](docs/security/phase-8-threat-model.md), and linked policies document backend-enforced forum ownership, privileged-user invariants, role allowlists, configurable rate limits, upload signature checks, minimized responses, and expanded object/organization tests. These are scoped hardening controls, not a penetration test or production-readiness certification.

Durable audit events, distributed rate-limit storage, private/scanned asset delivery, session revocation, production proxy/host/CSP/HSTS validation, and formal privacy/retention work remain explicit future work. Do not use the current demo with real learner or production data.
High-impact supported mutations now produce transaction-bound, privacy-minimized audit records with scoped review/export and application-level integrity verification. External anchoring, WORM storage, legal-retention guarantees, distributed rate-limit storage, private/scanned asset delivery, and session revocation remain future work. Do not use the current demo with real learner or production data.

## Reporting a Vulnerability

Expand Down Expand Up @@ -63,7 +63,7 @@ Please do not publicly disclose a suspected vulnerability until there has been a

## Diagnostic References and Sensitive Evidence

Unexpected API failures may display a bounded request reference. It is safe to include that reference, the approximate time, the action, and a non-sensitive page name in a report. Do not provide passwords, tokens, cookies, authorization headers, invitation/reset links, uploaded files, learner records, assessment responses, or private course content. Backend operational logs and metrics are privacy-redacted diagnostics; they are not a durable or tamper-resistant audit record. See the [observability runbook](docs/operations/observability-runbook.md).
Unexpected API failures may display a bounded request reference. It is safe to include that reference, the approximate time, the action, and a non-sensitive page name in a report. Do not provide passwords, tokens, cookies, authorization headers, invitation/reset links, uploaded files, learner records, assessment responses, or private course content. Backend operational logs and metrics remain privacy-redacted diagnostics; durable high-impact action records are governed separately by [the audit-event policy](docs/audit/audit-event-policy.md).

Production configuration fails closed and never loads dotenv. Allowed hosts are enforced, and forwarded client/protocol/host metadata is ignored unless the direct peer belongs to an explicitly configured CIDR. Operators must never attach secrets, database URLs, backup contents, or raw environment dumps to issues; share only setting categories, safe request references, release identifiers, timestamps, and pass/fail results. See the [production configuration contract](docs/operations/production-configuration.md).

Expand Down
59 changes: 59 additions & 0 deletions backend/alembic/versions/b8f4c2d6e1a0_add_platform_audit_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Add durable platform audit events.

Revision ID: b8f4c2d6e1a0
Revises: 9a7b6c5d4e3f
Create Date: 2026-08-13 18:00:00.000000
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


revision = "b8f4c2d6e1a0"
down_revision = "9a7b6c5d4e3f"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"audit_events",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("schema_version", sa.Integer(), nullable=False),
sa.Column("scope_key", sa.String(length=80), nullable=False),
sa.Column("scope_sequence", sa.Integer(), nullable=False),
sa.Column("organization_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("actor_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("actor_role", sa.String(length=40), nullable=False),
sa.Column("action", sa.String(length=100), nullable=False),
sa.Column("category", sa.String(length=40), nullable=False),
sa.Column("outcome", sa.String(length=24), nullable=False),
sa.Column("target_type", sa.String(length=60), nullable=False),
sa.Column("target_id", sa.String(length=100), nullable=False),
sa.Column("request_id", sa.String(length=128), nullable=True),
sa.Column("correlation_id", sa.String(length=64), nullable=True),
sa.Column("reason_code", sa.String(length=80), nullable=True),
sa.Column("before_state", sa.JSON(), nullable=False),
sa.Column("after_state", sa.JSON(), nullable=False),
sa.Column("previous_hash", sa.String(length=64), nullable=False),
sa.Column("event_hash", sa.String(length=64), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("scope_key", "scope_sequence", name="uq_audit_events_scope_sequence"),
sa.UniqueConstraint("event_hash"),
)
op.create_index("ix_audit_events_scope_created", "audit_events", ["scope_key", "created_at", "id"])
op.create_index("ix_audit_events_action_created", "audit_events", ["action", "created_at"])
op.create_index("ix_audit_events_actor_created", "audit_events", ["actor_id", "created_at"])
op.create_index(
"ix_audit_events_target_created", "audit_events", ["target_type", "target_id", "created_at"]
)


def downgrade() -> None:
op.drop_index("ix_audit_events_target_created", table_name="audit_events")
op.drop_index("ix_audit_events_actor_created", table_name="audit_events")
op.drop_index("ix_audit_events_action_created", table_name="audit_events")
op.drop_index("ix_audit_events_scope_created", table_name="audit_events")
op.drop_table("audit_events")
Loading
Loading