aws-inventory-manager 0.17.12__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.
Files changed (152) hide show
  1. aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
  2. aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
  3. aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
  4. aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
  5. aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
  6. aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
  7. src/__init__.py +3 -0
  8. src/aws/__init__.py +11 -0
  9. src/aws/client.py +128 -0
  10. src/aws/credentials.py +191 -0
  11. src/aws/rate_limiter.py +177 -0
  12. src/cli/__init__.py +12 -0
  13. src/cli/config.py +130 -0
  14. src/cli/main.py +4046 -0
  15. src/cloudtrail/__init__.py +5 -0
  16. src/cloudtrail/query.py +642 -0
  17. src/config_service/__init__.py +21 -0
  18. src/config_service/collector.py +346 -0
  19. src/config_service/detector.py +256 -0
  20. src/config_service/resource_type_mapping.py +328 -0
  21. src/cost/__init__.py +5 -0
  22. src/cost/analyzer.py +226 -0
  23. src/cost/explorer.py +209 -0
  24. src/cost/reporter.py +237 -0
  25. src/delta/__init__.py +5 -0
  26. src/delta/calculator.py +206 -0
  27. src/delta/differ.py +185 -0
  28. src/delta/formatters.py +272 -0
  29. src/delta/models.py +154 -0
  30. src/delta/reporter.py +234 -0
  31. src/matching/__init__.py +6 -0
  32. src/matching/config.py +52 -0
  33. src/matching/normalizer.py +450 -0
  34. src/matching/prompts.py +33 -0
  35. src/models/__init__.py +21 -0
  36. src/models/config_diff.py +135 -0
  37. src/models/cost_report.py +87 -0
  38. src/models/deletion_operation.py +104 -0
  39. src/models/deletion_record.py +97 -0
  40. src/models/delta_report.py +122 -0
  41. src/models/efs_resource.py +80 -0
  42. src/models/elasticache_resource.py +90 -0
  43. src/models/group.py +318 -0
  44. src/models/inventory.py +133 -0
  45. src/models/protection_rule.py +123 -0
  46. src/models/report.py +288 -0
  47. src/models/resource.py +111 -0
  48. src/models/security_finding.py +102 -0
  49. src/models/snapshot.py +122 -0
  50. src/restore/__init__.py +20 -0
  51. src/restore/audit.py +175 -0
  52. src/restore/cleaner.py +461 -0
  53. src/restore/config.py +209 -0
  54. src/restore/deleter.py +976 -0
  55. src/restore/dependency.py +254 -0
  56. src/restore/safety.py +115 -0
  57. src/security/__init__.py +0 -0
  58. src/security/checks/__init__.py +0 -0
  59. src/security/checks/base.py +56 -0
  60. src/security/checks/ec2_checks.py +88 -0
  61. src/security/checks/elasticache_checks.py +149 -0
  62. src/security/checks/iam_checks.py +102 -0
  63. src/security/checks/rds_checks.py +140 -0
  64. src/security/checks/s3_checks.py +95 -0
  65. src/security/checks/secrets_checks.py +96 -0
  66. src/security/checks/sg_checks.py +142 -0
  67. src/security/cis_mapper.py +97 -0
  68. src/security/models.py +53 -0
  69. src/security/reporter.py +174 -0
  70. src/security/scanner.py +87 -0
  71. src/snapshot/__init__.py +6 -0
  72. src/snapshot/capturer.py +453 -0
  73. src/snapshot/filter.py +259 -0
  74. src/snapshot/inventory_storage.py +236 -0
  75. src/snapshot/report_formatter.py +250 -0
  76. src/snapshot/reporter.py +189 -0
  77. src/snapshot/resource_collectors/__init__.py +5 -0
  78. src/snapshot/resource_collectors/apigateway.py +140 -0
  79. src/snapshot/resource_collectors/backup.py +136 -0
  80. src/snapshot/resource_collectors/base.py +81 -0
  81. src/snapshot/resource_collectors/cloudformation.py +55 -0
  82. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  83. src/snapshot/resource_collectors/codebuild.py +69 -0
  84. src/snapshot/resource_collectors/codepipeline.py +82 -0
  85. src/snapshot/resource_collectors/dynamodb.py +65 -0
  86. src/snapshot/resource_collectors/ec2.py +240 -0
  87. src/snapshot/resource_collectors/ecs.py +215 -0
  88. src/snapshot/resource_collectors/efs_collector.py +102 -0
  89. src/snapshot/resource_collectors/eks.py +200 -0
  90. src/snapshot/resource_collectors/elasticache_collector.py +79 -0
  91. src/snapshot/resource_collectors/elb.py +126 -0
  92. src/snapshot/resource_collectors/eventbridge.py +156 -0
  93. src/snapshot/resource_collectors/glue.py +199 -0
  94. src/snapshot/resource_collectors/iam.py +188 -0
  95. src/snapshot/resource_collectors/kms.py +111 -0
  96. src/snapshot/resource_collectors/lambda_func.py +139 -0
  97. src/snapshot/resource_collectors/rds.py +109 -0
  98. src/snapshot/resource_collectors/route53.py +86 -0
  99. src/snapshot/resource_collectors/s3.py +105 -0
  100. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  101. src/snapshot/resource_collectors/sns.py +68 -0
  102. src/snapshot/resource_collectors/sqs.py +82 -0
  103. src/snapshot/resource_collectors/ssm.py +160 -0
  104. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  105. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  106. src/snapshot/resource_collectors/waf.py +159 -0
  107. src/snapshot/storage.py +351 -0
  108. src/storage/__init__.py +21 -0
  109. src/storage/audit_store.py +419 -0
  110. src/storage/database.py +294 -0
  111. src/storage/group_store.py +763 -0
  112. src/storage/inventory_store.py +320 -0
  113. src/storage/resource_store.py +416 -0
  114. src/storage/schema.py +339 -0
  115. src/storage/snapshot_store.py +363 -0
  116. src/utils/__init__.py +12 -0
  117. src/utils/export.py +305 -0
  118. src/utils/hash.py +60 -0
  119. src/utils/logging.py +63 -0
  120. src/utils/pagination.py +41 -0
  121. src/utils/paths.py +51 -0
  122. src/utils/progress.py +41 -0
  123. src/utils/unsupported_resources.py +306 -0
  124. src/web/__init__.py +5 -0
  125. src/web/app.py +97 -0
  126. src/web/dependencies.py +69 -0
  127. src/web/routes/__init__.py +1 -0
  128. src/web/routes/api/__init__.py +18 -0
  129. src/web/routes/api/charts.py +156 -0
  130. src/web/routes/api/cleanup.py +186 -0
  131. src/web/routes/api/filters.py +253 -0
  132. src/web/routes/api/groups.py +305 -0
  133. src/web/routes/api/inventories.py +80 -0
  134. src/web/routes/api/queries.py +202 -0
  135. src/web/routes/api/resources.py +393 -0
  136. src/web/routes/api/snapshots.py +314 -0
  137. src/web/routes/api/views.py +260 -0
  138. src/web/routes/pages.py +198 -0
  139. src/web/services/__init__.py +1 -0
  140. src/web/templates/base.html +955 -0
  141. src/web/templates/components/navbar.html +31 -0
  142. src/web/templates/components/sidebar.html +104 -0
  143. src/web/templates/pages/audit_logs.html +86 -0
  144. src/web/templates/pages/cleanup.html +279 -0
  145. src/web/templates/pages/dashboard.html +227 -0
  146. src/web/templates/pages/diff.html +175 -0
  147. src/web/templates/pages/error.html +30 -0
  148. src/web/templates/pages/groups.html +721 -0
  149. src/web/templates/pages/queries.html +246 -0
  150. src/web/templates/pages/resources.html +2429 -0
  151. src/web/templates/pages/snapshot_detail.html +271 -0
  152. src/web/templates/pages/snapshots.html +429 -0
