diff --git a/.gitignore b/.gitignore index 408e06d1..2156b24e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /waf-ids-state*.json /runtime-state*.json +/.codegraph diff --git a/AGENTS.md b/AGENTS.md index 2a32694b..192c3dad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ Cross-agent conventions for any agent (Claude, Codex, Cursor, opencode, …) wor ### Code exploration -- There is no `.codegraph/` index in this repo, so use normal search (grep/ripgrep, `cargo` tooling, editor navigation). If a `.codegraph/` index is added later, prefer CodeGraph (`codegraph explore ""` or the code-review-graph MCP tools) before grep/find — it surfaces callers/callees/impact that text search misses. +- A local `.codegraph/` index may exist at the repo root (gitignored, not committed). If it is present, prefer CodeGraph (`codegraph explore ""` or the code-review-graph MCP tools) before grep/find — it surfaces callers/callees/impact that text search misses. If it is absent, use grep/`cargo`/editor navigation, and `codegraph init` is permitted. If `codegraph status` reports an unhealthy index, run `codegraph sync`. ### Config & secrets (KV, not env) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..84e4e0ba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Security + +- Fail closed before readiness when `BIND_ADDR` is not loopback-only and no write-capable admin principal is configured (`ADMIN_TOKEN`, `ADMIN_TOKENS`, or `WAF_IDS_CREDENTIALS_PATH`). Loopback development may still start without a token and reports `auth_mode=development` on `/healthz`. +- A blank `WAF_IDS_STATE_PATH` is treated as in-memory state instead of becoming ready and then failing to replace an empty path. +- Management writes now distinguish `401` (unauthenticated) from `403` (authenticated, not permitted to write) without naming the expected role. +- Presented admin secrets are compared in constant time, including when lengths differ by a multiple of 256. Duplicate, blank, and unknown `ADMIN_TOKENS` roles fail startup. A blank `WAF_IDS_CREDENTIALS_PATH` is treated as unset. +- `scripts/smoke.sh` generates a per-process admin token instead of embedding a shared secret (CWE-798). + +### Documentation + +- Product/technical gap baseline at `docs/product-technical-gap-baseline.md` (open PRs/Issues inventory, operator-perceptible gaps, Figma file IDs, UI-UX areas). +- File://-openable admin-console scene and edge-case inventory at `docs/ui-ux/storybook-scene-inventory.md`. +- Figma file IDs recorded in `docs/adr/0001-figma-and-design-system.md` and `docs/architecture.md`. diff --git a/CLAUDE.md b/CLAUDE.md index 742e3096..6218da6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ The core stays an in-repo workspace crate on purpose (no git submodule) until it ## Runtime Configuration -Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`), `WAF_IDS_STATE_PATH` (optional JSON state file; omitted = seeded in-memory state), `DNSBL_ORIGIN` (default `dnsbl.local`), `EVENT_LIMIT` (default 1000, must be > 0), `RATE_LIMIT` / `RATE_LIMIT_WINDOW`, `WAF_IDS_CREDENTIALS_PATH` (optional JSON bootstrap file for process-local credentials/config), `ADMIN_TOKEN` (bootstrap transport for the shared write token), and `ADMIN_TOKENS` (bootstrap transport for comma-separated `token:actor[:role]` RBAC entries). `ADMIN_TOKEN` and `ADMIN_TOKENS` are loaded into `CredentialRegistry` before the server starts; handlers read the in-process registry/AppState copy, not raw env vars. KEV imports use the built-in CISA endpoint at runtime; only in-crate tests can override it through `AppState::with_kev_catalog_url` to point at a loopback mock server. +Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`; non-loopback requires a write-capable admin principal before bind), `WAF_IDS_STATE_PATH` (optional JSON state file; omitted or blank = seeded in-memory state), `DNSBL_ORIGIN` (default `dnsbl.local`), `EVENT_LIMIT` (default 1000, must be > 0), `RATE_LIMIT` / `RATE_LIMIT_WINDOW`, `WAF_IDS_CREDENTIALS_PATH` (optional JSON bootstrap file; omitted or blank = unset; file values take precedence over environment values for the same key), `ADMIN_TOKEN` (bootstrap transport for the shared write token), and `ADMIN_TOKENS` (bootstrap transport for comma-separated `token:actor[:role]` RBAC entries). `ADMIN_TOKEN` and `ADMIN_TOKENS` are loaded into `CredentialRegistry` before the server starts; handlers read the in-process registry/AppState copy, not raw env vars. KEV imports use the built-in CISA endpoint at runtime; only in-crate tests can override it through `AppState::with_kev_catalog_url` to point at a loopback mock server. ## Key Conventions diff --git a/README.md b/README.md index d1587583..668182e0 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Open `http://127.0.0.1:8080/admin`. Useful environment variables: - `BIND_ADDR`: listen address, default `127.0.0.1:8080` -- `ADMIN_TOKEN`: optional write token for management writes via `X-Admin-Token` +- `ADMIN_TOKEN`: write token for management writes via `X-Admin-Token`. Optional only for numeric loopback binds (`127.0.0.0/8` or `::1`). Hostnames such as `localhost` fail closed because Wardnet cannot prove before binding that every resolved address is loopback. Required before readiness on any other `BIND_ADDR` (`0.0.0.0`, `::`, LAN, public). - `WAF_IDS_STATE_PATH`: optional JSON state path. When omitted, the service runs with seeded in-memory state. - `DNSBL_ORIGIN`: DNSBL zone origin, default `dnsbl.local` - `EVENT_LIMIT`: retained event count, default `1000`; must be greater than zero diff --git a/docs/adr/0001-figma-and-design-system.md b/docs/adr/0001-figma-and-design-system.md new file mode 100644 index 00000000..acfa7ce9 --- /dev/null +++ b/docs/adr/0001-figma-and-design-system.md @@ -0,0 +1,40 @@ +# ADR 0001 — Figma design-system source and embedded admin console + +Status: accepted +Date: 2026-08-23 + +## Context + +Wardnet ships an operator console as vanilla HTML/CSS/JS embedded in the Rust +binary (`ADMIN_HTML` in `src/lib.rs`, served at `GET /` and `/admin`). Repeating +objects (KPI tiles, cards, tables, badges, buttons, forms, toasts) must stay +token-based. A Node Storybook toolchain cannot be loaded by that console without +a separate static site. + +## Decision + +- Canonical design-system tokens live in CSS custom properties on `:root` + (`docs/design-system.md` matches the running `/admin` CSS). +- Figma is the visual mirror, not a runtime dependency. **Figma Code Connect is + not used** (repo `AGENTS.md`). +- Record file IDs here so operators and agents can open the same files. + +## Figma file IDs + +| Artifact | File ID | URL | +| --- | --- | --- | +| Design system / console frames | `QTH5UuU0FJv2VyM2xb02Fp` | https://www.figma.com/design/QTH5UuU0FJv2VyM2xb02Fp | +| Enterprise product architecture FigJam | `JExziD87eUWKLERECUGhWQ` | https://www.figma.com/board/JExziD87eUWKLERECUGhWQ | + +## Scene and edge-case events + +Scene-by-scene and edge-case event definitions for the ten UI-UX areas live in +`docs/ui-ux/storybook-scene-inventory.md`, which opens from disk (`file://`) +without a Node Storybook server. That inventory is the Storybook-equivalent +contract for this embedded-console architecture. + +## Consequences + +- Token changes must land in `ADMIN_HTML` and `docs/design-system.md` together. +- Do not add a frontend framework to `/admin`; it would break the + binary-embedded load path used by `scripts/smoke.sh`. diff --git a/docs/architecture.md b/docs/architecture.md index e1ee578b..882d79cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,6 +59,7 @@ flowchart LR - Default bind address is localhost. - Remote management requires `ADMIN_TOKEN` plus external TLS and identity controls. +- Non-loopback listeners fail closed before readiness unless a write-capable admin principal is configured. - `WAF_IDS_STATE_PATH` enables JSON state persistence for standalone operation. Without it, the service uses seeded in-memory state. - File-backed writes use temporary sibling files followed by atomic rename. Management API mutations roll back in memory if the state file cannot be replaced. - Block mode is route-scoped to avoid global accidental enforcement. @@ -68,7 +69,17 @@ flowchart LR ## Product Architecture Evidence +- Figma design-system file ID: `QTH5UuU0FJv2VyM2xb02Fp` (see `docs/design-system.md` and `docs/adr/0001-figma-and-design-system.md`) +- FigJam architecture file ID: `JExziD87eUWKLERECUGhWQ` (`docs/figma/enterprise-product-architecture.md`) - FigJam: `docs/figma/enterprise-product-architecture.md` - Product workflows: `docs/product-design/enterprise-operator-workflows.md` - Enterprise scorecard: `docs/analytics/enterprise-value-scorecard.md` - Complexity audit: `docs/ponytail/2026-07-02-complexity-audit.md` +- UI-UX scene / edge-case inventory (file://-openable): `docs/ui-ux/storybook-scene-inventory.md` +- Product/technical gap baseline: `docs/product-technical-gap-baseline.md` + +## Security Boundaries (credentials) + +- Default bind address is loopback. Loopback-only development may start without an admin token and reports `auth_mode=development` on `/healthz`. +- Any non-loopback `BIND_ADDR` fails closed before readiness unless a write-capable principal is present in the credential registry (`ADMIN_TOKEN`, `ADMIN_TOKENS`, or `WAF_IDS_CREDENTIALS_PATH`). +- Management writes return `401` when unauthenticated and `403` when authenticated but not permitted to write. Response bodies do not name the expected role. diff --git a/docs/deployment/production.md b/docs/deployment/production.md index 34ff1978..a8d5eca1 100644 --- a/docs/deployment/production.md +++ b/docs/deployment/production.md @@ -40,7 +40,7 @@ kubectl apply -f deploy/kubernetes/waf-ids-ai-soc.yaml - Terminate TLS in front of the service. - Expose `/admin` and `/api/*` only through identity-aware access. - Configure upstream allowlists and egress policy. -- Store `ADMIN_TOKEN` in a secret manager. +- Store `ADMIN_TOKEN` in a secret manager. The process **will not become ready** on `BIND_ADDR=0.0.0.0:8080` (or any non-loopback address) if no write-capable credential is configured. That is intentional fail-closed behavior (issue #78). Recovery: inject the Secret, restart; do not disable the gate. - Mount persistent state or replace JSON persistence with a database. - Run `scripts/smoke.sh` before promoting a release. - Keep block mode route-scoped and reversible. diff --git a/docs/design-system.md b/docs/design-system.md index c30523ac..4826ec3e 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -53,7 +53,13 @@ carried by token swaps only, no component markup changes between modes. | `--fs-metric` | 28px/700 | KPI tile value | | `--radius` | 8px | cards, inputs (6px), badges (pill) | -Controls (`button`, `input`, `select`) are `min-height: 44px` (WCAG 2.5.5 target size). +Primary form controls (`button`, `input`, `select`) use `min-height: 44px` as a +Wardnet ergonomic floor. **Height alone is not target-size conformance evidence.** +WCAG 2.2 SC 2.5.8 Target Size (Minimum), Level AA, requires a pointer target to be +at least 24 × 24 CSS pixels or satisfy one of the criterion's specified exceptions, +including its spacing rule. SC 2.5.5 Target Size (Enhanced), Level AAA, requires +44 × 44 CSS pixels except its specified exceptions. Do not claim SC 2.5.5 from the +44px height rule unless both axes of the actual target/hit area have been measured. ## Components @@ -87,7 +93,7 @@ Each entry: **anatomy · states · usage · a11y · data**. ### Button - **Variants** `btn-primary` (brand fill — one primary action per form), `btn-secondary` (bordered, on surface), `btn-ghost` (in the brand header). -- **States** default / `:focus-visible` ring / `aria-pressed` (toggle). 44px min. +- **States** default / `:focus-visible` ring / `aria-pressed` (toggle). Runtime CSS guarantees a 44px minimum height; target width/hit area must still be verified before making a WCAG target-size claim. ### Form field - **Anatomy** `label.field` wrapping caption + control + optional `.field-help`. @@ -124,7 +130,8 @@ First tab stop, off-screen until focused, jumps to `#main`. ## Accessibility checklist (per screen) - [ ] All text pairs ≥ 4.5:1 (see table); non-text state has a text label too. -- [ ] Every control ≥ 44×44 and reachable by keyboard with a visible focus ring. +- [ ] Every pointer target is verified against WCAG 2.2 SC 2.5.8: at least 24 × 24 CSS px or a documented applicable exception/spacing result. Important controls should aim for 44 × 44 CSS px; SC 2.5.5 Level AAA is not claimed from height alone. +- [ ] Every interactive control is reachable by keyboard with a visible focus ring. - [ ] Tables use `/
`; forms use wrapping `