From f25e0acd9866bc417c7c4909c19cdf77093a8a56 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:36:49 +0900 Subject: [PATCH 01/19] test(deploy): require Wardnet Kubernetes manifest path --- tests/kubernetes_manifest_path.rs | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/kubernetes_manifest_path.rs diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs new file mode 100644 index 00000000..e5f703c0 --- /dev/null +++ b/tests/kubernetes_manifest_path.rs @@ -0,0 +1,63 @@ +//! Repository path contract for the production Kubernetes manifest. + +use std::fs; +use std::path::{Path, PathBuf}; + +const MANIFEST: &str = include_str!("../deploy/kubernetes/wardnet.yaml"); +const CURRENT_MANIFEST_PATH: &str = "deploy/kubernetes/wardnet.yaml"; + +/// Build the retired filename without embedding it as a searchable repository reference. +fn legacy_manifest_path() -> String { + ["deploy/kubernetes/", "waf-ids-ai-soc", ".yaml"].concat() +} + +/// Collect source-controlled text candidates while excluding generated and VCS directories. +fn repository_text_files(root: &Path) -> Vec { + let mut pending = vec![root.to_path_buf()]; + let mut files = Vec::new(); + while let Some(directory) = pending.pop() { + for entry in fs::read_dir(&directory).expect("repository directory must be readable") { + let entry = entry.expect("repository entry must be readable"); + let path = entry.path(); + let name = entry.file_name(); + if path.is_dir() { + if name != ".git" && name != "target" { + pending.push(path); + } + continue; + } + if matches!( + path.extension().and_then(|value| value.to_str()), + Some("md" | "rs" | "toml" | "yml" | "yaml" | "sh") + ) { + files.push(path); + } + } + } + files +} + +#[test] +fn production_manifest_uses_the_wardnet_path_only() { + let legacy = legacy_manifest_path(); + assert!(Path::new(CURRENT_MANIFEST_PATH).is_file()); + assert!(!Path::new(&legacy).exists(), "retired manifest path still exists"); + + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + for path in repository_text_files(root) { + let contents = fs::read_to_string(&path).expect("tracked text candidate must be UTF-8"); + assert!( + !contents.contains(&legacy), + "retired Kubernetes manifest path remains referenced by {}", + path.display() + ); + } +} + +#[test] +fn renamed_manifest_preserves_external_secret_hardening() { + assert!(!MANIFEST.lines().any(|line| line.trim() == "kind: Secret")); + assert!(!MANIFEST.contains("replace-with-secret-manager-sync")); + assert!(MANIFEST.contains("secretKeyRef:")); + assert!(MANIFEST.contains("optional: false")); +} From cec889c4da68c249ad61e3ed73a817d8a87a9c1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:38:20 +0900 Subject: [PATCH 02/19] chore: abandon incomplete manifest-path experiment --- tests/kubernetes_manifest_path.rs | 63 ------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 tests/kubernetes_manifest_path.rs diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs deleted file mode 100644 index e5f703c0..00000000 --- a/tests/kubernetes_manifest_path.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Repository path contract for the production Kubernetes manifest. - -use std::fs; -use std::path::{Path, PathBuf}; - -const MANIFEST: &str = include_str!("../deploy/kubernetes/wardnet.yaml"); -const CURRENT_MANIFEST_PATH: &str = "deploy/kubernetes/wardnet.yaml"; - -/// Build the retired filename without embedding it as a searchable repository reference. -fn legacy_manifest_path() -> String { - ["deploy/kubernetes/", "waf-ids-ai-soc", ".yaml"].concat() -} - -/// Collect source-controlled text candidates while excluding generated and VCS directories. -fn repository_text_files(root: &Path) -> Vec { - let mut pending = vec![root.to_path_buf()]; - let mut files = Vec::new(); - while let Some(directory) = pending.pop() { - for entry in fs::read_dir(&directory).expect("repository directory must be readable") { - let entry = entry.expect("repository entry must be readable"); - let path = entry.path(); - let name = entry.file_name(); - if path.is_dir() { - if name != ".git" && name != "target" { - pending.push(path); - } - continue; - } - if matches!( - path.extension().and_then(|value| value.to_str()), - Some("md" | "rs" | "toml" | "yml" | "yaml" | "sh") - ) { - files.push(path); - } - } - } - files -} - -#[test] -fn production_manifest_uses_the_wardnet_path_only() { - let legacy = legacy_manifest_path(); - assert!(Path::new(CURRENT_MANIFEST_PATH).is_file()); - assert!(!Path::new(&legacy).exists(), "retired manifest path still exists"); - - let root = Path::new(env!("CARGO_MANIFEST_DIR")); - for path in repository_text_files(root) { - let contents = fs::read_to_string(&path).expect("tracked text candidate must be UTF-8"); - assert!( - !contents.contains(&legacy), - "retired Kubernetes manifest path remains referenced by {}", - path.display() - ); - } -} - -#[test] -fn renamed_manifest_preserves_external_secret_hardening() { - assert!(!MANIFEST.lines().any(|line| line.trim() == "kind: Secret")); - assert!(!MANIFEST.contains("replace-with-secret-manager-sync")); - assert!(MANIFEST.contains("secretKeyRef:")); - assert!(MANIFEST.contains("optional: false")); -} From e06d5697ad65d90e84caf2ad99f5b348cf1da5e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:56:27 +0900 Subject: [PATCH 03/19] test(deploy): require canonical Wardnet manifest path --- tests/kubernetes_manifest_path.rs | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/kubernetes_manifest_path.rs diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs new file mode 100644 index 00000000..7a0fbc78 --- /dev/null +++ b/tests/kubernetes_manifest_path.rs @@ -0,0 +1,75 @@ +//! 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", +]; + +/// 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 +} + +#[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 stale_references = text_source_files(repository) + .into_iter() + .filter_map(|path| { + let content = fs::read_to_string(&path).ok()?; + content + .contains(&legacy_reference) + .then(|| path.strip_prefix(repository).unwrap_or(&path).display().to_string()) + }) + .collect::>(); + + assert!( + stale_references.is_empty(), + "the legacy Kubernetes manifest path is still referenced by: {}", + stale_references.join(", ") + ); +} From 10bf291c97640ff933220db553e6384c2c37dc96 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:18:00 +0900 Subject: [PATCH 04/19] fix(deploy): move hardened manifest to canonical Wardnet path --- AGENTS.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 2 +- .../kubernetes/{waf-ids-ai-soc.yaml => wardnet.yaml} | 0 docs/commercial/buyer-due-diligence.md | 2 +- docs/deployment/production.md | 4 +++- docs/doctoring/kubernetes-admin-secret-boundary.md | 4 ++-- tests/deployment_manifest.rs | 12 +++++++++++- 9 files changed, 21 insertions(+), 8 deletions(-) rename deploy/kubernetes/{waf-ids-ai-soc.yaml => wardnet.yaml} (100%) diff --git a/AGENTS.md b/AGENTS.md index 2a32694b..b74d02a7 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d80680..3f9b9a57 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 742e3096..578c59d3 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 d1587583..8ff15e8e 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Deployment assets: - `Dockerfile` - `deploy/docker-compose.yml` -- `deploy/kubernetes/waf-ids-ai-soc.yaml` +- `deploy/kubernetes/wardnet.yaml` ## Workspace 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 a9402535..823f0cad 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 1c46ac73..efb45d38 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 02518661..4f29402c 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 legacy `deploy/kubernetes/waf-ids-ai-soc.yaml` 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/tests/deployment_manifest.rs b/tests/deployment_manifest.rs index e169913d..70d168ff 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" + ); +} From 3bc9fe797e53fc489e8d9d73a8ee3a114c47734a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:24:45 +0900 Subject: [PATCH 05/19] fix(commercial): publish canonical Wardnet manifest path --- crates/waf-ids-core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/waf-ids-core/src/lib.rs b/crates/waf-ids-core/src/lib.rs index f9673e0c..b29db5d9 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(), From 57386f0ef5ee5beeb8995003fdbfd584eb6ce950 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:34:38 +0900 Subject: [PATCH 06/19] test(deploy): distinguish migration history from stale manifest references --- tests/kubernetes_manifest_path.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs index 7a0fbc78..3709876f 100644 --- a/tests/kubernetes_manifest_path.rs +++ b/tests/kubernetes_manifest_path.rs @@ -9,6 +9,14 @@ const TEXT_EXTENSIONS: &[&str] = &[ "txt", "yaml", "yml", ]; +/// Files allowed to mention the retired path as migration history or a negative +/// regression fixture. Operational source and documentation must use the new path. +const LEGACY_REFERENCE_ALLOWLIST: &[&str] = &[ + "CHANGELOG.md", + "docs/deployment/production.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()]; @@ -60,10 +68,17 @@ fn kubernetes_manifest_uses_the_wardnet_filename_only() { let stale_references = text_source_files(repository) .into_iter() .filter_map(|path| { + let relative = path.strip_prefix(repository).unwrap_or(&path); + if LEGACY_REFERENCE_ALLOWLIST + .iter() + .any(|allowed| relative == Path::new(allowed)) + { + return None; + } let content = fs::read_to_string(&path).ok()?; content .contains(&legacy_reference) - .then(|| path.strip_prefix(repository).unwrap_or(&path).display().to_string()) + .then(|| relative.display().to_string()) }) .collect::>(); From 6cd0b16052fb8c35487c9da8ce96a3d17462371b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:00:47 +0900 Subject: [PATCH 07/19] test(deploy): narrow legacy manifest path exemptions --- tests/kubernetes_manifest_path.rs | 110 ++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 19 deletions(-) diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs index 3709876f..1e09048c 100644 --- a/tests/kubernetes_manifest_path.rs +++ b/tests/kubernetes_manifest_path.rs @@ -9,13 +9,12 @@ const TEXT_EXTENSIONS: &[&str] = &[ "txt", "yaml", "yml", ]; -/// Files allowed to mention the retired path as migration history or a negative -/// regression fixture. Operational source and documentation must use the new path. -const LEGACY_REFERENCE_ALLOWLIST: &[&str] = &[ - "CHANGELOG.md", - "docs/deployment/production.md", - "tests/deployment_manifest.rs", -]; +/// 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 { @@ -48,6 +47,36 @@ fn text_source_files(root: &Path) -> Vec { 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")); @@ -65,26 +94,69 @@ fn kubernetes_manifest_uses_the_wardnet_filename_only() { ); 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() - .filter_map(|path| { - let relative = path.strip_prefix(repository).unwrap_or(&path); - if LEGACY_REFERENCE_ALLOWLIST - .iter() - .any(|allowed| relative == Path::new(allowed)) - { - return None; - } - let content = fs::read_to_string(&path).ok()?; + .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 - .contains(&legacy_reference) - .then(|| relative.display().to_string()) + .lines() + .enumerate() + .filter_map(move |(index, line)| { + (line.contains(&legacy_reference) + && !legacy_reference_is_allowed( + &relative, + line, + &legacy_reference, + &canonical_reference, + )) + .then(|| format!("{}:{}", relative.display(), index + 1)) + }) }) .collect::>(); assert!( stale_references.is_empty(), - "the legacy Kubernetes manifest path is still referenced by: {}", + "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"; + let canonical_reference = "deploy/kubernetes/wardnet.yaml"; + + assert!(legacy_reference_is_allowed( + relative, + "The repository path changed from `deploy/kubernetes/waf-ids-ai-soc.yaml` to `deploy/kubernetes/wardnet.yaml`.", + legacy_reference, + canonical_reference, + )); + assert!(legacy_reference_is_allowed( + relative, + "Rollback to a repository version before this path migration uses `deploy/kubernetes/waf-ids-ai-soc.yaml`.", + legacy_reference, + canonical_reference, + )); + assert!(!legacy_reference_is_allowed( + relative, + "kubectl apply -f deploy/kubernetes/waf-ids-ai-soc.yaml", + legacy_reference, + canonical_reference, + )); + assert!(!legacy_reference_is_allowed( + relative, + "Copy deploy/kubernetes/waf-ids-ai-soc.yaml into the GitOps repository.", + legacy_reference, + canonical_reference, + )); +} From 6ced885e43b60e44f614e15ac6d13026dbf76af5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:21:27 +0900 Subject: [PATCH 08/19] docs: make Wardnet README product-first --- README.md | 292 +++++++++++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 8ff15e8e..89b7735b 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/wardnet.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. From e6518b53a41cc7410a5c662b90fd73be1495510d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:42:50 +0900 Subject: [PATCH 09/19] docs: add public Pages landing source --- docs/index.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..54717531 --- /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. +- [Product and technical gap baseline](https://github.com/ContextualWisdomLab/wardnet/blob/main/docs/product-technical-gap-baseline.md) — current gaps and evidence status when present on protected `main`. +- [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. From b9eb18c6122e467d4f24435a41534a7950f805d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:02:31 +0900 Subject: [PATCH 10/19] test(deploy): keep stale-path fixture self-scannable --- tests/kubernetes_manifest_path.rs | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs index 1e09048c..076d91b2 100644 --- a/tests/kubernetes_manifest_path.rs +++ b/tests/kubernetes_manifest_path.rs @@ -132,31 +132,39 @@ fn kubernetes_manifest_uses_the_wardnet_filename_only() { #[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"; - let canonical_reference = "deploy/kubernetes/wardnet.yaml"; + 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, - "The repository path changed from `deploy/kubernetes/waf-ids-ai-soc.yaml` to `deploy/kubernetes/wardnet.yaml`.", - legacy_reference, - canonical_reference, + &migration_history, + &legacy_reference, + &canonical_reference, )); assert!(legacy_reference_is_allowed( relative, - "Rollback to a repository version before this path migration uses `deploy/kubernetes/waf-ids-ai-soc.yaml`.", - legacy_reference, - canonical_reference, + &rollback_history, + &legacy_reference, + &canonical_reference, )); assert!(!legacy_reference_is_allowed( relative, - "kubectl apply -f deploy/kubernetes/waf-ids-ai-soc.yaml", - legacy_reference, - canonical_reference, + &stale_apply_command, + &legacy_reference, + &canonical_reference, )); assert!(!legacy_reference_is_allowed( relative, - "Copy deploy/kubernetes/waf-ids-ai-soc.yaml into the GitOps repository.", - legacy_reference, - canonical_reference, + &stale_gitops_instruction, + &legacy_reference, + &canonical_reference, )); } From 170d906f1edb8e25bde87fbb5a689e1d9747e8b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:03:13 +0900 Subject: [PATCH 11/19] test(docs): reject broken repository links from Pages landing --- tests/documentation_landing.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/documentation_landing.rs diff --git a/tests/documentation_landing.rs b/tests/documentation_landing.rs new file mode 100644 index 00000000..59112dee --- /dev/null +++ b/tests/documentation_landing.rs @@ -0,0 +1,34 @@ +//! Repository contract for links published by the GitHub Pages landing source. + +use std::fs; +use std::path::Path; + +const REPOSITORY_BLOB_PREFIX: &str = + "https://github.com/ContextualWisdomLab/wardnet/blob/main/"; +const REPOSITORY_TREE_PREFIX: &str = + "https://github.com/ContextualWisdomLab/wardnet/tree/main/"; + +#[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) = target.strip_prefix(REPOSITORY_BLOB_PREFIX) { + assert!( + repository.join(relative).is_file(), + "Pages landing links to a missing repository file: {relative}" + ); + } else if let Some(relative) = target.strip_prefix(REPOSITORY_TREE_PREFIX) { + assert!( + repository.join(relative).is_dir(), + "Pages landing links to a missing repository directory: {relative}" + ); + } + } +} From deba232aae89d4055d627c5b4fa8733e62e54699 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:03:26 +0900 Subject: [PATCH 12/19] docs: replace unpublished gap-baseline link --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 54717531..2728bdc0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ Wardnet owns its gateway and SOC control-plane surface: route policy, current lo - [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. -- [Product and technical gap baseline](https://github.com/ContextualWisdomLab/wardnet/blob/main/docs/product-technical-gap-baseline.md) — current gaps and evidence status when present on protected `main`. +- [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. From 4b9869f015e552fe5c05c740162466b8d0539f88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:12:30 +0900 Subject: [PATCH 13/19] docs: remove stale Kubernetes path literal --- docs/doctoring/kubernetes-admin-secret-boundary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/kubernetes-admin-secret-boundary.md b/docs/doctoring/kubernetes-admin-secret-boundary.md index 4f29402c..31274155 100644 --- a/docs/doctoring/kubernetes-admin-secret-boundary.md +++ b/docs/doctoring/kubernetes-admin-secret-boundary.md @@ -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 and rejects restoration of the legacy `deploy/kubernetes/waf-ids-ai-soc.yaml` path. +`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. From 4ab1e8a85e7c133b9cda1886e72a36e97595a1b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:10:43 +0900 Subject: [PATCH 14/19] style(tests): apply rustfmt to documentation landing contract --- tests/documentation_landing.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/documentation_landing.rs b/tests/documentation_landing.rs index 59112dee..651f3cbc 100644 --- a/tests/documentation_landing.rs +++ b/tests/documentation_landing.rs @@ -3,10 +3,8 @@ use std::fs; use std::path::Path; -const REPOSITORY_BLOB_PREFIX: &str = - "https://github.com/ContextualWisdomLab/wardnet/blob/main/"; -const REPOSITORY_TREE_PREFIX: &str = - "https://github.com/ContextualWisdomLab/wardnet/tree/main/"; +const REPOSITORY_BLOB_PREFIX: &str = "https://github.com/ContextualWisdomLab/wardnet/blob/main/"; +const REPOSITORY_TREE_PREFIX: &str = "https://github.com/ContextualWisdomLab/wardnet/tree/main/"; #[test] fn pages_landing_repository_links_resolve_in_source_tree() { From 8fd2461c6a1cadcf27f81f7fe29da62691eee7ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:11:08 +0900 Subject: [PATCH 15/19] style(tests): apply rustfmt to Kubernetes path contract --- tests/kubernetes_manifest_path.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs index 076d91b2..5ed9a05f 100644 --- a/tests/kubernetes_manifest_path.rs +++ b/tests/kubernetes_manifest_path.rs @@ -5,8 +5,8 @@ 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", + "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. @@ -28,7 +28,10 @@ fn text_source_files(root: &Path) -> Vec { 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") { + if candidate + .file_name() + .is_some_and(|name| name == ".git" || name == "target") + { continue; } pending.push(candidate); @@ -98,10 +101,7 @@ fn kubernetes_manifest_uses_the_wardnet_filename_only() { 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 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(); From a76c64183143e4d9c10057d34330455f4bae6a78 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:53:19 +0900 Subject: [PATCH 16/19] test(docs): expose unsafe landing-link handling --- tests/documentation_landing.rs | 52 +++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/tests/documentation_landing.rs b/tests/documentation_landing.rs index 651f3cbc..445b715c 100644 --- a/tests/documentation_landing.rs +++ b/tests/documentation_landing.rs @@ -3,9 +3,38 @@ use std::fs; use std::path::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 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 { + Path::new(relative) +} + +#[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")); @@ -17,16 +46,19 @@ fn pages_landing_repository_links_resolve_in_source_tree() { .skip(1) .filter_map(|candidate| candidate.split(')').next()) { - if let Some(relative) = target.strip_prefix(REPOSITORY_BLOB_PREFIX) { - assert!( - repository.join(relative).is_file(), - "Pages landing links to a missing repository file: {relative}" - ); - } else if let Some(relative) = target.strip_prefix(REPOSITORY_TREE_PREFIX) { - assert!( - repository.join(relative).is_dir(), - "Pages landing links to a missing repository directory: {relative}" - ); + 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}" + ); + } } } } From 9616b94ac1ecf70038071a8c9395348694e6312c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:53:45 +0900 Subject: [PATCH 17/19] fix(docs): validate landing links within repository --- tests/documentation_landing.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/documentation_landing.rs b/tests/documentation_landing.rs index 445b715c..589369d1 100644 --- a/tests/documentation_landing.rs +++ b/tests/documentation_landing.rs @@ -1,14 +1,16 @@ //! Repository contract for links published by the GitHub Pages landing source. use std::fs; -use std::path::Path; +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 let Some(relative) = target.strip_prefix(REPOSITORY_BLOB_PREFIX) { + 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)) @@ -18,7 +20,16 @@ fn repository_target(target: &str) -> Option<(&str, bool)> { } fn validated_repository_relative_path(relative: &str) -> &Path { - Path::new(relative) + 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] From b07e4d21057df4dd4e706b584e5fad00338f20b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 12:12:31 +0900 Subject: [PATCH 18/19] fix(test): own Kubernetes path scan results --- tests/kubernetes_manifest_path.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs index 5ed9a05f..926dce86 100644 --- a/tests/kubernetes_manifest_path.rs +++ b/tests/kubernetes_manifest_path.rs @@ -119,6 +119,7 @@ fn kubernetes_manifest_uses_the_wardnet_filename_only() { )) .then(|| format!("{}:{}", relative.display(), index + 1)) }) + .collect::>() }) .collect::>(); From 8d656d313c18c2b304d3e1a563317a967c23e250 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 18:44:27 +0900 Subject: [PATCH 19/19] test(deploy): satisfy Rust 1.98 clippy contract --- tests/kubernetes_manifest_path.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/kubernetes_manifest_path.rs b/tests/kubernetes_manifest_path.rs index 926dce86..d75c3571 100644 --- a/tests/kubernetes_manifest_path.rs +++ b/tests/kubernetes_manifest_path.rs @@ -110,14 +110,18 @@ fn kubernetes_manifest_uses_the_wardnet_filename_only() { .lines() .enumerate() .filter_map(move |(index, line)| { - (line.contains(&legacy_reference) + if line.contains(&legacy_reference) && !legacy_reference_is_allowed( &relative, line, &legacy_reference, &canonical_reference, - )) - .then(|| format!("{}:{}", relative.display(), index + 1)) + ) + { + Some(format!("{}:{}", relative.display(), index + 1)) + } else { + None + } }) .collect::>() })