aws-inventory-manager 0.13.2__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.

Potentially problematic release.


This version of aws-inventory-manager might be problematic. Click here for more details.

Files changed (145) hide show
  1. aws_inventory_manager-0.13.2.dist-info/LICENSE +21 -0
  2. aws_inventory_manager-0.13.2.dist-info/METADATA +1226 -0
  3. aws_inventory_manager-0.13.2.dist-info/RECORD +145 -0
  4. aws_inventory_manager-0.13.2.dist-info/WHEEL +5 -0
  5. aws_inventory_manager-0.13.2.dist-info/entry_points.txt +2 -0
  6. aws_inventory_manager-0.13.2.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 +3626 -0
  15. src/config_service/__init__.py +21 -0
  16. src/config_service/collector.py +346 -0
  17. src/config_service/detector.py +256 -0
  18. src/config_service/resource_type_mapping.py +328 -0
  19. src/cost/__init__.py +5 -0
  20. src/cost/analyzer.py +226 -0
  21. src/cost/explorer.py +209 -0
  22. src/cost/reporter.py +237 -0
  23. src/delta/__init__.py +5 -0
  24. src/delta/calculator.py +206 -0
  25. src/delta/differ.py +185 -0
  26. src/delta/formatters.py +272 -0
  27. src/delta/models.py +154 -0
  28. src/delta/reporter.py +234 -0
  29. src/models/__init__.py +21 -0
  30. src/models/config_diff.py +135 -0
  31. src/models/cost_report.py +87 -0
  32. src/models/deletion_operation.py +104 -0
  33. src/models/deletion_record.py +97 -0
  34. src/models/delta_report.py +122 -0
  35. src/models/efs_resource.py +80 -0
  36. src/models/elasticache_resource.py +90 -0
  37. src/models/group.py +318 -0
  38. src/models/inventory.py +133 -0
  39. src/models/protection_rule.py +123 -0
  40. src/models/report.py +288 -0
  41. src/models/resource.py +111 -0
  42. src/models/security_finding.py +102 -0
  43. src/models/snapshot.py +122 -0
  44. src/restore/__init__.py +20 -0
  45. src/restore/audit.py +175 -0
  46. src/restore/cleaner.py +461 -0
  47. src/restore/config.py +209 -0
  48. src/restore/deleter.py +976 -0
  49. src/restore/dependency.py +254 -0
  50. src/restore/safety.py +115 -0
  51. src/security/__init__.py +0 -0
  52. src/security/checks/__init__.py +0 -0
  53. src/security/checks/base.py +56 -0
  54. src/security/checks/ec2_checks.py +88 -0
  55. src/security/checks/elasticache_checks.py +149 -0
  56. src/security/checks/iam_checks.py +102 -0
  57. src/security/checks/rds_checks.py +140 -0
  58. src/security/checks/s3_checks.py +95 -0
  59. src/security/checks/secrets_checks.py +96 -0
  60. src/security/checks/sg_checks.py +142 -0
  61. src/security/cis_mapper.py +97 -0
  62. src/security/models.py +53 -0
  63. src/security/reporter.py +174 -0
  64. src/security/scanner.py +87 -0
  65. src/snapshot/__init__.py +6 -0
  66. src/snapshot/capturer.py +451 -0
  67. src/snapshot/filter.py +259 -0
  68. src/snapshot/inventory_storage.py +236 -0
  69. src/snapshot/report_formatter.py +250 -0
  70. src/snapshot/reporter.py +189 -0
  71. src/snapshot/resource_collectors/__init__.py +5 -0
  72. src/snapshot/resource_collectors/apigateway.py +140 -0
  73. src/snapshot/resource_collectors/backup.py +136 -0
  74. src/snapshot/resource_collectors/base.py +81 -0
  75. src/snapshot/resource_collectors/cloudformation.py +55 -0
  76. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  77. src/snapshot/resource_collectors/codebuild.py +69 -0
  78. src/snapshot/resource_collectors/codepipeline.py +82 -0
  79. src/snapshot/resource_collectors/dynamodb.py +65 -0
  80. src/snapshot/resource_collectors/ec2.py +240 -0
  81. src/snapshot/resource_collectors/ecs.py +215 -0
  82. src/snapshot/resource_collectors/efs_collector.py +102 -0
  83. src/snapshot/resource_collectors/eks.py +200 -0
  84. src/snapshot/resource_collectors/elasticache_collector.py +79 -0
  85. src/snapshot/resource_collectors/elb.py +126 -0
  86. src/snapshot/resource_collectors/eventbridge.py +156 -0
  87. src/snapshot/resource_collectors/iam.py +188 -0
  88. src/snapshot/resource_collectors/kms.py +111 -0
  89. src/snapshot/resource_collectors/lambda_func.py +139 -0
  90. src/snapshot/resource_collectors/rds.py +109 -0
  91. src/snapshot/resource_collectors/route53.py +86 -0
  92. src/snapshot/resource_collectors/s3.py +105 -0
  93. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  94. src/snapshot/resource_collectors/sns.py +68 -0
  95. src/snapshot/resource_collectors/sqs.py +82 -0
  96. src/snapshot/resource_collectors/ssm.py +160 -0
  97. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  98. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  99. src/snapshot/resource_collectors/waf.py +159 -0
  100. src/snapshot/storage.py +351 -0
  101. src/storage/__init__.py +21 -0
  102. src/storage/audit_store.py +419 -0
  103. src/storage/database.py +294 -0
  104. src/storage/group_store.py +749 -0
  105. src/storage/inventory_store.py +320 -0
  106. src/storage/resource_store.py +413 -0
  107. src/storage/schema.py +288 -0
  108. src/storage/snapshot_store.py +346 -0
  109. src/utils/__init__.py +12 -0
  110. src/utils/export.py +305 -0
  111. src/utils/hash.py +60 -0
  112. src/utils/logging.py +63 -0
  113. src/utils/pagination.py +41 -0
  114. src/utils/paths.py +51 -0
  115. src/utils/progress.py +41 -0
  116. src/utils/unsupported_resources.py +306 -0
  117. src/web/__init__.py +5 -0
  118. src/web/app.py +97 -0
  119. src/web/dependencies.py +69 -0
  120. src/web/routes/__init__.py +1 -0
  121. src/web/routes/api/__init__.py +18 -0
  122. src/web/routes/api/charts.py +156 -0
  123. src/web/routes/api/cleanup.py +186 -0
  124. src/web/routes/api/filters.py +253 -0
  125. src/web/routes/api/groups.py +305 -0
  126. src/web/routes/api/inventories.py +80 -0
  127. src/web/routes/api/queries.py +202 -0
  128. src/web/routes/api/resources.py +379 -0
  129. src/web/routes/api/snapshots.py +314 -0
  130. src/web/routes/api/views.py +260 -0
  131. src/web/routes/pages.py +198 -0
  132. src/web/services/__init__.py +1 -0
  133. src/web/templates/base.html +949 -0
  134. src/web/templates/components/navbar.html +31 -0
  135. src/web/templates/components/sidebar.html +104 -0
  136. src/web/templates/pages/audit_logs.html +86 -0
  137. src/web/templates/pages/cleanup.html +279 -0
  138. src/web/templates/pages/dashboard.html +227 -0
  139. src/web/templates/pages/diff.html +175 -0
  140. src/web/templates/pages/error.html +30 -0
  141. src/web/templates/pages/groups.html +721 -0
  142. src/web/templates/pages/queries.html +246 -0
  143. src/web/templates/pages/resources.html +2251 -0
  144. src/web/templates/pages/snapshot_detail.html +271 -0
  145. src/web/templates/pages/snapshots.html +429 -0
