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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- PostgreSQL outbox consumers for TAXII poll, Clearfolio document submit, and contextual-orchestrator SOC analysis (issue #81 remainder). Operator-triggered HTTP leaves through `taxii.collection_polled`, `clearfolio.document_submitted`, and `soc.analysis_requested` with leased-worker retries and unique receipts. Request path returns HTTP 202 and `GET /api/outbox/{message_id}` exposes receipt evidence. Secrets never enter outbox payloads (TAXII bearer lives in the credential registry). File/memory adapters keep the previous synchronous path. Client IPs, paths, indicator values, and actor names stay unmasked. LLM analysis remains advisory and never auto-enforces.


- PostgreSQL `security_event` is HASH-partitioned by `tenant_id` (8 children). Unpartitioned tables convert in place and keep unmasked client IPs and paths. `/healthz.event_partitions` reports the child count (0 on file/memory). Logical restore still accepts schema 2 through the current migration version; HASH does not change the snapshot shape.
- PostgreSQL snapshot persist is optimistic-concurrency: `tenant_account.snapshot_version` must match the loaded token or the write returns a snapshot conflict (HTTP 409). Restores overwrite. File/memory adapters stay single-writer.
- PostgreSQL control-plane runtime is `wardnet_runtime` (NOSUPERUSER, NOBYPASSRLS, not the table owner). Migrations run as the login role, then `SET ROLE` so FORCE RLS binds even when the URL user is a superuser. Missing `wardnet.tenant_id` yields no rows. DDL (`DROP TABLE`, `DISABLE ROW LEVEL SECURITY`) is denied. Logical restore accepts schema 2 through the current migration version so a role-only upgrade cannot void the last pre-upgrade backup.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ flowchart LR
- `src/main.rs`: process startup and operator configuration from `BIND_ADDR`, `ADMIN_TOKEN`, `WAF_IDS_STATE_PATH`, `DNSBL_ORIGIN`, and `EVENT_LIMIT`.
- `src/lib.rs`: Axum app, routing, management APIs, optional JSON persistence, gateway handler, upstream proxying, admin console, support bundle assembly, NDJSON event export, and in-crate HTTP tests. Persistence, destination-list, and sidecar settings validate before the readiness line is printed.
- `src/control_plane.rs`: PostgreSQL production authority (issue #80). Non-loopback binds require `CONTROL_PLANE_DATABASE_URL`. Tenant isolation is default-deny RLS under `wardnet_runtime` (not superuser/owner). Snapshot persist is optimistic-concurrency on `snapshot_version` (HTTP 409). `sslmode=require` uses rustls. `security_event` is HASH-partitioned by `tenant_id` (`/healthz.event_partitions`). The JSON file adapter remains loopback/community only.
- `src/outbox.rs`: transactional outbox + leased workers (issue #81). Security events append incrementally with an outbox row in the same transaction. Workers claim with `SKIP LOCKED`. `GET /api/outbox` is bounded to `EVENT_LIMIT` (processed rows pruned; receipts kept). `/healthz.outbox` is operator-visible.
- `src/outbox.rs`: transactional outbox + leased workers (issue #81). Security events append incrementally with an outbox row in the same transaction. Workers claim with `SKIP LOCKED`. `GET /api/outbox` is bounded to `EVENT_LIMIT` (processed rows pruned; receipts kept). `GET /api/outbox/{id}` returns receipt evidence for TAXII, Clearfolio, and SOC analysis consumers. `/healthz.outbox` is operator-visible. HTTP consumers release the PostgreSQL client lock before outbound I/O.
- `src/destination.rs`: fail-closed outbound URL policy (issue #79) for every `http`/`https` send. CIDR allowlist exceptions are per resolved address; blocking DNS is offloaded from Tokio workers. The outbound HTTP client DNS resolver returns only addresses that already passed policy (TCP peer pin / DNS-rebinding TOCTOU close).
- `crates/waf-ids-core`: reusable domain models plus validation, upsert, scoring, DNSBL zone export, event retention, threat-feed freshness, KPI snapshot, and commercial readiness logic.
- `/admin`: embedded web console.
Expand Down
9 changes: 6 additions & 3 deletions docs/doctoring/outbox-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ the exactly-once ack. Dead letters are never pruned.

Loopback file/memory adapters keep in-process stdout SIEM and report
`outbox=disabled`. `security_event` HASH partitioning is on the PostgreSQL
plane. Remaining consumers: TAXII poll, Clearfolio, contextual-orchestrator
on the same message/receipt contract. Backup/restore drill is on the
PostgreSQL plane.
plane. TAXII poll, Clearfolio submit, and contextual-orchestrator analysis
use the same message/receipt contract on PostgreSQL (`GET /api/outbox/{id}`
for receipt evidence). HTTP dispatch releases the database lock for the
outbound call. Inline TAXII secrets are rejected on the durable path;
`taxii_bearer` is a credential-registry secret. Backup/restore drill is on
the PostgreSQL plane.
47 changes: 28 additions & 19 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Product and technical gap baseline

Snapshot date: 2026-08-23T19:20Z (exact-head inventory of then-open GitHub PRs
Snapshot date: 2026-08-23T20:06Z (exact-head inventory of then-open GitHub PRs
and Issues plus operator-perceptible gaps). Update this file on every hourly loop.

Commercial contract and `/api/commercial/readiness` remain **2B KRW**. The
Expand All @@ -25,7 +25,8 @@ not “waiting on review/CI time”.

| PR | Title | Head | Checks | Reviews | Merge blocker |
| --- | --- | --- | --- | --- | --- |
| [#105](https://github.com/ContextualWisdomLab/wardnet/pull/105) | feat(store): optimistic concurrency on postgres snapshots | `feat/issue-80-optimistic-concurrency` stacked on #99 | local fmt/test/clippy + two `/healthz` smokes; live `postgres_stale_snapshot_save_conflicts` | Author this pass | Org 2-approval + self-author. #100/#103/#104 folded into #99. Merge #95 then #96 then #97 then #98 then #99 first. Do not `--admin`. |
| [#106](https://github.com/ContextualWisdomLab/wardnet/pull/106) | feat(store): outbox consumers for TAXII, Clearfolio, and orchestrator | `feat/issue-81-outbox-consumers` stacked on #105 | local fmt/test/clippy + smoke.sh + two `/healthz` and `/admin`/`/api/commercial/readiness` (2B KRW) | Author this pass | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 then #105 first. Do not `--admin`. Do not re-implement OCC or prior store slices. |
| [#105](https://github.com/ContextualWisdomLab/wardnet/pull/105) | feat(store): optimistic concurrency on postgres snapshots | `feat/issue-80-optimistic-concurrency` stacked on #99 | Devin still-valid startup-version 409 fixed this pass (`load_postgres` advances `snapshot_version` after save); local fmt/test/clippy + two `/healthz` smokes | Author; Devin COMMENTED (startup false-conflict addressed, thread resolved) | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 first. Do not `--admin`. Do not re-implement HASH/role/backup. |
| [#104](https://github.com/ContextualWisdomLab/wardnet/pull/104) | feat(store): HASH-partition security_event by tenant | merged into rustls stack then #99 | prior hour | Author prior hour | Folded into #99. Do not re-implement. |
| [#103](https://github.com/ContextualWisdomLab/wardnet/pull/103) | feat(store): non-owner PostgreSQL runtime role after migrate | `feat/issue-80-runtime-role` stacked on #100 | still-valid Devin restore-window finding fixed this pass (`MIN_RESTORABLE_SCHEMA_VERSION=2`); local fmt/test/clippy + smokes | Author this pass; Devin COMMENTED (v3 backup voiding addressed) | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 then #100 first. Do not `--admin`. Do not re-implement rustls, outbox, retention, backup, or HASH. |
| [#102](https://github.com/ContextualWisdomLab/wardnet/pull/102) | feat(store): logical backup and isolated restore drill | squash-merged into #100 (`321e792`) | prior hour | Author prior hour | Folded into rustls stack. Do not re-implement. |
Expand Down Expand Up @@ -60,8 +61,8 @@ by ruleset `18156473` (not by failing Checks). Do not `--admin` merge.
| [#84](https://github.com/ContextualWisdomLab/wardnet/issues/84) | [P1] Build an immutable signed release, promotion, and rollback pipeline | high |
| [#83](https://github.com/ContextualWisdomLab/wardnet/issues/83) | [P1] Add bounded distributed admission control, trusted client attribution, and overload behavior | high |
| [#82](https://github.com/ContextualWisdomLab/wardnet/issues/82) | [P1] Integrate Keyverse identity, tenant authorization, consent, and human approval evidence | high (blocked) |
| [#81](https://github.com/ContextualWisdomLab/wardnet/issues/81) | [P0] Add a transactional outbox and idempotent leased workers for external effects | **critical — first slice on #99; bounded list/retention on #101** |
| [#80](https://github.com/ContextualWisdomLab/wardnet/issues/80) | [P0] Add an authoritative PostgreSQL control plane with tenant isolation and recoverable migrations | **critical — gate on #98; rustls/backup/role/HASH on #99; OCC this pass** |
| [#81](https://github.com/ContextualWisdomLab/wardnet/issues/81) | [P0] Add a transactional outbox and idempotent leased workers for external effects | **critical — first slice on #99; retention on #101; TAXII/Clearfolio/orchestrator consumers this pass** |
| [#80](https://github.com/ContextualWisdomLab/wardnet/issues/80) | [P0] Add an authoritative PostgreSQL control plane with tenant isolation and recoverable migrations | **critical — gate on #98; rustls/backup/role/HASH on #99; OCC on #105** |
| [#79](https://github.com/ContextualWisdomLab/wardnet/issues/79) | [P0] Enforce a fail-closed destination policy for all outbound traffic | **critical — closed in runtime on #96** |
| [#78](https://github.com/ContextualWisdomLab/wardnet/issues/78) | [P0] Fail closed when management credentials are absent | **critical — closed in runtime on #94** |
| [#75](https://github.com/ContextualWisdomLab/wardnet/issues/75) | Rename Kubernetes manifest to wardnet.yaml after external-secret hardening lands | medium |
Expand Down Expand Up @@ -117,8 +118,9 @@ Logical restore accepts schema 2 through the current migration version
void pre-upgrade snapshots. `security_event` is `PARTITION BY HASH (tenant_id)`
with 8 children. Unpartitioned tables convert in place under `pg_advisory_lock`.
`/healthz.event_partitions` is 8 on PostgreSQL, 0 on file/memory. Client IPs
and paths stay unmasked across convert. Remaining: optimistic concurrency.
Physical/PITR backups stay a DBA concern.
and paths stay unmasked across convert. Optimistic concurrency is on #105
(`tenant_account.snapshot_version`, HTTP 409; startup save now advances the
in-memory token). Physical/PITR backups stay a DBA concern.

### Transactional outbox (issue #81) — **first slice on #99; retention on #101**

Expand All @@ -133,8 +135,13 @@ dead-letter counts, `GET /api/outbox` (admin read), `POST /api/outbox/{id}/repla
(admin write + audit). Client IPs and paths in payloads are not masked.
File/memory adapters stay `outbox=disabled` with in-process stdout. `GET /api/outbox`
is bounded to `EVENT_LIMIT`; processed rows prune to that cap on append, snapshot
save, and worker ack; dead letters stay. Remaining consumers: TAXII poll,
Clearfolio, contextual-orchestrator on the same contract.
save, and worker ack; dead letters stay. TAXII poll, Clearfolio submit, and
contextual-orchestrator analysis enqueue on PostgreSQL (`taxii.collection_polled`,
`clearfolio.document_submitted`, `soc.analysis_requested`) and return HTTP 202.
`GET /api/outbox/{id}` returns receipt evidence. Secrets never enter payloads
(`taxii_bearer` / `soc_llm_token` in the credential registry). File/memory stays
synchronous. LLM analysis is advisory and never auto-enforces. Client IPs, paths,
and indicator values stay unmasked.

### Fail-closed credentials (issue #78) — **closed on PR #94**

Expand Down Expand Up @@ -184,25 +191,27 @@ holes on untouched handlers stay listed for later loops.
### Ecosystem connectors (leverage order)

1. **keyverse** — identity for management plane (#82).
2. **contextual-orchestrator** — SOC LLM already optional via
`SOC_LLM_BASE_URL`; keep adapter, do not fork routing. Next: same outbox
contract.
2. **contextual-orchestrator** — SOC LLM optional via `SOC_LLM_BASE_URL`;
token from credential registry (`soc_llm_token`). Same outbox contract on
PostgreSQL this pass. Do not fork routing.
3. **naruon** / **clearfolio** — document viewer already optional.
4. **TEPP / RankWeave / ThreadWeave / LineageWeave / disksage / fast-mlsirm** —
not on the gateway data path; no connector this pass.

## This loop’s shipped gap

Issue **#80** last remainder: optimistic concurrency on
`tenant_account.snapshot_version`. Stale snapshot persist returns HTTP 409.
Restores overwrite. Do not re-implement #78, sidecar, pin, libcoraza, the
postgres gate, outbox, rustls, retention, backup/restore, runtime role, or HASH.
Issue **#81** extra consumers stacked on #105: TAXII poll, Clearfolio submit,
and contextual-orchestrator SOC analysis go through the leased outbox on
PostgreSQL. `load_postgres` also advances `snapshot_version` after the startup
save so the first management write cannot false-conflict (Devin #105). Do not
re-implement #78, sidecar, pin, libcoraza, the postgres gate, outbox, rustls,
retention, backup/restore, runtime role, HASH, or OCC.

## Next hourly loop (do, do not report)

1. Second independent APPROVE on #91/#92. Do not `--admin`.
2. Keep #94/#95/#96/#97/#98/#99 and this OCC PR merge-ready. Merge order
#94 independently; #95 then #96 then #97 then #98 then #99 then this.
3. Next runtime gap if policy still blocks: extra #81 consumers (TAXII /
Clearfolio / orchestrator).
2. Keep #94 independently; #95 then #96 then #97 then #98 then #99 then #105
then #106 merge-ready. Do not `--admin`.
3. Next runtime gap if policy still blocks: signed release/promotion (#84) or
Keyverse identity (#82) after the postgres stack.
4. Refresh this file’s PR/Issue tables from `gh pr list` / `gh issue list`.
Loading
Loading