kailash 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,7 +56,9 @@ class BaseRepository:
56
56
  """Execute database query using SDK node."""
57
57
  try:
58
58
  if self.use_async:
59
- result = await self.db_node.execute(query=query, params=params or {})
59
+ result = await self.db_node.execute_async(
60
+ query=query, params=params or {}
61
+ )
60
62
  else:
61
63
  result = self.db_node.execute(query=query, params=params or {})
62
64
 
@@ -18,8 +18,9 @@ Features:
18
18
 
19
19
  import hashlib
20
20
  import json
21
+ import uuid
21
22
  from dataclasses import dataclass
22
- from datetime import UTC, datetime, timedelta
23
+ from datetime import UTC, datetime, timedelta, timezone
23
24
  from enum import Enum
24
25
  from typing import Any, Dict, List, Optional, Union
25
26
 
@@ -775,20 +776,377 @@ class EnterpriseAuditLogNode(Node):
775
776
  # Additional operations would follow similar patterns
776
777
  def _export_logs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
777
778
  """Export audit logs in various formats."""
778
- raise NotImplementedError("Export logs operation will be implemented")
779
+ format_type = inputs.get("export_format", "json")
780
+ query_filters = inputs.get("query_filters", {})
781
+ date_range = inputs.get("date_range", {})
782
+
783
+ # Query logs
784
+ query_result = self._query_logs(
785
+ {
786
+ "query_filters": query_filters,
787
+ "date_range": date_range,
788
+ "pagination": {"page": 1, "size": 10000}, # Export all matching records
789
+ }
790
+ )
791
+
792
+ logs = query_result.get("logs", [])
793
+
794
+ if format_type == "json":
795
+ export_data = {
796
+ "export_date": datetime.now(timezone.utc).isoformat(),
797
+ "total_records": len(logs),
798
+ "filters": query_filters,
799
+ "date_range": date_range,
800
+ "logs": logs,
801
+ }
802
+ filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
803
+
804
+ elif format_type == "csv":
805
+ # Convert to CSV format
806
+ import csv
807
+ import io
808
+
809
+ output = io.StringIO()
810
+ if logs:
811
+ writer = csv.DictWriter(output, fieldnames=logs[0].keys())
812
+ writer.writeheader()
813
+ writer.writerows(logs)
814
+
815
+ export_data = output.getvalue()
816
+ filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
817
+
818
+ elif format_type == "pdf":
819
+ # For PDF, we'll return structured data that can be rendered
820
+ export_data = {
821
+ "title": "Audit Log Report",
822
+ "generated_date": datetime.now(timezone.utc).isoformat(),
823
+ "summary": {
824
+ "total_records": len(logs),
825
+ "date_range": date_range,
826
+ "filters": query_filters,
827
+ },
828
+ "logs": logs,
829
+ }
830
+ filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
831
+
832
+ return {
833
+ "success": True,
834
+ "filename": filename,
835
+ "format": format_type,
836
+ "record_count": len(logs),
837
+ "export_data": export_data,
838
+ }
779
839
 
780
840
  def _archive_logs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
781
841
  """Archive old audit logs for long-term storage."""
