1use crate::storage::AuthStorage;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::Arc;
15use tokio::time::{Duration, Instant};
16
17pub mod compliance;
18pub mod dashboard;
19pub mod metrics;
20pub mod reports;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct AnalyticsConfig {
25 pub real_time_enabled: bool,
27
28 pub data_retention_days: u32,
30
31 pub collection_interval: Duration,
33
34 pub compliance_monitoring: bool,
36
37 pub performance_monitoring: bool,
39
40 pub max_event_buffer: usize,
42}
43
44impl Default for AnalyticsConfig {
45 fn default() -> Self {
46 Self {
47 real_time_enabled: true,
48 data_retention_days: 90,
49 collection_interval: Duration::from_secs(60),
50 compliance_monitoring: true,
51 performance_monitoring: true,
52 max_event_buffer: 10000,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59pub enum RbacEventType {
60 RoleAssignment,
62 PermissionCheck,
64 RoleManagement,
66 Authentication,
68 Authorization,
70 PolicyViolation,
72 PrivilegeEscalation,
74 AccessAnomaly,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct AnalyticsEvent {
81 pub id: String,
83
84 pub event_type: RbacEventType,
86
87 pub timestamp: chrono::DateTime<chrono::Utc>,
89
90 pub user_id: Option<String>,
92
93 pub role_id: Option<String>,
95
96 pub resource: Option<String>,
98
99 pub action: Option<String>,
101
102 pub result: EventResult,
104
105 pub metadata: HashMap<String, String>,
107
108 pub duration_ms: Option<u64>,
110
111 pub source_ip: Option<String>,
113
114 pub user_agent: Option<String>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120pub enum EventResult {
121 Success,
122 Failure,
123 Denied,
124 Error,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RoleUsageStats {
130 pub role_id: String,
132
133 pub role_name: String,
135
136 pub user_count: u32,
138
139 pub permission_checks: u64,
141
142 pub successful_access: u64,
144
145 pub denied_access: u64,
147
148 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
150
151 pub avg_response_time_ms: f64,
153
154 pub top_resources: Vec<ResourceAccess>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ResourceAccess {
161 pub resource: String,
163
164 pub access_count: u64,
166
167 pub success_rate: f64,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct PermissionUsageStats {
174 pub permission_id: String,
176
177 pub check_count: u64,
179
180 pub success_rate: f64,
182
183 pub used_by_roles: crate::types::Roles,
185
186 pub top_users: Vec<UserActivity>,
188
189 pub peak_hours: Vec<u8>, }
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct UserActivity {
196 pub user_id: String,
198
199 pub activity_count: u64,
201
202 pub last_activity: chrono::DateTime<chrono::Utc>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ComplianceMetrics {
209 pub role_assignment_compliance: f64,
211
212 pub permission_scoping_compliance: f64,
214
215 pub orphaned_permissions: u32,
217
218 pub over_privileged_users: u32,
220
221 pub unused_roles: u32,
223
224 pub avg_access_revocation_time_hours: f64,
226
227 pub policy_violations: u32,
229
230 pub security_incidents: u32,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct PerformanceMetrics {
237 pub avg_permission_check_latency_ms: f64,
239
240 pub p95_permission_check_latency_ms: f64,
242
243 pub p99_permission_check_latency_ms: f64,
245
246 pub permission_checks_per_second: f64,
248
249 pub permission_cache_hit_rate: f64,
251
252 pub error_rate: f64,
254
255 pub cpu_usage_percent: f64,
257
258 pub memory_usage_mb: u64,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct TimeSeriesData {
265 pub timestamp: chrono::DateTime<chrono::Utc>,
267
268 pub value: f64,
270
271 pub tags: HashMap<String, String>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct TrendAnalysis {
278 pub metric_name: String,
280
281 pub current_value: f64,
283
284 pub previous_value: f64,
286
287 pub change_percent: f64,
289
290 pub trend: TrendDirection,
292
293 pub data_points: Vec<TimeSeriesData>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
299pub enum TrendDirection {
300 Increasing,
301 Decreasing,
302 Stable,
303 Volatile,
304}
305
306pub struct AnalyticsManager {
308 config: AnalyticsConfig,
309 storage: Arc<dyn AuthStorage>,
310 event_buffer: Vec<AnalyticsEvent>,
311 last_collection: Instant,
312}
313
314impl AnalyticsManager {
315 pub fn new(config: AnalyticsConfig, storage: Arc<dyn AuthStorage>) -> Self {
317 Self {
318 config,
319 storage,
320 event_buffer: Vec::new(),
321 last_collection: Instant::now(),
322 }
323 }
324
325 pub async fn record_event(&mut self, event: AnalyticsEvent) -> Result<(), AnalyticsError> {
327 if self.event_buffer.len() >= self.config.max_event_buffer {
328 self.flush_events().await?;
329 }
330
331 self.event_buffer.push(event);
332
333 if self.config.real_time_enabled {
334 self.process_real_time_event(
336 self.event_buffer
337 .last()
338 .expect("buffer non-empty after push"),
339 )
340 .await?;
341 }
342
343 Ok(())
344 }
345
346 pub async fn get_role_usage_stats(
348 &self,
349 _role_id: Option<&str>,
350 _time_range: Option<TimeRange>,
351 ) -> Result<Vec<RoleUsageStats>, AnalyticsError> {
352 let keys = self
353 .storage
354 .list_kv_keys("analytics_event_")
355 .await
356 .unwrap_or_default();
357 let mut stats: HashMap<String, RoleUsageStats> = HashMap::new();
358 for key in keys {
359 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
360 if let Ok(event) = serde_json::from_slice::<AnalyticsEvent>(&data) {
361 if let Some(ref role) = event.role_id {
362 let entry = stats.entry(role.clone()).or_insert_with(|| RoleUsageStats {
363 role_id: role.clone(),
364 role_name: role.clone(),
365 user_count: 1,
366 permission_checks: 0,
367 successful_access: 0,
368 denied_access: 0,
369 last_used: None,
370 avg_response_time_ms: 0.0,
371 top_resources: Vec::new(),
372 });
373 if event.event_type == RbacEventType::PermissionCheck {
374 entry.permission_checks += 1;
375 if let Some(action) = &event.action {
376 if action == "Granted" {
377 entry.successful_access += 1;
378 } else {
379 entry.denied_access += 1;
380 }
381 }
382 entry.last_used = Some(event.timestamp);
383 }
384 }
385 }
386 }
387 }
388 Ok(stats.into_values().collect())
389 }
390
391 pub async fn get_permission_usage_stats(
393 &self,
394 _permission_id: Option<&str>,
395 _time_range: Option<TimeRange>,
396 ) -> Result<Vec<PermissionUsageStats>, AnalyticsError> {
397 let keys = self
398 .storage
399 .list_kv_keys("analytics_event_")
400 .await
401 .unwrap_or_default();
402 let mut stats: HashMap<String, PermissionUsageStats> = HashMap::new();
403 for key in keys {
404 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
405 if let Ok(event) = serde_json::from_slice::<AnalyticsEvent>(&data) {
406 if let Some(ref perm) = event.resource {
407 let entry =
408 stats
409 .entry(perm.clone())
410 .or_insert_with(|| PermissionUsageStats {
411 permission_id: perm.clone(),
412 check_count: 0,
413 success_rate: 1.0,
414 used_by_roles: crate::types::Roles::empty(),
415 top_users: Vec::new(),
416 peak_hours: Vec::new(),
417 });
418 if event.event_type == RbacEventType::PermissionCheck {
419 entry.check_count += 1;
420 }
421 }
422 }
423 }
424 }
425 Ok(stats.into_values().collect())
426 }
427
428 pub async fn get_compliance_metrics(
430 &self,
431 _time_range: Option<TimeRange>,
432 ) -> Result<ComplianceMetrics, AnalyticsError> {
433 let keys = self
434 .storage
435 .list_kv_keys("analytics_event_")
436 .await
437 .unwrap_or_default();
438 let mut total_events = 0;
439 let mut policy_violations = 0;
440 let mut orphaned_permissions = 0;
441 let mut security_incidents = 0;
442 let mut revocation_durations = Vec::new();
444 let mut escalation_users = std::collections::HashSet::new();
446
447 for key in keys {
448 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
449 if let Ok(event) = serde_json::from_slice::<AnalyticsEvent>(&data) {
450 total_events += 1;
451 if let Some(action) = &event.action {
452 if action.contains("Violation") || action.contains("Denied") {
453 policy_violations += 1;
454 }
455 }
456 if event.event_type == RbacEventType::PermissionCheck
457 && event.action.as_deref() == Some("Orphaned")
458 {
459 orphaned_permissions += 1;
460 }
461 if matches!(
462 event.event_type,
463 RbacEventType::PolicyViolation
464 | RbacEventType::PrivilegeEscalation
465 | RbacEventType::AccessAnomaly
466 ) || event
467 .action
468 .as_deref()
469 .is_some_and(|action| action.contains("Incident"))
470 {
471 security_incidents += 1;
472 }
473 if event.event_type == RbacEventType::PrivilegeEscalation {
475 if let Some(ref user) = event.user_id {
476 escalation_users.insert(user.clone());
477 }
478 }
479 if let Some(ref action) = event.action {
481 if action.contains("Revoked") || action.contains("Revocation") {
482 if let Some(hours_str) = event.metadata.get("revocation_hours") {
483 if let Ok(hours) = hours_str.parse::<f64>() {
484 revocation_durations.push(hours);
485 }
486 }
487 }
488 }
489 }
490 }
491 }
492
493 let compliance_score = if total_events > 0 {
494 100.0 - ((policy_violations as f64 / total_events as f64) * 100.0)
495 } else {
496 100.0
497 };
498
499 let avg_access_revocation_time_hours = if !revocation_durations.is_empty() {
500 revocation_durations.iter().sum::<f64>() / revocation_durations.len() as f64
501 } else {
502 0.0 };
504
505 let unused_roles = {
507 let defined_roles = self
508 .storage
509 .list_kv_keys("rbac:role:")
510 .await
511 .unwrap_or_default();
512 let assigned: std::collections::HashSet<String> = {
513 let user_role_keys = self
514 .storage
515 .list_kv_keys("rbac:user_roles:")
516 .await
517 .unwrap_or_default();
518 let mut set = std::collections::HashSet::new();
519 for key in &user_role_keys {
520 if let Ok(Some(data)) = self.storage.get_kv(key).await {
521 if let Ok(roles) = serde_json::from_slice::<Vec<String>>(&data) {
522 set.extend(roles);
523 }
524 }
525 }
526 set
527 };
528 defined_roles
529 .iter()
530 .filter(|k: &&String| {
531 let role_name = k.strip_prefix("rbac:role:").unwrap_or(k);
532 !assigned.contains(role_name)
533 })
534 .count() as u32
535 };
536
537 Ok(ComplianceMetrics {
538 role_assignment_compliance: compliance_score,
539 permission_scoping_compliance: compliance_score,
540 orphaned_permissions,
541 over_privileged_users: escalation_users.len() as u32,
542 unused_roles,
543 avg_access_revocation_time_hours,
544 policy_violations,
545 security_incidents,
546 })
547 }
548
549 pub async fn get_performance_metrics(
551 &self,
552 _time_range: Option<TimeRange>,
553 ) -> Result<PerformanceMetrics, AnalyticsError> {
554 let keys = self
555 .storage
556 .list_kv_keys("analytics_event_")
557 .await
558 .unwrap_or_default();
559 let mut total_events = 0;
560 let mut total_duration_ms = 0.0;
561 let mut duration_samples = Vec::new();
562 let mut errors = 0;
563 let mut permission_check_timestamps = Vec::new();
564
565 for key in keys {
566 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
567 if let Ok(event) = serde_json::from_slice::<AnalyticsEvent>(&data) {
568 total_events += 1;
569 if let Some(duration_ms) = event.duration_ms {
570 total_duration_ms += duration_ms as f64;
571 duration_samples.push(duration_ms);
572 }
573 if event.event_type == RbacEventType::PermissionCheck {
574 permission_check_timestamps.push(event.timestamp);
575 }
576 if matches!(event.result, EventResult::Failure | EventResult::Error) {
577 errors += 1;
578 }
579 }
580 }
581 }
582
583 duration_samples.sort_unstable();
584 let p95 = if duration_samples.is_empty() {
585 0.0
586 } else {
587 let index = ((duration_samples.len() as f64 * 0.95).floor() as usize)
588 .min(duration_samples.len() - 1);
589 duration_samples[index] as f64
590 };
591 let p99 = if duration_samples.is_empty() {
592 0.0
593 } else {
594 let index = ((duration_samples.len() as f64 * 0.99).floor() as usize)
595 .min(duration_samples.len() - 1);
596 duration_samples[index] as f64
597 };
598 let avg = if duration_samples.is_empty() {
599 0.0
600 } else {
601 total_duration_ms / duration_samples.len() as f64
602 };
603 let error_rate = if total_events > 0 {
604 errors as f64 / total_events as f64
605 } else {
606 0.0
607 };
608 permission_check_timestamps.sort_unstable();
609 let permission_checks_per_second = match (
610 permission_check_timestamps.first(),
611 permission_check_timestamps.last(),
612 ) {
613 (Some(first), Some(last)) if permission_check_timestamps.len() > 1 => {
614 let span_seconds = (*last - *first).num_seconds().max(1) as f64;
615 permission_check_timestamps.len() as f64 / span_seconds
616 }
617 _ => 0.0,
618 };
619
620 Ok(PerformanceMetrics {
621 avg_permission_check_latency_ms: avg,
622 p95_permission_check_latency_ms: p95,
623 p99_permission_check_latency_ms: p99,
624 permission_checks_per_second,
625 permission_cache_hit_rate: {
628 let cache_hits = self
629 .event_buffer
630 .iter()
631 .filter(|e| e.event_type == RbacEventType::PermissionCheck)
632 .filter(|e| e.metadata.get("cache_hit").is_some_and(|v| v == "true"))
633 .count();
634 let cache_total = self
635 .event_buffer
636 .iter()
637 .filter(|e| e.event_type == RbacEventType::PermissionCheck)
638 .count();
639 if cache_total > 0 {
640 cache_hits as f64 / cache_total as f64
641 } else {
642 0.0
643 }
644 },
645 error_rate,
646 cpu_usage_percent: {
648 let mut sys = sysinfo::System::new();
649 let pid = sysinfo::get_current_pid().ok();
650 if let Some(pid) = pid {
651 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
652 sys.process(pid)
653 .map(|p| p.cpu_usage() as f64)
654 .unwrap_or(0.0)
655 } else {
656 0.0
657 }
658 },
659 memory_usage_mb: {
660 #[cfg(target_os = "linux")]
663 {
664 std::fs::read_to_string("/proc/self/statm")
665 .ok()
666 .and_then(|s| s.split_whitespace().nth(1)?.parse::<u64>().ok())
667 .map(|pages| pages * 4096 / (1024 * 1024))
668 .unwrap_or(0)
669 }
670 #[cfg(not(target_os = "linux"))]
671 {
672 0
673 }
674 },
675 })
676 }
677
678 pub async fn get_trend_analysis(
680 &self,
681 metric_name: &str,
682 _time_range: Option<TimeRange>,
683 ) -> Result<TrendAnalysis, AnalyticsError> {
684 let keys = self
685 .storage
686 .list_kv_keys("analytics_event_")
687 .await
688 .unwrap_or_default();
689 let mut data_points = Vec::new();
690
691 for key in keys {
692 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
693 if let Ok(event) = serde_json::from_slice::<AnalyticsEvent>(&data) {
694 data_points.push(TimeSeriesData {
695 timestamp: event.timestamp,
696 value: 1.0,
697 tags: event.metadata.clone(),
698 });
699 }
700 }
701 }
702
703 data_points.sort_by_key(|point| point.timestamp);
704 let midpoint = data_points.len() / 2;
705 let previous_value = midpoint as f64;
706 let current_value = (data_points.len() - midpoint) as f64;
707 let change_percent = if previous_value == 0.0 {
708 if current_value == 0.0 { 0.0 } else { 100.0 }
709 } else {
710 ((current_value - previous_value) / previous_value) * 100.0
711 };
712 let direction = if change_percent.abs() < 5.0 {
713 TrendDirection::Stable
714 } else if change_percent > 0.0 {
715 TrendDirection::Increasing
716 } else {
717 TrendDirection::Decreasing
718 };
719
720 Ok(TrendAnalysis {
721 metric_name: metric_name.to_string(),
722 current_value,
723 previous_value,
724 change_percent,
725 trend: direction,
726 data_points,
727 })
728 }
729
730 pub async fn generate_report(
732 &self,
733 report_type: ReportType,
734 time_range: TimeRange,
735 ) -> Result<AnalyticsReport, AnalyticsError> {
736 let role_stats = self
737 .get_role_usage_stats(None, Some(time_range.clone()))
738 .await?;
739 let permission_stats = self
740 .get_permission_usage_stats(None, Some(time_range.clone()))
741 .await?;
742 let compliance_metrics = self
743 .get_compliance_metrics(Some(time_range.clone()))
744 .await?;
745 let performance_metrics = self
746 .get_performance_metrics(Some(time_range.clone()))
747 .await?;
748
749 Ok(AnalyticsReport {
750 report_type,
751 time_range,
752 generated_at: chrono::Utc::now(),
753 role_stats,
754 permission_stats,
755 compliance_metrics: compliance_metrics.clone(),
756 performance_metrics: performance_metrics.clone(),
757 summary: self.generate_report_summary(&compliance_metrics, &performance_metrics),
758 })
759 }
760
761 async fn flush_events(&mut self) -> Result<(), AnalyticsError> {
763 if self.event_buffer.is_empty() {
764 return Ok(());
765 }
766
767 for event in &self.event_buffer {
769 if let Ok(json_data) = serde_json::to_vec(event) {
770 let key = format!("analytics_event_{}", event.id);
771 let ttl =
773 std::time::Duration::from_secs(self.config.data_retention_days as u64 * 86400);
774 let _ = self.storage.store_kv(&key, &json_data, Some(ttl)).await;
775 }
776 }
777
778 self.event_buffer.clear();
780 self.last_collection = Instant::now();
781
782 Ok(())
783 }
784
785 async fn process_real_time_event(&self, event: &AnalyticsEvent) -> Result<(), AnalyticsError> {
789 let json_data = serde_json::to_vec(event)?;
790 let key = format!("analytics_event_{}", event.id);
791 let ttl = std::time::Duration::from_secs(self.config.data_retention_days as u64 * 86400);
792 self.storage
793 .store_kv(&key, &json_data, Some(ttl))
794 .await
795 .map_err(|e| AnalyticsError::StorageError(e.to_string()))?;
796 Ok(())
797 }
798
799 fn generate_report_summary(
801 &self,
802 compliance: &ComplianceMetrics,
803 performance: &PerformanceMetrics,
804 ) -> String {
805 format!(
806 "RBAC Analytics Summary: {}% compliance, {:.1}ms avg latency, {:.1}% error rate",
807 compliance.role_assignment_compliance,
808 performance.avg_permission_check_latency_ms,
809 performance.error_rate * 100.0
810 )
811 }
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize)]
816pub struct TimeRange {
817 pub start: chrono::DateTime<chrono::Utc>,
818 pub end: chrono::DateTime<chrono::Utc>,
819}
820
821impl TimeRange {
822 pub fn last_hours(hours: u32) -> Self {
824 let end = chrono::Utc::now();
825 let start = end - chrono::Duration::hours(hours as i64);
826 Self { start, end }
827 }
828
829 pub fn last_days(days: u32) -> Self {
831 let end = chrono::Utc::now();
832 let start = end - chrono::Duration::days(days as i64);
833 Self { start, end }
834 }
835
836 pub fn today() -> Self {
838 let now = chrono::Utc::now();
839 let start = now
840 .date_naive()
841 .and_hms_opt(0, 0, 0)
842 .expect("0,0,0 are valid h/m/s values")
843 .and_utc();
844 let end = now;
845 Self { start, end }
846 }
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
851pub enum ReportType {
852 Daily,
854 Weekly,
856 Monthly,
858 Compliance,
860 Performance,
862 Custom(String),
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize)]
868pub struct AnalyticsReport {
869 pub report_type: ReportType,
870 pub time_range: TimeRange,
871 pub generated_at: chrono::DateTime<chrono::Utc>,
872 pub role_stats: Vec<RoleUsageStats>,
873 pub permission_stats: Vec<PermissionUsageStats>,
874 pub compliance_metrics: ComplianceMetrics,
875 pub performance_metrics: PerformanceMetrics,
876 pub summary: String,
877}
878
879#[derive(Debug, thiserror::Error)]
881pub enum AnalyticsError {
882 #[error("Data processing error: {0}")]
883 ProcessingError(String),
884
885 #[error("Storage error: {0}")]
886 StorageError(String),
887
888 #[error("Query error: {0}")]
889 QueryError(String),
890
891 #[error("Configuration error: {0}")]
892 ConfigError(String),
893
894 #[error("IO error: {0}")]
895 IoError(#[from] std::io::Error),
896
897 #[error("Serialization error: {0}")]
898 SerializationError(#[from] serde_json::Error),
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904
905 #[test]
906 fn test_analytics_config_default() {
907 let config = AnalyticsConfig::default();
908 assert!(config.real_time_enabled);
909 assert_eq!(config.data_retention_days, 90);
910 assert!(config.compliance_monitoring);
911 }
912
913 #[test]
914 fn test_time_range_creation() {
915 let range = TimeRange::last_hours(24);
916 assert!(range.end > range.start);
917
918 let today = TimeRange::today();
919 assert!(today.end > today.start);
920 }
921
922 #[tokio::test]
923 async fn test_analytics_manager_creation() {
924 let config = AnalyticsConfig::default();
925 let manager = AnalyticsManager::new(
926 config,
927 std::sync::Arc::new(crate::storage::MemoryStorage::new()),
928 );
929 assert_eq!(manager.event_buffer.len(), 0);
930 }
931
932 #[tokio::test]
933 async fn test_record_event() {
934 let config = AnalyticsConfig::default();
935 let mut manager = AnalyticsManager::new(
936 config,
937 std::sync::Arc::new(crate::storage::MemoryStorage::new()),
938 );
939
940 let event = AnalyticsEvent {
941 id: "test_event_1".to_string(),
942 event_type: RbacEventType::PermissionCheck,
943 timestamp: chrono::Utc::now(),
944 user_id: Some("user123".to_string()),
945 role_id: Some("admin".to_string()),
946 resource: Some("user_data".to_string()),
947 action: Some("read".to_string()),
948 result: EventResult::Success,
949 metadata: HashMap::new(),
950 duration_ms: Some(15),
951 source_ip: Some("192.168.1.1".to_string()),
952 user_agent: Some("TestAgent/1.0".to_string()),
953 };
954
955 let result = manager.record_event(event).await;
956 assert!(result.is_ok());
957 assert_eq!(manager.event_buffer.len(), 1);
958 }
959}