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.
- aws_inventory_manager-0.13.2.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.13.2.dist-info/METADATA +1226 -0
- aws_inventory_manager-0.13.2.dist-info/RECORD +145 -0
- aws_inventory_manager-0.13.2.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.13.2.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.13.2.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 +3626 -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/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 +451 -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/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 +749 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +413 -0
- src/storage/schema.py +288 -0
- src/storage/snapshot_store.py +346 -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 +379 -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 +949 -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 +2251 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- 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)
|