782
- raise NotImplementedError("Archive logs operation will be implemented")
842
+ archive_days = inputs.get("archive_older_than_days", 90)
843
+ archive_path = inputs.get("archive_path", "/archives/audit_logs")
844
+ tenant_id = inputs.get("tenant_id")
845
+
846
+ # Calculate cutoff date
847
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=archive_days)
848
+
849
+ # Query old logs
850
+ query_result = self._query_logs(
851
+ {
852
+ "date_range": {"end": cutoff_date.isoformat()},
853
+ "tenant_id": tenant_id,
854
+ "pagination": {"page": 1, "size": 10000},
855
+ }
856
+ )
857
+
858
+ logs_to_archive = query_result.get("logs", [])
859
+
860
+ if not logs_to_archive:
861
+ return {
862
+ "success": True,
863
+ "message": "No logs to archive",
864
+ "archived_count": 0,
865
+ }
866
+
867
+ # Create archive
868
+ archive_filename = f"audit_archive_{cutoff_date.strftime('%Y%m%d')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
869
+ archive_data = {
870
+ "archive_date": datetime.now(timezone.utc).isoformat(),
871
+ "cutoff_date": cutoff_date.isoformat(),
872
+ "total_records": len(logs_to_archive),
873
+ "tenant_id": tenant_id,
874
+ "logs": logs_to_archive,
875
+ }
876
+
877
+ # In a real implementation, this would save to cloud storage or archive system
878
+ archive_location = f"{archive_path}/{archive_filename}"
879
+
880
+ # Delete archived logs from main database
881
+ log_ids = [log.get("id") for log in logs_to_archive if log.get("id")]
882
+
883
+ if log_ids:
884
+ # Delete logs
885
+ delete_query = """
886
+ DELETE FROM audit_logs
887
+ WHERE id IN (%s)
888
+ """ % ",".join(
889
+ ["?" for _ in log_ids]
890
+ )
891
+
892
+ if tenant_id:
893
+ delete_query += " AND tenant_id = ?"
894
+ log_ids.append(tenant_id)
895
+
896
+ self._ensure_db_node(inputs)
897
+ self._db_node.execute(query=delete_query, params=log_ids)
898
+
899
+ return {
900
+ "success": True,
901
+ "archived_count": len(logs_to_archive),
902
+ "archive_location": archive_location,
903
+ "archive_filename": archive_filename,
904
+ "cutoff_date": cutoff_date.isoformat(),
905
+ "message": f"Archived {len(logs_to_archive)} logs older than {archive_days} days",
906
+ }
783
907
 
784
908
  def _delete_logs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
785
909
  """Delete old audit logs based on retention policy."""
786
- raise NotImplementedError("Delete logs operation will be implemented")
910
+ retention_days = inputs.get("retention_days", 365)
911
+ tenant_id = inputs.get("tenant_id")
912
+ dry_run = inputs.get("dry_run", False)
913
+
914
+ # Calculate cutoff date
915
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=retention_days)
916
+
917
+ # First count logs to be deleted
918
+ count_query = """
919
+ SELECT COUNT(*) as count FROM audit_logs
920
+ WHERE created_at < ?
921
+ """
922
+ params = [cutoff_date.isoformat()]
923
+
924
+ if tenant_id:
925
+ count_query += " AND tenant_id = ?"
926
+ params.append(tenant_id)
927
+
928
+ self._ensure_db_node(inputs)
929
+ count_result = self._db_node.execute(query=count_query, params=params)
930
+ total_to_delete = count_result.get("rows", [{}])[0].get("count", 0)
931
+
932
+ if dry_run:
933
+ return {
934
+ "success": True,
935
+ "dry_run": True,
936
+ "would_delete": total_to_delete,
937
+ "cutoff_date": cutoff_date.isoformat(),
938
+ "message": f"Dry run: Would delete {total_to_delete} logs older than {retention_days} days",
939
+ }
940
+
941
+ if total_to_delete == 0:
942
+ return {"success": True, "deleted_count": 0, "message": "No logs to delete"}
943
+
944
+ # Delete logs in batches to avoid locking
945
+ batch_size = 1000
946
+ deleted_total = 0
947
+
948
+ while deleted_total < total_to_delete:
949
+ delete_query = f"""
950
+ DELETE FROM audit_logs
951
+ WHERE id IN (
952
+ SELECT id FROM audit_logs
953
+ WHERE created_at < ?
954
+ {' AND tenant_id = ?' if tenant_id else ''}
955
+ LIMIT {batch_size}
956
+ )
957
+ """
958
+
959
+ delete_params = [cutoff_date.isoformat()]
960
+ if tenant_id:
961
+ delete_params.append(tenant_id)
962
+
963
+ result = self._db_node.execute(query=delete_query, params=delete_params)
964
+ batch_deleted = result.get("rows_affected", 0)
965
+ deleted_total += batch_deleted
966
+
967
+ if batch_deleted == 0:
968
+ break
969
+
970
+ return {
971
+ "success": True,
972
+ "deleted_count": deleted_total,
973
+ "cutoff_date": cutoff_date.isoformat(),
974
+ "retention_days": retention_days,
975
+ "message": f"Deleted {deleted_total} logs older than {retention_days} days",
976
+ }
787
977
 
