Skip to content

feat: Milestone 0.8 — trustworthy numbers & security model - #8

Merged
asharahmed merged 4 commits into
mainfrom
feat/0.8-trustworthy
Jul 17, 2026
Merged

feat: Milestone 0.8 — trustworthy numbers & security model#8
asharahmed merged 4 commits into
mainfrom
feat/0.8-trustworthy

Conversation

@asharahmed

Copy link
Copy Markdown
Owner

Implements all of Milestone 0.8 from docs/ROADMAP.md — the load-bearing correctness + security work for 1.0. Four reviewable commits:

0.8.1 — Per-project authorization (the top 1.0 blocker)

Any authenticated Viewer could read every project's findings/dashboard/reports. Reads are now scoped to what a user may see: public projects, projects they're a member of, or all projects for Admins. Unauthorized single-project access returns 404 (existence not disclosed).

  • ProjectMembership + Project.IsPublic; Registration:Mode config (Disabled by default; demo sets Open); admin grant/revoke/list-members + set-visibility endpoints.
  • Demo preserved: demo projects are seeded public (existing ones flipped on startup) and Terraform sets Registration__Mode=Open, so open-signup Viewers still see the showcase.

0.8.2 — Token revocation

JWTs carry the Identity security stamp, validated every request. A role change / password reset / disable revokes existing tokens immediately instead of at expiry.

0.8.4 — Injectable clock

TimeProvider injected into the time-dependent services (ingestion, re-eval, enrichment) — the enabler for deterministic time tests.

0.8.3 — Immutable posture snapshots

An append-only PostureSnapshot is captured once per day (via the hourly re-eval), so historical compliance figures stay fixed as live deadlines pass. GET /api/posture/history (Admin-only).


Migrations: 3 additive (membership+IsPublic, snapshots) — safe on existing data. Tests: build clean, 107 unit tests green; new integration tests (cross-project isolation, registration-disabled-by-default, membership grant→revoke, token revocation, snapshot immutability) run in CI on real SQL.

Note: merging changes the default security posture — self-registration is off unless Registration:Mode=Open, and reads are membership-scoped. The Terraform env keeps the public demo working.

Closes the top 1.0 blocker: any authenticated Viewer could read EVERY project's
findings/dashboard/reports. Reads are now scoped to the projects a user may see.

- ProjectMembership (user↔project) + Project.IsPublic (+ migration).
- Reads scoped everywhere: /api/projects, /projects/{key}/findings, /dashboard,
  /report/breaches, /sla-summary, /alerts(+count), and triage. Public projects are
  visible to any authenticated user; members see their projects; Admins see all.
  Unauthorized single-project access returns 404 (existence not disclosed).
- Registration:Mode config (Disabled by default; the public demo sets Open).
- Admin endpoints: grant/revoke membership (by email), list members, set visibility;
  create-project takes IsPublic.
- Demo preserved: demo projects seeded public (and existing ones flipped public on
  startup) so open-signup Viewers still see the showcase; Terraform sets
  Registration__Mode=Open on the demo.
- Integration tests: cross-project isolation, public visibility, admin-sees-all,
  registration-disabled-by-default, membership grant→revoke visibility.

Build clean; 107 unit tests green; new authz tests run in CI (Docker).
JWTs now carry the user's Identity security stamp, validated on every request
(JwtBearer OnTokenValidated). Rotating the stamp revokes already-issued tokens
immediately instead of leaving them valid until the 60-minute expiry.

- JwtTokenService issues an 'sstamp' claim; login/register include it.
- Per-request validation rejects a token whose stamp no longer matches the user
  (or whose user was deleted).
- Role change explicitly rotates the stamp (password change/reset already do via
  Identity), so a demoted/off-boarded user loses access at once.
- Integration test: a token is rejected after the stamp rotates. Test fixture
  updated to mint stamped tokens.
Register TimeProvider (System) and inject it into IngestionService, SlaReEvaluator,
and FindingEnrichmentService, replacing direct DateTimeOffset.UtcNow reads. This
makes their time-dependent behavior driveable by a fake clock (the enabler for
deterministic SLA-transition and posture-snapshot tests). The pure domain already
takes 'now' as a parameter; read-time endpoint evaluation still uses UtcNow (an
inherently 'now' operation).
The live dashboard/trend is recomputed from current findings, so historical
compliance silently changes as deadlines pass. Capture an append-only
PostureSnapshot once per UTC day so the number an auditor saw stays fixed.

- PostureSnapshot entity (org rollup: compliance %, open/breached/due-soon/on-track,
  severity counts, KEV count) + migration; unique index on the day = append-only,
  idempotent, replica-safe (losing concurrent write no-ops).
- PostureSnapshotService.EnsureSnapshotAsync (uses the injected TimeProvider),
  called from the hourly re-evaluation so a daily snapshot lands automatically.
- GET /api/posture/history (Admin-only — it aggregates all projects).
- Integration tests: a snapshot is captured once/day and stays frozen after the
  findings change; history endpoint is admin-only.
@asharahmed
asharahmed merged commit ae41029 into main Jul 17, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant