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/models/report.py ADDED
@@ -0,0 +1,288 @@
1
+ """
2
+ Data models for snapshot resource reporting.
3
+
4
+ This module defines the data structures used for generating and displaying
5
+ snapshot resource reports, including metadata, summaries, filtered views,
6
+ and detailed resource information.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime
13
+ from typing import Dict, List, Literal, Optional, Tuple
14
+
15
+
16
+ @dataclass
17
+ class SnapshotMetadata:
18
+ """
19
+ Snapshot identification information for report header.
20
+
21
+ Attributes:
22
+ name: Snapshot name (e.g., "baseline-2025-01-29")
23
+ created_at: Snapshot creation timestamp
24
+ account_id: AWS account ID
25
+ regions: List of AWS regions included in snapshot
26
+ inventory_name: Parent inventory name
27
+ total_resource_count: Total number of resources in snapshot
28
+ """
29
+
30
+ name: str
31
+ created_at: datetime
32
+ account_id: str
33
+ regions: List[str]
34
+ inventory_name: str
35
+ total_resource_count: int
36
+
37
+ @property
38
+ def region_summary(self) -> str:
39
+ """Human-readable region list (e.g., 'us-east-1, us-west-2 (2 regions)')."""
40
+ if len(self.regions) <= 3:
41
+ return ", ".join(self.regions)
42
+ else:
43
+ return f"{', '.join(self.regions[:3])} ... ({len(self.regions)} regions)"
44
+
45
+
46
+ @dataclass
47
+ class ResourceSummary:
48
+ """
49
+ Aggregated resource counts for summary view.
50
+
51
+ Attributes:
52
+ total_count: Total number of resources
53
+ by_service: Count per AWS service (e.g., {"EC2": 100, "S3": 50})
54
+ by_region: Count per AWS region (e.g., {"us-east-1": 80, "us-west-2": 70})
55
+ by_type: Count per resource type (e.g., {"AWS::EC2::Instance": 50})
56
+ """
57
+
58
+ total_count: int = 0
59
+ by_service: Dict[str, int] = field(default_factory=dict)
60
+ by_region: Dict[str, int] = field(default_factory=dict)
61
+ by_type: Dict[str, int] = field(default_factory=dict)
62
+
63
+ @property
64
+ def service_count(self) -> int:
65
+ """Number of distinct AWS services."""
66
+ return len(self.by_service)
67
+
68
+ @property
69
+ def region_count(self) -> int:
70
+ """Number of distinct AWS regions."""
71
+ return len(self.by_region)
72
+
73
+ @property
74
+ def type_count(self) -> int:
75
+ """Number of distinct resource types."""
76
+ return len(self.by_type)
77
+
78
+ def top_services(self, limit: int = 5) -> List[Tuple[str, int]]:
79
+ """Return top N services by resource count."""
80
+ return sorted(self.by_service.items(), key=lambda x: x[1], reverse=True)[:limit]
81
+
82
+ def top_regions(self, limit: int = 5) -> List[Tuple[str, int]]:
83
+ """Return top N regions by resource count."""
84
+ return sorted(self.by_region.items(), key=lambda x: x[1], reverse=True)[:limit]
85
+
86
+
87
+ @dataclass
88
+ class FilteredResource:
89
+ """
90
+ Minimal resource info for filtered view (non-detailed).
91
+
92
+ Attributes:
93
+ arn: AWS Resource Name (unique identifier)
94
+ resource_type: CloudFormation resource type (e.g., "AWS::EC2::Instance")
95
+ name: Resource name or identifier
96
+ region: AWS region (e.g., "us-east-1")
97
+ """
98
+
99
+ arn: str
100
+ resource_type: str
101
+ name: str
102
+ region: str
103
+
104
+ @property
105
+ def service(self) -> str:
106
+ """Extract service name from resource type (e.g., "EC2" from "AWS::EC2::Instance")."""
107
+ parts = self.resource_type.split("::")
108
+ return parts[1] if len(parts) >= 2 else "Unknown"
109
+
110
+ @property
111
+ def short_type(self) -> str:
112
+ """Short resource type (e.g., "Instance" from "AWS::EC2::Instance")."""
113
+ parts = self.resource_type.split("::")
114
+ return parts[-1] if parts else self.resource_type
115
+
116
+
117
+ @dataclass
118
+ class DetailedResource:
119
+ """
120
+ Full resource details for detailed view.
121
+
122
+ Attributes:
123
+ arn: AWS Resource Name
124
+ resource_type: CloudFormation resource type
125
+ name: Resource name or identifier
126
+ region: AWS region
127
+ tags: Key-value tag pairs
128
+ created_at: Resource creation timestamp (if available)
129
+ config_hash: Configuration hash for change detection
130
+ """
131
+
132
+ arn: str
133
+ resource_type: str
134
+ name: str
135
+ region: str
136
+ tags: Dict[str, str]
137
+ created_at: Optional[datetime]
138
+ config_hash: str
139
+
140
+ @property
141
+ def service(self) -> str:
142
+ """Extract service name from resource type."""
143
+ parts = self.resource_type.split("::")
144
+ return parts[1] if len(parts) >= 2 else "Unknown"
145
+
146
+ @property
147
+ def age_days(self) -> Optional[int]:
148
+ """Calculate resource age in days (if creation date available)."""
149
+ if self.created_at:
150
+ from datetime import timezone
151
+
152
+ # Handle string dates (convert to datetime)
153
+ created_at = self.created_at
154
+ if isinstance(created_at, str):
155
+ try:
156
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
157
+ except ValueError:
158
+ return None
159
+
160
+ # Ensure we have a datetime object
161
+ if not isinstance(created_at, datetime):
162
+ return None
163
+
164
+ # Ensure we're comparing timezone-aware datetimes
165
+ now_utc = datetime.now(timezone.utc)
166
+ created = created_at if created_at.tzinfo else created_at.replace(tzinfo=timezone.utc)
167
+ return (now_utc - created).days
168
+ return None
169
+
170
+ @property
171
+ def tag_count(self) -> int:
172
+ """Number of tags applied to resource."""
173
+ return len(self.tags)
174
+
175
+ def has_tag(self, key: str, value: Optional[str] = None) -> bool:
176
+ """Check if resource has specific tag (optionally with value)."""
177
+ if key not in self.tags:
178
+ return False
179
+ if value is not None:
180
+ return self.tags[key] == value
181
+ return True
182
+
183
+
184
+ @dataclass
185
+ class FilterCriteria:
186
+ """
187
+ Filter specification for narrowing report results.
188
+
189
+ Attributes:
190
+ resource_types: List of resource types to include (flexible matching)
191
+ regions: List of AWS regions to include (exact matching)
192
+ match_mode: Matching strategy ("flexible" or "exact")
193
+ """
194
+
195
+ resource_types: Optional[List[str]] = None
196
+ regions: Optional[List[str]] = None
197
+ match_mode: Literal["flexible", "exact"] = "flexible"
198
+
199
+ def __post_init__(self) -> None:
200
+ """Normalize filter values (lowercase for case-insensitive matching)."""
201
+ if self.resource_types:
202
+ self.resource_types = [rt.lower() for rt in self.resource_types]
203
+ if self.regions:
204
+ self.regions = [r.lower() for r in self.regions]
205
+
206
+ @property
207
+ def has_filters(self) -> bool:
208
+ """Check if any filters are applied."""
209
+ return bool(self.resource_types or self.regions)
210
+
211
+ @property
212
+ def filter_count(self) -> int:
213
+ """Total number of filter criteria."""
214
+ count = 0
215
+ if self.resource_types:
216
+ count += len(self.resource_types)
217
+ if self.regions:
218
+ count += len(self.regions)
219
+ return count
220
+
221
+ def matches_resource(self, resource: FilteredResource) -> bool:
222
+ """
223
+ Check if resource matches filter criteria.
224
+
225
+ Uses flexible matching for resource types (see research.md Task 2).
226
+ Uses exact matching for regions (case-insensitive).
227
+ """
228
+ # Region filter (exact match, case-insensitive)
229
+ if self.regions and resource.region.lower() not in self.regions:
230
+ return False
231
+
232
+ # Resource type filter (flexible matching)
233
+ if self.resource_types:
234
+ type_match = any(
235
+ self._match_resource_type(resource.resource_type, filter_type) for filter_type in self.resource_types
236
+ )
237
+ if not type_match:
238
+ return False
239
+
240
+ return True
241
+
242
+ def _match_resource_type(self, resource_type: str, filter_value: str) -> bool:
243
+ """Three-tier matching: exact → prefix → contains (see research.md)."""
244
+ resource_lower = resource_type.lower()
245
+ filter_lower = filter_value.lower()
246
+
247
+ # Tier 1: Exact match
248
+ if resource_lower == filter_lower:
249
+ return True
250
+
251
+ # Tier 2: Service prefix match
252
+ service_prefix = f"aws::{filter_lower}::"
253
+ if resource_lower.startswith(service_prefix):
254
+ return True
255
+
256
+ # Tier 3: Contains match
257
+ if filter_lower in resource_lower:
258
+ return True
259
+
260
+ return False
261
+
262
+
263
+ @dataclass
264
+ class ResourceReport:
265
+ """
266
+ Complete report data structure.
267
+
268
+ Attributes:
269
+ snapshot_metadata: Information about the snapshot being reported
270
+ summary: Aggregated resource counts by service/region/type
271
+ filtered_resources: Minimal resource list (if filtering applied)
272
+ detailed_resources: Full resource details (if detailed view requested)
273
+ """
274
+
275
+ snapshot_metadata: SnapshotMetadata
276
+ summary: ResourceSummary
277
+ filtered_resources: Optional[List[FilteredResource]] = None
278
+ detailed_resources: Optional[List[DetailedResource]] = None
279
+
280
+ @property
281
+ def has_filters(self) -> bool:
282
+ """Check if report includes filtered results."""
283
+ return self.filtered_resources is not None
284
+
285
+ @property
286
+ def has_details(self) -> bool:
287
+ """Check if report includes detailed resource info."""
288
+ return self.detailed_resources is not None
src/models/resource.py ADDED
@@ -0,0 +1,111 @@
1
+ """Resource data model representing a single AWS resource."""
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Any, Dict, Optional
7
+
8
+
9
+ @dataclass
10
+ class Resource:
11
+ """Represents a single AWS resource captured in a snapshot."""
12
+
13
+ arn: str
14
+ resource_type: str
15
+ name: str
16
+ region: str
17
+ config_hash: str
18
+ raw_config: Optional[Dict[str, Any]] = None # Optional for backward compatibility with v1.0 snapshots
19
+ tags: Dict[str, str] = field(default_factory=dict)
20
+ created_at: Optional[datetime] = None
21
+ source: str = "direct_api" # Collection source: "config" or "direct_api"
22
+
23
+ def to_dict(self) -> Dict[str, Any]:
24
+ """Convert resource to dictionary for serialization.
25
+
26
+ Note: raw_config is included in v1.1+ snapshots for drift analysis support.
27
+ """
28
+ return {
29
+ "arn": self.arn,
30
+ "type": self.resource_type,
31
+ "name": self.name,
32
+ "region": self.region,
33
+ "tags": self.tags,
34
+ "config_hash": self.config_hash,
35
+ "created_at": self.created_at.isoformat() if self.created_at else None,
36
+ "raw_config": self.raw_config if self.raw_config is not None else {},
37
+ "source": self.source,
38
+ }
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: Dict[str, Any]) -> "Resource":
42
+ """Create resource from dictionary.
43
+
44
+ Supports both v1.0 (without raw_config) and v1.1+ (with raw_config) snapshots.
45
+ """
46
+ created_at = None
47
+ if data.get("created_at"):
48
+ if isinstance(data["created_at"], str):
49
+ created_at = datetime.fromisoformat(data["created_at"])
50
+ else:
51
+ created_at = data["created_at"] # Already a datetime
52
+
53
+ return cls(
54
+ arn=data["arn"],
55
+ resource_type=data["type"],
56
+ name=data["name"],
57
+ region=data["region"],
58
+ config_hash=data["config_hash"],
59
+ raw_config=data.get("raw_config"), # Optional for backward compatibility
60
+ tags=data.get("tags", {}),
61
+ created_at=created_at,
62
+ source=data.get("source", "direct_api"), # Default for older snapshots
63
+ )
64
+
65
+ def validate(self) -> bool:
66
+ """Validate resource data integrity.
67
+
68
+ Returns:
69
+ True if valid, raises ValueError if invalid
70
+ """
71
+ # Validate ARN format
72
+ arn_pattern = r"^arn:aws:[a-z0-9-]+:[a-z0-9-]*:[0-9]*:.*$"
73
+ if not re.match(arn_pattern, self.arn):
74
+ raise ValueError(f"Invalid ARN format: {self.arn}")
75
+
76
+ # Validate config_hash is 64-character hex string (SHA256)
77
+ if not re.match(r"^[a-fA-F0-9]{64}$", self.config_hash):
78
+ raise ValueError(f"Invalid config_hash: {self.config_hash}. Must be 64-character SHA256 hex string.")
79
+
80
+ # Validate region format
81
+ valid_regions = ["global"] + [
82
+ "us-east-1",
83
+ "us-east-2",
84
+ "us-west-1",
85
+ "us-west-2",
86
+ "eu-west-1",
87
+ "eu-west-2",
88
+ "eu-west-3",
89
+ "eu-central-1",
90
+ "ap-southeast-1",
91
+ "ap-southeast-2",
92
+ "ap-northeast-1",
93
+ "ap-northeast-2",
94
+ "ca-central-1",
95
+ "sa-east-1",
96
+ "ap-south-1",
97
+ ]
98
+ # Basic validation - starts with region pattern or is 'global'
99
+ if self.region != "global" and not any(self.region.startswith(r[:6]) for r in valid_regions if r != "global"):
100
+ # Allow it anyway - AWS adds new regions regularly
101
+ pass
102
+
103
+ return True
104
+
105
+ @property
106
+ def service(self) -> str:
107
+ """Extract service name from resource type.
108
+
109
+ Example: 'iam:role' -> 'iam'
110
+ """
111
+ return self.resource_type.split(":")[0] if ":" in self.resource_type else self.resource_type
@@ -0,0 +1,102 @@
1
+ """Security finding model for representing detected security issues in AWS resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any, Dict, Optional
8
+
9
+
10
+ class Severity(Enum):
11
+ """Security finding severity levels."""
12
+
13
+ CRITICAL = "critical"
14
+ HIGH = "high"
15
+ MEDIUM = "medium"
16
+ LOW = "low"
17
+
18
+
19
+ @dataclass
20
+ class SecurityFinding:
21
+ """Represents a detected security misconfiguration in an AWS resource.
22
+
23
+ Attributes:
24
+ resource_arn: AWS ARN of the resource with the security issue
25
+ finding_type: Type of security issue (e.g., "public_s3_bucket", "open_security_group")
26
+ severity: Severity level of the finding
27
+ description: Human-readable description of the issue
28
+ remediation: Guidance on how to fix the issue
29
+ cis_control: Optional CIS AWS Foundations Benchmark control ID (e.g., "2.1.5")
30
+ metadata: Additional context-specific metadata
31
+ """
32
+
33
+ resource_arn: str
34
+ finding_type: str
35
+ severity: Severity
36
+ description: str
37
+ remediation: str
38
+ cis_control: Optional[str] = None
39
+ metadata: Optional[Dict[str, Any]] = None
40
+
41
+ def __post_init__(self) -> None:
42
+ """Validate SecurityFinding fields after initialization."""
43
+ # Validate ARN format (basic check)
44
+ if not self.resource_arn or not self.resource_arn.startswith("arn:"):
45
+ raise ValueError(f"Invalid ARN format: {self.resource_arn}")
46
+
47
+ # Validate severity is Severity enum
48
+ if not isinstance(self.severity, Severity):
49
+ raise ValueError(f"Invalid severity type: {type(self.severity)}. Must be Severity enum.")
50
+
51
+ # Validate required string fields are not empty
52
+ if not self.finding_type:
53
+ raise ValueError("finding_type cannot be empty")
54
+ if not self.description:
55
+ raise ValueError("description cannot be empty")
56
+ if not self.remediation:
57
+ raise ValueError("remediation cannot be empty")
58
+
59
+ def to_dict(self) -> Dict[str, Any]:
60
+ """Convert SecurityFinding to dictionary representation.
61
+
62
+ Returns:
63
+ Dictionary with all finding attributes
64
+ """
65
+ return {
66
+ "resource_arn": self.resource_arn,
67
+ "finding_type": self.finding_type,
68
+ "severity": self.severity.value,
69
+ "description": self.description,
70
+ "remediation": self.remediation,
71
+ "cis_control": self.cis_control,
72
+ "metadata": self.metadata or {},
73
+ }
74
+
75
+ @classmethod
76
+ def from_dict(cls, data: Dict[str, Any]) -> SecurityFinding:
77
+ """Create SecurityFinding from dictionary representation.
78
+
79
+ Args:
80
+ data: Dictionary with finding attributes
81
+
82
+ Returns:
83
+ SecurityFinding instance
84
+
85
+ Raises:
86
+ ValueError: If severity value is invalid
87
+ """
88
+ severity_str = data.get("severity", "").lower()
89
+ try:
90
+ severity = Severity(severity_str)
91
+ except ValueError:
92
+ raise ValueError(f"Invalid severity value: {severity_str}")
93
+
94
+ return cls(
95
+ resource_arn=data["resource_arn"],
96
+ finding_type=data["finding_type"],
97
+ severity=severity,
98
+ description=data["description"],
99
+ remediation=data["remediation"],
100
+ cis_control=data.get("cis_control"),
101
+ metadata=data.get("metadata"),
102
+ )
src/models/snapshot.py ADDED
@@ -0,0 +1,122 @@
1
+ """Snapshot data model representing a point-in-time inventory of AWS resources."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ @dataclass
9
+ class Snapshot:
10
+ """Represents a point-in-time inventory of AWS resources.
11
+
12
+ This serves as the baseline reference for delta tracking and cost analysis.
13
+
14
+ Schema Versions:
15
+ - v1.0: Basic snapshot with config_hash only
16
+ - v1.1: Added raw_config for drift analysis support
17
+ """
18
+
19
+ name: str
20
+ created_at: datetime
21
+ account_id: str
22
+ regions: List[str]
23
+ resources: List[Any] # List[Resource] - avoiding circular import
24
+ is_active: bool = True
25
+ resource_count: int = 0
26
+ service_counts: Dict[str, int] = field(default_factory=dict)
27
+ metadata: Dict[str, Any] = field(default_factory=dict)
28
+ filters_applied: Optional[Dict[str, Any]] = None
29
+ total_resources_before_filter: Optional[int] = None
30
+ inventory_name: str = "default" # Name of inventory this snapshot belongs to
31
+ schema_version: str = "1.1" # Schema version for forward/backward compatibility
32
+
33
+ def __post_init__(self) -> None:
34
+ """Calculate derived fields after initialization."""
35
+ if self.resource_count == 0:
36
+ self.resource_count = len(self.resources)
37
+
38
+ if not self.service_counts:
39
+ self._calculate_service_counts()
40
+
41
+ def _calculate_service_counts(self) -> None:
42
+ """Calculate resource counts by service type."""
43
+ counts: Dict[str, int] = {}
44
+ for resource in self.resources:
45
+ service = resource.resource_type.split(":")[0] if ":" in resource.resource_type else resource.resource_type
46
+ counts[service] = counts.get(service, 0) + 1
47
+ self.service_counts = counts
48
+
49
+ def to_dict(self) -> Dict[str, Any]:
50
+ """Convert snapshot to dictionary for serialization."""
51
+ return {
52
+ "schema_version": self.schema_version,
53
+ "name": self.name,
54
+ "created_at": self.created_at.isoformat(),
55
+ "account_id": self.account_id,
56
+ "regions": self.regions,
57
+ "is_active": self.is_active,
58
+ "resource_count": self.resource_count,
59
+ "service_counts": self.service_counts,
60
+ "metadata": self.metadata,
61
+ "filters_applied": self.filters_applied,
62
+ "total_resources_before_filter": self.total_resources_before_filter,
63
+ "inventory_name": self.inventory_name,
64
+ "resources": [r.to_dict() for r in self.resources],
65
+ }
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: Dict[str, Any]) -> "Snapshot":
69
+ """Create snapshot from dictionary.
70
+
71
+ Supports both v1.0 and v1.1+ snapshot formats for backward compatibility.
72
+
73
+ Note: This requires Resource class to be imported at call time
74
+ to avoid circular imports.
75
+ """
76
+ from .resource import Resource
77
+
78
+ # Handle created_at being either string or datetime (PyYAML can auto-parse)
79
+ created_at = data["created_at"]
80
+ if isinstance(created_at, str):
81
+ created_at = datetime.fromisoformat(created_at)
82
+
83
+ return cls(
84
+ name=data["name"],
85
+ created_at=created_at,
86
+ account_id=data["account_id"],
87
+ regions=data["regions"],
88
+ resources=[Resource.from_dict(r) for r in data["resources"]],
89
+ is_active=data.get("is_active", True),
90
+ resource_count=data.get("resource_count", 0),
91
+ service_counts=data.get("service_counts", {}),
92
+ metadata=data.get("metadata", {}),
93
+ filters_applied=data.get("filters_applied"),
94
+ total_resources_before_filter=data.get("total_resources_before_filter"),
95
+ inventory_name=data.get("inventory_name", "default"), # Default for backward compatibility
96
+ schema_version=data.get("schema_version", "1.0"), # Default to 1.0 for old snapshots
97
+ )
98
+
99
+ def validate(self) -> bool:
100
+ """Validate snapshot data integrity.
101
+
102
+ Returns:
103
+ True if valid, raises ValueError if invalid
104
+ """
105
+ import re
106
+
107
+ # Validate name format (alphanumeric, hyphens, underscores)
108
+ if not re.match(r"^[a-zA-Z0-9_-]+$", self.name):
109
+ raise ValueError(
110
+ f"Invalid snapshot name: {self.name}. "
111
+ f"Must contain only alphanumeric characters, hyphens, and underscores."
112
+ )
113
+
114
+ # Validate account ID (12-digit string)
115
+ if not re.match(r"^\d{12}$", self.account_id):
116
+ raise ValueError(f"Invalid AWS account ID: {self.account_id}. Must be a 12-digit string.")
117
+
118
+ # Validate regions list is not empty
119
+ if not self.regions:
120
+ raise ValueError("Snapshot must include at least one AWS region.")
121
+
122
+ return True
@@ -0,0 +1,20 @@
1
+ """Resource cleanup/restoration module.
2
+
3
+ This module provides functionality to delete AWS resources created after a baseline
4
+ snapshot, with comprehensive safety protections.
5
+
6
+ Classes:
7
+ ResourceCleaner: Main orchestrator for restore operations
8
+ DependencyResolver: Dependency graph construction and deletion ordering
9
+ SafetyChecker: Protection rule evaluation
10
+ AuditStorage: Audit log storage and retrieval
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = [
16
+ "ResourceCleaner",
17
+ "DependencyResolver",
18
+ "SafetyChecker",
19
+ "AuditStorage",
20
+ ]