788
978
  def _get_statistics(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
789
979
  """Get audit log statistics and metrics."""
790
- raise NotImplementedError("Get statistics operation will be implemented")
980
+ tenant_id = inputs.get("tenant_id")
981
+ date_range = inputs.get("date_range", {})
982
+ group_by = inputs.get(
983
+ "group_by", ["event_type", "severity"]
984
+ ) # What to group statistics by
985
+
986
+ self._ensure_db_node(inputs)
987
+
988
+ # Build base WHERE clause
989
+ where_conditions = []
990
+ params = []
991
+
992
+ if tenant_id:
993
+ where_conditions.append("tenant_id = ?")
994
+ params.append(tenant_id)
995
+
996
+ if date_range:
997
+ if date_range.get("start"):
998
+ where_conditions.append("created_at >= ?")
999
+ params.append(date_range["start"])
1000
+ if date_range.get("end"):
1001
+ where_conditions.append("created_at <= ?")
1002
+ params.append(date_range["end"])
1003
+
1004
+ where_clause = (
1005
+ " WHERE " + " AND ".join(where_conditions) if where_conditions else ""
1006
+ )
1007
+
1008
+ # Get total count
1009
+ total_query = f"SELECT COUNT(*) as total FROM audit_logs{where_clause}"
1010
+ total_result = self._db_node.execute(query=total_query, params=params)
1011
+ total_count = total_result.get("rows", [{}])[0].get("total", 0)
1012
+
1013
+ # Get counts by severity
1014
+ severity_query = f"""
1015
+ SELECT severity, COUNT(*) as count
1016
+ FROM audit_logs{where_clause}
1017
+ GROUP BY severity
1018
+ """
1019
+ severity_result = self._db_node.execute(query=severity_query, params=params)
1020
+ severity_counts = {
1021
+ row["severity"]: row["count"] for row in severity_result.get("rows", [])
1022
+ }
1023
+
1024
+ # Get counts by event type
1025
+ event_type_query = f"""
1026
+ SELECT event_type, COUNT(*) as count
1027
+ FROM audit_logs{where_clause}
1028
+ GROUP BY event_type
1029
+ ORDER BY count DESC
1030
+ LIMIT 20
1031
+ """
1032
+ event_type_result = self._db_node.execute(query=event_type_query, params=params)
1033
+ event_type_counts = {
1034
+ row["event_type"]: row["count"] for row in event_type_result.get("rows", [])
1035
+ }
1036
+
1037
+ # Get hourly distribution for the date range
1038
+ hourly_query = f"""
1039
+ SELECT
1040
+ strftime('%Y-%m-%d %H:00:00', created_at) as hour,
1041
+ COUNT(*) as count
1042
+ FROM audit_logs{where_clause}
1043
+ GROUP BY hour
1044
+ ORDER BY hour DESC
1045
+ LIMIT 168
1046
+ """ # Last 7 days of hourly data
1047
+ hourly_result = self._db_node.execute(query=hourly_query, params=params)
1048
+ hourly_distribution = [
1049
+ {"hour": row["hour"], "count": row["count"]}
1050
+ for row in hourly_result.get("rows", [])
1051
+ ]
1052
+
1053
+ # Get top users by activity
1054
+ user_activity_query = f"""
1055
+ SELECT user_id, COUNT(*) as action_count
1056
+ FROM audit_logs{where_clause}
1057
+ GROUP BY user_id
1058
+ ORDER BY action_count DESC
1059
+ LIMIT 10
1060
+ """
1061
+ user_activity_result = self._db_node.execute(
1062
+ query=user_activity_query, params=params
1063
+ )
1064
+ top_users = [
1065
+ {"user_id": row["user_id"], "action_count": row["action_count"]}
1066
+ for row in user_activity_result.get("rows", [])
1067
+ ]
1068
+
1069
+ # Get failed actions
1070
+ failed_query = f"""
1071
+ SELECT COUNT(*) as failed_count
1072
+ FROM audit_logs{where_clause}
1073
+ {' AND ' if where_clause else ' WHERE '}
1074
+ status = 'failed' OR severity = 'error'
1075
+ """
1076
+ failed_params = params.copy()
1077
+ failed_result = self._db_node.execute(query=failed_query, params=failed_params)
1078
+ failed_count = failed_result.get("rows", [{}])[0].get("failed_count", 0)
1079
+
1080
+ statistics = {
1081
+ "total_events": total_count,
1082
+ "failed_events": failed_count,
1083
+ "success_rate": (
1084
+ ((total_count - failed_count) / total_count * 100)
1085
+ if total_count > 0
1086
+ else 0
1087
+ ),
1088
+ "severity_distribution": severity_counts,
1089
+ "event_type_distribution": event_type_counts,
1090
+ "hourly_distribution": hourly_distribution,
1091
+ "top_users": top_users,
1092
+ "date_range": date_range,
1093
+ "tenant_id": tenant_id,
1094
+ }
1095
+
1096
+ return {"success": True, "statistics": statistics}
791
1097
 
792
1098
  def _monitor_realtime(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
793
1099
  """Monitor audit logs in real-time."""
794
- raise NotImplementedError("Monitor realtime operation will be implemented")
1100
+ # This operation would typically set up a subscription or polling mechanism
1101
+ # For now, we'll return the latest events and configuration for real-time monitoring
1102
+
1103
+ tenant_id = inputs.get("tenant_id")
1104
+ event_types = inputs.get("event_types", []) # Filter by specific event types
1105
+ severity_filter = inputs.get("severity", AuditSeverity.INFO.value)
1106
+ polling_interval = inputs.get("polling_interval", 5) # seconds
1107
+ max_events = inputs.get("max_events", 100)
1108
+
1109
+ # Get latest events
1110
+ query_result = self._query_logs(
1111
+ {
1112
+ "tenant_id": tenant_id,
1113
+ "event_types": event_types,
1114
+ "severity": severity_filter,
1115
+ "pagination": {
1116
+ "page": 1,
1117
+ "size": max_events,
1118
+ "sort": [{"field": "created_at", "order": "desc"}],
1119
+ },
1120
+ }
1121
+ )
1122
+
1123
+ latest_events = query_result.get("logs", [])
1124
+
1125
+ # Create monitoring configuration
1126
+ monitor_config = {
1127
+ "monitor_id": str(uuid.uuid4()),
1128
+ "created_at": datetime.now(timezone.utc).isoformat(),
1129
+ "filters": {
1130
+ "tenant_id": tenant_id,
1131
+ "event_types": event_types,
1132
+ "severity": severity_filter,
1133
+ },
1134
+ "polling_interval": polling_interval,
1135
+ "max_events": max_events,
1136
+ "status": "active",
1137
+ "last_poll": datetime.now(timezone.utc).isoformat(),
1138
+ "endpoint": f"/api/audit/monitor/{uuid.uuid4()}", # Webhook or WebSocket endpoint
1139
+ }
1140
+
1141
+ # In a real implementation, this would:
1142
+ # 1. Set up a WebSocket connection or Server-Sent Events stream
1143
+ # 2. Create database triggers or use change data capture
1144
+ # 3. Set up a message queue subscription
1145
+
1146
+ return {
1147
+ "success": True,
1148
+ "monitor_config": monitor_config,
1149
+ "latest_events": latest_events,
1150
+ "event_count": len(latest_events),
1151
+ "message": "Real-time monitoring configured. Use the endpoint for live updates.",
1152
+ }