1use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::Arc;
15
16use crate::access::AccessListManager;
17use crate::actor::{ActorManager, ActorState, ActorStatsSnapshot};
18use crate::block_log::{BlockEvent, BlockLog};
19use crate::config_manager::{
20 AccessListRequest, ConfigManager, CreateSiteRequest, MutationResult, RateLimitRequest,
21 SiteDetailResponse, SiteWafRequest, UpdateSiteRequest,
22};
23use crate::correlation::CampaignManager;
24use crate::crawler::CrawlerDetector;
25use crate::dlp::DlpScanner;
26use crate::entity::{EntityManager, EntitySnapshot};
27use crate::health::{HealthChecker, HealthResponse};
28use crate::horizon::{HorizonClient, ThreatSignal};
29use crate::intelligence::{Signal, SignalManager, SignalQueryOptions, SignalSummary};
30use crate::metrics::MetricsRegistry;
31use crate::payload::{EndpointSortBy, PayloadManager};
32use crate::ratelimit::{RateLimitManager, RateLimitStats};
33use crate::reload::{ConfigReloader, ReloadResult};
34use crate::session::{SessionManager, SessionState, SessionStatsSnapshot};
35use crate::trends::{
36 AnomalyQueryOptions, TimeRange, TopSignalType, TrendQueryOptions, TrendsManager,
37};
38use crate::waf::{
39 Action as SynapseAction, Header as SynapseHeader, Request as SynapseRequest, Synapse, TraceSink,
40};
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ApiResponse<T> {
45 pub success: bool,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub data: Option<T>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub error: Option<String>,
53}
54
55impl<T> ApiResponse<T> {
56 pub fn ok(data: T) -> Self {
58 Self {
59 success: true,
60 data: Some(data),
61 error: None,
62 }
63 }
64
65 pub fn err(message: impl Into<String>) -> Self {
67 Self {
68 success: false,
69 data: None,
70 error: Some(message.into()),
71 }
72 }
73}
74
75pub struct ApiHandler {
77 health: Arc<HealthChecker>,
79 metrics: Arc<MetricsRegistry>,
81 reloader: Option<Arc<ConfigReloader>>,
83 rate_limiter: Arc<RwLock<RateLimitManager>>,
85 access_lists: Arc<RwLock<AccessListManager>>,
87 config_manager: Option<Arc<ConfigManager>>,
89 auth_token: Option<String>,
91 entity_manager: Option<Arc<EntityManager>>,
93 block_log: Option<Arc<BlockLog>>,
95 campaign_manager: Option<Arc<CampaignManager>>,
97 actor_manager: Option<Arc<ActorManager>>,
99 session_manager: Option<Arc<SessionManager>>,
101 synapse_engine: Option<Arc<RwLock<Synapse>>>,
103 payload_manager: Option<Arc<PayloadManager>>,
105 trends_manager: Option<Arc<TrendsManager>>,
107 signal_manager: Option<Arc<SignalManager>>,
109 crawler_detector: Option<Arc<CrawlerDetector>>,
111 dlp_scanner: Option<Arc<DlpScanner>>,
113 horizon_client: Option<Arc<HorizonClient>>,
115 signal_dispatcher: Arc<crate::signals::dispatcher::SignalDispatcher>,
117}
118
119fn constant_time_eq(expected: &[u8], provided: &[u8]) -> bool {
121 let mut diff = expected.len() ^ provided.len();
122 for (idx, expected_byte) in expected.iter().enumerate() {
123 let provided_byte = provided.get(idx).copied().unwrap_or(0);
124 diff |= (expected_byte ^ provided_byte) as usize;
125 }
126 diff == 0
127}
128
129impl ApiHandler {
130 pub fn builder() -> ApiHandlerBuilder {
132 ApiHandlerBuilder::default()
133 }
134
135 pub fn access_lists(&self) -> Arc<RwLock<AccessListManager>> {
136 Arc::clone(&self.access_lists)
137 }
138
139 pub fn dlp_scanner(&self) -> Option<Arc<DlpScanner>> {
141 self.dlp_scanner.as_ref().map(Arc::clone)
142 }
143
144 pub fn signal_dispatcher(&self) -> Arc<crate::signals::dispatcher::SignalDispatcher> {
146 Arc::clone(&self.signal_dispatcher)
147 }
148
149 pub async fn report_signal(&self, signal: ThreatSignal) -> Result<(), String> {
151 self.dispatch_horizon_signal(signal).await
152 }
153
154 pub fn is_horizon_blocked(&self, ip: Option<&str>, fingerprint: Option<&str>) -> bool {
156 match &self.horizon_client {
157 Some(client) => client.is_blocked(ip, fingerprint),
158 None => false,
159 }
160 }
161
162 pub async fn sync_horizon_blocklist(&self) -> Result<(), String> {
164 match &self.horizon_client {
165 Some(client) => {
166 client.flush_signals().await; Ok(())
170 }
171 None => Err("Horizon client not available".to_string()),
172 }
173 }
174
175 pub async fn dispatch_horizon_signal(&self, signal: ThreatSignal) -> Result<(), String> {
177 match &self.horizon_client {
178 Some(client) => {
179 if !client.circuit_breaker().allow_request().await {
180 return Err("Horizon circuit breaker open".to_string());
181 }
182 client.report_signal(signal);
183 Ok(())
184 }
185 None => Err("Horizon client not available".to_string()),
186 }
187 }
188
189 pub fn handle_health(&self) -> ApiResponse<HealthResponse> {
191 ApiResponse::ok(self.health.check())
192 }
193
194 pub fn handle_metrics(&self) -> String {
197 self.metrics.render_prometheus()
198 }
199
200 pub fn handle_reload(&self) -> ApiResponse<ReloadResultResponse> {
202 match &self.reloader {
203 Some(reloader) => {
204 let result = reloader.reload();
205 ApiResponse::ok(ReloadResultResponse::from(result))
206 }
207 None => ApiResponse::err("Configuration reloader not available"),
208 }
209 }
210
211 pub fn handle_list_sites(&self) -> ApiResponse<SiteListResponse> {
213 if let Some(reloader) = &self.reloader {
215 let config = reloader.config();
216 let config_read = config.read();
217 let sites: Vec<SiteInfo> = config_read
218 .sites
219 .iter()
220 .map(|s| SiteInfo {
221 hostname: s.hostname.clone(),
222 upstreams: s
223 .upstreams
224 .iter()
225 .map(|u| format!("{}:{}", u.host, u.port))
226 .collect(),
227 tls_enabled: s.tls.is_some(),
228 waf_enabled: s.waf.as_ref().map(|w| w.enabled).unwrap_or(true),
229 })
230 .collect();
231 return ApiResponse::ok(SiteListResponse { sites });
232 }
233
234 if let Some(config_manager) = &self.config_manager {
236 let sites = config_manager.get_sites_info();
237 return ApiResponse::ok(SiteListResponse { sites });
238 }
239
240 ApiResponse::ok(SiteListResponse { sites: vec![] })
242 }
243
244 pub fn handle_stats(&self) -> ApiResponse<StatsResponse> {
246 let rate_limit_stats = self.rate_limiter.read().stats();
247 let uptime = self.health.uptime();
248
249 ApiResponse::ok(StatsResponse {
250 uptime_secs: uptime.as_secs(),
251 rate_limit: rate_limit_stats,
252 access_list_sites: self.access_lists.read().site_count(),
253 })
254 }
255
256 pub fn handle_waf_stats(&self) -> ApiResponse<WafStatsResponse> {
258 let health = self.health.check();
259 ApiResponse::ok(WafStatsResponse {
260 enabled: health.waf.enabled,
261 analyzed: health.waf.analyzed,
262 blocked: health.waf.blocked,
263 block_rate_percent: health.waf.block_rate_percent,
264 avg_detection_us: health.waf.avg_detection_us,
265 })
266 }
267
268 pub fn handle_get_profiles(&self) -> ApiResponse<Vec<crate::profiler::EndpointProfile>> {
272 ApiResponse::ok(Vec::new())
275 }
276
277 pub fn handle_reset_profiles(&self) {
280 self.metrics.reset_profiles();
283 tracing::info!("Endpoint profiles reset via API");
284 }
285
286 pub fn handle_reset_schemas(&self) {
289 self.metrics.reset_schemas();
292 tracing::info!("Schema learner reset via API");
293 }
294
295 pub fn handle_create_site(&self, request: CreateSiteRequest) -> ApiResponse<MutationResult> {
301 match &self.config_manager {
302 Some(manager) => match manager.create_site(request) {
303 Ok(result) => ApiResponse::ok(result),
304 Err(e) => ApiResponse::err(e.to_string()),
305 },
306 None => ApiResponse::err("Configuration manager not available"),
307 }
308 }
309
310 pub fn handle_get_site(&self, hostname: &str) -> ApiResponse<SiteDetailResponse> {
312 match &self.config_manager {
313 Some(manager) => match manager.get_site(hostname) {
314 Ok(site) => ApiResponse::ok(site),
315 Err(e) => ApiResponse::err(e.to_string()),
316 },
317 None => ApiResponse::err("Configuration manager not available"),
318 }
319 }
320
321 pub fn handle_update_site(
323 &self,
324 hostname: &str,
325 request: UpdateSiteRequest,
326 ) -> ApiResponse<MutationResult> {
327 match &self.config_manager {
328 Some(manager) => match manager.update_site(hostname, request) {
329 Ok(result) => ApiResponse::ok(result),
330 Err(e) => ApiResponse::err(e.to_string()),
331 },
332 None => ApiResponse::err("Configuration manager not available"),
333 }
334 }
335
336 pub fn handle_delete_site(&self, hostname: &str) -> ApiResponse<MutationResult> {
338 match &self.config_manager {
339 Some(manager) => match manager.delete_site(hostname) {
340 Ok(result) => ApiResponse::ok(result),
341 Err(e) => ApiResponse::err(e.to_string()),
342 },
343 None => ApiResponse::err("Configuration manager not available"),
344 }
345 }
346
347 pub fn handle_update_site_waf(
349 &self,
350 hostname: &str,
351 request: SiteWafRequest,
352 ) -> ApiResponse<MutationResult> {
353 match &self.config_manager {
354 Some(manager) => match manager.update_site_waf(hostname, request) {
355 Ok(result) => ApiResponse::ok(result),
356 Err(e) => ApiResponse::err(e.to_string()),
357 },
358 None => ApiResponse::err("Configuration manager not available"),
359 }
360 }
361
362 pub fn handle_update_site_rate_limit(
364 &self,
365 hostname: &str,
366 request: RateLimitRequest,
367 ) -> ApiResponse<MutationResult> {
368 match &self.config_manager {
369 Some(manager) => match manager.update_site_rate_limit(hostname, request) {
370 Ok(result) => ApiResponse::ok(result),
371 Err(e) => ApiResponse::err(e.to_string()),
372 },
373 None => ApiResponse::err("Configuration manager not available"),
374 }
375 }
376
377 pub fn handle_update_site_access_list(
379 &self,
380 hostname: &str,
381 request: AccessListRequest,
382 ) -> ApiResponse<MutationResult> {
383 match &self.config_manager {
384 Some(manager) => match manager.update_site_access_list(hostname, request) {
385 Ok(result) => ApiResponse::ok(result),
386 Err(e) => ApiResponse::err(e.to_string()),
387 },
388 None => ApiResponse::err("Configuration manager not available"),
389 }
390 }
391
392 pub fn handle_get_config(&self) -> ApiResponse<crate::config::ConfigFile> {
394 match &self.config_manager {
395 Some(manager) => ApiResponse::ok(manager.get_full_config()),
396 None => ApiResponse::err("Configuration manager not available"),
397 }
398 }
399
400 pub fn handle_update_config(
402 &self,
403 config: crate::config::ConfigFile,
404 ) -> ApiResponse<MutationResult> {
405 match &self.config_manager {
406 Some(manager) => match manager.update_full_config(config) {
407 Ok(result) => ApiResponse::ok(result),
408 Err(e) => ApiResponse::err(e.to_string()),
409 },
410 None => ApiResponse::err("Configuration manager not available"),
411 }
412 }
413
414 pub fn validate_auth(&self, token: Option<&str>) -> bool {
419 match (self.auth_token.as_deref(), token) {
420 (Some(expected), Some(provided)) => {
421 let expected_bytes = expected.as_bytes();
422 let provided_bytes = provided.as_bytes();
423 constant_time_eq(expected_bytes, provided_bytes)
425 }
426 _ => false,
427 }
428 }
429
430 pub fn metrics(&self) -> Arc<MetricsRegistry> {
432 Arc::clone(&self.metrics)
433 }
434
435 pub fn health(&self) -> Arc<HealthChecker> {
437 Arc::clone(&self.health)
438 }
439
440 pub fn entity_manager(&self) -> Option<Arc<EntityManager>> {
442 self.entity_manager.as_ref().map(Arc::clone)
443 }
444
445 pub fn block_log(&self) -> Option<Arc<BlockLog>> {
447 self.block_log.as_ref().map(Arc::clone)
448 }
449
450 pub fn config_manager(&self) -> Option<&Arc<ConfigManager>> {
452 self.config_manager.as_ref()
453 }
454
455 pub fn campaign_manager(&self) -> Option<&Arc<CampaignManager>> {
457 self.campaign_manager.as_ref()
458 }
459
460 pub fn actor_manager(&self) -> Option<Arc<ActorManager>> {
462 self.actor_manager.as_ref().map(Arc::clone)
463 }
464
465 pub fn session_manager(&self) -> Option<Arc<SessionManager>> {
467 self.session_manager.as_ref().map(Arc::clone)
468 }
469
470 pub fn signal_manager(&self) -> Option<Arc<SignalManager>> {
472 self.signal_manager.as_ref().map(Arc::clone)
473 }
474
475 pub fn synapse_engine(&self) -> Option<Arc<RwLock<Synapse>>> {
477 self.synapse_engine.as_ref().map(Arc::clone)
478 }
479
480 pub fn evaluate_request(
483 &self,
484 method: &str,
485 uri: &str,
486 headers: &[(String, String)],
487 body: Option<&[u8]>,
488 client_ip: &str,
489 ) -> Option<EvaluateResult> {
490 let engine = self.synapse_engine.as_ref()?;
491
492 let start = std::time::Instant::now();
493
494 let synapse_headers: Vec<SynapseHeader> = headers
496 .iter()
497 .map(|(name, value)| SynapseHeader::new(name, value))
498 .collect();
499
500 let request = SynapseRequest {
501 method,
502 path: uri,
503 query: None,
504 headers: synapse_headers,
505 body,
506 client_ip,
507 is_static: false,
508 };
509
510 let verdict = engine.read().analyze(&request);
512 let elapsed = start.elapsed();
513
514 Some(EvaluateResult {
515 blocked: matches!(verdict.action, SynapseAction::Block),
516 risk_score: verdict.risk_score,
517 matched_rules: verdict.matched_rules.clone(),
518 block_reason: verdict.block_reason.clone(),
519 detection_time_us: elapsed.as_micros() as u64,
520 })
521 }
522
523 pub fn evaluate_request_trace(
525 &self,
526 method: &str,
527 uri: &str,
528 headers: &[(String, String)],
529 body: Option<&[u8]>,
530 client_ip: &str,
531 trace: &mut dyn TraceSink,
532 ) -> Option<EvaluateResult> {
533 let engine = self.synapse_engine.as_ref()?;
534
535 let start = std::time::Instant::now();
536
537 let synapse_headers: Vec<SynapseHeader> = headers
538 .iter()
539 .map(|(name, value)| SynapseHeader::new(name, value))
540 .collect();
541
542 let request = SynapseRequest {
543 method,
544 path: uri,
545 query: None,
546 headers: synapse_headers,
547 body,
548 client_ip,
549 is_static: false,
550 };
551
552 let verdict = engine.read().analyze_with_trace(&request, trace);
553 let elapsed = start.elapsed();
554
555 Some(EvaluateResult {
556 blocked: matches!(verdict.action, SynapseAction::Block),
557 risk_score: verdict.risk_score,
558 matched_rules: verdict.matched_rules.clone(),
559 block_reason: verdict.block_reason.clone(),
560 detection_time_us: elapsed.as_micros() as u64,
561 })
562 }
563
564 pub fn handle_list_actors(&self, limit: usize) -> Vec<ActorState> {
566 match &self.actor_manager {
567 Some(manager) => manager.list_actors(limit, 0),
568 None => Vec::new(),
569 }
570 }
571
572 pub fn handle_actor_stats(&self) -> Option<ActorStatsSnapshot> {
574 self.actor_manager
575 .as_ref()
576 .map(|manager| manager.stats().snapshot())
577 }
578
579 pub fn handle_list_sessions(&self, limit: usize) -> Vec<SessionState> {
581 match &self.session_manager {
582 Some(manager) => manager.list_sessions(limit, 0),
583 None => Vec::new(),
584 }
585 }
586
587 pub fn handle_session_stats(&self) -> Option<SessionStatsSnapshot> {
589 self.session_manager
590 .as_ref()
591 .map(|manager| manager.stats().snapshot())
592 }
593
594 pub fn handle_list_entities(&self, limit: usize) -> Vec<EntitySnapshot> {
596 match &self.entity_manager {
597 Some(manager) => manager.list_top_risk(limit),
598 None => Vec::new(),
599 }
600 }
601
602 pub fn handle_list_blocks(&self, limit: usize) -> Vec<BlockEvent> {
604 match &self.block_log {
605 Some(log) => log.recent(limit),
606 None => Vec::new(),
607 }
608 }
609
610 pub fn handle_payload_stats(&self) -> ApiResponse<PayloadSummaryResponse> {
616 match &self.payload_manager {
617 Some(manager) => ApiResponse::ok(PayloadSummaryResponse::from(manager.get_summary())),
618 None => ApiResponse::err("Payload manager not available"),
619 }
620 }
621
622 pub fn handle_payload_endpoints(
624 &self,
625 limit: usize,
626 ) -> ApiResponse<Vec<EndpointPayloadSummary>> {
627 match &self.payload_manager {
628 Some(manager) => {
629 let endpoints = manager.list_top_endpoints(limit, EndpointSortBy::RequestCount);
630 let summaries: Vec<EndpointPayloadSummary> = endpoints
631 .into_iter()
632 .map(|stats| EndpointPayloadSummary {
633 template: stats.template,
634 request_count: stats.request_count,
635 avg_request_size: stats.request.avg_bytes(),
636 avg_response_size: stats.response.avg_bytes(),
637 })
638 .collect();
639 ApiResponse::ok(summaries)
640 }
641 None => ApiResponse::err("Payload manager not available"),
642 }
643 }
644
645 pub fn handle_payload_anomalies(
647 &self,
648 limit: usize,
649 ) -> ApiResponse<Vec<PayloadAnomalyResponse>> {
650 match &self.payload_manager {
651 Some(manager) => {
652 let anomalies = manager.get_anomalies(limit);
653 let responses: Vec<PayloadAnomalyResponse> = anomalies
654 .into_iter()
655 .map(|a| PayloadAnomalyResponse {
656 anomaly_type: format!("{:?}", a.anomaly_type),
657 severity: format!("{:?}", a.severity),
658 risk_applied: a.risk_applied,
659 template: a.template,
660 entity_id: a.entity_id,
661 detected_at_ms: a.detected_at,
662 description: a.description,
663 })
664 .collect();
665 ApiResponse::ok(responses)
666 }
667 None => ApiResponse::err("Payload manager not available"),
668 }
669 }
670
671 pub fn handle_trends_summary(&self) -> ApiResponse<TrendsSummaryResponse> {
677 match &self.trends_manager {
678 Some(manager) => {
679 let summary = manager.get_summary(TrendQueryOptions::default());
680 let signal_counts: HashMap<String, usize> = summary
681 .by_category
682 .iter()
683 .map(|(category, data)| (category.to_string(), data.count))
684 .collect();
685 ApiResponse::ok(TrendsSummaryResponse {
686 total_signals: summary.total_signals,
687 signal_counts,
688 top_signal_types: summary.top_signal_types.clone(),
689 time_range: summary.time_range,
690 anomaly_count: summary.anomaly_count,
691 })
692 }
693 None => ApiResponse::err("Trends manager not available"),
694 }
695 }
696
697 pub fn handle_trends_anomalies(&self, limit: usize) -> ApiResponse<Vec<TrendsAnomalyResponse>> {
699 match &self.trends_manager {
700 Some(manager) => {
701 let mut opts = AnomalyQueryOptions::default();
702 opts.limit = Some(limit);
703 let anomalies = manager.get_anomalies(opts);
704 let responses: Vec<TrendsAnomalyResponse> = anomalies
705 .into_iter()
706 .map(|a| TrendsAnomalyResponse {
707 anomaly_type: format!("{:?}", a.anomaly_type),
708 severity: format!("{:?}", a.severity),
709 entities: a.entities,
710 description: a.description,
711 detected_at_ms: a.detected_at,
712 })
713 .collect();
714 ApiResponse::ok(responses)
715 }
716 None => ApiResponse::err("Trends manager not available"),
717 }
718 }
719
720 pub fn handle_signals(&self, options: SignalQueryOptions) -> ApiResponse<SignalListResponse> {
726 match &self.signal_manager {
727 Some(manager) => {
728 let signals = manager.list_signals(options);
729 let summary = manager.summary();
730 ApiResponse::ok(SignalListResponse { signals, summary })
731 }
732 None => ApiResponse::err("Signal manager not available"),
733 }
734 }
735
736 pub fn handle_crawler_stats(&self) -> ApiResponse<CrawlerStatsResponse> {
742 match &self.crawler_detector {
743 Some(detector) => {
744 let stats = detector.stats();
745 let total = stats.cache_hits + stats.cache_misses;
746 let cache_hit_rate = if total > 0 {
747 stats.cache_hits as f64 / total as f64
748 } else {
749 0.0
750 };
751 ApiResponse::ok(CrawlerStatsResponse {
752 total_verifications: stats.total_verifications,
753 verified_crawlers: stats.verified_crawlers,
754 unverified_crawlers: stats.unverified_crawlers,
755 bad_bots: stats.bad_bots,
756 cache_hit_rate,
757 })
758 }
759 None => ApiResponse::err("Crawler detector not available"),
760 }
761 }
762
763 pub fn handle_horizon_stats(&self) -> ApiResponse<HorizonStatsResponse> {
769 match &self.horizon_client {
770 Some(client) => {
771 let stats = client.stats();
772 ApiResponse::ok(HorizonStatsResponse {
773 signals_sent: stats.signals_sent,
774 signals_acked: stats.signals_acked,
775 signals_queued: stats.signals_queued,
776 signals_dropped: stats.signals_dropped,
777 batches_sent: stats.batches_sent,
778 heartbeats_sent: stats.heartbeats_sent,
779 heartbeat_failures: stats.heartbeat_failures,
780 reconnect_attempts: stats.reconnect_attempts,
781 blocklist_size: client.blocklist_size(),
782 })
783 }
784 None => ApiResponse::err("Horizon client not available"),
785 }
786 }
787
788 pub fn handle_horizon_blocklist(
790 &self,
791 limit: usize,
792 ) -> ApiResponse<Vec<BlocklistEntryResponse>> {
793 match &self.horizon_client {
794 Some(client) => {
795 let blocklist = client.blocklist();
796 let mut entries: Vec<BlocklistEntryResponse> = blocklist
797 .all_ips()
798 .into_iter()
799 .chain(blocklist.all_fingerprints())
800 .take(limit)
801 .map(|e| BlocklistEntryResponse {
802 entry_type: format!("{:?}", e.block_type),
803 value: e.indicator,
804 reason: e.reason.unwrap_or_default(),
805 source: e.source,
806 expires_at: e.expires_at,
807 })
808 .collect();
809 entries.truncate(limit);
810 ApiResponse::ok(entries)
811 }
812 None => ApiResponse::err("Horizon client not available"),
813 }
814 }
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct PayloadSummaryResponse {
824 pub total_endpoints: usize,
825 pub total_entities: usize,
826 pub total_requests: u64,
827 pub total_request_bytes: u64,
828 pub total_response_bytes: u64,
829 pub avg_request_size: f64,
830 pub avg_response_size: f64,
831 pub active_anomalies: usize,
832}
833
834impl From<crate::payload::PayloadSummary> for PayloadSummaryResponse {
835 fn from(s: crate::payload::PayloadSummary) -> Self {
836 Self {
837 total_endpoints: s.total_endpoints,
838 total_entities: s.total_entities,
839 total_requests: s.total_requests,
840 total_request_bytes: s.total_request_bytes,
841 total_response_bytes: s.total_response_bytes,
842 avg_request_size: s.avg_request_size,
843 avg_response_size: s.avg_response_size,
844 active_anomalies: s.active_anomalies,
845 }
846 }
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
851pub struct EndpointPayloadSummary {
852 pub template: String,
853 pub request_count: u64,
854 pub avg_request_size: f64,
855 pub avg_response_size: f64,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct PayloadAnomalyResponse {
861 pub anomaly_type: String,
862 pub severity: String,
863 pub risk_applied: Option<f64>,
864 pub template: String,
865 pub entity_id: String,
866 pub detected_at_ms: i64,
867 pub description: String,
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct TrendsSummaryResponse {
873 pub total_signals: usize,
874 pub signal_counts: HashMap<String, usize>,
875 pub top_signal_types: Vec<TopSignalType>,
876 pub time_range: TimeRange,
877 pub anomaly_count: usize,
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
882pub struct TrendsAnomalyResponse {
883 pub anomaly_type: String,
884 pub severity: String,
885 pub entities: Vec<String>,
886 pub description: String,
887 pub detected_at_ms: i64,
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
892pub struct SignalListResponse {
893 pub signals: Vec<Signal>,
894 pub summary: SignalSummary,
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize)]
899pub struct CrawlerStatsResponse {
900 pub total_verifications: u64,
901 pub verified_crawlers: u64,
902 pub unverified_crawlers: u64,
903 pub bad_bots: u64,
904 pub cache_hit_rate: f64,
905}
906
907#[derive(Debug, Clone, Serialize, Deserialize)]
909pub struct HorizonStatsResponse {
910 pub signals_sent: u64,
911 pub signals_acked: u64,
912 pub signals_queued: u64,
913 pub signals_dropped: u64,
914 pub batches_sent: u64,
915 pub heartbeats_sent: u64,
916 pub heartbeat_failures: u64,
917 pub reconnect_attempts: u32,
918 pub blocklist_size: usize,
919}
920
921#[derive(Debug, Clone, Serialize, Deserialize)]
923pub struct BlocklistEntryResponse {
924 pub entry_type: String,
925 pub value: String,
926 pub reason: String,
927 pub source: String,
928 pub expires_at: Option<String>,
929}
930
931#[derive(Default)]
933pub struct ApiHandlerBuilder {
934 health: Option<Arc<HealthChecker>>,
935 metrics: Option<Arc<MetricsRegistry>>,
936 reloader: Option<Arc<ConfigReloader>>,
937 rate_limiter: Option<Arc<RwLock<RateLimitManager>>>,
938 access_lists: Option<Arc<RwLock<AccessListManager>>>,
939 config_manager: Option<Arc<ConfigManager>>,
940 auth_token: Option<String>,
941 entity_manager: Option<Arc<EntityManager>>,
942 block_log: Option<Arc<BlockLog>>,
943 campaign_manager: Option<Arc<CampaignManager>>,
944 actor_manager: Option<Arc<ActorManager>>,
945 session_manager: Option<Arc<SessionManager>>,
946 synapse_engine: Option<Arc<RwLock<Synapse>>>,
947 payload_manager: Option<Arc<PayloadManager>>,
948 trends_manager: Option<Arc<TrendsManager>>,
949 signal_manager: Option<Arc<SignalManager>>,
950 crawler_detector: Option<Arc<CrawlerDetector>>,
951 dlp_scanner: Option<Arc<DlpScanner>>,
952 horizon_client: Option<Arc<HorizonClient>>,
953}
954
955impl ApiHandlerBuilder {
956 pub fn health(mut self, health: Arc<HealthChecker>) -> Self {
958 self.health = Some(health);
959 self
960 }
961
962 pub fn metrics(mut self, metrics: Arc<MetricsRegistry>) -> Self {
964 self.metrics = Some(metrics);
965 self
966 }
967
968 pub fn reloader(mut self, reloader: Arc<ConfigReloader>) -> Self {
970 self.reloader = Some(reloader);
971 self
972 }
973
974 pub fn rate_limiter(mut self, rate_limiter: Arc<RwLock<RateLimitManager>>) -> Self {
976 self.rate_limiter = Some(rate_limiter);
977 self
978 }
979
980 pub fn access_lists(mut self, access_lists: Arc<RwLock<AccessListManager>>) -> Self {
982 self.access_lists = Some(access_lists);
983 self
984 }
985
986 pub fn config_manager(mut self, config_manager: Arc<ConfigManager>) -> Self {
988 self.config_manager = Some(config_manager);
989 self
990 }
991
992 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
994 self.auth_token = Some(token.into());
995 self
996 }
997
998 pub fn entity_manager(mut self, entity_manager: Arc<EntityManager>) -> Self {
1000 self.entity_manager = Some(entity_manager);
1001 self
1002 }
1003
1004 pub fn block_log(mut self, block_log: Arc<BlockLog>) -> Self {
1006 self.block_log = Some(block_log);
1007 self
1008 }
1009
1010 pub fn campaign_manager(mut self, manager: Arc<CampaignManager>) -> Self {
1012 self.campaign_manager = Some(manager);
1013 self
1014 }
1015
1016 pub fn actor_manager(mut self, manager: Arc<ActorManager>) -> Self {
1018 self.actor_manager = Some(manager);
1019 self
1020 }
1021
1022 pub fn session_manager(mut self, manager: Arc<SessionManager>) -> Self {
1024 self.session_manager = Some(manager);
1025 self
1026 }
1027
1028 pub fn synapse_engine(mut self, engine: Arc<RwLock<Synapse>>) -> Self {
1030 self.synapse_engine = Some(engine);
1031 self
1032 }
1033
1034 pub fn payload_manager(mut self, manager: Arc<PayloadManager>) -> Self {
1036 self.payload_manager = Some(manager);
1037 self
1038 }
1039
1040 pub fn trends_manager(mut self, manager: Arc<TrendsManager>) -> Self {
1042 self.trends_manager = Some(manager);
1043 self
1044 }
1045
1046 pub fn signal_manager(mut self, manager: Arc<SignalManager>) -> Self {
1048 self.signal_manager = Some(manager);
1049 self
1050 }
1051
1052 pub fn crawler_detector(mut self, detector: Arc<CrawlerDetector>) -> Self {
1054 self.crawler_detector = Some(detector);
1055 self
1056 }
1057
1058 pub fn dlp_scanner(mut self, scanner: Arc<DlpScanner>) -> Self {
1060 self.dlp_scanner = Some(scanner);
1061 self
1062 }
1063
1064 pub fn horizon_client(mut self, client: Arc<HorizonClient>) -> Self {
1066 self.horizon_client = Some(client);
1067 self
1068 }
1069
1070 pub fn build(self) -> ApiHandler {
1072 let metrics = self
1073 .metrics
1074 .unwrap_or_else(|| Arc::new(MetricsRegistry::new()));
1075 ApiHandler {
1076 health: self
1077 .health
1078 .unwrap_or_else(|| Arc::new(HealthChecker::default())),
1079 metrics: metrics.clone(),
1080 reloader: self.reloader,
1081 rate_limiter: self
1082 .rate_limiter
1083 .unwrap_or_else(|| Arc::new(RwLock::new(RateLimitManager::new()))),
1084 access_lists: self
1085 .access_lists
1086 .unwrap_or_else(|| Arc::new(RwLock::new(AccessListManager::new()))),
1087 config_manager: self.config_manager,
1088 auth_token: self.auth_token,
1089 entity_manager: self.entity_manager,
1090 block_log: self.block_log,
1091 campaign_manager: self.campaign_manager,
1092 actor_manager: self.actor_manager,
1093 session_manager: self.session_manager,
1094 synapse_engine: self.synapse_engine,
1095 payload_manager: self.payload_manager,
1096 trends_manager: self.trends_manager,
1097 signal_manager: self.signal_manager.clone(),
1098 crawler_detector: self.crawler_detector,
1099 dlp_scanner: self.dlp_scanner,
1100 horizon_client: self.horizon_client.clone(),
1101 signal_dispatcher: {
1102 let mut sinks: Vec<Arc<dyn crate::horizon::SignalSink>> = Vec::new();
1103 if let Some(ref client) = self.horizon_client {
1104 sinks.push(Arc::clone(client) as Arc<dyn crate::horizon::SignalSink>);
1105 }
1106 Arc::new(crate::signals::dispatcher::SignalDispatcher::new(
1107 sinks,
1108 self.signal_manager,
1109 metrics,
1110 ))
1111 },
1112 }
1113 }
1114}
1115
1116#[derive(Debug, Clone, Serialize, Deserialize)]
1118pub struct ReloadResultResponse {
1119 pub success: bool,
1120 #[serde(skip_serializing_if = "Option::is_none")]
1121 pub error: Option<String>,
1122 pub sites_loaded: usize,
1123 pub certs_loaded: usize,
1124 pub duration_ms: u64,
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1129pub struct EvaluateResult {
1130 pub blocked: bool,
1132 pub risk_score: u16,
1134 pub matched_rules: Vec<u32>,
1136 pub block_reason: Option<String>,
1138 pub detection_time_us: u64,
1140}
1141
1142impl From<ReloadResult> for ReloadResultResponse {
1143 fn from(r: ReloadResult) -> Self {
1144 Self {
1145 success: r.success,
1146 error: r.error,
1147 sites_loaded: r.sites_loaded,
1148 certs_loaded: r.certs_loaded,
1149 duration_ms: r.duration_ms,
1150 }
1151 }
1152}
1153
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1156pub struct SiteListResponse {
1157 pub sites: Vec<SiteInfo>,
1158}
1159
1160#[derive(Debug, Clone, Serialize, Deserialize)]
1162pub struct SiteInfo {
1163 pub hostname: String,
1164 pub upstreams: Vec<String>,
1165 pub tls_enabled: bool,
1166 pub waf_enabled: bool,
1167}
1168
1169#[derive(Debug, Clone, Serialize, Deserialize)]
1171pub struct StatsResponse {
1172 pub uptime_secs: u64,
1173 pub rate_limit: RateLimitStats,
1174 pub access_list_sites: usize,
1175}
1176
1177#[derive(Debug, Clone, Serialize, Deserialize)]
1179pub struct WafStatsResponse {
1180 pub enabled: bool,
1181 pub analyzed: u64,
1182 pub blocked: u64,
1183 pub block_rate_percent: f64,
1184 pub avg_detection_us: u64,
1185}
1186
1187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1189pub enum HttpMethod {
1190 Get,
1191 Post,
1192 Put,
1193 Delete,
1194}
1195
1196#[derive(Debug, Clone)]
1198pub struct ApiRoute {
1199 pub method: HttpMethod,
1200 pub path: &'static str,
1201 pub description: &'static str,
1202 pub auth_required: bool,
1203}
1204
1205pub const API_ROUTES: &[ApiRoute] = &[
1207 ApiRoute {
1209 method: HttpMethod::Get,
1210 path: "/health",
1211 description: "Health check endpoint",
1212 auth_required: false,
1213 },
1214 ApiRoute {
1215 method: HttpMethod::Get,
1216 path: "/metrics",
1217 description: "Prometheus metrics endpoint",
1218 auth_required: false,
1219 },
1220 ApiRoute {
1222 method: HttpMethod::Post,
1223 path: "/reload",
1224 description: "Reload configuration from file",
1225 auth_required: true,
1226 },
1227 ApiRoute {
1228 method: HttpMethod::Get,
1229 path: "/sites",
1230 description: "List all configured sites",
1231 auth_required: true,
1232 },
1233 ApiRoute {
1234 method: HttpMethod::Post,
1235 path: "/sites",
1236 description: "Create a new site",
1237 auth_required: true,
1238 },
1239 ApiRoute {
1240 method: HttpMethod::Get,
1241 path: "/sites/:hostname",
1242 description: "Get site details",
1243 auth_required: true,
1244 },
1245 ApiRoute {
1246 method: HttpMethod::Put,
1247 path: "/sites/:hostname",
1248 description: "Update site configuration",
1249 auth_required: true,
1250 },
1251 ApiRoute {
1252 method: HttpMethod::Delete,
1253 path: "/sites/:hostname",
1254 description: "Delete a site",
1255 auth_required: true,
1256 },
1257 ApiRoute {
1258 method: HttpMethod::Put,
1259 path: "/sites/:hostname/waf",
1260 description: "Update site WAF configuration",
1261 auth_required: true,
1262 },
1263 ApiRoute {
1264 method: HttpMethod::Put,
1265 path: "/sites/:hostname/rate-limit",
1266 description: "Update site rate limit configuration",
1267 auth_required: true,
1268 },
1269 ApiRoute {
1270 method: HttpMethod::Put,
1271 path: "/sites/:hostname/access-list",
1272 description: "Update site access list",
1273 auth_required: true,
1274 },
1275 ApiRoute {
1277 method: HttpMethod::Get,
1278 path: "/stats",
1279 description: "Runtime statistics",
1280 auth_required: true,
1281 },
1282 ApiRoute {
1283 method: HttpMethod::Get,
1284 path: "/waf/stats",
1285 description: "WAF statistics",
1286 auth_required: true,
1287 },
1288];
1289
1290#[cfg(test)]
1291mod tests {
1292 use super::*;
1293
1294 #[test]
1295 fn test_api_response_ok() {
1296 let response: ApiResponse<String> = ApiResponse::ok("test".to_string());
1297 assert!(response.success);
1298 assert_eq!(response.data, Some("test".to_string()));
1299 assert!(response.error.is_none());
1300 }
1301
1302 #[test]
1303 fn test_api_response_err() {
1304 let response: ApiResponse<String> = ApiResponse::err("error message");
1305 assert!(!response.success);
1306 assert!(response.data.is_none());
1307 assert_eq!(response.error, Some("error message".to_string()));
1308 }
1309
1310 #[test]
1311 fn test_api_handler_builder() {
1312 let handler = ApiHandler::builder().auth_token("secret").build();
1313
1314 assert!(handler.validate_auth(Some("secret")));
1315 assert!(!handler.validate_auth(Some("wrong")));
1316 assert!(!handler.validate_auth(None));
1317 }
1318
1319 #[test]
1320 fn test_api_handler_no_auth() {
1321 let handler = ApiHandler::builder().build();
1322
1323 assert!(!handler.validate_auth(None));
1325 assert!(!handler.validate_auth(Some("anything")));
1326 }
1327
1328 #[test]
1329 fn test_handle_health() {
1330 let handler = ApiHandler::builder().build();
1331 let response = handler.handle_health();
1332
1333 assert!(response.success);
1334 assert!(response.data.is_some());
1335 }
1336
1337 #[test]
1338 fn test_handle_metrics() {
1339 let handler = ApiHandler::builder().build();
1340 let metrics = handler.handle_metrics();
1341
1342 assert!(metrics.contains("synapse_"));
1343 }
1344
1345 #[test]
1346 fn test_handle_stats() {
1347 let handler = ApiHandler::builder().build();
1348 let response = handler.handle_stats();
1349
1350 assert!(response.success);
1351 let stats = response.data.unwrap();
1352 assert!(stats.uptime_secs < 1); }
1354
1355 #[test]
1356 fn test_handle_waf_stats() {
1357 let handler = ApiHandler::builder().build();
1358 let response = handler.handle_waf_stats();
1359
1360 assert!(response.success);
1361 let waf = response.data.unwrap();
1362 assert!(waf.enabled);
1363 }
1364
1365 #[test]
1366 fn test_handle_reload_no_reloader() {
1367 let handler = ApiHandler::builder().build();
1368 let response = handler.handle_reload();
1369
1370 assert!(!response.success);
1371 assert!(response.error.is_some());
1372 }
1373
1374 #[test]
1375 fn test_handle_list_sites_no_reloader() {
1376 let handler = ApiHandler::builder().build();
1377 let response = handler.handle_list_sites();
1378
1379 assert!(response.success);
1381 assert!(response.error.is_none());
1382 assert!(response.data.unwrap().sites.is_empty());
1383 }
1384
1385 #[test]
1386 fn test_api_routes() {
1387 assert!(!API_ROUTES.is_empty());
1388
1389 let health_route = API_ROUTES.iter().find(|r| r.path == "/health").unwrap();
1391 assert!(!health_route.auth_required);
1392
1393 let reload_route = API_ROUTES.iter().find(|r| r.path == "/reload").unwrap();
1395 assert!(reload_route.auth_required);
1396 }
1397
1398 #[test]
1399 fn test_reload_result_response() {
1400 let result = ReloadResult {
1401 success: true,
1402 error: None,
1403 sites_loaded: 5,
1404 certs_loaded: 3,
1405 duration_ms: 100,
1406 };
1407
1408 let response = ReloadResultResponse::from(result);
1409 assert!(response.success);
1410 assert_eq!(response.sites_loaded, 5);
1411 assert_eq!(response.certs_loaded, 3);
1412 }
1413
1414 #[test]
1415 fn test_site_info_serialization() {
1416 let site = SiteInfo {
1417 hostname: "example.com".to_string(),
1418 upstreams: vec!["127.0.0.1:8080".to_string()],
1419 tls_enabled: true,
1420 waf_enabled: true,
1421 };
1422
1423 let json = serde_json::to_string(&site).unwrap();
1424 assert!(json.contains("example.com"));
1425 assert!(json.contains("tls_enabled"));
1426 }
1427}