src/delta/differ.py ADDED
@@ -0,0 +1,185 @@
1
+ """Configuration differ for recursive comparison of resource configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.config_diff import SECURITY_CRITICAL_FIELDS, ChangeCategory, ConfigDiff
8
+
9
+
10
+ class ConfigDiffer:
11
+ """Recursive configuration comparison engine.
12
+
13
+ Compares two resource configuration dictionaries and produces a list
14
+ of ConfigDiff objects representing field-level changes.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize the configuration differ."""
19
+ pass
20
+
21
+ def compare(
22
+ self,
23
+ resource_arn: str,
24
+ old_config: dict[str, Any],
25
+ new_config: dict[str, Any],
26
+ field_prefix: str = "",
27
+ ) -> list[ConfigDiff]:
28
+ """Recursively compare two configuration dictionaries.
29
+
30
+ Args:
31
+ resource_arn: ARN of the resource being compared
32
+ old_config: Previous configuration
33
+ new_config: Current configuration
34
+ field_prefix: Dot-notation prefix for nested fields (internal use)
35
+
36
+ Returns:
37
+ List of ConfigDiff objects representing changes
38
+ """
39
+ diffs: list[ConfigDiff] = []
40
+
41
+ # Get all unique keys from both configs
42
+ all_keys = set(old_config.keys()) | set(new_config.keys())
43
+
44
+ for key in all_keys:
45
+ field_path = f"{field_prefix}.{key}" if field_prefix else key
46
+
47
+ old_value = old_config.get(key)
48
+ new_value = new_config.get(key)
49
+
50
+ # Skip if values are identical
51
+ if old_value == new_value:
52
+ continue
53
+
54
+ # Recursively compare nested dictionaries
55
+ if isinstance(old_value, dict) and isinstance(new_value, dict):
56
+ nested_diffs = self.compare(resource_arn, old_value, new_value, field_path)
57
+ diffs.extend(nested_diffs)
58
+ # Recursively compare lists
59
+ elif isinstance(old_value, list) and isinstance(new_value, list):
60
+ list_diffs = self._compare_lists(resource_arn, old_value, new_value, field_path)
61
+ diffs.extend(list_diffs)
62
+ # Handle nested dict in old but not in new (or vice versa)
63
+ elif isinstance(old_value, dict) and not isinstance(new_value, dict):
64
+ # Dict was replaced with non-dict or removed
65
+ category = self._categorize_change(field_path, resource_arn)
66
+ diff = ConfigDiff(
67
+ resource_arn=resource_arn,
68
+ field_path=field_path,
69
+ old_value=old_value,
70
+ new_value=new_value,
71
+ category=category,
72
+ )
73
+ diffs.append(diff)
74
+ elif isinstance(new_value, dict) and not isinstance(old_value, dict):
75
+ # Dict was added or replaced
76
+ category = self._categorize_change(field_path, resource_arn)
77
+ diff = ConfigDiff(
78
+ resource_arn=resource_arn,
79
+ field_path=field_path,
80
+ old_value=old_value,
81
+ new_value=new_value,
82
+ category=category,
83
+ )
84
+ diffs.append(diff)
85
+ else:
86
+ # Simple value change, addition, or removal
87
+ category = self._categorize_change(field_path, resource_arn)
88
+ diff = ConfigDiff(
89
+ resource_arn=resource_arn,
90
+ field_path=field_path,
91
+ old_value=old_value,
92
+ new_value=new_value,
93
+ category=category,
94
+ )
95
+ diffs.append(diff)
96
+
97
+ return diffs
98
+
99
+ def _compare_lists(
100
+ self,
101
+ resource_arn: str,
102
+ old_list: list[Any],
103
+ new_list: list[Any],
104
+ field_path: str,
105
+ ) -> list[ConfigDiff]:
106
+ """Compare two lists element by element.
107
+
108
+ Args:
109
+ resource_arn: ARN of the resource
110
+ old_list: Previous list
111
+ new_list: Current list
112
+ field_path: Field path for the list
113
+
114
+ Returns:
115
+ List of ConfigDiff objects for list changes
116
+ """
117
+ diffs: list[ConfigDiff] = []
118
+
119
+ max_len = max(len(old_list), len(new_list))
120
+
121
+ for i in range(max_len):
122
+ element_path = f"{field_path}.{i}"
123
+
124
+ old_value = old_list[i] if i < len(old_list) else None
125
+ new_value = new_list[i] if i < len(new_list) else None
126
+
127
+ if old_value == new_value:
128
+ continue
129
+
130
+ # Recursively compare nested dicts in lists
131
+ if isinstance(old_value, dict) and isinstance(new_value, dict):
132
+ nested_diffs = self.compare(resource_arn, old_value, new_value, element_path)
133
+ diffs.extend(nested_diffs)
134
+ else:
135
+ # Simple value change
136
+ category = self._categorize_change(element_path, resource_arn)
137
+ diff = ConfigDiff(
138
+ resource_arn=resource_arn,
139
+ field_path=element_path,
140
+ old_value=old_value,
141
+ new_value=new_value,
142
+ category=category,
143
+ )
144
+ diffs.append(diff)
145
+
146
+ return diffs
147
+
148
+ def _categorize_change(self, field_path: str, resource_arn: str) -> ChangeCategory:
149
+ """Categorize a configuration change based on field path and resource type.
150
+
151
+ Args:
152
+ field_path: Dot-notation field path
153
+ resource_arn: ARN of the resource
154
+
155
+ Returns:
156
+ ChangeCategory enum value
157
+ """
158
+ field_lower = field_path.lower()
159
+ arn_lower = resource_arn.lower()
160
+
161
+ # Check for Tags category
162
+ if "tags" in field_lower or field_path.startswith("Tags."):
163
+ return ChangeCategory.TAGS
164
+
165
+ # Check for Permissions category (IAM-related) - do this BEFORE security check
166
+ # because some IAM fields like "Policy" could match security keywords
167
+ permissions_keywords = [
168
+ "policy",
169
+ "permission",
170
+ "role",
171
+ "assumerolepolicydocument",
172
+ "statement",
173
+ ]
174
+ if "iam" in arn_lower:
175
+ # For IAM resources, policy/permission changes are PERMISSIONS
176
+ if any(keyword in field_lower for keyword in permissions_keywords):
177
+ return ChangeCategory.PERMISSIONS
178
+
179
+ # Check for Security category
180
+ for security_field in SECURITY_CRITICAL_FIELDS:
181
+ if security_field.lower() in field_lower:
182
+ return ChangeCategory.SECURITY
183
+
184
+ # Default to Configuration
185
+ return ChangeCategory.CONFIGURATION
@@ -0,0 +1,272 @@
1
+ """Drift formatter for color-coded terminal output of configuration changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Optional
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from ..models.config_diff import ChangeCategory
12
+ from .models import DriftReport
13
+
14
+ if TYPE_CHECKING:
15
+ from ..models.config_diff import ConfigDiff
16
+
17
+
18
+ class DriftFormatter:
19
+ """Format and display configuration drift with color coding.
20
+
21
+ Color scheme:
22
+ - Red: Removed fields
23
+ - Green: Added fields
24
+ - Yellow: Security-critical changes
25
+ - Cyan: Configuration changes
26
+ """
27
+
28
+ def __init__(self, console: Optional[Console] = None):
29
+ """Initialize drift formatter.
30
+
31
+ Args:
32
+ console: Rich console instance (creates new one if not provided)
33
+ """
34
+ self.console = console or Console()
35
+
36
+ def display(
37
+ self,
38
+ report: DriftReport,
39
+ group_by: str = "category",
40
+ resource_type_filter: Optional[str] = None,
41
+ region_filter: Optional[str] = None,
42
+ ) -> None:
43
+ """Display drift report to console with color coding.
44
+
45
+ Args:
46
+ report: DriftReport to display
47
+ group_by: How to group changes ("category" or "resource")
48
+ resource_type_filter: Optional resource type filter (e.g., "ec2")
49
+ region_filter: Optional region filter (e.g., "us-east-1")
50
+ """
51
+ # Apply filters
52
+ diffs = report.get_all_diffs()
53
+
54
+ if resource_type_filter:
55
+ diffs = [d for d in diffs if resource_type_filter.lower() in d.resource_arn.lower()]
56
+
57
+ if region_filter:
58
+ diffs = [d for d in diffs if region_filter in d.resource_arn]
59
+
60
+ # Check if there are any changes after filtering
61
+ if not diffs:
62
+ self.console.print()
63
+ self.console.print(
64
+ Panel(
65
+ "[bold green]No configuration changes detected[/bold green]",
66
+ style="green",
67
+ )
68
+ )
69
+ self.console.print()
70
+ return
71
+
72
+ # Display header
73
+ self.console.print()
74
+ self.console.print(
75
+ Panel(
76
+ f"[bold]Configuration Drift Details[/bold]\n" f"Total Changes: {len(diffs)}",
77
+ style="cyan",
78
+ )
79
+ )
80
+ self.console.print()
81
+
82
+ # Display summary statistics
83
+ self._display_summary(diffs)
84
+
85
+ # Group and display changes
86
+ if group_by == "resource":
87
+ self._display_by_resource(diffs)
88
+ else:
89
+ self._display_by_category(diffs)
90
+
91
+ def _display_summary(self, diffs: list) -> None:
92
+ """Display summary statistics for drift.
93
+
94
+ Args:
95
+ diffs: List of ConfigDiff objects
96
+ """
97
+ # Count by category
98
+ tags_count = sum(1 for d in diffs if d.category == ChangeCategory.TAGS)
99
+ config_count = sum(1 for d in diffs if d.category == ChangeCategory.CONFIGURATION)
100
+ security_count = sum(1 for d in diffs if d.category == ChangeCategory.SECURITY)
101
+ permissions_count = sum(1 for d in diffs if d.category == ChangeCategory.PERMISSIONS)
102
+
103
+ # Create summary table
104
+ table = Table(title="Summary", show_header=True, header_style="bold magenta")
105
+ table.add_column("Category", style="cyan", width=20)
106
+ table.add_column("Count", justify="right", style="yellow", width=10)
107
+
108
+ if tags_count > 0:
109
+ table.add_row("Tags", str(tags_count))
110
+ if config_count > 0:
111
+ table.add_row("Configuration", str(config_count))
112
+ if security_count > 0:
113
+ table.add_row("[yellow]Security[/yellow]", f"[yellow]{security_count}[/yellow]")
114
+ if permissions_count > 0:
115
+ table.add_row("Permissions", str(permissions_count))
116
+
117
+ self.console.print(table)
118
+ self.console.print()
119
+
120
+ def _display_by_category(self, diffs: list) -> None:
121
+ """Display diffs grouped by category.
122
+
123
+ Args:
124
+ diffs: List of ConfigDiff objects
125
+ """
126
+ # Group by category
127
+ from typing import Dict, List
128
+
129
+ categories: Dict[ChangeCategory, List] = {
130
+ ChangeCategory.TAGS: [],
131
+ ChangeCategory.CONFIGURATION: [],
132
+ ChangeCategory.SECURITY: [],
133
+ ChangeCategory.PERMISSIONS: [],
134
+ }
135
+
136
+ for diff in diffs:
137
+ categories[diff.category].append(diff)
138
+
139
+ # Display each category
140
+ category_names = {
141
+ ChangeCategory.TAGS: "Tags",
142
+ ChangeCategory.CONFIGURATION: "Configuration",
143
+ ChangeCategory.SECURITY: "Security",
144
+ ChangeCategory.PERMISSIONS: "Permissions",
145
+ }
146
+
147
+ for category, category_diffs in categories.items():
148
+ if not category_diffs:
149
+ continue
150
+
151
+ self.console.print(f"[bold]{category_names[category]} Changes:[/bold]")
152
+ table = Table(show_header=True, box=None, padding=(0, 2))
153
+ table.add_column("Resource", style="dim", width=50)
154
+ table.add_column("Field", style="cyan", width=30)
155
+ table.add_column("Change", width=40)
156
+
157
+ for diff in category_diffs:
158
+ # Extract resource name/ID from ARN
159
+ resource_display = self._format_resource_name(diff.resource_arn)
160
+
161
+ # Format the change with color coding
162
+ change_display = self._format_change(diff)
163
+
164
+ table.add_row(resource_display, diff.field_path, change_display)
165
+
166
+ self.console.print(table)
167
+ self.console.print()
168
+
169
+ def _display_by_resource(self, diffs: list) -> None:
170
+ """Display diffs grouped by resource ARN.
171
+
172
+ Args:
173
+ diffs: List of ConfigDiff objects
174
+ """
175
+ # Group by resource
176
+ by_resource: dict[str, list] = {}
177
+ for diff in diffs:
178
+ if diff.resource_arn not in by_resource:
179
+ by_resource[diff.resource_arn] = []
180
+ by_resource[diff.resource_arn].append(diff)
181
+
182
+ # Display each resource's changes
183
+ for resource_arn, resource_diffs in by_resource.items():
184
+ resource_display = self._format_resource_name(resource_arn)
185
+ self.console.print(f"[bold cyan]{resource_display}[/bold cyan]")
186
+
187
+ table = Table(show_header=True, box=None, padding=(0, 2))
188
+ table.add_column("Field", style="cyan", width=35)
189
+ table.add_column("Change", width=50)
190
+ table.add_column("Category", width=15)
191
+
192
+ for diff in resource_diffs:
193
+ change_display = self._format_change(diff)
194
+ category_display = diff.category.value.capitalize()
195
+
196
+ table.add_row(diff.field_path, change_display, category_display)
197
+
198
+ self.console.print(table)
199
+ self.console.print()
200
+
201
+ def _format_resource_name(self, arn: str) -> str:
202
+ """Extract a readable resource name from ARN.
203
+
204
+ Args:
205
+ arn: Resource ARN
206
+
207
+ Returns:
208
+ Formatted resource name
209
+ """
210
+ # Extract the resource ID/name from ARN
211
+ # ARN format: arn:aws:service:region:account:resourceType/resourceId
212
+ parts = arn.split(":")
213
+ if len(parts) >= 6:
214
+ resource_part = parts[5]
215
+ if "/" in resource_part:
216
+ return resource_part.split("/", 1)[1]
217
+ return resource_part
218
+ return arn
219
+
220
+ def _format_change(self, diff: ConfigDiff) -> str:
221
+ """Format a change with color coding and arrow notation.
222
+
223
+ Args:
224
+ diff: ConfigDiff object
225
+
226
+ Returns:
227
+ Rich-formatted change string
228
+ """
229
+ old_str = self._format_value(diff.old_value)
230
+ new_str = self._format_value(diff.new_value)
231
+
232
+ # Color code based on change type
233
+ if diff.old_value is None:
234
+ # Field added
235
+ return f"[green]+[/green] {new_str}"
236
+ elif diff.new_value is None:
237
+ # Field removed
238
+ return f"[red]-[/red] {old_str}"
239
+ else:
240
+ # Field modified
241
+ if diff.is_security_critical():
242
+ # Security-critical change - yellow
243
+ return f"[yellow]{old_str} → {new_str}[/yellow]"
244
+ else:
245
+ # Regular change - cyan
246
+ return f"{old_str} → {new_str}"
247
+
248
+ def _format_value(self, value: Any) -> str:
249
+ """Format a configuration value for display.
250
+
251
+ Args:
252
+ value: Configuration value
253
+
254
+ Returns:
255
+ Formatted string representation
256
+ """
257
+ if value is None:
258
+ return "[dim](none)[/dim]"
259
+ elif isinstance(value, bool):
260
+ return "true" if value else "false"
261
+ elif isinstance(value, (dict, list)):
262
+ # Truncate complex values
263
+ str_value = str(value)
264
+ if len(str_value) > 50:
265
+ return str_value[:47] + "..."
266
+ return str_value
267
+ else:
268
+ str_value = str(value)
269
+ # Truncate very long values
270
+ if len(str_value) > 100:
271
+ return str_value[:97] + "..."
272
+ return str_value
src/delta/models.py ADDED
@@ -0,0 +1,154 @@
1
+ """Drift report model for containing configuration differences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.config_diff import ChangeCategory, ConfigDiff
8
+
9
+
10
+ class DriftReport:
11
+ """Container for configuration drift analysis results.
12
+
13
+ Stores and organizes ConfigDiff objects representing field-level changes
14
+ between snapshot versions.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize an empty drift report."""
19
+ self._diffs: list[ConfigDiff] = []
20
+
21
+ @property
22
+ def total_changes(self) -> int:
23
+ """Get total number of configuration changes."""
24
+ return len(self._diffs)
25
+
26
+ def add_diff(self, diff: ConfigDiff) -> None:
27
+ """Add a configuration diff to the report.
28
+
29
+ Args:
30
+ diff: ConfigDiff to add
31
+ """
32
+ self._diffs.append(diff)
33
+
34
+ def get_all_diffs(self) -> list[ConfigDiff]:
35
+ """Get all configuration diffs.
36
+
37
+ Returns:
38
+ List of all ConfigDiff objects
39
+ """
40
+ return self._diffs.copy()
41
+
42
+ def get_diffs_by_category(self, category: ChangeCategory) -> list[ConfigDiff]:
43
+ """Get diffs filtered by change category.
44
+
45
+ Args:
46
+ category: ChangeCategory to filter by
47
+
48
+ Returns:
49
+ List of diffs matching the category
50
+ """
51
+ return [d for d in self._diffs if d.category == category]
52
+
53
+ def get_security_critical_diffs(self) -> list[ConfigDiff]:
54
+ """Get diffs that affect security-critical settings.
55
+
56
+ Returns:
57
+ List of security-critical diffs
58
+ """
59
+ return [d for d in self._diffs if d.is_security_critical()]
60
+
61
+ def get_diffs_by_resource(self, resource_arn: str) -> list[ConfigDiff]:
62
+ """Get diffs for a specific resource ARN.
63
+
64
+ Args:
65
+ resource_arn: ARN of the resource
66
+
67
+ Returns:
68
+ List of diffs for the specified resource
69
+ """
70
+ return [d for d in self._diffs if d.resource_arn == resource_arn]
71
+
72
+ def get_diffs_by_resource_type(self, resource_type: str) -> list[ConfigDiff]:
73
+ """Get diffs filtered by resource type.
74
+
75
+ Args:
76
+ resource_type: Resource type to filter by (e.g., "ec2", "rds")
77
+
78
+ Returns:
79
+ List of diffs matching the resource type (case insensitive)
80
+ """
81
+ resource_type_lower = resource_type.lower()
82
+ return [d for d in self._diffs if resource_type_lower in d.resource_arn.lower()]
83
+
84
+ def get_diffs_by_region(self, region: str) -> list[ConfigDiff]:
85
+ """Get diffs filtered by AWS region.
86
+
87
+ Args:
88
+ region: AWS region (e.g., "us-east-1")
89
+
90
+ Returns:
91
+ List of diffs in the specified region
92
+ """
93
+ return [d for d in self._diffs if region in d.resource_arn]
94
+
95
+ def get_summary(self) -> dict[str, Any]:
96
+ """Generate summary statistics for the drift report.
97
+
98
+ Returns:
99
+ Dictionary with total counts by category and security criticality
100
+ """
101
+ return {
102
+ "total_changes": self.total_changes,
103
+ "tags_count": len(self.get_diffs_by_category(ChangeCategory.TAGS)),
104
+ "configuration_count": len(self.get_diffs_by_category(ChangeCategory.CONFIGURATION)),
105
+ "security_count": len(self.get_diffs_by_category(ChangeCategory.SECURITY)),
106
+ "permissions_count": len(self.get_diffs_by_category(ChangeCategory.PERMISSIONS)),
107
+ "security_critical_count": len(self.get_security_critical_diffs()),
108
+ }
109
+
110
+ def group_by_resource(self) -> dict[str, list[ConfigDiff]]:
111
+ """Group diffs by resource ARN.
112
+
113
+ Returns:
114
+ Dictionary mapping resource ARN to list of diffs
115
+ """
116
+ grouped: dict[str, list[ConfigDiff]] = {}
117
+ for diff in self._diffs:
118
+ if diff.resource_arn not in grouped:
119
+ grouped[diff.resource_arn] = []
120
+ grouped[diff.resource_arn].append(diff)
121
+ return grouped
122
+
123
+ def group_by_category(self) -> dict[ChangeCategory, list[ConfigDiff]]:
124
+ """Group diffs by change category.
125
+
126
+ Returns:
127
+ Dictionary mapping ChangeCategory to list of diffs
128
+ """
129
+ grouped: dict[ChangeCategory, list[ConfigDiff]] = {}
130
+ for diff in self._diffs:
131
+ if diff.category not in grouped:
132
+ grouped[diff.category] = []
133
+ grouped[diff.category].append(diff)
134
+ return grouped
135
+
136
+ def has_security_critical_changes(self) -> bool:
137
+ """Check if the report contains any security-critical changes.
138
+
139
+ Returns:
140
+ True if there are security-critical changes, False otherwise
141
+ """
142
+ return len(self.get_security_critical_diffs()) > 0
143
+
144
+ def to_dict(self) -> dict[str, Any]:
145
+ """Convert drift report to dictionary representation.
146
+
147
+ Returns:
148
+ Dictionary with all diffs and summary statistics
149
+ """
150
+ return {
151
+ "total_changes": self.total_changes,
152
+ "summary": self.get_summary(),
153
+ "diffs": [diff.to_dict() for diff in self._diffs],
154
+ }