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.
- aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
- aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
- aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/aws/__init__.py +11 -0
- src/aws/client.py +128 -0
- src/aws/credentials.py +191 -0
- src/aws/rate_limiter.py +177 -0
- src/cli/__init__.py +12 -0
- src/cli/config.py +130 -0
- src/cli/main.py +4046 -0
- src/cloudtrail/__init__.py +5 -0
- src/cloudtrail/query.py +642 -0
- src/config_service/__init__.py +21 -0
- src/config_service/collector.py +346 -0
- src/config_service/detector.py +256 -0
- src/config_service/resource_type_mapping.py +328 -0
- src/cost/__init__.py +5 -0
- src/cost/analyzer.py +226 -0
- src/cost/explorer.py +209 -0
- src/cost/reporter.py +237 -0
- src/delta/__init__.py +5 -0
- src/delta/calculator.py +206 -0
- src/delta/differ.py +185 -0
- src/delta/formatters.py +272 -0
- src/delta/models.py +154 -0
- src/delta/reporter.py +234 -0
- src/matching/__init__.py +6 -0
- src/matching/config.py +52 -0
- src/matching/normalizer.py +450 -0
- src/matching/prompts.py +33 -0
- src/models/__init__.py +21 -0
- src/models/config_diff.py +135 -0
- src/models/cost_report.py +87 -0
- src/models/deletion_operation.py +104 -0
- src/models/deletion_record.py +97 -0
- src/models/delta_report.py +122 -0
- src/models/efs_resource.py +80 -0
- src/models/elasticache_resource.py +90 -0
- src/models/group.py +318 -0
- src/models/inventory.py +133 -0
- src/models/protection_rule.py +123 -0
- src/models/report.py +288 -0
- src/models/resource.py +111 -0
- src/models/security_finding.py +102 -0
- src/models/snapshot.py +122 -0
- src/restore/__init__.py +20 -0
- src/restore/audit.py +175 -0
- src/restore/cleaner.py +461 -0
- src/restore/config.py +209 -0
- src/restore/deleter.py +976 -0
- src/restore/dependency.py +254 -0
- src/restore/safety.py +115 -0
- src/security/__init__.py +0 -0
- src/security/checks/__init__.py +0 -0
- src/security/checks/base.py +56 -0
- src/security/checks/ec2_checks.py +88 -0
- src/security/checks/elasticache_checks.py +149 -0
- src/security/checks/iam_checks.py +102 -0
- src/security/checks/rds_checks.py +140 -0
- src/security/checks/s3_checks.py +95 -0
- src/security/checks/secrets_checks.py +96 -0
- src/security/checks/sg_checks.py +142 -0
- src/security/cis_mapper.py +97 -0
- src/security/models.py +53 -0
- src/security/reporter.py +174 -0
- src/security/scanner.py +87 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +453 -0
- src/snapshot/filter.py +259 -0
- src/snapshot/inventory_storage.py +236 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -0
- src/snapshot/resource_collectors/__init__.py +5 -0
- src/snapshot/resource_collectors/apigateway.py +140 -0
- src/snapshot/resource_collectors/backup.py +136 -0
- src/snapshot/resource_collectors/base.py +81 -0
- src/snapshot/resource_collectors/cloudformation.py +55 -0
- src/snapshot/resource_collectors/cloudwatch.py +109 -0
- src/snapshot/resource_collectors/codebuild.py +69 -0
- src/snapshot/resource_collectors/codepipeline.py +82 -0
- src/snapshot/resource_collectors/dynamodb.py +65 -0
- src/snapshot/resource_collectors/ec2.py +240 -0
- src/snapshot/resource_collectors/ecs.py +215 -0
- src/snapshot/resource_collectors/efs_collector.py +102 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elasticache_collector.py +79 -0
- src/snapshot/resource_collectors/elb.py +126 -0
- src/snapshot/resource_collectors/eventbridge.py +156 -0
- src/snapshot/resource_collectors/glue.py +199 -0
- src/snapshot/resource_collectors/iam.py +188 -0
- src/snapshot/resource_collectors/kms.py +111 -0
- src/snapshot/resource_collectors/lambda_func.py +139 -0
- src/snapshot/resource_collectors/rds.py +109 -0
- src/snapshot/resource_collectors/route53.py +86 -0
- src/snapshot/resource_collectors/s3.py +105 -0
- src/snapshot/resource_collectors/secretsmanager.py +70 -0
- src/snapshot/resource_collectors/sns.py +68 -0
- src/snapshot/resource_collectors/sqs.py +82 -0
- src/snapshot/resource_collectors/ssm.py +160 -0
- src/snapshot/resource_collectors/stepfunctions.py +74 -0
- src/snapshot/resource_collectors/vpcendpoints.py +79 -0
- src/snapshot/resource_collectors/waf.py +159 -0
- src/snapshot/storage.py +351 -0
- src/storage/__init__.py +21 -0
- src/storage/audit_store.py +419 -0
- src/storage/database.py +294 -0
- src/storage/group_store.py +763 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +416 -0
- src/storage/schema.py +339 -0
- src/storage/snapshot_store.py +363 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +305 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/pagination.py +41 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
- src/utils/unsupported_resources.py +306 -0
- src/web/__init__.py +5 -0
- src/web/app.py +97 -0
- src/web/dependencies.py +69 -0
- src/web/routes/__init__.py +1 -0
- src/web/routes/api/__init__.py +18 -0
- src/web/routes/api/charts.py +156 -0
- src/web/routes/api/cleanup.py +186 -0
- src/web/routes/api/filters.py +253 -0
- src/web/routes/api/groups.py +305 -0
- src/web/routes/api/inventories.py +80 -0
- src/web/routes/api/queries.py +202 -0
- src/web/routes/api/resources.py +393 -0
- src/web/routes/api/snapshots.py +314 -0
- src/web/routes/api/views.py +260 -0
- src/web/routes/pages.py +198 -0
- src/web/services/__init__.py +1 -0
- src/web/templates/base.html +955 -0
- src/web/templates/components/navbar.html +31 -0
- src/web/templates/components/sidebar.html +104 -0
- src/web/templates/pages/audit_logs.html +86 -0
- src/web/templates/pages/cleanup.html +279 -0
- src/web/templates/pages/dashboard.html +227 -0
- src/web/templates/pages/diff.html +175 -0
- src/web/templates/pages/error.html +30 -0
- src/web/templates/pages/groups.html +721 -0
- src/web/templates/pages/queries.html +246 -0
- src/web/templates/pages/resources.html +2429 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- 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
|
src/delta/formatters.py
ADDED
|
@@ -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
|
+
}
|