@@ -0,0 +1,328 @@
1
+ """Resource type mapping between AWS Config and collectors.
2
+
3
+ AWS Config supports ~80+ resource types. This module maps which types
4
+ can be collected via Config vs which must use direct API calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Set
10
+
11
+ # Resource types supported by AWS Config
12
+ # Reference: https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html
13
+ CONFIG_SUPPORTED_TYPES: Set[str] = {
14
+ # EC2
15
+ "AWS::EC2::CustomerGateway",
16
+ "AWS::EC2::EIP",
17
+ "AWS::EC2::Host",
18
+ "AWS::EC2::Instance",
19
+ "AWS::EC2::InternetGateway",
20
+ "AWS::EC2::NetworkAcl",
21
+ "AWS::EC2::NetworkInterface",
22
+ "AWS::EC2::RouteTable",
23
+ "AWS::EC2::SecurityGroup",
24
+ "AWS::EC2::Subnet",
25
+ "AWS::EC2::Volume",
26
+ "AWS::EC2::VPC",
27
+ "AWS::EC2::VPNConnection",
28
+ "AWS::EC2::VPNGateway",
29
+ "AWS::EC2::NatGateway",
30
+ "AWS::EC2::EgressOnlyInternetGateway",
31
+ "AWS::EC2::FlowLog",
32
+ "AWS::EC2::TransitGateway",
33
+ "AWS::EC2::TransitGatewayAttachment",
34
+ "AWS::EC2::TransitGatewayRouteTable",
35
+ "AWS::EC2::LaunchTemplate",
36
+ # IAM
37
+ "AWS::IAM::User",
38
+ "AWS::IAM::Group",
39
+ "AWS::IAM::Role",
40
+ "AWS::IAM::Policy",
41
+ # S3
42
+ "AWS::S3::Bucket",
43
+ "AWS::S3::AccountPublicAccessBlock",
44
+ # Lambda
45
+ "AWS::Lambda::Function",
46
+ "AWS::Lambda::Alias",
47
+ # RDS
48
+ "AWS::RDS::DBInstance",
49
+ "AWS::RDS::DBCluster",
50
+ "AWS::RDS::DBClusterSnapshot",
51
+ "AWS::RDS::DBSecurityGroup",
52
+ "AWS::RDS::DBSnapshot",
53
+ "AWS::RDS::DBSubnetGroup",
54
+ "AWS::RDS::EventSubscription",
55
+ # DynamoDB
56
+ "AWS::DynamoDB::Table",
57
+ # CloudWatch
58
+ "AWS::Logs::LogGroup",
59
+ "AWS::CloudWatch::Alarm",
60
+ # SNS
61
+ "AWS::SNS::Topic",
62
+ # SQS
63
+ "AWS::SQS::Queue",
64
+ # ELB
65
+ "AWS::ElasticLoadBalancing::LoadBalancer",
66
+ "AWS::ElasticLoadBalancingV2::LoadBalancer",
67
+ "AWS::ElasticLoadBalancingV2::TargetGroup",
68
+ # ECS
69
+ "AWS::ECS::Cluster",
70
+ "AWS::ECS::Service",
71
+ "AWS::ECS::TaskDefinition",
72
+ # EKS
73
+ "AWS::EKS::Cluster",
74
+ "AWS::EKS::FargateProfile",
75
+ "AWS::EKS::Nodegroup",
76
+ # KMS
77
+ "AWS::KMS::Key",
78
+ # Secrets Manager
79
+ "AWS::SecretsManager::Secret",
80
+ # API Gateway
81
+ "AWS::ApiGateway::RestApi",
82
+ "AWS::ApiGateway::Stage",
83
+ "AWS::ApiGatewayV2::Api",
84
+ "AWS::ApiGatewayV2::Stage",
85
+ # CloudFormation
86
+ "AWS::CloudFormation::Stack",
87
+ # Auto Scaling
88
+ "AWS::AutoScaling::AutoScalingGroup",
89
+ "AWS::AutoScaling::LaunchConfiguration",
90
+ "AWS::AutoScaling::ScalingPolicy",
91
+ # CloudTrail
92
+ "AWS::CloudTrail::Trail",
93
+ # CodeBuild
94
+ "AWS::CodeBuild::Project",
95
+ # CodePipeline
96
+ "AWS::CodePipeline::Pipeline",
97
+ # Config
98
+ "AWS::Config::ConfigurationRecorder",
99
+ "AWS::Config::ConformancePackCompliance",
100
+ "AWS::Config::ResourceCompliance",
101
+ # Elasticsearch/OpenSearch
102
+ "AWS::Elasticsearch::Domain",
103
+ "AWS::OpenSearch::Domain",
104
+ # ElastiCache
105
+ "AWS::ElastiCache::CacheCluster",
106
+ "AWS::ElastiCache::ReplicationGroup",
107
+ # EFS
108
+ "AWS::EFS::FileSystem",
109
+ "AWS::EFS::AccessPoint",
110
+ # EventBridge
111
+ "AWS::Events::Rule",
112
+ "AWS::Events::EventBus",
113
+ # Kinesis
114
+ "AWS::Kinesis::Stream",
115
+ "AWS::KinesisFirehose::DeliveryStream",
116
+ # Redshift
117
+ "AWS::Redshift::Cluster",
118
+ "AWS::Redshift::ClusterParameterGroup",
119
+ "AWS::Redshift::ClusterSecurityGroup",
120
+ "AWS::Redshift::ClusterSubnetGroup",
121
+ # SSM
122
+ "AWS::SSM::ManagedInstanceInventory",
123
+ "AWS::SSM::PatchCompliance",
124
+ "AWS::SSM::AssociationCompliance",
125
+ "AWS::SSM::FileData",
126
+ # Step Functions
127
+ "AWS::StepFunctions::StateMachine",
128
+ "AWS::StepFunctions::Activity",
129
+ # VPC
130
+ "AWS::EC2::VPCEndpoint",
131
+ "AWS::EC2::VPCEndpointService",
132
+ "AWS::EC2::VPCPeeringConnection",
133
+ # ACM
134
+ "AWS::ACM::Certificate",
135
+ # Backup
136
+ "AWS::Backup::BackupPlan",
137
+ "AWS::Backup::BackupSelection",
138
+ "AWS::Backup::BackupVault",
139
+ "AWS::Backup::RecoveryPoint",
140
+ # Network Firewall
141
+ "AWS::NetworkFirewall::Firewall",
142
+ "AWS::NetworkFirewall::FirewallPolicy",
143
+ "AWS::NetworkFirewall::RuleGroup",
144
+ # WAF
145
+ "AWS::WAF::RateBasedRule",
146
+ "AWS::WAF::Rule",
147
+ "AWS::WAF::RuleGroup",
148
+ "AWS::WAF::WebACL",
149
+ "AWS::WAFv2::WebACL",
150
+ "AWS::WAFv2::RuleGroup",
151
+ "AWS::WAFv2::IPSet",
152
+ "AWS::WAFv2::RegexPatternSet",
153
+ "AWS::WAFv2::ManagedRuleSet",
154
+ # Shield
155
+ "AWS::Shield::Protection",
156
+ "AWS::ShieldRegional::Protection",
157
+ # Systems Manager
158
+ "AWS::SSM::Parameter",
159
+ # Route53 (limited support)
160
+ "AWS::Route53::HostedZone",
161
+ # Glue
162
+ "AWS::Glue::Job",
163
+ "AWS::Glue::Classifier",
164
+ "AWS::Glue::Crawler",
165
+ "AWS::Glue::Database",
166
+ "AWS::Glue::Table",
167
+ # Athena
168
+ "AWS::Athena::WorkGroup",
169
+ "AWS::Athena::DataCatalog",
170
+ # EMR
171
+ "AWS::EMR::Cluster",
172
+ "AWS::EMR::SecurityConfiguration",
173
+ # SageMaker
174
+ "AWS::SageMaker::CodeRepository",
175
+ "AWS::SageMaker::Model",
176
+ "AWS::SageMaker::NotebookInstance",
177
+ "AWS::SageMaker::Workteam",
178
+ }
179
+
180
+ # Resource types that should ALWAYS use direct API collectors
181
+ # (even if technically supported by Config, the direct API is better)
182
+ DIRECT_API_ONLY_TYPES: Set[str] = {
183
+ # Route53 has better data via direct API
184
+ "AWS::Route53::HostedZone",
185
+ # WAF has complex regional/CloudFront distinction
186
+ "AWS::WAFv2::WebACL::Regional",
187
+ "AWS::WAFv2::WebACL::CloudFront",
188
+ }
189
+
190
+ # Mapping from collector service_name to AWS Config resource types
191
+ COLLECTOR_TO_CONFIG_TYPES: dict[str, list[str]] = {
192
+ "ec2": [
193
+ "AWS::EC2::Instance",
194
+ "AWS::EC2::Volume",
195
+ "AWS::EC2::VPC",
196
+ "AWS::EC2::SecurityGroup",
197
+ "AWS::EC2::Subnet",
198
+ "AWS::EC2::NatGateway",
199
+ "AWS::EC2::InternetGateway",
200
+ "AWS::EC2::RouteTable",
201
+ "AWS::EC2::NetworkAcl",
202
+ "AWS::EC2::NetworkInterface",
203
+ "AWS::EC2::EIP",
204
+ ],
205
+ "iam": [
206
+ "AWS::IAM::Role",
207
+ "AWS::IAM::User",
208
+ "AWS::IAM::Group",
209
+ "AWS::IAM::Policy",
210
+ ],
211
+ "s3": [
212
+ "AWS::S3::Bucket",
213
+ ],
214
+ "lambda": [
215
+ "AWS::Lambda::Function",
216
+ ],
217
+ "rds": [
218
+ "AWS::RDS::DBInstance",
219
+ "AWS::RDS::DBCluster",
220
+ ],
221
+ "dynamodb": [
222
+ "AWS::DynamoDB::Table",
223
+ ],
224
+ "cloudwatch": [
225
+ "AWS::Logs::LogGroup",
226
+ "AWS::CloudWatch::Alarm",
227
+ ],
228
+ "sns": [
229
+ "AWS::SNS::Topic",
230
+ ],
231
+ "sqs": [
232
+ "AWS::SQS::Queue",
233
+ ],
234
+ "elb": [
235
+ "AWS::ElasticLoadBalancing::LoadBalancer",
236
+ "AWS::ElasticLoadBalancingV2::LoadBalancer",
237
+ "AWS::ElasticLoadBalancingV2::TargetGroup",
238
+ ],
239
+ "ecs": [
240
+ "AWS::ECS::Cluster",
241
+ "AWS::ECS::Service",
242
+ "AWS::ECS::TaskDefinition",
243
+ ],
244
+ "eks": [
245
+ "AWS::EKS::Cluster",
246
+ "AWS::EKS::Nodegroup",
247
+ "AWS::EKS::FargateProfile",
248
+ ],
249
+ "kms": [
250
+ "AWS::KMS::Key",
251
+ ],
252
+ "secretsmanager": [
253
+ "AWS::SecretsManager::Secret",
254
+ ],
255
+ "apigateway": [
256
+ "AWS::ApiGateway::RestApi",
257
+ "AWS::ApiGateway::Stage",
258
+ "AWS::ApiGatewayV2::Api",
259
+ ],
260
+ "cloudformation": [
261
+ "AWS::CloudFormation::Stack",
262
+ ],
263
+ "codebuild": [
264
+ "AWS::CodeBuild::Project",
265
+ ],
266
+ "codepipeline": [
267
+ "AWS::CodePipeline::Pipeline",
268
+ ],
269
+ "elasticache": [
270
+ "AWS::ElastiCache::CacheCluster",
271
+ "AWS::ElastiCache::ReplicationGroup",
272
+ ],
273
+ "efs": [
274
+ "AWS::EFS::FileSystem",
275
+ "AWS::EFS::AccessPoint",
276
+ ],
277
+ "eventbridge": [
278
+ "AWS::Events::Rule",
279
+ "AWS::Events::EventBus",
280
+ ],
281
+ "stepfunctions": [
282
+ "AWS::StepFunctions::StateMachine",
283
+ ],
284
+ "backup": [
285
+ "AWS::Backup::BackupPlan",
286
+ "AWS::Backup::BackupVault",
287
+ ],
288
+ "ssm": [
289
+ "AWS::SSM::Parameter",
290
+ ],
291
+ "vpcendpoints": [
292
+ "AWS::EC2::VPCEndpoint",
293
+ ],
294
+ "waf": [
295
+ "AWS::WAFv2::WebACL",
296
+ "AWS::WAFv2::RuleGroup",
297
+ "AWS::WAFv2::IPSet",
298
+ ],
299
+ "route53": [
300
+ "AWS::Route53::HostedZone",
301
+ ],
302
+ }
303
+
304
+
305
+ def is_config_supported_type(resource_type: str) -> bool:
306
+ """Check if a resource type is supported by AWS Config.
307
+
308
+ Args:
309
+ resource_type: AWS resource type (e.g., "AWS::EC2::Instance")
310
+
311
+ Returns:
312
+ True if the type can be collected via AWS Config
313
+ """
314
+ if resource_type in DIRECT_API_ONLY_TYPES:
315
+ return False
316
+ return resource_type in CONFIG_SUPPORTED_TYPES
317
+
318
+
319
+ def get_config_types_for_service(service_name: str) -> list[str]:
320
+ """Get AWS Config resource types for a collector service.
321
+
322
+ Args:
323
+ service_name: Collector service name (e.g., "ec2", "s3")
324
+
325
+ Returns:
326
+ List of AWS Config resource types for this service
327
+ """
328
+ return COLLECTOR_TO_CONFIG_TYPES.get(service_name, [])
src/cost/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Cost analysis module for tracking baseline vs non-baseline costs."""
2
+
3
+ from typing import List
4
+
5
+ __all__: List[str] = []
src/cost/analyzer.py ADDED
@@ -0,0 +1,226 @@
1
+ """Cost analyzer for inventory snapshots."""
2
+
3
+ import logging
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from datetime import datetime, timedelta
6
+ from typing import Any, Dict, Optional, Set
7
+
8
+ from ..models.cost_report import CostBreakdown, CostReport
9
+ from ..models.snapshot import Snapshot
10
+ from .explorer import CostExplorerClient
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CostAnalyzer:
16
+ """Analyze costs for inventory snapshots."""
17
+
18
+ def __init__(self, cost_explorer: CostExplorerClient):
19
+ """Initialize cost analyzer.
20
+
21
+ Args:
22
+ cost_explorer: Cost Explorer client instance
23
+ """
24
+ self.cost_explorer = cost_explorer
25
+
26
+ def analyze(
27
+ self,
28
+ snapshot: Snapshot,
29
+ start_date: Optional[datetime] = None,
30
+ end_date: Optional[datetime] = None,
31
+ granularity: str = "MONTHLY",
32
+ has_deltas: bool = False,
33
+ delta_report: Optional[Any] = None,
34
+ ) -> CostReport:
35
+ """Analyze costs and separate snapshot resources.
36
+
37
+ This implementation uses a simplified heuristic approach:
38
+ 1. Get total costs by service
39
+ 2. Estimate baseline portion based on resource counts in snapshot
40
+ 3. Remaining costs are attributed to non-baseline resources
41
+
42
+ Note: For precise cost attribution, AWS would need to provide
43
+ per-resource cost data, which Cost Explorer doesn't directly expose.
44
+ This gives a good approximation based on service-level costs.
45
+
46
+ Args:
47
+ snapshot: The baseline snapshot
48
+ start_date: Start date for cost analysis (default: snapshot date)
49
+ end_date: End date for cost analysis (default: today)
50
+ granularity: Cost granularity - DAILY or MONTHLY
51
+
52
+ Returns:
53
+ CostReport with baseline and non-baseline cost breakdown
54
+ """
55
+ # Default date range: from snapshot creation to today
56
+ if not start_date:
57
+ start_date = snapshot.created_at
58
+ # Remove timezone for Cost Explorer API (uses dates only, no time)
59
+ start_date = start_date.replace(tzinfo=None)
60
+
61
+ if not end_date:
62
+ end_date = datetime.now()
63
+
64
+ # Ensure both dates are timezone-naive for comparison
65
+ if hasattr(start_date, "tzinfo") and start_date.tzinfo is not None:
66
+ start_date = start_date.replace(tzinfo=None)
67
+ if hasattr(end_date, "tzinfo") and end_date.tzinfo is not None:
68
+ end_date = end_date.replace(tzinfo=None)
69
+
70
+ # Ensure start_date is before end_date
71
+ # AWS Cost Explorer requires at least 1 day difference
72
+ if start_date >= end_date:
73
+ # If dates are the same or inverted, set end_date to start_date + 1 day
74
+ end_date = start_date + timedelta(days=1)
75
+
76
+ logger.debug(f"Analyzing costs from {start_date.strftime('%Y-%m-%d')} " f"to {end_date.strftime('%Y-%m-%d')}")
77
+
78
+ # Execute data completeness check and cost retrieval in parallel
79
+ with ThreadPoolExecutor(max_workers=2) as executor:
80
+ # Submit both tasks
81
+ completeness_future = executor.submit(self.cost_explorer.check_data_completeness, end_date)
82
+ costs_future = executor.submit(self.cost_explorer.get_costs_by_service, start_date, end_date, granularity)
83
+
84
+ # Wait for both to complete
85
+ is_complete, data_through, lag_days = completeness_future.result()
86
+ service_costs = costs_future.result()
87
+
88
+ # If no deltas (no resource changes), ALL costs are baseline
89
+ if not has_deltas:
90
+ logger.debug("No resource changes detected - all costs are from snapshot resources")
91
+ baseline_costs = service_costs.copy()
92
+ non_baseline_costs: Dict[str, float] = {}
93
+ baseline_total = sum(baseline_costs.values())
94
+ non_baseline_total = 0.0
95
+ total_cost = baseline_total
96
+ baseline_pct = 100.0
97
+ non_baseline_pct = 0.0
98
+ else:
99
+ # There are deltas - we can't accurately split costs without per-resource pricing
100
+ # Show total only with a note that we can't split accurately
101
+ logger.debug("Resource changes detected - showing total costs only")
102
+ baseline_costs = service_costs.copy()
103
+ non_baseline_costs = {}
104
+ baseline_total = sum(baseline_costs.values())
105
+ non_baseline_total = 0.0
106
+ total_cost = baseline_total
107
+ baseline_pct = 100.0
108
+ non_baseline_pct = 0.0
109
+ # Note: We could enhance this in the future to track specific resource costs
110
+ # For now, we show total and list the delta resources separately
111
+
112
+ # Create cost breakdowns
113
+ baseline_breakdown = CostBreakdown(
114
+ total=baseline_total,
115
+ by_service=baseline_costs,
116
+ percentage=baseline_pct,
117
+ )
118
+
119
+ non_baseline_breakdown = CostBreakdown(
120
+ total=non_baseline_total,
121
+ by_service=non_baseline_costs,
122
+ percentage=non_baseline_pct,
123
+ )
124
+
125
+ # Create cost report
126
+ report = CostReport(
127
+ generated_at=datetime.now(),
128
+ baseline_snapshot_name=snapshot.name,
129
+ period_start=start_date,
130
+ period_end=end_date,
131
+ baseline_costs=baseline_breakdown,
132
+ non_baseline_costs=non_baseline_breakdown,
133
+ total_cost=total_cost,
134
+ data_complete=is_complete,
135
+ data_through=data_through,
136
+ lag_days=lag_days,
137
+ )
138
+
139
+ logger.info(
140
+ f"Cost analysis complete: Baseline=${baseline_total:.2f}, "
141
+ f"Non-baseline=${non_baseline_total:.2f}, Total=${total_cost:.2f}"
142
+ )
143
+
144
+ return report
145
+
146
+ def _get_baseline_service_mapping(self, snapshot: Snapshot) -> Set[str]:
147
+ """Get set of AWS service names that have baseline resources.
148
+
149
+ Maps our resource types (e.g., 'AWS::EC2::Instance') to Cost Explorer
150
+ service names (e.g., 'Amazon Elastic Compute Cloud - Compute').
151
+
152
+ Args:
153
+ snapshot: Baseline snapshot
154
+
155
+ Returns:
156
+ Set of AWS service names from Cost Explorer
157
+ """
158
+ # Mapping from our resource types to Cost Explorer service names
159
+ service_name_map = {
160
+ "AWS::EC2::Instance": "Amazon Elastic Compute Cloud - Compute",
161
+ "AWS::EC2::Volume": "Amazon Elastic Compute Cloud - Compute",
162
+ "AWS::EC2::VPC": "Amazon Elastic Compute Cloud - Compute",
163
+ "AWS::EC2::SecurityGroup": "Amazon Elastic Compute Cloud - Compute",
164
+ "AWS::EC2::Subnet": "Amazon Elastic Compute Cloud - Compute",
165
+ "AWS::EC2::VPCEndpoint::Interface": "Amazon Elastic Compute Cloud - Compute",
166
+ "AWS::EC2::VPCEndpoint::Gateway": "Amazon Elastic Compute Cloud - Compute",
167
+ "AWS::Lambda::Function": "AWS Lambda",
168
+ "AWS::Lambda::LayerVersion": "AWS Lambda",
169
+ "AWS::S3::Bucket": "Amazon Simple Storage Service",
170
+ "AWS::RDS::DBInstance": "Amazon Relational Database Service",
171
+ "AWS::RDS::DBCluster": "Amazon Relational Database Service",
172
+ "AWS::IAM::Role": "AWS Identity and Access Management",
173
+ "AWS::IAM::User": "AWS Identity and Access Management",
174
+ "AWS::IAM::Policy": "AWS Identity and Access Management",
175
+ "AWS::IAM::Group": "AWS Identity and Access Management",
176
+ "AWS::CloudWatch::Alarm": "Amazon CloudWatch",
177
+ "AWS::CloudWatch::CompositeAlarm": "Amazon CloudWatch",
178
+ "AWS::Logs::LogGroup": "Amazon CloudWatch",
179
+ "AWS::SNS::Topic": "Amazon Simple Notification Service",
180
+ "AWS::SQS::Queue": "Amazon Simple Queue Service",
181
+ "AWS::DynamoDB::Table": "Amazon DynamoDB",
182
+ "AWS::ElasticLoadBalancing::LoadBalancer": "Elastic Load Balancing",
183
+ "AWS::ElasticLoadBalancingV2::LoadBalancer::Application": "Elastic Load Balancing",
184
+ "AWS::ElasticLoadBalancingV2::LoadBalancer::Network": "Elastic Load Balancing",
185
+ "AWS::ElasticLoadBalancingV2::LoadBalancer::Gateway": "Elastic Load Balancing",
186
+ "AWS::CloudFormation::Stack": "AWS CloudFormation",
187
+ "AWS::ApiGateway::RestApi": "Amazon API Gateway",
188
+ "AWS::ApiGatewayV2::Api::HTTP": "Amazon API Gateway",
189
+ "AWS::ApiGatewayV2::Api::WebSocket": "Amazon API Gateway",
190
+ "AWS::Events::EventBus": "Amazon EventBridge",
191
+ "AWS::Events::Rule": "Amazon EventBridge",
192
+ "AWS::SecretsManager::Secret": "AWS Secrets Manager",
193
+ "AWS::KMS::Key": "AWS Key Management Service",
194
+ "AWS::SSM::Parameter": "AWS Systems Manager",
195
+ "AWS::SSM::Document": "AWS Systems Manager",
196
+ "AWS::Route53::HostedZone": "Amazon Route 53",
197
+ "AWS::ECS::Cluster": "Amazon EC2 Container Service",
198
+ "AWS::ECS::Service": "Amazon EC2 Container Service",
199
+ "AWS::ECS::TaskDefinition": "Amazon EC2 Container Service",
200
+ "AWS::StepFunctions::StateMachine": "AWS Step Functions",
201
+ "AWS::WAFv2::WebACL::Regional": "AWS WAF",
202
+ "AWS::WAFv2::WebACL::CloudFront": "AWS WAF",
203
+ "AWS::EKS::Cluster": "Amazon Elastic Kubernetes Service",
204
+ "AWS::EKS::Nodegroup": "Amazon Elastic Kubernetes Service",
205
+ "AWS::EKS::FargateProfile": "Amazon Elastic Kubernetes Service",
206
+ "AWS::CodePipeline::Pipeline": "AWS CodePipeline",
207
+ "AWS::CodeBuild::Project": "AWS CodeBuild",
208
+ "AWS::Backup::BackupPlan": "AWS Backup",
209
+ "AWS::Backup::BackupVault": "AWS Backup",
210
+ }
211
+
212
+ baseline_services = set()
213
+
214
+ # Get unique resource types from snapshot
215
+ resource_types = set()
216
+ for resource in snapshot.resources:
217
+ resource_types.add(resource.resource_type)
218
+
219
+ # Map to Cost Explorer service names
220
+ for resource_type in resource_types:
221
+ if resource_type in service_name_map:
222
+ baseline_services.add(service_name_map[resource_type])
223
+
224
+ logger.debug(f"Baseline services: {baseline_services}")
225
+
226
+ return baseline_services