From 7e7f9d036f3cd3c5776c790773a57e9363b36dd2 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 1 Sep 2026 21:38:32 +0900 Subject: [PATCH 01/10] feat(observability): export readiness evidence gauges --- CHANGELOG.md | 1 + docs/analytics/soc-kpis.md | 2 +- docs/commercial/buyer-due-diligence.md | 3 ++ src/lib.rs | 75 +++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d8068..d13a230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ ### Operations - 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. +- Added buyer-facing readiness gauges to `GET /metrics`, including enterprise sale readiness, readiness check pass/fail counts, enabled-route readiness, and admin-auth configuration. diff --git a/docs/analytics/soc-kpis.md b/docs/analytics/soc-kpis.md index b322d69..cb29c2b 100644 --- a/docs/analytics/soc-kpis.md +++ b/docs/analytics/soc-kpis.md @@ -37,4 +37,4 @@ ## MVP Measurement -The baseline exposes `GET /api/kpis` with counts for routes, indicators, DNSBL entries, threat feeds, fresh feeds, stale feeds, events, blocked events, monitored events, and management audit logs. `GET /api/commercial/evidence-manifest` adds the buyer-facing checklist that maps those signals to required runtime endpoints, committed documents, and deployment assets. Latency, precision, triage time, and full feed freshness percentages require the next telemetry and analyst-disposition work. +The baseline exposes `GET /api/kpis` with counts for routes, indicators, DNSBL entries, threat feeds, fresh feeds, stale feeds, events, blocked events, monitored events, and management audit logs. `GET /api/commercial/evidence-manifest` adds the buyer-facing checklist that maps those signals to required runtime endpoints, committed documents, and deployment assets. `GET /metrics` now exports those gauges plus buyer-meaningful operational readiness signals: enterprise sale readiness, readiness check pass/fail counts, gateway route readiness, and whether admin write authentication is configured. Latency, precision, triage time, and full feed freshness percentages still require the next telemetry and analyst-disposition work. diff --git a/docs/commercial/buyer-due-diligence.md b/docs/commercial/buyer-due-diligence.md index a940253..6af5fd1 100644 --- a/docs/commercial/buyer-due-diligence.md +++ b/docs/commercial/buyer-due-diligence.md @@ -11,6 +11,7 @@ - Security events: `GET /api/events` - SOC event export: `GET /api/events.ndjson` - SOC KPIs: `GET /api/kpis` +- Prometheus operations metrics: `GET /metrics` - License profile: `GET /api/commercial/license` - Sale readiness: `GET /api/commercial/readiness` - Buyer evidence manifest: `GET /api/commercial/evidence-manifest` @@ -27,6 +28,7 @@ - Authenticated management writes through `X-Admin-Token`. - Automated tests for management APIs, gateway scoring, DNSBL export, event NDJSON export, feed freshness, persistence failures, commercial readiness, and legacy state compatibility. - Buyer evidence manifest that lists required runtime endpoints, committed document paths, deployment assets, blockers, and runtime evidence counts from one API. +- Prometheus metrics that expose readiness blockers, passing/failing readiness checks, route readiness, and admin-auth configuration without scraping the admin console. - `scripts/smoke.sh` verifies a full local lifecycle including restart persistence. ## Security Review Packet @@ -58,6 +60,7 @@ Then inspect: ```bash curl -fsS http://127.0.0.1:8080/api/commercial/readiness curl -fsS http://127.0.0.1:8080/api/commercial/evidence-manifest +curl -fsS http://127.0.0.1:8080/metrics 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 diff --git a/src/lib.rs b/src/lib.rs index ab902ca..c2fc89f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1104,7 +1104,7 @@ async fn evaluate_request( async fn metrics(State(state): State) -> impl IntoResponse { let body = { let data = state.inner.read().await; - prometheus_exposition(&kpi_snapshot_at(&data, now_unix())) + operational_prometheus_exposition(&state, &data, now_unix()) }; ( [( @@ -1115,6 +1115,72 @@ async fn metrics(State(state): State) -> impl IntoResponse { ) } +fn operational_prometheus_exposition(state: &AppState, data: &AppData, now_unix: u64) -> String { + let kpis = kpi_snapshot_at(data, now_unix); + let readiness = commercial_readiness_snapshot_at(data, now_unix); + let routes_enabled = data.routes.iter().filter(|route| route.enabled).count(); + let checks_passed = readiness + .checks + .iter() + .filter(|check| check.status == ReadinessStatus::Pass) + .count(); + let checks_failed = readiness + .checks + .iter() + .filter(|check| check.status == ReadinessStatus::Fail) + .count(); + let mut out = prometheus_exposition(&kpis); + append_prometheus_gauge( + &mut out, + "waf_ids_commercial_ready", + "Enterprise sale readiness flag (1=ready, 0=blocked).", + usize::from(readiness.ready_for_enterprise_sale), + ); + append_prometheus_gauge( + &mut out, + "waf_ids_commercial_blockers", + "Current count of commercial-readiness blockers.", + readiness.blockers.len(), + ); + append_prometheus_gauge( + &mut out, + "waf_ids_readiness_checks_passed", + "Commercial-readiness checks currently passing.", + checks_passed, + ); + append_prometheus_gauge( + &mut out, + "waf_ids_readiness_checks_failed", + "Commercial-readiness checks currently failing.", + checks_failed, + ); + append_prometheus_gauge( + &mut out, + "waf_ids_gateway_ready", + "Kubernetes readiness-style route readiness (1=ready, 0=not ready).", + usize::from(routes_enabled > 0), + ); + append_prometheus_gauge( + &mut out, + "waf_ids_gateway_routes_enabled", + "Enabled gateway route count used by the readiness probe.", + routes_enabled, + ); + append_prometheus_gauge( + &mut out, + "waf_ids_admin_auth_configured", + "Admin write authentication configured (1=yes, 0=auth disabled).", + usize::from(state.health_status().admin_auth_configured), + ); + out +} + +fn append_prometheus_gauge(out: &mut String, name: &str, help: &str, value: usize) { + out.push_str(&format!( + "# HELP {name} {help}\n# TYPE {name} gauge\n{name} {value}\n" + )); +} + async fn get_commercial_license(State(state): State) -> Json { Json(state.inner.read().await.commercial.clone()) } @@ -3707,6 +3773,13 @@ mod tests { ); let body = body_text(response).await; assert!(body.contains("waf_ids_security_events_blocked")); + assert!(body.contains("waf_ids_commercial_ready 0")); + assert!(body.contains("waf_ids_commercial_blockers 4")); + assert!(body.contains("waf_ids_readiness_checks_passed 2")); + assert!(body.contains("waf_ids_readiness_checks_failed 4")); + assert!(body.contains("waf_ids_gateway_ready 1")); + assert!(body.contains("waf_ids_gateway_routes_enabled 1")); + assert!(body.contains("waf_ids_admin_auth_configured 0")); } #[tokio::test] From fa9713bd549595d5fe7abfdf2d9b208cdf8419f2 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:15:30 +0000 Subject: [PATCH 02/10] Fix CodeRabbit issues in PR #142 --- docs/analytics/soc-kpis.md | 8 +++++++- src/lib.rs | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/analytics/soc-kpis.md b/docs/analytics/soc-kpis.md index cb29c2b..9bc1b7a 100644 --- a/docs/analytics/soc-kpis.md +++ b/docs/analytics/soc-kpis.md @@ -37,4 +37,10 @@ ## MVP Measurement -The baseline exposes `GET /api/kpis` with counts for routes, indicators, DNSBL entries, threat feeds, fresh feeds, stale feeds, events, blocked events, monitored events, and management audit logs. `GET /api/commercial/evidence-manifest` adds the buyer-facing checklist that maps those signals to required runtime endpoints, committed documents, and deployment assets. `GET /metrics` now exports those gauges plus buyer-meaningful operational readiness signals: enterprise sale readiness, readiness check pass/fail counts, gateway route readiness, and whether admin write authentication is configured. Latency, precision, triage time, and full feed freshness percentages still require the next telemetry and analyst-disposition work. +The baseline exposes `GET /api/kpis` with counts for routes, indicators, DNSBL entries, threat feeds, fresh feeds, stale feeds, events, blocked events, monitored events, and management audit logs. `GET /api/commercial/evidence-manifest` adds the buyer-facing checklist that maps those signals to required runtime endpoints, committed documents, and deployment assets. `GET /metrics` now exports those gauges plus buyer-meaningful operational readiness signals: enterprise sale readiness, readiness check pass/fail counts, gateway route readiness, and whether admin write authentication is configured. These counters and readiness gauges provide the continuous, broadly available operational signals advocated by [Sigelman et al.](https://research.google.com/pubs/pub36356.html), but they do not yet provide the request-level traces needed to diagnose latency across components. Latency, precision, triage time, and full feed freshness percentages still require the next telemetry and analyst-disposition work; in particular, [Chandola et al.](https://doi.org/10.1145/1541880.1541882) show that anomaly-detection methods have different trade-offs, so detection quality must be validated with precision and false-positive measurements rather than inferred from event volume alone. + +## Research Grounding + +- **Sigelman et al. (2010), [*Dapper, a Large-Scale Distributed Systems Tracing Infrastructure*](https://research.google.com/pubs/pub36356.html).** Production tracing benefits from low-overhead, ubiquitous instrumentation and continuous monitoring. This supports collecting gateway latency and readiness signals continuously, while also motivating future request-level traces for cross-component diagnosis. +- **Xu et al. (2009), [*Detecting Large-Scale System Problems by Mining Console Logs*](https://doi.org/10.1145/1629575.1629587).** Structured features derived from operational logs can expose runtime problems and produce operator-facing explanations. This grounds retaining event, feed-error, and triage signals as inputs to future anomaly detection rather than treating raw log volume as a detection result. +- **Chandola, Banerjee, and Kumar (2009), [*Anomaly Detection: A Survey*](https://doi.org/10.1145/1541880.1541882).** Anomaly-detection techniques vary by data assumptions, computational cost, and application context. This supports pairing detection counts with analyst-confirmed precision, false-positive rate, and time-to-triage guardrails. diff --git a/src/lib.rs b/src/lib.rs index c2fc89f..7fbd641 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1170,7 +1170,7 @@ fn operational_prometheus_exposition(state: &AppState, data: &AppData, now_unix: &mut out, "waf_ids_admin_auth_configured", "Admin write authentication configured (1=yes, 0=auth disabled).", - usize::from(state.health_status().admin_auth_configured), + usize::from(has_write_admin_credential(state)), ); out } @@ -3782,6 +3782,24 @@ mod tests { assert!(body.contains("waf_ids_admin_auth_configured 0")); } + #[tokio::test] + async fn admin_auth_metric_requires_a_write_capable_credential() { + let readonly_app = build_app( + AppState::seeded(None).with_admin_tokens(parse_admin_tokens("read:auditor:readonly")), + ); + let readonly_body = + body_text(app_request(&readonly_app, empty_request(Method::GET, "/metrics")).await) + .await; + assert!(readonly_body.contains("waf_ids_admin_auth_configured 0")); + + let writer_app = build_app( + AppState::seeded(None).with_admin_tokens(parse_admin_tokens("write:operator:write")), + ); + let writer_body = + body_text(app_request(&writer_app, empty_request(Method::GET, "/metrics")).await).await; + assert!(writer_body.contains("waf_ids_admin_auth_configured 1")); + } + #[tokio::test] async fn signatures_endpoint_lists_redacted_catalog() { let app = build_app(AppState::seeded(None)); From 021d51dc18448964fb4aab8ea119bf37825af036 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 1 Sep 2026 22:43:53 +0900 Subject: [PATCH 03/10] fix(metrics): align admin auth readiness surfaces --- src/lib.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 7fbd641..510f989 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,7 +282,7 @@ impl AppState { dnsbl_origin: self.dnsbl_origin.clone(), event_limit: self.event_limit, credentials_source: self.credentials_source.as_str().to_string(), - admin_auth_configured: self.admin_token.is_some() || !self.admin_tokens.is_empty(), + admin_auth_configured: has_write_admin_credential(self), } } } @@ -3787,6 +3787,10 @@ mod tests { let readonly_app = build_app( AppState::seeded(None).with_admin_tokens(parse_admin_tokens("read:auditor:readonly")), ); + let readonly_health: HealthStatus = + json_body(app_request(&readonly_app, empty_request(Method::GET, "/healthz")).await) + .await; + assert!(!readonly_health.admin_auth_configured); let readonly_body = body_text(app_request(&readonly_app, empty_request(Method::GET, "/metrics")).await) .await; @@ -3795,6 +3799,9 @@ mod tests { let writer_app = build_app( AppState::seeded(None).with_admin_tokens(parse_admin_tokens("write:operator:write")), ); + let writer_health: HealthStatus = + json_body(app_request(&writer_app, empty_request(Method::GET, "/healthz")).await).await; + assert!(writer_health.admin_auth_configured); let writer_body = body_text(app_request(&writer_app, empty_request(Method::GET, "/metrics")).await).await; assert!(writer_body.contains("waf_ids_admin_auth_configured 1")); @@ -7736,6 +7743,14 @@ mod tests { let health = authed.health_status(); assert_eq!(health.credentials_source, "file"); assert!(health.admin_auth_configured); + + let readonly = state + .clone() + .with_admin_tokens(parse_admin_tokens("tok:auditor:readonly")) + .with_credentials_source(CredentialSource::File); + let health = readonly.health_status(); + assert_eq!(health.credentials_source, "file"); + assert!(!health.admin_auth_configured); } fn clearfolio_test_config(base_url: &str) -> ClearfolioConfig { From aa548076fba4c122de50f8821e880e222fedf5fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:06:57 +0900 Subject: [PATCH 04/10] test(ci): require explicit hosted runner image --- tests/workflow_runner_contract.rs | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/workflow_runner_contract.rs diff --git a/tests/workflow_runner_contract.rs b/tests/workflow_runner_contract.rs new file mode 100644 index 0000000..f63eee0 --- /dev/null +++ b/tests/workflow_runner_contract.rs @@ -0,0 +1,39 @@ +//! Repository contract for deterministic GitHub-hosted runner selection. +//! +//! Wardnet's required pull-request workflows must not depend on GitHub's floating +//! `ubuntu-latest` alias. A floating image can change independently of the +//! repository and, during hosted-runner transitions, can leave exact-head jobs +//! queued before checkout. Pinning the Ubuntu image makes runner acquisition a +//! reviewed repository change while preserving GitHub-hosted execution. + +use std::fs; +use std::path::Path; + +const PINNED_UBUNTU_RUNNER: &str = "ubuntu-24.04"; +const FLOATING_UBUNTU_RUNNER: &str = "ubuntu-latest"; + +const RUNNER_BACKED_WORKFLOWS: &[&str] = &[ + ".github/workflows/ci.yml", + ".github/workflows/fuzz.yml", + ".github/workflows/scorecard-analysis.yml", +]; + +#[test] +fn runner_backed_workflows_pin_the_hosted_ubuntu_image() { + let repository = Path::new(env!("CARGO_MANIFEST_DIR")); + + for relative in RUNNER_BACKED_WORKFLOWS { + let path = repository.join(relative); + let workflow = fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + + assert!( + !workflow.contains(FLOATING_UBUNTU_RUNNER), + "{relative} must not use the floating {FLOATING_UBUNTU_RUNNER} runner alias" + ); + assert!( + workflow.contains(&format!("runs-on: {PINNED_UBUNTU_RUNNER}")), + "{relative} must pin runner-backed jobs to {PINNED_UBUNTU_RUNNER}" + ); + } +} From 2770b57abebacae00b8624ba4213cdc032d9a9c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:07:11 +0900 Subject: [PATCH 05/10] fix(ci): pin hosted Ubuntu runner --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df09275..9409a89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ permissions: jobs: rust: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable From 33ebae0208382f7e6582bd88188050d42b5ddbcf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:07:28 +0900 Subject: [PATCH 06/10] fix(ci): pin fuzz runner image --- .github/workflows/fuzz.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ebcb3ce..b2a5366 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -23,7 +23,7 @@ concurrency: jobs: fuzz: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: From 2d41c4079f9a4465c3142a0aa2dd5895cb11f793 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:07:41 +0900 Subject: [PATCH 07/10] fix(ci): pin scorecard runner image --- .github/workflows/scorecard-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index dfa6420..2d147be 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -13,7 +13,7 @@ permissions: jobs: analysis: name: Scorecard Analysis - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read security-events: write From 6667046c1da24424b5cf24cf445e6ef7e6b85e55 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 2 Sep 2026 02:53:12 +0900 Subject: [PATCH 08/10] fix(metrics): clarify write-capable admin auth gauge --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 510f989..c5cb947 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1169,7 +1169,7 @@ fn operational_prometheus_exposition(state: &AppState, data: &AppData, now_unix: append_prometheus_gauge( &mut out, "waf_ids_admin_auth_configured", - "Admin write authentication configured (1=yes, 0=auth disabled).", + "Admin write authentication configured (1=write-capable credential configured, 0=no write-capable credential configured; readonly auth may still exist).", usize::from(has_write_admin_credential(state)), ); out @@ -3795,6 +3795,9 @@ mod tests { body_text(app_request(&readonly_app, empty_request(Method::GET, "/metrics")).await) .await; assert!(readonly_body.contains("waf_ids_admin_auth_configured 0")); + assert!(readonly_body.contains( + "# HELP waf_ids_admin_auth_configured Admin write authentication configured (1=write-capable credential configured, 0=no write-capable credential configured; readonly auth may still exist)." + )); let writer_app = build_app( AppState::seeded(None).with_admin_tokens(parse_admin_tokens("write:operator:write")), From 9a159dde773a970166e1c38619fc2094d2273f79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 04:04:15 +0900 Subject: [PATCH 09/10] test(ci): validate every workflow runner declaration --- tests/workflow_runner_contract.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/workflow_runner_contract.rs b/tests/workflow_runner_contract.rs index f63eee0..f8a626e 100644 --- a/tests/workflow_runner_contract.rs +++ b/tests/workflow_runner_contract.rs @@ -31,9 +31,21 @@ fn runner_backed_workflows_pin_the_hosted_ubuntu_image() { !workflow.contains(FLOATING_UBUNTU_RUNNER), "{relative} must not use the floating {FLOATING_UBUNTU_RUNNER} runner alias" ); + + let runners = workflow + .lines() + .filter_map(|line| line.trim().strip_prefix("runs-on:")) + .map(str::trim) + .collect::>(); + assert!( + !runners.is_empty(), + "{relative} must define at least one runs-on value" + ); assert!( - workflow.contains(&format!("runs-on: {PINNED_UBUNTU_RUNNER}")), - "{relative} must pin runner-backed jobs to {PINNED_UBUNTU_RUNNER}" + runners + .iter() + .all(|runner| *runner == PINNED_UBUNTU_RUNNER), + "{relative} must use {PINNED_UBUNTU_RUNNER} for every runs-on value; found {runners:?}" ); } } From b663f9d200e5f385c7dd067d074940a02836c68e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 04:33:50 +0900 Subject: [PATCH 10/10] fix(ci): format runner contract test --- tests/workflow_runner_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/workflow_runner_contract.rs b/tests/workflow_runner_contract.rs index f8a626e..ff42bad 100644 --- a/tests/workflow_runner_contract.rs +++ b/tests/workflow_runner_contract.rs @@ -42,9 +42,7 @@ fn runner_backed_workflows_pin_the_hosted_ubuntu_image() { "{relative} must define at least one runs-on value" ); assert!( - runners - .iter() - .all(|runner| *runner == PINNED_UBUNTU_RUNNER), + runners.iter().all(|runner| *runner == PINNED_UBUNTU_RUNNER), "{relative} must use {PINNED_UBUNTU_RUNNER} for every runs-on value; found {runners:?}" ); }