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,21 @@
1
+ """AWS Config service integration for resource collection.
2
+
3
+ This module provides Config-first resource collection with fallback to direct API.
4
+ """
5
+
6
+ from .detector import ConfigAvailability, detect_config_availability
7
+ from .collector import ConfigResourceCollector
8
+ from .resource_type_mapping import (
9
+ CONFIG_SUPPORTED_TYPES,
10
+ DIRECT_API_ONLY_TYPES,
11
+ is_config_supported_type,
12
+ )
13
+
14
+ __all__ = [
15
+ "ConfigAvailability",
16
+ "detect_config_availability",
17
+ "ConfigResourceCollector",
18
+ "CONFIG_SUPPORTED_TYPES",
19
+ "DIRECT_API_ONLY_TYPES",
20
+ "is_config_supported_type",
21
+ ]
@@ -0,0 +1,346 @@
1
+ """AWS Config-based resource collector.
2
+
3
+ Collects resources using AWS Config APIs instead of direct service API calls.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from datetime import datetime, timezone
10
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ import boto3
14
+
15
+ from ..aws.client import create_boto_client
16
+ from ..models.resource import Resource
17
+ from ..utils.hash import compute_config_hash
18
+ from .detector import ConfigAvailability
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Maximum resources per batch_get_resource_config call
23
+ MAX_BATCH_SIZE = 100
24
+
25
+
26
+ class ConfigResourceCollector:
27
+ """Collect AWS resources using AWS Config APIs.
28
+
29
+ Uses list_discovered_resources to find resources and
30
+ batch_get_resource_config to get their configurations.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ session: "boto3.Session",
36
+ region: str,
37
+ profile_name: Optional[str] = None,
38
+ config_availability: Optional[ConfigAvailability] = None,
39
+ ):
40
+ """Initialize the Config resource collector.
41
+
42
+ Args:
43
+ session: Boto3 session
44
+ region: AWS region to collect from
45
+ profile_name: Optional AWS profile name
46
+ config_availability: Pre-computed Config availability (optional)
47
+ """
48
+ self.session = session
49
+ self.region = region
50
+ self.profile_name = profile_name or (
51
+ session.profile_name if hasattr(session, "profile_name") else None
52
+ )
53
+ self.config_availability = config_availability
54
+ self._client = None
55
+ self._account_id = None
56
+
57
+ @property
58
+ def client(self):
59
+ """Get or create the Config client."""
60
+ if self._client is None:
61
+ self._client = create_boto_client(
62
+ service_name="config",
63
+ region_name=self.region,
64
+ profile_name=self.profile_name,
65
+ )
66
+ return self._client
67
+
68
+ @property
69
+ def account_id(self) -> str:
70
+ """Get the AWS account ID."""
71
+ if self._account_id is None:
72
+ sts_client = create_boto_client(
73
+ service_name="sts",
74
+ region_name=self.region,
75
+ profile_name=self.profile_name,
76
+ )
77
+ self._account_id = sts_client.get_caller_identity()["Account"]
78
+ return self._account_id
79
+
80
+ def collect_by_type(self, resource_type: str) -> List[Resource]:
81
+ """Collect all resources of a specific type using AWS Config.
82
+
83
+ Args:
84
+ resource_type: AWS resource type (e.g., "AWS::EC2::Instance")
85
+
86
+ Returns:
87
+ List of Resource objects
88
+ """
89
+ resources = []
90
+
91
+ try:
92
+ # Step 1: List all discovered resources of this type
93
+ resource_ids = self._list_discovered_resources(resource_type)
94
+
95
+ if not resource_ids:
96
+ logger.debug(f"No {resource_type} resources found via Config in {self.region}")
97
+ return resources
98
+
99
+ logger.debug(
100
+ f"Found {len(resource_ids)} {resource_type} resources via Config in {self.region}"
101
+ )
102
+
103
+ # Step 2: Batch get configurations
104
+ config_items = self._batch_get_resource_configs(resource_type, resource_ids)
105
+
106
+ # Step 3: Convert to Resource model
107
+ for config_item in config_items:
108
+ try:
109
+ resource = self._normalize_config_item(config_item)
110
+ if resource:
111
+ resources.append(resource)
112
+ except Exception as e:
113
+ logger.warning(
114
+ f"Failed to normalize Config item for {resource_type}: {e}"
115
+ )
116
+
117
+ except Exception as e:
118
+ logger.error(f"Error collecting {resource_type} via Config in {self.region}: {e}")
119
+ raise
120
+
121
+ return resources
122
+
123
+ def _list_discovered_resources(self, resource_type: str) -> List[str]:
124
+ """List all discovered resources of a type.
125
+
126
+ Args:
127
+ resource_type: AWS resource type
128
+
129
+ Returns:
130
+ List of resource IDs
131
+ """
132
+ resource_ids = []
133
+
134
+ try:
135
+ paginator = self.client.get_paginator("list_discovered_resources")
136
+
137
+ for page in paginator.paginate(resourceType=resource_type):
138
+ for resource_id_info in page.get("resourceIdentifiers", []):
139
+ resource_id = resource_id_info.get("resourceId")
140
+ if resource_id:
141
+ resource_ids.append(resource_id)
142
+
143
+ except Exception as e:
144
+ logger.error(f"Error listing {resource_type} resources in {self.region}: {e}")
145
+ raise
146
+
147
+ return resource_ids
148
+
149
+ def _batch_get_resource_configs(
150
+ self, resource_type: str, resource_ids: List[str]
151
+ ) -> List[Dict[str, Any]]:
152
+ """Batch get resource configurations.
153
+
154
+ Args:
155
+ resource_type: AWS resource type
156
+ resource_ids: List of resource IDs to fetch
157
+
158
+ Returns:
159
+ List of configuration items from Config
160
+ """
161
+ config_items = []
162
+
163
+ # Process in batches of MAX_BATCH_SIZE
164
+ for i in range(0, len(resource_ids), MAX_BATCH_SIZE):
165
+ batch_ids = resource_ids[i : i + MAX_BATCH_SIZE]
166
+
167
+ resource_keys = [
168
+ {"resourceType": resource_type, "resourceId": rid} for rid in batch_ids
169
+ ]
170
+
171
+ try:
172
+ response = self.client.batch_get_resource_config(
173
+ resourceKeys=resource_keys
174
+ )
175
+
176
+ # Get base configuration items
177
+ base_items = response.get("baseConfigurationItems", [])
178
+ config_items.extend(base_items)
179
+
180
+ # Log any unprocessed keys
181
+ unprocessed = response.get("unprocessedResourceKeys", [])
182
+ if unprocessed:
183
+ logger.warning(
184
+ f"Config could not process {len(unprocessed)} {resource_type} resources"
185
+ )
186
+
187
+ except Exception as e:
188
+ logger.error(
189
+ f"Error batch getting {resource_type} configs in {self.region}: {e}"
190
+ )
191
+ # Continue with other batches
192
+ continue
193
+
194
+ return config_items
195
+
196
+ def _normalize_config_item(self, config_item: Dict[str, Any]) -> Optional[Resource]:
197
+ """Convert an AWS Config item to our Resource model.
198
+
199
+ Args:
200
+ config_item: Config item from batch_get_resource_config
201
+
202
+ Returns:
203
+ Resource object or None if conversion fails
204
+ """
205
+ try:
206
+ # Extract basic fields
207
+ resource_type = config_item.get("resourceType", "")
208
+ resource_id = config_item.get("resourceId", "")
209
+ arn = config_item.get("arn", "")
210
+ resource_name = config_item.get("resourceName", resource_id)
211
+
212
+ # If no ARN provided, try to construct one
213
+ if not arn:
214
+ arn = self._construct_arn(resource_type, resource_id)
215
+
216
+ # Get the configuration data
217
+ # Config stores this as a JSON string in 'configuration'
218
+ import json
219
+
220
+ configuration_str = config_item.get("configuration", "{}")
221
+ if isinstance(configuration_str, str):
222
+ raw_config = json.loads(configuration_str)
223
+ else:
224
+ raw_config = configuration_str
225
+
226
+ # Add Config-specific metadata to raw_config
227
+ raw_config["_config_metadata"] = {
228
+ "configurationItemCaptureTime": config_item.get(
229
+ "configurationItemCaptureTime"
230
+ ),
231
+ "configurationStateId": config_item.get("configurationStateId"),
232
+ "awsAccountId": config_item.get("accountId"),
233
+ "configurationItemStatus": config_item.get("configurationItemStatus"),
234
+ }
235
+
236
+ # Extract tags (Config stores supplementary configuration)
237
+ tags = {}
238
+ supplementary_config = config_item.get("supplementaryConfiguration", {})
239
+ if "Tags" in supplementary_config:
240
+ tags_data = supplementary_config["Tags"]
241
+ if isinstance(tags_data, str):
242
+ tags_list = json.loads(tags_data)
243
+ else:
244
+ tags_list = tags_data
245
+ if isinstance(tags_list, list):
246
+ for tag in tags_list:
247
+ if isinstance(tag, dict) and "Key" in tag and "Value" in tag:
248
+ tags[tag["Key"]] = tag["Value"]
249
+ elif isinstance(tags_list, dict):
250
+ tags = tags_list
251
+
252
+ # Parse creation time if available
253
+ created_at = None
254
+ capture_time = config_item.get("configurationItemCaptureTime")
255
+ if capture_time:
256
+ if isinstance(capture_time, datetime):
257
+ created_at = capture_time
258
+ elif isinstance(capture_time, str):
259
+ try:
260
+ created_at = datetime.fromisoformat(
261
+ capture_time.replace("Z", "+00:00")
262
+ )
263
+ except ValueError:
264
+ pass
265
+
266
+ # Determine region
267
+ region = config_item.get("awsRegion", self.region)
268
+ if resource_type.startswith("AWS::IAM::"):
269
+ region = "global"
270
+
271
+ # Create Resource
272
+ resource = Resource(
273
+ arn=arn,
274
+ resource_type=resource_type,
275
+ name=resource_name,
276
+ region=region,
277
+ tags=tags,
278
+ config_hash=compute_config_hash(raw_config),
279
+ created_at=created_at,
280
+ raw_config=raw_config,
281
+ source="config",
282
+ )
283
+
284
+ return resource
285
+
286
+ except Exception as e:
287
+ logger.warning(f"Failed to normalize Config item: {e}")
288
+ return None
289
+
290
+ def _construct_arn(self, resource_type: str, resource_id: str) -> str:
291
+ """Construct an ARN for a resource.
292
+
293
+ Args:
294
+ resource_type: AWS resource type
295
+ resource_id: Resource ID
296
+
297
+ Returns:
298
+ Constructed ARN string
299
+ """
300
+ # Parse resource type: AWS::Service::Type
301
+ parts = resource_type.split("::")
302
+ if len(parts) != 3:
303
+ return f"arn:aws:unknown:{self.region}:{self.account_id}:{resource_id}"
304
+
305
+ service = parts[1].lower()
306
+ type_name = parts[2].lower()
307
+
308
+ # Service-specific ARN formats
309
+ if service == "s3":
310
+ return f"arn:aws:s3:::{resource_id}"
311
+ elif service == "iam":
312
+ return f"arn:aws:iam::{self.account_id}:{type_name}/{resource_id}"
313
+ elif service == "lambda":
314
+ return f"arn:aws:lambda:{self.region}:{self.account_id}:function:{resource_id}"
315
+ elif service == "dynamodb":
316
+ return f"arn:aws:dynamodb:{self.region}:{self.account_id}:table/{resource_id}"
317
+ elif service == "sns":
318
+ return f"arn:aws:sns:{self.region}:{self.account_id}:{resource_id}"
319
+ elif service == "sqs":
320
+ return f"arn:aws:sqs:{self.region}:{self.account_id}:{resource_id}"
321
+ elif service == "logs":
322
+ return f"arn:aws:logs:{self.region}:{self.account_id}:log-group:{resource_id}"
323
+ else:
324
+ # Generic format
325
+ return f"arn:aws:{service}:{self.region}:{self.account_id}:{type_name}/{resource_id}"
326
+
327
+ def collect_multiple_types(self, resource_types: List[str]) -> List[Resource]:
328
+ """Collect resources of multiple types.
329
+
330
+ Args:
331
+ resource_types: List of AWS resource types
332
+
333
+ Returns:
334
+ List of all collected resources
335
+ """
336
+ all_resources = []
337
+
338
+ for resource_type in resource_types:
339
+ try:
340
+ resources = self.collect_by_type(resource_type)
341
+ all_resources.extend(resources)
342
+ except Exception as e:
343
+ logger.error(f"Failed to collect {resource_type}: {e}")
344
+ # Continue with other types
345
+
346
+ return all_resources
@@ -0,0 +1,256 @@
1
+ """AWS Config availability detection.
2
+
3
+ Detects if AWS Config is enabled in a region and what resource types it supports.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, List, Optional, Set
11
+
12
+ if TYPE_CHECKING:
13
+ import boto3
14
+
15
+ from ..aws.client import create_boto_client
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ConfigAvailability:
22
+ """AWS Config availability status for a region.
23
+
24
+ Attributes:
25
+ region: AWS region
26
+ is_enabled: Whether Config is enabled and recording
27
+ recorder_name: Name of the configuration recorder (if enabled)
28
+ recording_group_all_supported: Whether recorder captures all supported types
29
+ resource_types_recorded: Specific resource types being recorded
30
+ delivery_channel_configured: Whether a delivery channel is set up
31
+ error_message: Error message if detection failed
32
+ """
33
+
34
+ region: str
35
+ is_enabled: bool = False
36
+ recorder_name: Optional[str] = None
37
+ recording_group_all_supported: bool = False
38
+ resource_types_recorded: Set[str] = field(default_factory=set)
39
+ delivery_channel_configured: bool = False
40
+ error_message: Optional[str] = None
41
+
42
+ def supports_resource_type(self, resource_type: str) -> bool:
43
+ """Check if this region's Config supports a specific resource type.
44
+
45
+ Args:
46
+ resource_type: AWS resource type (e.g., "AWS::EC2::Instance")
47
+
48
+ Returns:
49
+ True if Config can collect this resource type in this region
50
+ """
51
+ if not self.is_enabled:
52
+ return False
53
+
54
+ # If recording all supported types, check against the global list
55
+ if self.recording_group_all_supported:
56
+ from .resource_type_mapping import is_config_supported_type
57
+
58
+ return is_config_supported_type(resource_type)
59
+
60
+ # Otherwise, check specific recorded types
61
+ return resource_type in self.resource_types_recorded
62
+
63
+
64
+ def detect_config_availability(
65
+ session: "boto3.Session",
66
+ region: str,
67
+ profile_name: Optional[str] = None,
68
+ ) -> ConfigAvailability:
69
+ """Detect AWS Config availability in a region.
70
+
71
+ Checks if AWS Config is enabled, has a recorder configured,
72
+ and what resource types it's recording.
73
+
74
+ Args:
75
+ session: Boto3 session
76
+ region: AWS region to check
77
+ profile_name: Optional AWS profile name
78
+
79
+ Returns:
80
+ ConfigAvailability with detection results
81
+ """
82
+ availability = ConfigAvailability(region=region)
83
+
84
+ try:
85
+ # Create Config client
86
+ profile = profile_name or (
87
+ session.profile_name if hasattr(session, "profile_name") else None
88
+ )
89
+ client = create_boto_client(
90
+ service_name="config",
91
+ region_name=region,
92
+ profile_name=profile,
93
+ )
94
+
95
+ # Check configuration recorders
96
+ recorders_response = client.describe_configuration_recorders()
97
+ recorders = recorders_response.get("ConfigurationRecorders", [])
98
+
99
+ if not recorders:
100
+ availability.error_message = "No configuration recorders found"
101
+ logger.debug(f"Config not enabled in {region}: no recorders")
102
+ return availability
103
+
104
+ # Get the first (usually only) recorder
105
+ recorder = recorders[0]
106
+ availability.recorder_name = recorder.get("name")
107
+
108
+ # Check recording group settings
109
+ recording_group = recorder.get("recordingGroup", {})
110
+ availability.recording_group_all_supported = recording_group.get(
111
+ "allSupported", False
112
+ )
113
+
114
+ # Get specific resource types if not recording all
115
+ if not availability.recording_group_all_supported:
116
+ resource_types = recording_group.get("resourceTypes", [])
117
+ availability.resource_types_recorded = set(resource_types)
118
+
119
+ # Check recorder status
120
+ status_response = client.describe_configuration_recorder_status()
121
+ statuses = status_response.get("ConfigurationRecordersStatus", [])
122
+
123
+ recorder_is_recording = False
124
+ for status in statuses:
125
+ if status.get("name") == availability.recorder_name:
126
+ recorder_is_recording = status.get("recording", False)
127
+ break
128
+
129
+ if not recorder_is_recording:
130
+ availability.error_message = "Configuration recorder is not recording"
131
+ logger.debug(f"Config recorder in {region} is not recording")
132
+ return availability
133
+
134
+ # Check delivery channel
135
+ try:
136
+ channels_response = client.describe_delivery_channels()
137
+ channels = channels_response.get("DeliveryChannels", [])
138
+ availability.delivery_channel_configured = len(channels) > 0
139
+ except Exception as e:
140
+ logger.debug(f"Could not check delivery channels in {region}: {e}")
141
+ # Not a critical failure, continue
142
+
143
+ # All checks passed
144
+ availability.is_enabled = True
145
+ logger.debug(
146
+ f"Config enabled in {region}: recorder={availability.recorder_name}, "
147
+ f"all_supported={availability.recording_group_all_supported}"
148
+ )
149
+
150
+ except client.exceptions.NoSuchConfigurationRecorderException:
151
+ availability.error_message = "No configuration recorder exists"
152
+ logger.debug(f"Config not enabled in {region}: no recorder exists")
153
+
154
+ except Exception as e:
155
+ availability.error_message = str(e)
156
+ logger.debug(f"Error detecting Config in {region}: {e}")
157
+
158
+ return availability
159
+
160
+
161
+ def detect_config_availability_multi_region(
162
+ session: "boto3.Session",
163
+ regions: List[str],
164
+ profile_name: Optional[str] = None,
165
+ ) -> dict[str, ConfigAvailability]:
166
+ """Detect AWS Config availability across multiple regions.
167
+
168
+ Args:
169
+ session: Boto3 session
170
+ regions: List of AWS regions to check
171
+ profile_name: Optional AWS profile name
172
+
173
+ Returns:
174
+ Dict mapping region to ConfigAvailability
175
+ """
176
+ results = {}
177
+ for region in regions:
178
+ results[region] = detect_config_availability(session, region, profile_name)
179
+ return results
180
+
181
+
182
+ def get_config_supported_resource_types(
183
+ session: "boto3.Session",
184
+ region: str,
185
+ profile_name: Optional[str] = None,
186
+ ) -> Set[str]:
187
+ """Get the set of resource types that AWS Config can collect in a region.
188
+
189
+ This queries what the Config recorder is actually recording, not just
190
+ what's theoretically supported.
191
+
192
+ Args:
193
+ session: Boto3 session
194
+ region: AWS region
195
+ profile_name: Optional AWS profile name
196
+
197
+ Returns:
198
+ Set of resource type strings (e.g., {"AWS::EC2::Instance", "AWS::S3::Bucket"})
199
+ """
200
+ availability = detect_config_availability(session, region, profile_name)
201
+
202
+ if not availability.is_enabled:
203
+ return set()
204
+
205
+ if availability.recording_group_all_supported:
206
+ # Return all Config-supported types
207
+ from .resource_type_mapping import CONFIG_SUPPORTED_TYPES
208
+
209
+ return CONFIG_SUPPORTED_TYPES.copy()
210
+
211
+ return availability.resource_types_recorded
212
+
213
+
214
+ def check_aggregator_availability(
215
+ session: "boto3.Session",
216
+ aggregator_name: str,
217
+ region: str = "us-east-1",
218
+ profile_name: Optional[str] = None,
219
+ ) -> tuple[bool, Optional[str]]:
220
+ """Check if a Config aggregator is available and accessible.
221
+
222
+ Args:
223
+ session: Boto3 session
224
+ aggregator_name: Name of the Config aggregator
225
+ region: Region where aggregator is configured
226
+ profile_name: Optional AWS profile name
227
+
228
+ Returns:
229
+ Tuple of (is_available, error_message)
230
+ """
231
+ try:
232
+ profile = profile_name or (
233
+ session.profile_name if hasattr(session, "profile_name") else None
234
+ )
235
+ client = create_boto_client(
236
+ service_name="config",
237
+ region_name=region,
238
+ profile_name=profile,
239
+ )
240
+
241
+ response = client.describe_configuration_aggregators(
242
+ ConfigurationAggregatorNames=[aggregator_name]
243
+ )
244
+
245
+ aggregators = response.get("ConfigurationAggregators", [])
246
+ if aggregators:
247
+ logger.debug(f"Config aggregator '{aggregator_name}' is available")
248
+ return True, None
249
+
250
+ return False, f"Aggregator '{aggregator_name}' not found"
251
+
252
+ except client.exceptions.NoSuchConfigurationAggregatorException:
253
+ return False, f"Aggregator '{aggregator_name}' does not exist"
254
+
255
+ except Exception as e:
256
+ return False, str(e)