Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
54604c2
feat(waf): consult Coraza sidecar on live gateway transactions
seonghobae Aug 23, 2026
ba9ee3a
docs: record PR #95 in the product-technical gap baseline
seonghobae Aug 23, 2026
80daf7a
fix(waf): bound, attribute, and evidence the Coraza sidecar path
seonghobae Aug 24, 2026
63b7930
fix(waf): treat any 403 sidecar answer as an interruption
seonghobae Aug 24, 2026
ee40769
Merge branch 'main' into feat/issue-86-in-path-coraza
seonghobae Aug 26, 2026
26d2064
Merge branch 'main' into feat/issue-86-in-path-coraza
seonghobae Aug 26, 2026
ad3675a
feat(security): fail-closed destination policy for outbound HTTP (#96)
seonghobae Aug 26, 2026
5860f42
fix(waf): honor explicit engine interruptions
seonghobae Aug 26, 2026
4a8de40
fix(runtime): close sidecar and DNS fail-open gaps
seonghobae Aug 26, 2026
4d8f7c1
fix(control-plane): preserve fresh tenant identity
seonghobae Aug 26, 2026
9001f6f
fix(security): update Hickory and close protocol gaps
seonghobae Aug 26, 2026
454f640
docs: refresh exact-head product gap baseline
seonghobae Aug 26, 2026
ae66601
fix: harden DNS and engine fail-closed paths
seonghobae Aug 26, 2026
f6eabf3
feat(store): optimistic concurrency on postgres snapshots (#105)
seonghobae Aug 26, 2026
4f1ce3f
feat(mcp): expose stateless Wardnet status tool (#117)
seonghobae Aug 26, 2026
33c692d
fix(control-plane): replace snapshot security events
seonghobae Aug 26, 2026
723aed0
fix(release): scope publishing permissions to job
seonghobae Aug 26, 2026
255de59
feat(dnsbl): serve authoritative DNS responses (#118)
seonghobae Aug 26, 2026
52825e6
fix(dns): avoid duplicate approved-address cache write
seonghobae Aug 26, 2026
9cdd2d7
fix(waf): prefer explicit sidecar interruptions
seonghobae Aug 26, 2026
5901550
perf(gateway): add k6 load evidence and skip no-op clones
seonghobae Aug 26, 2026
ad1208a
docs(gaps): record measured coverage shortfall
seonghobae Aug 26, 2026
e69a72d
docs(gateway): ground tail-latency evidence
seonghobae Aug 26, 2026
6aa8e23
Merge pull request #119 from ContextualWisdomLab/test/gateway-k6-e2e
seonghobae Aug 26, 2026
53d5403
docs(gaps): refresh exact-head delivery evidence
seonghobae Aug 26, 2026
8efdf53
Merge pull request #120 from ContextualWisdomLab/docs/refresh-gap-bas…
seonghobae Aug 26, 2026
061da0c
test(gateway): distinguish connection churn load (#121)
seonghobae Aug 26, 2026
8b97784
feat(dnsbl): resolve IPv6 nibble queries
seonghobae Aug 26, 2026
97d69f8
Merge pull request #122 from ContextualWisdomLab/feat/dnsbl-ipv6-nibble
seonghobae Aug 26, 2026
cc83ded
fix(runtime): install SIGTERM handler before readiness
seonghobae Aug 26, 2026
44263b6
docs: refresh live product gap evidence
seonghobae Aug 26, 2026
3f8717f
fix(runtime): preserve mixed credential provenance
seonghobae Aug 26, 2026
233c831
fix(control-plane): require TLS for public bind
seonghobae Aug 26, 2026
5934c13
docs(operations): document mixed credential source
seonghobae Aug 26, 2026
58a2217
feat(api): expose egress boundary diagnostics (#125)
seonghobae Aug 27, 2026
129bb96
fix(release): tighten exact-head review contracts
seonghobae Aug 27, 2026
9114d2c
fix(control-plane): harden exact-head PR95 gates
codex Aug 27, 2026
9e79e44
fix(control-plane): version durable event appends
codex Aug 28, 2026
a95e116
test(control-plane): re-seal mutated logical backup
codex Aug 28, 2026
7932dd6
fix(control-plane): fail closed on stalled effects
codex Aug 28, 2026
3c5b3eb
fix(control-plane): reject duplicate sslmode values
codex Aug 28, 2026
d0e93ee
fix(control-plane): export backups from one snapshot
codex Aug 28, 2026
902e46e
fix(control-plane): keep event RLS on partition parent
codex Aug 28, 2026
413bdd0
fix(control-plane): set backup isolation before tenant scope
codex Aug 28, 2026
dc388a5
fix(control-plane): serialize restore and allocate event ids
codex Aug 28, 2026
0e02594
fix(control-plane): serialize restore audit persistence
codex Aug 29, 2026
fcf200b
docs: tighten evidence claims and research grounding
codex Aug 30, 2026
304f053
docs(doctoring): add research impact summaries
codex Aug 30, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ permissions:
jobs:
rust:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: wardnet
POSTGRES_PASSWORD: wardnet
POSTGRES_DB: wardnet
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U wardnet -d wardnet"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
CONTROL_PLANE_TEST_DATABASE_URL: postgres://wardnet:wardnet@localhost:5432/wardnet
Comment thread
seonghobae marked this conversation as resolved.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
Expand Down
136 changes: 136 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Release

on:
push:
tags:
- "v*.*.*"

permissions:
contents: read

jobs:
release:
permissions:
contents: write
packages: write
id-token: write
attestations: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Admit annotated vX.Y.Z tag only
run: scripts/admit-release-tag.sh "$GITHUB_REF_NAME"
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
toolchain: stable
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
- uses: anchore/sbom-action/download-syft@a930d0ac434e3182448fe678398ba5713717112a # v0.21.0
- name: Build release binary
run: cargo build --locked --release
- name: Stage binary and binary SBOM
run: |
mkdir -p dist
cp target/release/waf-ids-ai-soc dist/waf-ids-ai-soc-linux-x86_64
scripts/release-sbom.sh --output dist/sbom.spdx.json dist/waf-ids-ai-soc-linux-x86_64
- name: Publish GHCR image by digest
id: image
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
image="ghcr.io/contextualwisdomlab/waf-ids-ai-soc"
tag="${GITHUB_REF_NAME}"
docker build -t "${image}:${tag}" .
push_out="$(docker push "${image}:${tag}")"
digest="$(printf '%s\n' "$push_out" | awk '{for (i=1;i<=NF;i++) if ($i ~ /^sha256:/) d=$i} END{print d}')"
test -n "$digest"
ref="${image}@${digest}"
printf '%s\n' "$ref" > dist/IMAGE-DIGEST.txt
scripts/release-sbom.sh --output dist/image.sbom.spdx.json "$ref"
{
echo "ref=${ref}"
echo "digest=${digest}"
echo "image=${image}"
} >> "$GITHUB_OUTPUT"
- name: Checksums and keyless blob signatures
run: |
set -euo pipefail
(cd dist && ../scripts/release-checksums.sh \
waf-ids-ai-soc-linux-x86_64 \
sbom.spdx.json \
image.sbom.spdx.json \
IMAGE-DIGEST.txt > SHA256SUMS)
cat dist/SHA256SUMS
cosign sign-blob --yes \
--bundle dist/waf-ids-ai-soc-linux-x86_64.sigstore.json \
dist/waf-ids-ai-soc-linux-x86_64
cosign sign-blob --yes \
--bundle dist/SHA256SUMS.sigstore.json \
dist/SHA256SUMS
cosign sign-blob --yes \
--bundle dist/sbom.spdx.json.sigstore.json \
dist/sbom.spdx.json
cosign sign-blob --yes \
--bundle dist/image.sbom.spdx.json.sigstore.json \
dist/image.sbom.spdx.json
cosign sign-blob --yes \
--bundle dist/IMAGE-DIGEST.txt.sigstore.json \
dist/IMAGE-DIGEST.txt
- name: Sign image and attest image SBOM (keyless)
run: |
set -euo pipefail
ref="${{ steps.image.outputs.ref }}"
cosign sign --yes "$ref"
cosign attest --yes --predicate dist/image.sbom.spdx.json --type spdxjson "$ref"
- name: SLSA provenance for binary
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: dist/waf-ids-ai-soc-linux-x86_64
- name: SLSA provenance for image
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ghcr.io/contextualwisdomlab/waf-ids-ai-soc
subject-digest: ${{ steps.image.outputs.digest }}
push-to-registry: true
- name: Attest binary SBOM
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
with:
subject-path: dist/waf-ids-ai-soc-linux-x86_64
sbom-path: dist/sbom.spdx.json
- name: Attest image SBOM
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
with:
subject-name: ghcr.io/contextualwisdomlab/waf-ids-ai-soc
subject-digest: ${{ steps.image.outputs.digest }}
sbom-path: dist/image.sbom.spdx.json
push-to-registry: true
- name: GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
notes="$(mktemp)"
{
echo "Promotion authority is the image digest and Sigstore signatures, not the tag."
echo
echo "Image: \`${{ steps.image.outputs.ref }}\`"
echo "Tag alias: \`ghcr.io/contextualwisdomlab/waf-ids-ai-soc:${GITHUB_REF_NAME}\`"
echo
echo "Verify: \`docs/runbooks/release.md\`"
} > "$notes"
gh release create "$GITHUB_REF_NAME" \
dist/waf-ids-ai-soc-linux-x86_64 \
dist/SHA256SUMS \
dist/sbom.spdx.json \
dist/image.sbom.spdx.json \
dist/IMAGE-DIGEST.txt \
dist/waf-ids-ai-soc-linux-x86_64.sigstore.json \
dist/SHA256SUMS.sigstore.json \
dist/sbom.spdx.json.sigstore.json \
dist/image.sbom.spdx.json.sigstore.json \
dist/IMAGE-DIGEST.txt.sigstore.json \
--notes-file "$notes" \
--verify-tag
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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

- Release admission refuses lightweight/unsigned `vX.Y.Z` tags (`scripts/admit-release-tag.sh`). Kubernetes pin is the GHCR content digest (`scripts/pin-k8s-digest.sh`); tag aliases are rejected.
- Tagged releases (`vX.Y.Z`) build a locked binary, basename SHA-256 checksums, SPDX SBOMs (binary and image), keyless Sigstore signatures (OIDC, no stored Cosign key), GitHub SLSA provenance, and a GHCR image signed **by digest**. The GitHub Release is created only after signatures succeed. Promotion authority is the digest in `IMAGE-DIGEST.txt`, not the tag alias. No moving `latest` tag (`docs/runbooks/release.md`).
- 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.
- PostgreSQL control-plane logical backup and isolated restore drill (issue #80 remainder). `GET /api/backup` exports a hashed tenant snapshot (policy, events, outbox, receipts). `POST /api/backup` restores after schema and payload-hash checks. `POST /api/backup/drill` restores into an isolated tenant, compares unmasked invariants, and drops the drill tenant. Declared RPO is the last successful export; declared RTO is 60 seconds. File/memory adapters report `/healthz.backup=disabled`. Client IPs, paths, and actor names stay unmasked.
- PostgreSQL control-plane mutations enqueue a transactional outbox row in the same transaction (issue #81). Security events append incrementally instead of rewriting the snapshot. A leased worker claims with `FOR UPDATE SKIP LOCKED`, retries with bounded backoff, dead-letters exhausted/permanent failures, and records unique receipts. Stdout SIEM export is at-least-once; the receipt is the exactly-once ack. `/healthz.outbox` and `GET /api/outbox` are operator-visible; `POST /api/outbox/{id}/replay` requeues dead letters with audit. File/memory adapters report `outbox=disabled`. `GET /api/outbox` is bounded to `EVENT_LIMIT` (dead letters and pending first). Processed `outbox_message` rows are pruned to that same cap on append, snapshot save, and worker ack; receipts stay as the exactly-once ack.
- Control-plane PostgreSQL URLs honor `sslmode=require` / `verify-ca` / `verify-full` with rustls and Mozilla roots (certificates always verified). tokio-postgres 0.7 only parses `require`, so verification modes are rewritten to `require` before connect. `sslmode=allow` / `prefer` are rejected so the process cannot silently drop to plaintext.
- Production (non-loopback) binds fail closed without `CONTROL_PLANE_DATABASE_URL`. PostgreSQL is the production control-plane authority (3NF two-word tables, default-deny row-level security, snapshot persist in one transaction). Loopback still uses the JSON file / memory adapter. `/healthz.persistence` reports `postgres`, `file`, or `memory`. The URL is a secret and is bootstrapped into the credential registry.
- Live `/gateway` transactions consult in-process libcoraza when `CORAZA_LIB_PATH` is set (with `CORAZA_RULES_PATH` and/or `CORAZA_DIRECTIVES`). Missing library, missing rules, or an empty ruleset fail startup before bind. Otherwise a Coraza sidecar is consulted when `CORAZA_WAF_URL` is set. The sidecar response is parsed with the existing Coraza audit adapter (OWASP CRS authority, not a hand-rolled engine). Engine outage is fail-closed when `PROVEN_ENGINE_FAIL_CLOSED` is true. `GET /api/waf/engine-status` and `/healthz.proven_engine` report `coraza_in_process`, `coraza_sidecar`, or `ingest_hints_only`.
- Fail-closed destination policy on every outbound `http`/`https` call, including the Coraza sidecar URL (gateway upstream, threat-intel fetch, Clearfolio, SOC LLM). Private/loopback/metadata classes are denied unless `DESTINATION_ALLOWLIST` (or loopback development) permits them; `DESTINATION_DENYLIST` wins. Clients ignore ambient HTTP proxies and do not follow redirects. CIDR allowlist matches apply per resolved address; CIDR entries authorize non-default ports; IPv6 site-local (`fec0::/10`) is denied; invalid CIDR prefixes fail startup; `/healthz.destination_mode` reports the policy class. Blocking DNS runs on `spawn_blocking` with a 2s timeout. Persistence and destination-list validation complete before the readiness line is printed. After a host is allowed, the HTTP client connects only to those evaluated addresses (Host/SNI unchanged) so a rebinding answer cannot bypass the policy.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`), `ADMIN_TOKEN` (write token for `X-Admin-Token`), `ADMIN_TOKENS` (comma-separated `token:actor` pairs for multi-token RBAC with per-token audit actors), `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`.
Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`), `ADMIN_TOKEN` (write token for `X-Admin-Token`), `ADMIN_TOKENS` (comma-separated `token:actor` pairs for multi-token RBAC with per-token audit actors), `WAF_IDS_STATE_PATH` (optional JSON state file; omitted = seeded in-memory state), `CONTROL_PLANE_DATABASE_URL` (required for non-loopback binds; secret `control_plane_url`), `DNSBL_ORIGIN` (default `dnsbl.local`), `EVENT_LIMIT` (default 1000, must be > 0), `RATE_LIMIT` / `RATE_LIMIT_WINDOW`, `CORAZA_LIB_PATH` / `CORAZA_RULES_PATH` / `CORAZA_DIRECTIVES` (optional in-process libcoraza), `CORAZA_WAF_URL` (optional in-path Coraza sidecar), `PROVEN_ENGINE_FAIL_CLOSED` (boolean; default false — set true in production when an engine is set). PostgreSQL mode starts a leased outbox worker (`GET /api/outbox`, `/healthz.outbox`).

## Key Conventions

Expand Down
Loading
Loading