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
src/snapshot/capturer.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Snapshot capture coordinator for AWS resources."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Type
|
|
8
|
+
|
|
9
|
+
import boto3
|
|
10
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
|
|
11
|
+
|
|
12
|
+
from ..models.snapshot import Snapshot
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .filter import ResourceFilter
|
|
16
|
+
|
|
17
|
+
# Import Config service integration
|
|
18
|
+
from ..config_service.collector import ConfigResourceCollector
|
|
19
|
+
from ..config_service.detector import (
|
|
20
|
+
ConfigAvailability,
|
|
21
|
+
detect_config_availability,
|
|
22
|
+
)
|
|
23
|
+
from ..config_service.resource_type_mapping import (
|
|
24
|
+
COLLECTOR_TO_CONFIG_TYPES,
|
|
25
|
+
is_config_supported_type,
|
|
26
|
+
)
|
|
27
|
+
from .resource_collectors.apigateway import APIGatewayCollector
|
|
28
|
+
from .resource_collectors.backup import BackupCollector
|
|
29
|
+
from .resource_collectors.base import BaseResourceCollector
|
|
30
|
+
from .resource_collectors.cloudformation import CloudFormationCollector
|
|
31
|
+
from .resource_collectors.cloudwatch import CloudWatchCollector
|
|
32
|
+
from .resource_collectors.codebuild import CodeBuildCollector
|
|
33
|
+
from .resource_collectors.codepipeline import CodePipelineCollector
|
|
34
|
+
from .resource_collectors.dynamodb import DynamoDBCollector
|
|
35
|
+
from .resource_collectors.ec2 import EC2Collector
|
|
36
|
+
from .resource_collectors.ecs import ECSCollector
|
|
37
|
+
from .resource_collectors.efs_collector import EFSCollector
|
|
38
|
+
from .resource_collectors.eks import EKSCollector
|
|
39
|
+
from .resource_collectors.elasticache_collector import ElastiCacheCollector
|
|
40
|
+
from .resource_collectors.elb import ELBCollector
|
|
41
|
+
from .resource_collectors.eventbridge import EventBridgeCollector
|
|
42
|
+
from .resource_collectors.iam import IAMCollector
|
|
43
|
+
from .resource_collectors.kms import KMSCollector
|
|
44
|
+
from .resource_collectors.lambda_func import LambdaCollector
|
|
45
|
+
from .resource_collectors.rds import RDSCollector
|
|
46
|
+
from .resource_collectors.route53 import Route53Collector
|
|
47
|
+
from .resource_collectors.s3 import S3Collector
|
|
48
|
+
from .resource_collectors.secretsmanager import SecretsManagerCollector
|
|
49
|
+
from .resource_collectors.sns import SNSCollector
|
|
50
|
+
from .resource_collectors.sqs import SQSCollector
|
|
51
|
+
from .resource_collectors.ssm import SSMCollector
|
|
52
|
+
from .resource_collectors.stepfunctions import StepFunctionsCollector
|
|
53
|
+
from .resource_collectors.vpcendpoints import VPCEndpointsCollector
|
|
54
|
+
from .resource_collectors.waf import WAFCollector
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Registry of all available collectors
|
|
60
|
+
COLLECTOR_REGISTRY: List[Type[BaseResourceCollector]] = [
|
|
61
|
+
IAMCollector,
|
|
62
|
+
LambdaCollector,
|
|
63
|
+
S3Collector,
|
|
64
|
+
EC2Collector,
|
|
65
|
+
RDSCollector,
|
|
66
|
+
CloudWatchCollector,
|
|
67
|
+
SNSCollector,
|
|
68
|
+
SQSCollector,
|
|
69
|
+
DynamoDBCollector,
|
|
70
|
+
ELBCollector,
|
|
71
|
+
EFSCollector,
|
|
72
|
+
ElastiCacheCollector,
|
|
73
|
+
CloudFormationCollector,
|
|
74
|
+
APIGatewayCollector,
|
|
75
|
+
EventBridgeCollector,
|
|
76
|
+
SecretsManagerCollector,
|
|
77
|
+
KMSCollector,
|
|
78
|
+
SSMCollector,
|
|
79
|
+
Route53Collector,
|
|
80
|
+
ECSCollector,
|
|
81
|
+
StepFunctionsCollector,
|
|
82
|
+
VPCEndpointsCollector,
|
|
83
|
+
WAFCollector,
|
|
84
|
+
EKSCollector,
|
|
85
|
+
CodePipelineCollector,
|
|
86
|
+
CodeBuildCollector,
|
|
87
|
+
BackupCollector,
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def create_snapshot(
|
|
92
|
+
name: str,
|
|
93
|
+
regions: List[str],
|
|
94
|
+
account_id: str,
|
|
95
|
+
profile_name: Optional[str] = None,
|
|
96
|
+
set_active: bool = True,
|
|
97
|
+
resource_types: Optional[List[str]] = None,
|
|
98
|
+
parallel_workers: int = 10,
|
|
99
|
+
resource_filter: Optional["ResourceFilter"] = None,
|
|
100
|
+
inventory_name: str = "default",
|
|
101
|
+
use_config: bool = True,
|
|
102
|
+
config_aggregator: Optional[str] = None,
|
|
103
|
+
) -> Snapshot:
|
|
104
|
+
"""Create a comprehensive snapshot of AWS resources.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name: Snapshot name
|
|
108
|
+
regions: List of AWS regions to scan
|
|
109
|
+
account_id: AWS account ID
|
|
110
|
+
profile_name: AWS profile name (optional)
|
|
111
|
+
set_active: Whether to set as active baseline
|
|
112
|
+
resource_types: Optional list of resource types to collect (e.g., ['iam', 'lambda'])
|
|
113
|
+
parallel_workers: Number of parallel collection tasks
|
|
114
|
+
resource_filter: Optional ResourceFilter for date/tag-based filtering
|
|
115
|
+
inventory_name: Name of inventory this snapshot belongs to (default: "default")
|
|
116
|
+
use_config: Use AWS Config for collection when available (default: True)
|
|
117
|
+
config_aggregator: Optional Config Aggregator name for multi-account collection
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Snapshot instance with captured resources
|
|
121
|
+
"""
|
|
122
|
+
logger.debug(f"Creating snapshot '{name}' for regions: {regions}")
|
|
123
|
+
if use_config:
|
|
124
|
+
logger.debug("AWS Config collection enabled (will fall back to direct API if unavailable)")
|
|
125
|
+
|
|
126
|
+
# Create session with optional profile
|
|
127
|
+
session_kwargs = {}
|
|
128
|
+
if profile_name:
|
|
129
|
+
session_kwargs["profile_name"] = profile_name
|
|
130
|
+
|
|
131
|
+
session = boto3.Session(**session_kwargs)
|
|
132
|
+
|
|
133
|
+
# Detect AWS Config availability per region if Config collection is enabled
|
|
134
|
+
config_availability: Dict[str, ConfigAvailability] = {}
|
|
135
|
+
collection_sources: Dict[str, str] = {} # Track source per resource type
|
|
136
|
+
|
|
137
|
+
if use_config:
|
|
138
|
+
logger.debug("Detecting AWS Config availability...")
|
|
139
|
+
for region in regions:
|
|
140
|
+
try:
|
|
141
|
+
availability = detect_config_availability(session, region, profile_name)
|
|
142
|
+
config_availability[region] = availability
|
|
143
|
+
if availability.is_enabled:
|
|
144
|
+
logger.debug(f" {region}: Config enabled (all_supported={availability.recording_group_all_supported})")
|
|
145
|
+
else:
|
|
146
|
+
logger.debug(f" {region}: Config not available ({availability.error_message})")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.debug(f" {region}: Config detection failed ({e})")
|
|
149
|
+
config_availability[region] = ConfigAvailability(region=region, error_message=str(e))
|
|
150
|
+
|
|
151
|
+
# Collect resources
|
|
152
|
+
all_resources = []
|
|
153
|
+
resource_counts = {} # Track counts per service for progress
|
|
154
|
+
collection_errors = [] # Track errors for summary
|
|
155
|
+
|
|
156
|
+
# Expected errors that we'll suppress (service not enabled, pagination issues, etc.)
|
|
157
|
+
expected_error_patterns = [
|
|
158
|
+
"Operation cannot be paginated",
|
|
159
|
+
"is not subscribed",
|
|
160
|
+
"AccessDenied",
|
|
161
|
+
"not authorized",
|
|
162
|
+
"InvalidAction",
|
|
163
|
+
"OptInRequired",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
def is_expected_error(error_msg: str) -> bool:
|
|
167
|
+
"""Check if error is expected and can be safely ignored."""
|
|
168
|
+
return any(pattern in error_msg for pattern in expected_error_patterns)
|
|
169
|
+
|
|
170
|
+
with Progress(
|
|
171
|
+
SpinnerColumn(),
|
|
172
|
+
TextColumn("[bold blue]{task.description}"),
|
|
173
|
+
BarColumn(),
|
|
174
|
+
TaskProgressColumn(),
|
|
175
|
+
) as progress:
|
|
176
|
+
# Determine which collectors to use
|
|
177
|
+
collectors_to_use = _get_collectors(resource_types)
|
|
178
|
+
|
|
179
|
+
# Separate global and regional collectors
|
|
180
|
+
# Create temporary instances to check is_global_service property
|
|
181
|
+
global_collectors = []
|
|
182
|
+
regional_collectors = []
|
|
183
|
+
for c in collectors_to_use:
|
|
184
|
+
temp_instance = c(session, "us-east-1")
|
|
185
|
+
if temp_instance.is_global_service:
|
|
186
|
+
global_collectors.append(c)
|
|
187
|
+
else:
|
|
188
|
+
regional_collectors.append(c)
|
|
189
|
+
|
|
190
|
+
total_tasks = len(global_collectors) + (len(regional_collectors) * len(regions))
|
|
191
|
+
main_task = progress.add_task(
|
|
192
|
+
f"[bold]Collecting AWS resources from {len(regions)} region(s)...", total=total_tasks
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Thread-safe lock for updating shared state
|
|
196
|
+
lock = Lock()
|
|
197
|
+
|
|
198
|
+
def collect_service(collector_class: Type[BaseResourceCollector], region: str, is_global: bool = False) -> Dict:
|
|
199
|
+
"""Collect resources for a single service in a region (thread-safe)."""
|
|
200
|
+
try:
|
|
201
|
+
collector = collector_class(session, region)
|
|
202
|
+
service_name = collector.service_name.upper()
|
|
203
|
+
region_label = "global" if is_global else region
|
|
204
|
+
|
|
205
|
+
# Update progress (thread-safe)
|
|
206
|
+
with lock:
|
|
207
|
+
progress.update(main_task, description=f"📦 {service_name} • {region_label}")
|
|
208
|
+
|
|
209
|
+
resources = []
|
|
210
|
+
source = "direct_api"
|
|
211
|
+
|
|
212
|
+
# Check if we should use AWS Config for this service/region
|
|
213
|
+
config_region = "us-east-1" if is_global else region
|
|
214
|
+
region_config = config_availability.get(config_region)
|
|
215
|
+
service_config_types = COLLECTOR_TO_CONFIG_TYPES.get(collector.service_name, [])
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
use_config
|
|
219
|
+
and region_config
|
|
220
|
+
and region_config.is_enabled
|
|
221
|
+
and service_config_types
|
|
222
|
+
):
|
|
223
|
+
# Try to collect via AWS Config
|
|
224
|
+
try:
|
|
225
|
+
config_collector = ConfigResourceCollector(
|
|
226
|
+
session, config_region, profile_name, region_config
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Collect each resource type this service handles
|
|
230
|
+
for config_type in service_config_types:
|
|
231
|
+
if region_config.supports_resource_type(config_type):
|
|
232
|
+
type_resources = config_collector.collect_by_type(config_type)
|
|
233
|
+
resources.extend(type_resources)
|
|
234
|
+
with lock:
|
|
235
|
+
collection_sources[config_type] = "config"
|
|
236
|
+
|
|
237
|
+
if resources:
|
|
238
|
+
source = "config"
|
|
239
|
+
logger.debug(
|
|
240
|
+
f"Collected {len(resources)} {service_name} resources via Config from {region_label}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
except Exception as config_error:
|
|
244
|
+
logger.debug(
|
|
245
|
+
f"Config collection failed for {service_name} in {region_label}, "
|
|
246
|
+
f"falling back to direct API: {config_error}"
|
|
247
|
+
)
|
|
248
|
+
resources = [] # Reset to trigger fallback
|
|
249
|
+
|
|
250
|
+
# Fall back to direct API if Config didn't work or isn't available
|
|
251
|
+
if not resources:
|
|
252
|
+
resources = collector.collect()
|
|
253
|
+
source = "direct_api"
|
|
254
|
+
# Track source for each resource type
|
|
255
|
+
for resource in resources:
|
|
256
|
+
with lock:
|
|
257
|
+
if resource.resource_type not in collection_sources:
|
|
258
|
+
collection_sources[resource.resource_type] = "direct_api"
|
|
259
|
+
|
|
260
|
+
logger.debug(f"Collected {len(resources)} {service_name} resources from {region_label} via {source}")
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"success": True,
|
|
264
|
+
"resources": resources,
|
|
265
|
+
"service": service_name,
|
|
266
|
+
"region": region_label,
|
|
267
|
+
"source": source,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
error_msg = str(e)
|
|
272
|
+
service_name = collector_class.__name__.replace("Collector", "").upper()
|
|
273
|
+
region_label = "global" if is_global else region
|
|
274
|
+
|
|
275
|
+
if not is_expected_error(error_msg):
|
|
276
|
+
logger.debug(f"Collection error - {service_name} ({region_label}): {error_msg[:80]}")
|
|
277
|
+
return {
|
|
278
|
+
"success": False,
|
|
279
|
+
"error": {"service": service_name, "region": region_label, "error": error_msg[:100]},
|
|
280
|
+
}
|
|
281
|
+
else:
|
|
282
|
+
logger.debug(f"Skipping {service_name} in {region_label} (not available): {error_msg[:80]}")
|
|
283
|
+
return {"success": False, "expected": True}
|
|
284
|
+
|
|
285
|
+
# Create list of collection tasks
|
|
286
|
+
collection_tasks = []
|
|
287
|
+
|
|
288
|
+
# Add global service tasks
|
|
289
|
+
for collector_class in global_collectors:
|
|
290
|
+
collection_tasks.append((collector_class, "us-east-1", True))
|
|
291
|
+
|
|
292
|
+
# Add regional service tasks
|
|
293
|
+
for region in regions:
|
|
294
|
+
for collector_class in regional_collectors:
|
|
295
|
+
collection_tasks.append((collector_class, region, False))
|
|
296
|
+
|
|
297
|
+
# Execute collections in parallel
|
|
298
|
+
with ThreadPoolExecutor(max_workers=parallel_workers) as executor:
|
|
299
|
+
# Submit all tasks
|
|
300
|
+
future_to_task = {
|
|
301
|
+
executor.submit(collect_service, collector_class, region, is_global): (
|
|
302
|
+
collector_class,
|
|
303
|
+
region,
|
|
304
|
+
is_global,
|
|
305
|
+
)
|
|
306
|
+
for collector_class, region, is_global in collection_tasks
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Process completed tasks
|
|
310
|
+
for future in as_completed(future_to_task):
|
|
311
|
+
result = future.result()
|
|
312
|
+
|
|
313
|
+
if result["success"]:
|
|
314
|
+
with lock:
|
|
315
|
+
all_resources.extend(result["resources"])
|
|
316
|
+
if result["region"] == "global":
|
|
317
|
+
resource_counts[result["service"]] = len(result["resources"])
|
|
318
|
+
else:
|
|
319
|
+
key = f"{result['service']}_{result['region']}"
|
|
320
|
+
resource_counts[key] = len(result["resources"])
|
|
321
|
+
elif not result.get("expected", False):
|
|
322
|
+
with lock:
|
|
323
|
+
collection_errors.append(result["error"])
|
|
324
|
+
|
|
325
|
+
# Advance progress (thread-safe)
|
|
326
|
+
with lock:
|
|
327
|
+
progress.advance(main_task)
|
|
328
|
+
|
|
329
|
+
progress.update(main_task, description=f"[bold green]✓ Successfully collected {len(all_resources)} resources")
|
|
330
|
+
|
|
331
|
+
# Log summary of collection errors if any (but not expected ones)
|
|
332
|
+
if collection_errors:
|
|
333
|
+
logger.debug(f"\nCollection completed with {len(collection_errors)} service(s) unavailable")
|
|
334
|
+
logger.debug("Services that failed:")
|
|
335
|
+
for error in collection_errors:
|
|
336
|
+
logger.debug(f" - {error['service']} ({error['region']}): {error['error']}")
|
|
337
|
+
|
|
338
|
+
# Apply filters if specified
|
|
339
|
+
total_before_filter = len(all_resources)
|
|
340
|
+
filters_applied = None
|
|
341
|
+
|
|
342
|
+
if resource_filter:
|
|
343
|
+
logger.debug(f"Applying filters: {resource_filter.get_filter_summary()}")
|
|
344
|
+
all_resources = resource_filter.apply(all_resources)
|
|
345
|
+
filter_stats = resource_filter.get_statistics_summary()
|
|
346
|
+
|
|
347
|
+
filters_applied = {
|
|
348
|
+
"date_filters": {
|
|
349
|
+
"before_date": resource_filter.before_date.isoformat() if resource_filter.before_date else None,
|
|
350
|
+
"after_date": resource_filter.after_date.isoformat() if resource_filter.after_date else None,
|
|
351
|
+
},
|
|
352
|
+
"tag_filters": resource_filter.required_tags,
|
|
353
|
+
"statistics": filter_stats,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
logger.debug(
|
|
357
|
+
f"Filtering complete: {filter_stats['total_collected']} collected, "
|
|
358
|
+
f"{filter_stats['final_count']} matched filters"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Calculate service counts
|
|
362
|
+
service_counts: Dict[str, int] = {}
|
|
363
|
+
for resource in all_resources:
|
|
364
|
+
service_counts[resource.resource_type] = service_counts.get(resource.resource_type, 0) + 1
|
|
365
|
+
|
|
366
|
+
# Build Config-related metadata
|
|
367
|
+
config_enabled_regions = [
|
|
368
|
+
region for region, avail in config_availability.items()
|
|
369
|
+
if avail.is_enabled
|
|
370
|
+
] if use_config else []
|
|
371
|
+
|
|
372
|
+
# Create snapshot
|
|
373
|
+
snapshot = Snapshot(
|
|
374
|
+
name=name,
|
|
375
|
+
created_at=datetime.now(timezone.utc),
|
|
376
|
+
account_id=account_id,
|
|
377
|
+
regions=regions,
|
|
378
|
+
resources=all_resources,
|
|
379
|
+
metadata={
|
|
380
|
+
"tool": "aws-inventory-manager",
|
|
381
|
+
"version": "1.0.0",
|
|
382
|
+
"collectors_used": [c(session, "us-east-1").service_name for c in collectors_to_use],
|
|
383
|
+
"collection_errors": collection_errors if collection_errors else None,
|
|
384
|
+
"use_config": use_config,
|
|
385
|
+
"config_aggregator": config_aggregator,
|
|
386
|
+
"config_enabled_regions": config_enabled_regions if config_enabled_regions else None,
|
|
387
|
+
"collection_sources": collection_sources if collection_sources else None,
|
|
388
|
+
},
|
|
389
|
+
is_active=set_active,
|
|
390
|
+
service_counts=service_counts,
|
|
391
|
+
filters_applied=filters_applied,
|
|
392
|
+
total_resources_before_filter=total_before_filter if resource_filter else None,
|
|
393
|
+
inventory_name=inventory_name,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
logger.debug(f"Snapshot '{name}' created with {len(all_resources)} resources")
|
|
397
|
+
|
|
398
|
+
return snapshot
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def create_snapshot_mvp(
|
|
402
|
+
name: str,
|
|
403
|
+
regions: List[str],
|
|
404
|
+
account_id: str,
|
|
405
|
+
profile_name: Optional[str] = None,
|
|
406
|
+
set_active: bool = True,
|
|
407
|
+
) -> Snapshot:
|
|
408
|
+
"""Create snapshot using the full implementation.
|
|
409
|
+
|
|
410
|
+
This is a wrapper for backward compatibility with the MVP CLI code.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
name: Snapshot name
|
|
414
|
+
regions: List of AWS regions to scan
|
|
415
|
+
account_id: AWS account ID
|
|
416
|
+
profile_name: AWS profile name (optional)
|
|
417
|
+
set_active: Whether to set as active baseline
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Snapshot instance with captured resources
|
|
421
|
+
"""
|
|
422
|
+
return create_snapshot(
|
|
423
|
+
name=name,
|
|
424
|
+
regions=regions,
|
|
425
|
+
account_id=account_id,
|
|
426
|
+
profile_name=profile_name,
|
|
427
|
+
set_active=set_active,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _get_collectors(resource_types: Optional[List[str]] = None) -> List[Type[BaseResourceCollector]]:
|
|
432
|
+
"""Get list of collectors to use based on resource type filter.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
resource_types: Optional list of service names to filter (e.g., ['iam', 'lambda'])
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
List of collector classes to use
|
|
439
|
+
"""
|
|
440
|
+
if not resource_types:
|
|
441
|
+
return COLLECTOR_REGISTRY
|
|
442
|
+
|
|
443
|
+
# Filter collectors based on service name
|
|
444
|
+
filtered = []
|
|
445
|
+
for collector_class in COLLECTOR_REGISTRY:
|
|
446
|
+
# Create temporary instance to check service name
|
|
447
|
+
temp_collector = collector_class(boto3.Session(), "us-east-1")
|
|
448
|
+
if temp_collector.service_name in resource_types:
|
|
449
|
+
filtered.append(collector_class)
|
|
450
|
+
|
|
451
|
+
return filtered if filtered else COLLECTOR_REGISTRY
|