diff --git a/AGENTS.md b/AGENTS.md index ae81252..b68e2cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Cross-agent conventions for any agent (Claude, Codex, Cursor, opencode, …) wor - A failing **`trivy-fs` is a REAL finding, not a flake.** Read the job log — it prints each finding's rule id / severity / file — or the run's SARIF results, then **remediate**: - Rust dependency CVE → bump the crate (`cargo update -p `, adjust `Cargo.toml`) and commit the updated `Cargo.lock`. - Container/OS finding → fix the base image or package in the `Dockerfile`. - - k8s/IaC misconfig → fix `deploy/kubernetes/waf-ids-ai-soc.yaml` or `deploy/docker-compose.yml`. + - k8s/IaC misconfig → fix `deploy/kubernetes/wardnet.yaml` or `deploy/docker-compose.yml`. - Genuine false positive only → add a narrow, commented entry to `.trivyignore` (see the existing `AVD-KSV-0125` note for the expected style). Never broaden it to silence a real vuln. - Do **not** weaken or disable the gate. A local scan with a stale DB misses findings: run `trivy --download-db-only` first, then scan the **merge ref**, not just the PR head (e.g. `trivy fs --scanners vuln,misconfig --severity CRITICAL,HIGH --ignore-unfixed .`). - Gating is by the Security Scan **job result**, not the `code_scanning` rule. That org ruleset is intentionally **CodeQL-only** (multiple code-scanning tools can't converge on one PR ref) — do **not** add tools to it. @@ -56,4 +56,4 @@ As of 2026-08-04, code-owner review requirements (`require_code_owner_reviews` i protection, `require_code_owner_review` in rulesets) are disabled across the ContextualWisdomLab org: there is a single maintainer (solo developer), so a code-owner approval gate can never be satisfied. This is ON HOLD until the org has multiple maintainers — do NOT re-enable these -settings or add CODEOWNERS-based merge gates before then. +settings or add CODEOWNERS-based merge gates before then. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d8068..3f9b9a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,5 @@ ### Operations +- Renamed the hardened Kubernetes deployment asset from `deploy/kubernetes/waf-ids-ai-soc.yaml` to `deploy/kubernetes/wardnet.yaml` without renaming in-cluster resources. Operators and GitOps/package references must use the new repository path; rollback to a pre-migration source revision uses that revision's old path. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. diff --git a/CLAUDE.md b/CLAUDE.md index 742e309..578c59d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`), ` - Audit logs must never leak admin tokens (`scripts/smoke.sh` asserts this). - Untrusted-input surfaces (request scorer, state deserializer, admin-token parser, DNSBL zone export) are fuzzed; if you change one, keep its libFuzzer target and proptest mirror in sync (`docs/fuzzing.md` lists the invariants per target). - Block mode is route-scoped; default bind is localhost. See `docs/architecture.md` for security boundaries and the near-term adapter roadmap. -- Deployment assets: `Dockerfile` (two-stage build, pinned base images, runs as non-root `wafids`), `deploy/docker-compose.yml`, `deploy/kubernetes/waf-ids-ai-soc.yaml`. +- Deployment assets: `Dockerfile` (two-stage build, pinned base images, runs as non-root `wafids`), `deploy/docker-compose.yml`, `deploy/kubernetes/wardnet.yaml`. ## Further Docs diff --git a/README.md b/README.md index d158758..89b7735 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,109 @@ -# WAF IDS AI SOC +# Wardnet -Rust-first gateway and SOC control-plane baseline for ContextualWisdomLab. +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/wardnet) -The project starts small on purpose: +**Rust-first gateway and SOC control plane for governed traffic policy, threat evidence, DNSBL operations, and security-operations handoff.** -- web-managed API gateway routes -- reusable `waf-ids-core` domain crate inside the same Cargo workspace -- request scoring from threat indicators and DNSBL entries -- monitor/block enforcement modes -- RFC 5782-style DNSBL zone export -- SOC event and KPI APIs -- tenant/license-aware commercial readiness APIs -- threat feed import status for real-time update operations -- support bundle API for buyer due diligence and support handoff -- threat-feed freshness evidence and SOC event NDJSON export -- optional JSON state persistence for standalone operation -- embedded admin console +Wardnet gives an operator one bounded place to manage gateway routes, threat indicators, DNSBL entries, enforcement mode, SOC events, feed freshness, and buyer/support evidence. It is deliberately small enough to run standalone while keeping room for proven external WAF, IDS, SIEM, and orchestration engines behind explicit adapters. -It does not pretend to be a full WAF, IDS, SIEM, or SOAR yet. Production WAF and IDS coverage should come from adapters to proven engines such as OWASP CRS/Coraza and Suricata. +It is **not** presented as a complete internet-edge WAF, IDS, SIEM, or SOAR. The current source is an operational baseline and evidence surface; production-grade detection coverage remains a separate integration and hardening responsibility. -## Completion Baseline +## What Wardnet provides -The program-complete baseline means the binary can run by itself, keep operator-managed routes/threats/DNSBL entries/events across restart when `WAF_IDS_STATE_PATH` is configured, enforce monitor/block decisions, export DNSBL records, and prove that loop through `scripts/smoke.sh`. +| Operator need | Current Wardnet responsibility | +| --- | --- | +| Gateway policy | Manage enabled routes and monitor/block mode through the control-plane API | +| Threat evidence | Store operator-reviewed indicators and DNSBL entries with source/TTL context | +| Request decisions | Score requests from the currently configured local threat evidence and apply route mode | +| DNSBL operations | Export RFC 5782-style loopback response codes and a DNSBL zone view | +| SOC evidence | Retain bounded security events, KPI snapshots, freshness state, and NDJSON event export | +| Support handoff | Produce health/readiness/evidence support bundles without returning administrator secrets | +| Buyer diligence | Expose bounded commercial/readiness/evidence reports without turning them into certification or transaction authority | +| Standalone durability | Optionally persist operator-managed state to a local JSON state file | -It is still not a hardened internet-facing deployment. Use TLS, identity-aware access, upstream allowlists, and route rollback procedures before production traffic. +## Current maturity -## Commercial Readiness Baseline +The current source package metadata is `0.1.0`, but **this repository has no published GitHub release yet**. A source version, buyer-readiness endpoint, successful smoke test, or open pull request is not release or production evidence. -The 2B KRW sale readiness baseline means the runtime can prove a buyer-facing pilot state through API evidence: +Wardnet can run locally, persist its current state model, enforce its current monitor/block decisions, expose an embedded admin console, and produce operational evidence. It is not yet a hardened public-edge deployment. Before real production traffic, operators still need a reviewed TLS/identity boundary, externally managed credentials, upstream/destination policy, rollback/recovery controls, and the required proven-engine integrations for the intended detection scope. -- `GET /api/commercial/license` returns tenant, edition, license, support, and annual contract metadata. -- `POST /api/commercial/license` updates that metadata with `X-Admin-Token`. -- `POST /api/threat-feeds/import` imports operator-reviewed threat indicators and DNSBL entries. -- `POST /api/threat-feeds/import/phishing-database` pulls active domains/IPs from `Phishing-Database/Phishing.Database` and converts them into local block signals. -- `GET /api/commercial/readiness` returns pass/fail checks and blockers against the 2B KRW target. -- `GET /api/threat-feeds/freshness` returns fresh/stale feed evidence from TTL and last update time. -- `GET /api/events.ndjson` exports events as newline-delimited JSON for SOC/SIEM ingestion tests. -- `GET /api/commercial/evidence-manifest` returns the buyer-verifiable runtime, document, and deployment evidence map. -- `GET /api/support-bundle` returns health, KPIs, license, readiness, and evidence counts without admin secrets. +## Product boundary -The formal acceptance criteria are in `docs/commercial/20b-krw-sale-readiness.md`. +Wardnet owns the **gateway and SOC control-plane boundary** represented by this repository: route policy, current local threat/DNSBL evidence, request scoring/enforcement mode, operational evidence, support handoff, and bounded management APIs. -The enterprise product package evidence is tracked in: +Adjacent systems remain independently authoritative: -- `docs/superpowers/specs/2026-07-02-enterprise-product-package-design.md` -- `docs/superpowers/plans/2026-07-02-enterprise-product-package.md` -- `docs/superpowers/specs/2026-07-02-feed-freshness-siem-evidence-design.md` -- `docs/superpowers/plans/2026-07-02-feed-freshness-siem-evidence.md` -- `docs/superpowers/specs/2026-07-03-buyer-evidence-manifest-design.md` -- `docs/superpowers/plans/2026-07-03-buyer-evidence-manifest.md` -- `docs/figma/enterprise-product-architecture.md` -- `docs/product-design/enterprise-operator-workflows.md` -- `docs/analytics/enterprise-value-scorecard.md` -- `docs/ponytail/2026-07-02-complexity-audit.md` +- proven WAF rule engines such as Coraza/OWASP CRS own their detection semantics when integrated; +- IDS/network telemetry engines such as Suricata own their packet/event detection semantics when integrated; +- external SIEM/OpenTelemetry destinations own downstream retention and investigation; +- threat-intelligence providers own their source datasets and terms; +- `ContextualWisdomLab/contextual-orchestrator` owns model/provider routing for any model-assisted SOC workflow; and +- customer identity, TLS termination, secrets, deployment policy, and network topology remain deployment authorities rather than README assumptions. -## Run +A threat score or buyer-readiness report is evidence produced by Wardnet, not authorization to make unrelated infrastructure changes. + +## Quick start + +Wardnet is a Rust workspace. From a source checkout: ```bash cargo run ``` -Open `http://127.0.0.1:8080/admin`. +The default listener is loopback-only. Open: + +```text +http://127.0.0.1:8080/admin +``` + +Check liveness: -Useful environment variables: +```bash +curl -fsS http://127.0.0.1:8080/healthz +``` -- `BIND_ADDR`: listen address, default `127.0.0.1:8080` -- `ADMIN_TOKEN`: optional write token for management writes via `X-Admin-Token` -- `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 +### Add local persistence -Example with persistent local state: +Current protected-source configuration uses `WAF_IDS_STATE_PATH` for the optional state file: ```bash -ADMIN_TOKEN=dev-secret \ -WAF_IDS_STATE_PATH=./waf-ids-state.local.json \ +ADMIN_TOKEN='replace-with-a-local-admin-secret' \ +WAF_IDS_STATE_PATH=./wardnet-state.local.json \ DNSBL_ORIGIN=dnsbl.example \ cargo run ``` -## API +`ADMIN_TOKEN` protects management writes through `X-Admin-Token`. The current baseline permits a credential-free local development mode; do not interpret that convenience as a safe public-bind configuration. + +Useful current settings: + +| Setting | Purpose | +| --- | --- | +| `BIND_ADDR` | Listener address; defaults to `127.0.0.1:8080` | +| `ADMIN_TOKEN` | Optional management-write token for the current baseline | +| `WAF_IDS_STATE_PATH` | Optional JSON persistence path | +| `DNSBL_ORIGIN` | DNSBL zone origin; defaults to `dnsbl.local` | +| `EVENT_LIMIT` | Retained event bound; must be greater than zero | + +## Core operator API + +A few read surfaces are enough to understand the running control plane: ```bash -curl http://127.0.0.1:8080/healthz -curl http://127.0.0.1:8080/api/routes -curl http://127.0.0.1:8080/api/threats -curl http://127.0.0.1:8080/api/dnsbl -curl http://127.0.0.1:8080/api/commercial/license -curl http://127.0.0.1:8080/api/commercial/readiness -curl http://127.0.0.1:8080/api/commercial/evidence-manifest -curl http://127.0.0.1:8080/api/threat-feeds -curl http://127.0.0.1:8080/api/threat-feeds/freshness -curl -X POST http://127.0.0.1:8080/api/threat-feeds/import/phishing-database \ - -H 'content-type: application/json' \ - -H 'x-admin-token: dev-secret' \ - -d '{}' -curl http://127.0.0.1:8080/api/events.ndjson -curl http://127.0.0.1:8080/api/support-bundle -curl http://127.0.0.1:8080/dnsbl/zone -curl http://127.0.0.1:8080/gateway/demo?q=union%20select +curl -fsS http://127.0.0.1:8080/api/routes +curl -fsS http://127.0.0.1:8080/api/threats +curl -fsS http://127.0.0.1:8080/api/dnsbl +curl -fsS http://127.0.0.1:8080/api/threat-feeds/freshness +curl -fsS http://127.0.0.1:8080/api/events.ndjson +curl -fsS http://127.0.0.1:8080/api/support-bundle ``` -Add a blocking route: +Management writes use the configured administrator boundary. For example, a local route can be added with: ```bash curl -X POST http://127.0.0.1:8080/api/routes \ -H 'content-type: application/json' \ - -H 'x-admin-token: dev-secret' \ + -H 'x-admin-token: replace-with-a-local-admin-secret' \ -d '{ "id": "api", "path_prefix": "/api", @@ -116,80 +113,83 @@ curl -X POST http://127.0.0.1:8080/api/routes \ }' ``` -Management writes are upserts: +Management writes use stable domain keys: routes by route identity, threat indicators by indicator type/value/source, and DNSBL entries by address. DNSBL response codes are constrained to IPv4 loopback-style values in `127.0.0.0/8`. + +For threat-feed ingestion, use only reviewed sources whose commercial terms and redistribution/use boundaries are acceptable for the deployment. Feed freshness and import success do not change the upstream provider's license or data-usage terms. + +## Buyer and support evidence + +Wardnet exposes bounded reporting surfaces that help a pilot operator or buyer inspect the current runtime: + +| Evidence | Endpoint | +| --- | --- | +| Commercial metadata | `GET /api/commercial/license` | +| Readiness checks and blockers | `GET /api/commercial/readiness` | +| Evidence inventory | `GET /api/commercial/evidence-manifest` | +| Threat-feed freshness | `GET /api/threat-feeds/freshness` | +| SOC event export | `GET /api/events.ndjson` | +| Support handoff | `GET /api/support-bundle` | + +These endpoints may report incomplete or blocked states. They are not a compliance certification, deployment approval, customer commitment, valuation, legal opinion, or completed transaction. The detailed diligence contract lives in [`docs/commercial/buyer-due-diligence.md`](docs/commercial/buyer-due-diligence.md). + +## Architecture at a glance + +```text +Client traffic + | + v ++-------------------------------+ +| Wardnet | +| gateway + SOC control plane | +|-------------------------------| +| route policy | +| threat / DNSBL evidence | +| monitor / block decision | +| events / KPI / freshness | +| admin + support evidence | ++---------------+---------------+ + | + explicit adapters + | + +---------+---------+ + | | + v v + proven WAF / IDS SIEM / SOC tools + engines and operators +``` -- routes are keyed by `id` -- threat indicators are keyed by `indicator_type`, `value`, and `source` -- DNSBL entries are keyed by `address` +The current core remains one Rust workspace because the reusable domain crate does not yet have an independent release cadence or external consumer contract. Repository boundaries should change only when those product/reuse responsibilities genuinely diverge. -DNSBL response codes must be IPv4 loopback-style values in `127.0.0.0/8`. +See [`docs/architecture.md`](docs/architecture.md) for the detailed component and trust boundaries. -Import a reviewed threat feed: +## Deployment -```bash -curl -X POST http://127.0.0.1:8080/api/threat-feeds/import \ - -H 'content-type: application/json' \ - -H 'x-admin-token: dev-secret' \ - -d '{ - "feed_id": "misp-seoul", - "source": "misp://soc.example", - "ttl_seconds": 600, - "threats": [{ - "value": "credential_dump", - "indicator_type": "malware", - "severity": "critical", - "source": "misp-seoul", - "ttl_seconds": 600 - }], - "dnsbl": [{ - "address": "198.51.100.23", - "code": "127.0.0.4", - "reason": "feed scanner", - "source": "misp-seoul", - "ttl_seconds": 600 - }] - }' -``` +The repository currently ships source deployment assets for local/container and Kubernetes evaluation: -Import active phishing domains/IPs directly from the public Phishing.Database project: - -```bash -curl -X POST http://127.0.0.1:8080/api/threat-feeds/import/phishing-database \ - -H 'content-type: application/json' \ - -H 'x-admin-token: dev-secret' \ - -d '{ - "feed_id": "phishing-db-seoul", - "domain_limit": 5000, - "ip_limit": 5000, - "severity": "high", - "ttl_seconds": 3600 - }' -``` +- [`Dockerfile`](Dockerfile) +- [`deploy/docker-compose.yml`](deploy/docker-compose.yml) +- [`deploy/kubernetes/wardnet.yaml`](deploy/kubernetes/wardnet.yaml) -Deployment assets: +The Kubernetes filename above is the canonical repository path on this branch. Renaming the source path does not rename live Kubernetes objects; stateful resource-identity migration is a separate operator concern. -- `Dockerfile` -- `deploy/docker-compose.yml` -- `deploy/kubernetes/waf-ids-ai-soc.yaml` +For production-oriented setup and rollback expectations, read [`docs/deployment/production.md`](docs/deployment/production.md) before exposing the service beyond loopback. -## Workspace +## Security posture -- `crates/waf-ids-core`: pure domain models, validation, upserts, scoring, DNSBL zone formatting, event retention, threat-feed freshness classification, KPI snapshots, commercial readiness snapshots, and buyer evidence manifests. -- `src/lib.rs`: Axum management API, admin console, optional state persistence, upstream proxying, NDJSON event export, evidence manifest/support bundle assembly, and in-crate HTTP tests. -- `src/main.rs`: process configuration and server startup. +The current baseline is designed to fail explicitly on malformed managed state and bounded input, but source-level controls do not replace deployment security. In particular: -The core is a local workspace crate rather than a git submodule because it does not yet have a separate release cadence or external consumers. +- keep management access behind a reviewed identity/credential boundary; +- do not commit administrator credentials, provider keys, customer traffic, or private threat data; +- validate upstream and threat-feed destinations before enabling remote network access; +- preserve source attribution and terms for imported intelligence; +- keep a rollback path for route and enforcement changes; and +- treat model-assisted SOC output as advisory until an authorized operator acts on it. -## Roadmap +Security-sensitive parsers and scoring/state boundaries are exercised through property tests and coverage-guided fuzzing; see [`docs/fuzzing.md`](docs/fuzzing.md). -1. Coraza/OWASP CRS adapter for HTTP transaction scoring. -2. Suricata EVE JSON ingest and correlation with gateway events. -3. Live MISP REST pull and live OpenCTI GraphQL pull jobs (HTTP STIX/MISP/OpenCTI document ingest and TAXII 2.1 collection poll already available). -4. Authoritative DNSBL service mode using Hickory DNS. -5. AI SOC analyst assist with human approval gates for blocking changes. -6. Full SIEM adapters after the NDJSON export contract is proven in buyer labs. +## Verify the source -## Verification +Use the locked workspace and strict compiler/lint path: ```bash cargo fmt --check @@ -198,6 +198,24 @@ cargo clippy --locked --workspace --all-targets -- -D warnings scripts/smoke.sh ``` -Untrusted-input surfaces (request scorer, state deserializer, admin-token and -DNSBL parsers) are covered by coverage-guided fuzzing plus stable property -tests. See [`docs/fuzzing.md`](docs/fuzzing.md). +A passing source suite is engineering evidence for that exact revision. It is not proof of a live deployment, third-party feed availability, external detection-engine coverage, or release publication. + +## Documentation map + +- [`docs/architecture.md`](docs/architecture.md) — component, domain, and trust boundaries. +- [`docs/adr/`](docs/adr/) — architecture decisions. +- [`docs/deployment/production.md`](docs/deployment/production.md) — production-oriented deployment and rollback guidance. +- [`docs/runbooks/`](docs/runbooks/) — operator procedures. +- [`docs/commercial/buyer-due-diligence.md`](docs/commercial/buyer-due-diligence.md) — buyer evidence and claim boundaries. +- [`docs/fuzzing.md`](docs/fuzzing.md) — hostile-input/property/fuzz verification. +- [`docs/doctoring/`](docs/doctoring/) — research and standards traceability. + +## Contributing + +Keep changes inside Wardnet's gateway/SOC control-plane responsibility. Do not copy a proven security engine, model router, external intelligence provider, or sibling product into this repository merely to avoid an integration boundary. Public behavior, security-sensitive parsing, persistence, and deployment-contract changes should update tests and operator documentation together. + +Before opening a change, run the source verification commands above and keep customer-facing claims tied to current protected-source behavior rather than planned PRs. + +## License + +Wardnet source is licensed under the [MIT License](LICENSE). Third-party crates, threat-intelligence sources, external rule engines, datasets, and deployment components retain their own terms and are not relicensed by this repository. diff --git a/crates/waf-ids-core/src/lib.rs b/crates/waf-ids-core/src/lib.rs index f9673e0..b29db5d 100644 --- a/crates/waf-ids-core/src/lib.rs +++ b/crates/waf-ids-core/src/lib.rs @@ -1175,7 +1175,7 @@ pub fn commercial_readiness_snapshot_at(data: &AppData, now_unix: u64) -> Commer deployment_assets: vec![ "Dockerfile".to_string(), "deploy/docker-compose.yml".to_string(), - "deploy/kubernetes/waf-ids-ai-soc.yaml".to_string(), + "deploy/kubernetes/wardnet.yaml".to_string(), ], buyer_evidence: vec![ "docs/commercial/20b-krw-sale-readiness.md".to_string(), diff --git a/deploy/kubernetes/waf-ids-ai-soc.yaml b/deploy/kubernetes/wardnet.yaml similarity index 100% rename from deploy/kubernetes/waf-ids-ai-soc.yaml rename to deploy/kubernetes/wardnet.yaml diff --git a/docs/commercial/buyer-due-diligence.md b/docs/commercial/buyer-due-diligence.md index a940253..823f0ca 100644 --- a/docs/commercial/buyer-due-diligence.md +++ b/docs/commercial/buyer-due-diligence.md @@ -42,7 +42,7 @@ - [Dockerfile](../../Dockerfile) - [Compose stack](../../deploy/docker-compose.yml) -- [Kubernetes manifest](../../deploy/kubernetes/waf-ids-ai-soc.yaml) +- [Kubernetes manifest](../../deploy/kubernetes/wardnet.yaml) ## Buyer Lab Script diff --git a/docs/deployment/production.md b/docs/deployment/production.md index 1c46ac7..efb45d3 100644 --- a/docs/deployment/production.md +++ b/docs/deployment/production.md @@ -42,9 +42,11 @@ The Deployment binds `ADMIN_TOKEN` only through that `secretKeyRef` with `option After the external secret controller reports successful synchronization, apply the complete manifest. Its Namespace object remains in the declarative asset so later applies retain the same ownership boundary: ```bash -kubectl apply -f deploy/kubernetes/waf-ids-ai-soc.yaml +kubectl apply -f deploy/kubernetes/wardnet.yaml ``` +The repository path changed from `deploy/kubernetes/waf-ids-ai-soc.yaml` to `deploy/kubernetes/wardnet.yaml`. This is a source-tree/operator path migration only: namespace, Deployment, Service, PVC, image, labels, ports, probes, security context, and Secret coordinates remain unchanged, so applying the renamed file updates the same in-cluster resources. Update scripts, GitOps sources, and packaging rules that referenced the old path before rollout. Rollback to a repository version before this path migration uses that version's old filename; do not create parallel Kubernetes resources as a workaround. + When rotating `ADMIN_TOKEN`, wait for the updated Secret to synchronize, then restart the Deployment because environment-variable-backed Secret values are fixed when a container starts. Verify the rollout and readiness before revoking the previous token: ```bash diff --git a/docs/doctoring/kubernetes-admin-secret-boundary.md b/docs/doctoring/kubernetes-admin-secret-boundary.md index 0251866..3127415 100644 --- a/docs/doctoring/kubernetes-admin-secret-boundary.md +++ b/docs/doctoring/kubernetes-admin-secret-boundary.md @@ -31,7 +31,7 @@ kubectl create namespace waf-ids-ai-soc --dry-run=client -o yaml | kubectl apply The deployment authority must then confirm that its external secret manager/controller has materialized `waf-ids-ai-soc-admin` in that namespace with a non-empty `ADMIN_TOKEN` key. The repository does not prescribe a vendor-specific controller; the integration boundary is the Kubernetes Secret coordinates above. -Apply `deploy/kubernetes/waf-ids-ai-soc.yaml` only after synchronization succeeds. The manifest retains its Namespace object so fresh installs and upgrades converge on the same declarative namespace ownership. Kubernetes resolves the `secretKeyRef` when creating the container. Because the reference is explicitly non-optional, absence of the Secret or key is an operator-visible startup failure instead of an authentication downgrade. +Apply `deploy/kubernetes/wardnet.yaml` only after synchronization succeeds. The repository-path rename does not rename namespace, Deployment, Service, PVC, image, labels, ports, probes, security context, or Secret coordinates. The manifest retains its Namespace object so fresh installs and upgrades converge on the same declarative namespace ownership. Kubernetes resolves the `secretKeyRef` when creating the container. Because the reference is explicitly non-optional, absence of the Secret or key is an operator-visible startup failure instead of an authentication downgrade. ## Rotation @@ -48,7 +48,7 @@ If rollout or authentication verification fails, keep or restore the previous cr ## Verification contract -`tests/deployment_manifest.rs` is the permanent regression boundary. It fails if the shipped manifest contains a `kind: Secret` document or the historical placeholder value. It structurally selects Deployment `waf-ids-ai-soc`, scopes the lookup to the `gateway` runtime container, requires exactly one `ADMIN_TOKEN` environment entry, rejects literal fallback values and duplicate `ADMIN_TOKEN` entries, and validates the expected namespace, Secret name, key, and non-optional reference. Decoy Deployments, `initContainers`, comments, duplicate environment entries, literal fallbacks, and `optional: true` cannot satisfy the contract. The same regression suite requires the production guide to bootstrap the namespace before namespaced Secret provisioning. +`tests/deployment_manifest.rs` is the permanent regression boundary. It fails if the shipped manifest contains a `kind: Secret` document or the historical placeholder value. It structurally selects Deployment `waf-ids-ai-soc`, scopes the lookup to the `gateway` runtime container, requires exactly one `ADMIN_TOKEN` environment entry, rejects literal fallback values and duplicate `ADMIN_TOKEN` entries, and validates the expected namespace, Secret name, key, and non-optional reference. Decoy Deployments, `initContainers`, comments, duplicate environment entries, literal fallbacks, and `optional: true` cannot satisfy the contract. The same regression suite requires the production guide to bootstrap the namespace before namespaced Secret provisioning and rejects restoration of the pre-migration Kubernetes repository path. For release evidence, run the repository's normal formatting, workspace test, Clippy, fuzz, SAST, and Security Scan gates on the exact PR head. A predecessor-head success, skipped required job, or security scan from another merge tree is not evidence for the current artifact. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2728bdc --- /dev/null +++ b/docs/index.md @@ -0,0 +1,32 @@ +--- +title: Wardnet +--- + +# Wardnet + +Wardnet is a Rust-first gateway and security-operations control plane for governed traffic policy, threat evidence, DNSBL operations, request enforcement, and operator handoff. + +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/wardnet) + +## Start here + +Use the [repository README](https://github.com/ContextualWisdomLab/wardnet#readme) for the current product boundary, maturity, local quick start, management APIs, deployment guidance, and verification posture. Wardnet deliberately does not present its current source as a complete hardened WAF, IDS, SIEM, or SOAR. + +## Product responsibility + +Wardnet owns its gateway and SOC control-plane surface: route policy, current local threat and DNSBL evidence, request scoring and enforcement mode, operational evidence, support handoff, and bounded management APIs. Proven external WAF/IDS engines, SIEM and telemetry destinations, threat-intelligence providers, model routing, identity, TLS, secrets, and deployment topology remain independently authoritative. + +## Documentation + +- [README](https://github.com/ContextualWisdomLab/wardnet#readme) — product overview, quick start, maturity, security and verification. +- [Architecture](https://github.com/ContextualWisdomLab/wardnet/blob/main/docs/architecture.md) — system boundaries and integration responsibilities. +- [Buyer due diligence](https://github.com/ContextualWisdomLab/wardnet/blob/main/docs/commercial/buyer-due-diligence.md) — current buyer-facing evidence surfaces and their verification boundaries. +- [Operations](https://github.com/ContextualWisdomLab/wardnet/tree/main/docs/runbooks) — operator and recovery guidance. +- [Releases](https://github.com/ContextualWisdomLab/wardnet/releases) — immutable release evidence when published. +- [Ask DeepWiki](https://deepwiki.com/ContextualWisdomLab/wardnet) — repository-grounded navigation and questions. + +## Evidence boundary + +A source version, readiness endpoint, passing test, support bundle, or open pull request is not by itself a production deployment, certification, customer adoption, or published release. Repository-facing claims should remain bound to protected source and the applicable immutable release, deployment, and verification evidence. + +This file is a GitHub Pages source prerequisite. Its presence does not mean GitHub Pages is published; publication is complete only after repository settings are reconciled, deployment succeeds, and the live HTTPS site is verified. diff --git a/tests/deployment_manifest.rs b/tests/deployment_manifest.rs index e169913..70d168f 100644 --- a/tests/deployment_manifest.rs +++ b/tests/deployment_manifest.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; -const MANIFEST: &str = include_str!("../deploy/kubernetes/waf-ids-ai-soc.yaml"); +const MANIFEST: &str = include_str!("../deploy/kubernetes/wardnet.yaml"); const PRODUCTION_GUIDE: &str = include_str!("../docs/deployment/production.md"); /// Secret coordinates the gateway Deployment must consume for `ADMIN_TOKEN`. @@ -648,3 +648,13 @@ fn fresh_install_bootstraps_namespace_before_secret_provisioning() { "namespace bootstrap must precede namespaced Secret provisioning" ); } + +#[test] +fn legacy_manifest_path_is_absent() { + let legacy_manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("deploy/kubernetes/waf-ids-ai-soc.yaml"); + assert!( + !legacy_manifest.exists(), + "the pre-rename Kubernetes manifest path must not remain in the distributable tree" + ); +} diff --git a/tests/documentation_landing.rs b/tests/documentation_landing.rs new file mode 100644 index 0000000..589369d --- /dev/null +++ b/tests/documentation_landing.rs @@ -0,0 +1,75 @@ +//! Repository contract for links published by the GitHub Pages landing source. + +use std::fs; +use std::path::{Component, Path}; + +const REPOSITORY_ROOT_README_URL: &str = "https://github.com/ContextualWisdomLab/wardnet#readme"; +const REPOSITORY_BLOB_PREFIX: &str = "https://github.com/ContextualWisdomLab/wardnet/blob/main/"; +const REPOSITORY_TREE_PREFIX: &str = "https://github.com/ContextualWisdomLab/wardnet/tree/main/"; + +fn repository_target(target: &str) -> Option<(&str, bool)> { + if target == REPOSITORY_ROOT_README_URL { + Some(("README.md", false)) + } else if let Some(relative) = target.strip_prefix(REPOSITORY_BLOB_PREFIX) { + Some((relative, false)) + } else if let Some(relative) = target.strip_prefix(REPOSITORY_TREE_PREFIX) { + Some((relative, true)) + } else { + None + } +} + +fn validated_repository_relative_path(relative: &str) -> &Path { + let path = Path::new(relative); + let stays_inside_repository = !path.is_absolute() + && path + .components() + .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)); + assert!( + stays_inside_repository, + "Pages landing repository link must stay inside repository: {relative}" + ); + path +} + +#[test] +fn repository_root_readme_target_is_checked() { + assert_eq!( + repository_target(REPOSITORY_ROOT_README_URL), + Some(("README.md", false)) + ); +} + +#[test] +#[should_panic(expected = "must stay inside repository")] +fn repository_link_rejects_parent_escape() { + let _ = validated_repository_relative_path("../outside.md"); +} + +#[test] +fn pages_landing_repository_links_resolve_in_source_tree() { + let repository = Path::new(env!("CARGO_MANIFEST_DIR")); + let landing = fs::read_to_string(repository.join("docs/index.md")) + .expect("docs/index.md must remain readable as the Pages landing source"); + + for target in landing + .split("](") + .skip(1) + .filter_map(|candidate| candidate.split(')').next()) + { + if let Some((relative, is_directory)) = repository_target(target) { + let candidate = repository.join(validated_repository_relative_path(relative)); + if is_directory { + assert!( + candidate.is_dir(), + "Pages landing links to a missing repository directory: {relative}" + ); + } else { + assert!( + candidate.is_file(), + "Pages landing links to a missing repository file: {relative}" + ); + } + } + } +} diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs new file mode 100644 index 0000000..d75c357 --- /dev/null +++ b/tests/kubernetes_manifest_path.rs @@ -0,0 +1,175 @@ +//! Repository contract for the canonical Kubernetes deployment manifest path. + +use std::fs; +use std::path::{Path, PathBuf}; + +/// Text source extensions whose contents may carry repository path references. +const TEXT_EXTENSIONS: &[&str] = &[ + "css", "html", "js", "json", "jsx", "md", "py", "rs", "sh", "toml", "ts", "tsx", "txt", "yaml", + "yml", +]; + +/// Files whose legacy-path references are necessarily historical or negative fixtures. +/// +/// Operational documentation is intentionally excluded from this file-level allowlist: +/// migration guidance must justify each legacy-path occurrence on the exact line where it +/// appears so a later copy/paste command cannot silently escape the repository contract. +const LEGACY_REFERENCE_FILE_ALLOWLIST: &[&str] = &["CHANGELOG.md", "tests/deployment_manifest.rs"]; + +/// Walk text-bearing source files without relying on platform-specific tooling. +fn text_source_files(root: &Path) -> Vec { + let mut pending = vec![root.to_path_buf()]; + let mut files = Vec::new(); + + while let Some(path) = pending.pop() { + let Ok(entries) = fs::read_dir(&path) else { + continue; + }; + for entry in entries.flatten() { + let candidate = entry.path(); + if candidate.is_dir() { + if candidate + .file_name() + .is_some_and(|name| name == ".git" || name == "target") + { + continue; + } + pending.push(candidate); + continue; + } + if candidate + .extension() + .and_then(|value| value.to_str()) + .is_some_and(|extension| TEXT_EXTENSIONS.contains(&extension)) + { + files.push(candidate); + } + } + } + + files +} + +/// Decide whether one legacy-path occurrence is explicit migration history rather than +/// an operational reference that could be copied into a deployment command. +fn legacy_reference_is_allowed( + relative: &Path, + line: &str, + legacy_reference: &str, + canonical_reference: &str, +) -> bool { + if LEGACY_REFERENCE_FILE_ALLOWLIST + .iter() + .any(|allowed| relative == Path::new(allowed)) + { + return true; + } + + if relative != Path::new("docs/deployment/production.md") { + return false; + } + + let normalized = line.to_ascii_lowercase(); + let explicit_migration = normalized.contains("path changed from") + && line.contains(legacy_reference) + && line.contains(canonical_reference); + let explicit_rollback = normalized.contains("rollback") + && normalized.contains("repository version before this path migration") + && line.contains(legacy_reference); + + explicit_migration || explicit_rollback +} + +#[test] +fn kubernetes_manifest_uses_the_wardnet_filename_only() { + let repository = Path::new(env!("CARGO_MANIFEST_DIR")); + let deployment_directory = repository.join("deploy/kubernetes"); + let canonical_name = ["wardnet", ".yaml"].concat(); + let legacy_name = ["waf-ids-ai-soc", ".yaml"].concat(); + + assert!( + deployment_directory.join(&canonical_name).is_file(), + "the hardened production manifest must be published as deploy/kubernetes/{canonical_name}" + ); + assert!( + !deployment_directory.join(&legacy_name).exists(), + "the pre-rename Kubernetes manifest path must be removed" + ); + + let legacy_reference = ["deploy/kubernetes/", legacy_name.as_str()].concat(); + let canonical_reference = ["deploy/kubernetes/", canonical_name.as_str()].concat(); + let stale_references = text_source_files(repository) + .into_iter() + .flat_map(|path| { + let relative = path.strip_prefix(repository).unwrap_or(&path).to_path_buf(); + let content = fs::read_to_string(&path).unwrap_or_default(); + let legacy_reference = legacy_reference.clone(); + let canonical_reference = canonical_reference.clone(); + + content + .lines() + .enumerate() + .filter_map(move |(index, line)| { + if line.contains(&legacy_reference) + && !legacy_reference_is_allowed( + &relative, + line, + &legacy_reference, + &canonical_reference, + ) + { + Some(format!("{}:{}", relative.display(), index + 1)) + } else { + None + } + }) + .collect::>() + }) + .collect::>(); + + assert!( + stale_references.is_empty(), + "the legacy Kubernetes manifest path is still referenced outside explicit migration history by: {}", + stale_references.join(", ") + ); +} + +#[test] +fn production_guide_allows_only_explicit_legacy_path_history() { + let relative = Path::new("docs/deployment/production.md"); + let legacy_reference = ["deploy/kubernetes/", "waf-ids-ai-soc", ".yaml"].concat(); + let canonical_reference = ["deploy/kubernetes/", "wardnet", ".yaml"].concat(); + let migration_history = format!( + "The repository path changed from `{legacy_reference}` to `{canonical_reference}`." + ); + let rollback_history = format!( + "Rollback to a repository version before this path migration uses `{legacy_reference}`." + ); + let stale_apply_command = format!("kubectl apply -f {legacy_reference}"); + let stale_gitops_instruction = format!("Copy {legacy_reference} into the GitOps repository."); + + assert!(legacy_reference_is_allowed( + relative, + &migration_history, + &legacy_reference, + &canonical_reference, + )); + assert!(legacy_reference_is_allowed( + relative, + &rollback_history, + &legacy_reference, + &canonical_reference, + )); + assert!(!legacy_reference_is_allowed( + relative, + &stale_apply_command, + &legacy_reference, + &canonical_reference, + )); + assert!(!legacy_reference_is_allowed( + relative, + &stale_gitops_instruction, + &legacy_reference, + &canonical_reference, + )); +}