From 7e7f9d036f3cd3c5776c790773a57e9363b36dd2 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 1 Sep 2026 21:38:32 +0900 Subject: [PATCH 1/4] 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 83d80680..d13a2309 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 b322d695..cb29c2bf 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 a9402535..6af5fd1a 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 ab902cae..c2fc89f5 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 2/4] 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 cb29c2bf..9bc1b7a9 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 c2fc89f5..7fbd641d 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 3/4] 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 7fbd641d..510f9890 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 6667046c1da24424b5cf24cf445e6ef7e6b85e55 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 2 Sep 2026 02:53:12 +0900 Subject: [PATCH 4/4] 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 510f9890..c5cb9477 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")),