aws-inventory-manager 0.17.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
- aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
- aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.17.12.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 +4046 -0
- src/cloudtrail/__init__.py +5 -0
- src/cloudtrail/query.py +642 -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/matching/__init__.py +6 -0
- src/matching/config.py +52 -0
- src/matching/normalizer.py +450 -0
- src/matching/prompts.py +33 -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 +453 -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/glue.py +199 -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 +763 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +416 -0
- src/storage/schema.py +339 -0
- src/storage/snapshot_store.py +363 -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 +393 -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 +955 -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 +2429 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- src/web/templates/pages/snapshots.html +429 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Report output formatting using Rich for terminal UI.
|
|
3
|
+
|
|
4
|
+
This module contains the ReportFormatter class responsible for rendering
|
|
5
|
+
reports to the terminal with Rich tables, progress bars, and formatted output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
from src.models.report import DetailedResource, ResourceSummary, SnapshotMetadata
|
|
16
|
+
from src.utils.pagination import paginate_resources
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReportFormatter:
|
|
20
|
+
"""
|
|
21
|
+
Formats and renders reports using Rich terminal UI.
|
|
22
|
+
|
|
23
|
+
Handles rendering of headers, summaries, tables, and progress bars
|
|
24
|
+
for snapshot resource reports.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, console: Optional[Console] = None) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize formatter with Rich Console.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
console: Rich Console instance (creates default if not provided)
|
|
33
|
+
"""
|
|
34
|
+
self.console = console or Console()
|
|
35
|
+
|
|
36
|
+
def format_summary(
|
|
37
|
+
self,
|
|
38
|
+
metadata: SnapshotMetadata,
|
|
39
|
+
summary: ResourceSummary,
|
|
40
|
+
has_filters: bool = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Format and display complete summary report.
|
|
44
|
+
|
|
45
|
+
Orchestrates all render methods to display header, service breakdown,
|
|
46
|
+
region breakdown, and type breakdown.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
metadata: Snapshot metadata for header
|
|
50
|
+
summary: Resource summary with aggregated counts
|
|
51
|
+
has_filters: Whether filters were applied (shows "Filtered" indicator)
|
|
52
|
+
"""
|
|
53
|
+
# Render header
|
|
54
|
+
self._render_header(metadata, has_filters)
|
|
55
|
+
|
|
56
|
+
# Add spacing
|
|
57
|
+
self.console.print()
|
|
58
|
+
|
|
59
|
+
# Render summary sections
|
|
60
|
+
self.console.print("📊 [bold]Resource Summary[/bold]\n")
|
|
61
|
+
self.console.print(f"Total Resources: [bold cyan]{summary.total_count:,}[/bold cyan]\n")
|
|
62
|
+
|
|
63
|
+
if summary.total_count > 0:
|
|
64
|
+
# Render service breakdown
|
|
65
|
+
self._render_service_breakdown(summary)
|
|
66
|
+
|
|
67
|
+
# Render region breakdown
|
|
68
|
+
self._render_region_breakdown(summary)
|
|
69
|
+
|
|
70
|
+
# Render type breakdown (top 10)
|
|
71
|
+
self._render_type_breakdown(summary)
|
|
72
|
+
|
|
73
|
+
def _render_header(self, metadata: SnapshotMetadata, has_filters: bool = False) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Render report header with snapshot metadata.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
metadata: Snapshot metadata
|
|
79
|
+
has_filters: Whether to show "Filtered" indicator
|
|
80
|
+
"""
|
|
81
|
+
title = f"Snapshot Report: {metadata.name}"
|
|
82
|
+
if has_filters:
|
|
83
|
+
title += " (Filtered)"
|
|
84
|
+
|
|
85
|
+
# Create header panel
|
|
86
|
+
header_content = f"""[bold]Inventory:[/bold] {metadata.inventory_name}
|
|
87
|
+
[bold]Account ID:[/bold] {metadata.account_id}
|
|
88
|
+
[bold]Created:[/bold] {metadata.created_at.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
89
|
+
[bold]Regions:[/bold] {metadata.region_summary}"""
|
|
90
|
+
|
|
91
|
+
panel = Panel(
|
|
92
|
+
header_content,
|
|
93
|
+
title=title,
|
|
94
|
+
border_style="blue",
|
|
95
|
+
expand=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.console.print(panel)
|
|
99
|
+
|
|
100
|
+
def _render_service_breakdown(self, summary: ResourceSummary) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Render service breakdown with progress bars.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
summary: Resource summary with service counts
|
|
106
|
+
"""
|
|
107
|
+
if not summary.by_service:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self.console.print("[bold]By Service:[/bold]")
|
|
111
|
+
|
|
112
|
+
# Get top services (sorted by count)
|
|
113
|
+
top_services = summary.top_services(limit=10)
|
|
114
|
+
|
|
115
|
+
for service, count in top_services:
|
|
116
|
+
percentage = (count / summary.total_count) * 100
|
|
117
|
+
bar_length = int((count / summary.total_count) * 30)
|
|
118
|
+
bar = "█" * bar_length + "░" * (30 - bar_length)
|
|
119
|
+
|
|
120
|
+
self.console.print(f" {service:15} {count:5} {bar} ({percentage:.1f}%)")
|
|
121
|
+
|
|
122
|
+
self.console.print()
|
|
123
|
+
|
|
124
|
+
def _render_region_breakdown(self, summary: ResourceSummary) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Render region breakdown with progress bars.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
summary: Resource summary with region counts
|
|
130
|
+
"""
|
|
131
|
+
if not summary.by_region:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
self.console.print("[bold]By Region:[/bold]")
|
|
135
|
+
|
|
136
|
+
# Get top regions (sorted by count)
|
|
137
|
+
top_regions = summary.top_regions(limit=10)
|
|
138
|
+
|
|
139
|
+
for region, count in top_regions:
|
|
140
|
+
percentage = (count / summary.total_count) * 100
|
|
141
|
+
bar_length = int((count / summary.total_count) * 30)
|
|
142
|
+
bar = "█" * bar_length + "░" * (30 - bar_length)
|
|
143
|
+
|
|
144
|
+
self.console.print(f" {region:15} {count:5} {bar} ({percentage:.1f}%)")
|
|
145
|
+
|
|
146
|
+
self.console.print()
|
|
147
|
+
|
|
148
|
+
def _render_type_breakdown(self, summary: ResourceSummary) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Render resource type breakdown showing top 10 types.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
summary: Resource summary with type counts
|
|
154
|
+
"""
|
|
155
|
+
if not summary.by_type:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
self.console.print("[bold]Top 10 Resource Types:[/bold]")
|
|
159
|
+
|
|
160
|
+
# Get top 10 resource types
|
|
161
|
+
sorted_types = sorted(
|
|
162
|
+
summary.by_type.items(),
|
|
163
|
+
key=lambda x: x[1],
|
|
164
|
+
reverse=True,
|
|
165
|
+
)[:10]
|
|
166
|
+
|
|
167
|
+
for resource_type, count in sorted_types:
|
|
168
|
+
percentage = (count / summary.total_count) * 100
|
|
169
|
+
self.console.print(f" {resource_type:45} {count:5} ({percentage:.1f}%)")
|
|
170
|
+
|
|
171
|
+
self.console.print()
|
|
172
|
+
|
|
173
|
+
def format_detailed(
|
|
174
|
+
self,
|
|
175
|
+
metadata: SnapshotMetadata,
|
|
176
|
+
resources: List[DetailedResource],
|
|
177
|
+
page_size: int = 100,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Format and display detailed resource view with pagination.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
metadata: Snapshot metadata for header
|
|
184
|
+
resources: List of detailed resources to display
|
|
185
|
+
page_size: Number of resources per page (default: 100)
|
|
186
|
+
"""
|
|
187
|
+
# Render header
|
|
188
|
+
self._render_header(metadata, has_filters=False)
|
|
189
|
+
self.console.print()
|
|
190
|
+
|
|
191
|
+
total_resources = len(resources)
|
|
192
|
+
self.console.print(f"Total Resources: [bold cyan]{total_resources:,}[/bold cyan]\n")
|
|
193
|
+
|
|
194
|
+
if total_resources == 0:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Paginate resources
|
|
198
|
+
pages = list(paginate_resources(resources, page_size=page_size))
|
|
199
|
+
total_pages = len(pages)
|
|
200
|
+
|
|
201
|
+
# Display pages
|
|
202
|
+
for page_num, page in enumerate(pages, start=1):
|
|
203
|
+
for idx, resource in enumerate(page, start=1):
|
|
204
|
+
global_idx = (page_num - 1) * page_size + idx
|
|
205
|
+
self._render_detailed_resource(resource, global_idx, total_resources)
|
|
206
|
+
|
|
207
|
+
# Show pagination info (but no prompts - non-interactive for now)
|
|
208
|
+
if page_num < total_pages:
|
|
209
|
+
self.console.print(f"\n[dim]─── Page {page_num} of {total_pages} ───[/dim]\n")
|
|
210
|
+
|
|
211
|
+
def _render_detailed_resource(
|
|
212
|
+
self,
|
|
213
|
+
resource: DetailedResource,
|
|
214
|
+
index: int,
|
|
215
|
+
total: int,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Render a single detailed resource.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
resource: DetailedResource to display
|
|
222
|
+
index: Resource index (1-based)
|
|
223
|
+
total: Total number of resources
|
|
224
|
+
"""
|
|
225
|
+
self.console.print("─" * 65)
|
|
226
|
+
self.console.print(f"[bold]Resource {index}/{total}[/bold]\n")
|
|
227
|
+
|
|
228
|
+
self.console.print(f"[bold]ARN:[/bold] {resource.arn}")
|
|
229
|
+
self.console.print(f"[bold]Type:[/bold] {resource.resource_type}")
|
|
230
|
+
self.console.print(f"[bold]Name:[/bold] {resource.name}")
|
|
231
|
+
self.console.print(f"[bold]Region:[/bold] {resource.region}")
|
|
232
|
+
|
|
233
|
+
# Show creation date if available
|
|
234
|
+
if resource.created_at:
|
|
235
|
+
age_str = ""
|
|
236
|
+
if resource.age_days is not None:
|
|
237
|
+
age_str = f" ({resource.age_days} days ago)"
|
|
238
|
+
created_str = resource.created_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
239
|
+
self.console.print(f"[bold]Created:[/bold] {created_str}{age_str}")
|
|
240
|
+
|
|
241
|
+
# Show tags
|
|
242
|
+
self.console.print()
|
|
243
|
+
if resource.tags:
|
|
244
|
+
self.console.print("[bold]Tags:[/bold]")
|
|
245
|
+
for key, value in resource.tags.items():
|
|
246
|
+
self.console.print(f" {key:20} {value}")
|
|
247
|
+
else:
|
|
248
|
+
self.console.print("[dim]No tags[/dim]")
|
|
249
|
+
|
|
250
|
+
self.console.print()
|
src/snapshot/reporter.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Snapshot report generation logic.
|
|
3
|
+
|
|
4
|
+
This module contains the SnapshotReporter class responsible for generating
|
|
5
|
+
reports from snapshot data, including summary generation, filtering, and
|
|
6
|
+
detailed resource views.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Generator, Optional
|
|
12
|
+
|
|
13
|
+
from src.models.report import DetailedResource, FilterCriteria, FilteredResource, ResourceSummary, SnapshotMetadata
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from src.models.snapshot import Snapshot
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SnapshotReporter:
|
|
20
|
+
"""
|
|
21
|
+
Generates reports from snapshot data.
|
|
22
|
+
|
|
23
|
+
This class handles extracting metadata, generating summaries, and
|
|
24
|
+
preparing data for formatting and display.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, snapshot: Snapshot) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize reporter with a snapshot.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
snapshot: The snapshot to generate reports from
|
|
33
|
+
"""
|
|
34
|
+
self.snapshot = snapshot
|
|
35
|
+
|
|
36
|
+
def _extract_metadata(self) -> SnapshotMetadata:
|
|
37
|
+
"""
|
|
38
|
+
Extract metadata from snapshot for report header.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
SnapshotMetadata with snapshot information
|
|
42
|
+
"""
|
|
43
|
+
return SnapshotMetadata(
|
|
44
|
+
name=self.snapshot.name,
|
|
45
|
+
created_at=self.snapshot.created_at,
|
|
46
|
+
account_id=self.snapshot.account_id,
|
|
47
|
+
regions=self.snapshot.regions,
|
|
48
|
+
inventory_name=self.snapshot.inventory_name,
|
|
49
|
+
total_resource_count=len(self.snapshot.resources),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def generate_summary(self) -> ResourceSummary:
|
|
53
|
+
"""
|
|
54
|
+
Generate aggregated resource summary from snapshot.
|
|
55
|
+
|
|
56
|
+
Uses streaming single-pass aggregation for memory efficiency.
|
|
57
|
+
Does not load entire dataset into memory.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
ResourceSummary with aggregated counts
|
|
61
|
+
"""
|
|
62
|
+
summary = ResourceSummary()
|
|
63
|
+
|
|
64
|
+
# Single-pass streaming aggregation
|
|
65
|
+
for resource in self.snapshot.resources:
|
|
66
|
+
summary.total_count += 1
|
|
67
|
+
|
|
68
|
+
# Extract service from resource type
|
|
69
|
+
# Handle both formats: "AWS::EC2::Instance" and "ec2:instance"
|
|
70
|
+
if "::" in resource.resource_type:
|
|
71
|
+
# CloudFormation format: AWS::EC2::Instance
|
|
72
|
+
parts = resource.resource_type.split("::")
|
|
73
|
+
service = parts[1] if len(parts) >= 2 else "Unknown"
|
|
74
|
+
else:
|
|
75
|
+
# Simplified format: ec2:instance
|
|
76
|
+
service = (
|
|
77
|
+
resource.resource_type.split(":")[0] if ":" in resource.resource_type else resource.resource_type
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Aggregate by service
|
|
81
|
+
summary.by_service[service] = summary.by_service.get(service, 0) + 1
|
|
82
|
+
|
|
83
|
+
# Aggregate by region
|
|
84
|
+
summary.by_region[resource.region] = summary.by_region.get(resource.region, 0) + 1
|
|
85
|
+
|
|
86
|
+
# Aggregate by type
|
|
87
|
+
summary.by_type[resource.resource_type] = summary.by_type.get(resource.resource_type, 0) + 1
|
|
88
|
+
|
|
89
|
+
return summary
|
|
90
|
+
|
|
91
|
+
def get_filtered_resources(self, criteria: FilterCriteria) -> Generator[FilteredResource, None, None]:
|
|
92
|
+
"""
|
|
93
|
+
Get filtered resources as generator (memory-efficient streaming).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
criteria: Filter criteria with resource types and/or regions
|
|
97
|
+
|
|
98
|
+
Yields:
|
|
99
|
+
FilteredResource objects matching the criteria
|
|
100
|
+
"""
|
|
101
|
+
for resource in self.snapshot.resources:
|
|
102
|
+
# Convert Resource to FilteredResource for matching
|
|
103
|
+
filtered_resource = FilteredResource(
|
|
104
|
+
arn=resource.arn,
|
|
105
|
+
resource_type=resource.resource_type,
|
|
106
|
+
name=resource.name,
|
|
107
|
+
region=resource.region,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Apply filter criteria
|
|
111
|
+
if criteria.matches_resource(filtered_resource):
|
|
112
|
+
yield filtered_resource
|
|
113
|
+
|
|
114
|
+
def generate_filtered_summary(self, criteria: FilterCriteria) -> ResourceSummary:
|
|
115
|
+
"""
|
|
116
|
+
Generate summary for filtered resources only.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
criteria: Filter criteria
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
ResourceSummary with counts for filtered resources only
|
|
123
|
+
"""
|
|
124
|
+
summary = ResourceSummary()
|
|
125
|
+
|
|
126
|
+
# Single-pass streaming aggregation of filtered resources
|
|
127
|
+
for filtered_resource in self.get_filtered_resources(criteria):
|
|
128
|
+
summary.total_count += 1
|
|
129
|
+
|
|
130
|
+
# Extract service from resource type
|
|
131
|
+
if "::" in filtered_resource.resource_type:
|
|
132
|
+
parts = filtered_resource.resource_type.split("::")
|
|
133
|
+
service = parts[1] if len(parts) >= 2 else "Unknown"
|
|
134
|
+
else:
|
|
135
|
+
service = (
|
|
136
|
+
filtered_resource.resource_type.split(":")[0]
|
|
137
|
+
if ":" in filtered_resource.resource_type
|
|
138
|
+
else filtered_resource.resource_type
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Aggregate by service
|
|
142
|
+
summary.by_service[service] = summary.by_service.get(service, 0) + 1
|
|
143
|
+
|
|
144
|
+
# Aggregate by region
|
|
145
|
+
summary.by_region[filtered_resource.region] = summary.by_region.get(filtered_resource.region, 0) + 1
|
|
146
|
+
|
|
147
|
+
# Aggregate by type
|
|
148
|
+
summary.by_type[filtered_resource.resource_type] = (
|
|
149
|
+
summary.by_type.get(filtered_resource.resource_type, 0) + 1
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return summary
|
|
153
|
+
|
|
154
|
+
def get_detailed_resources(
|
|
155
|
+
self, criteria: Optional[FilterCriteria] = None
|
|
156
|
+
) -> Generator[DetailedResource, None, None]:
|
|
157
|
+
"""
|
|
158
|
+
Get detailed resource information as generator.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
criteria: Optional filter criteria to limit resources
|
|
162
|
+
|
|
163
|
+
Yields:
|
|
164
|
+
DetailedResource objects with full information (tags, creation date, etc.)
|
|
165
|
+
"""
|
|
166
|
+
for resource in self.snapshot.resources:
|
|
167
|
+
# Apply filtering if criteria provided
|
|
168
|
+
if criteria:
|
|
169
|
+
filtered_resource = FilteredResource(
|
|
170
|
+
arn=resource.arn,
|
|
171
|
+
resource_type=resource.resource_type,
|
|
172
|
+
name=resource.name,
|
|
173
|
+
region=resource.region,
|
|
174
|
+
)
|
|
175
|
+
if not criteria.matches_resource(filtered_resource):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Convert Resource to DetailedResource
|
|
179
|
+
detailed_resource = DetailedResource(
|
|
180
|
+
arn=resource.arn,
|
|
181
|
+
resource_type=resource.resource_type,
|
|
182
|
+
name=resource.name,
|
|
183
|
+
region=resource.region,
|
|
184
|
+
tags=resource.tags,
|
|
185
|
+
created_at=resource.created_at,
|
|
186
|
+
config_hash=resource.config_hash,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
yield detailed_resource
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""API Gateway resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIGatewayCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS API Gateway resources (REST, HTTP, WebSocket APIs)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "apigateway"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect API Gateway resources.
|
|
19
|
+
|
|
20
|
+
Collects:
|
|
21
|
+
- REST APIs (v1)
|
|
22
|
+
- HTTP APIs (v2)
|
|
23
|
+
- WebSocket APIs (v2)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of API Gateway APIs
|
|
27
|
+
"""
|
|
28
|
+
resources = []
|
|
29
|
+
|
|
30
|
+
# Collect REST APIs (v1)
|
|
31
|
+
resources.extend(self._collect_rest_apis())
|
|
32
|
+
|
|
33
|
+
# Collect HTTP and WebSocket APIs (v2)
|
|
34
|
+
resources.extend(self._collect_v2_apis())
|
|
35
|
+
|
|
36
|
+
self.logger.debug(f"Collected {len(resources)} API Gateway APIs in {self.region}")
|
|
37
|
+
return resources
|
|
38
|
+
|
|
39
|
+
def _collect_rest_apis(self) -> List[Resource]:
|
|
40
|
+
"""Collect REST APIs (API Gateway v1).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of REST API resources
|
|
44
|
+
"""
|
|
45
|
+
resources = []
|
|
46
|
+
client = self._create_client()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
paginator = client.get_paginator("get_rest_apis")
|
|
50
|
+
for page in paginator.paginate():
|
|
51
|
+
for api in page.get("items", []):
|
|
52
|
+
api_id = api["id"]
|
|
53
|
+
api_name = api["name"]
|
|
54
|
+
|
|
55
|
+
# Get tags
|
|
56
|
+
tags = {}
|
|
57
|
+
try:
|
|
58
|
+
tag_response = client.get_tags(
|
|
59
|
+
resourceArn=f"arn:aws:apigateway:{self.region}::/restapis/{api_id}"
|
|
60
|
+
)
|
|
61
|
+
tags = tag_response.get("tags", {})
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.debug(f"Could not get tags for REST API {api_id}: {e}")
|
|
64
|
+
|
|
65
|
+
# Build ARN
|
|
66
|
+
arn = f"arn:aws:apigateway:{self.region}::/restapis/{api_id}"
|
|
67
|
+
|
|
68
|
+
# Extract creation date
|
|
69
|
+
created_at = api.get("createdDate")
|
|
70
|
+
|
|
71
|
+
# Create resource
|
|
72
|
+
resource = Resource(
|
|
73
|
+
arn=arn,
|
|
74
|
+
resource_type="AWS::ApiGateway::RestApi",
|
|
75
|
+
name=api_name,
|
|
76
|
+
region=self.region,
|
|
77
|
+
tags=tags,
|
|
78
|
+
config_hash=compute_config_hash(api),
|
|
79
|
+
created_at=created_at,
|
|
80
|
+
raw_config=api,
|
|
81
|
+
)
|
|
82
|
+
resources.append(resource)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.error(f"Error collecting REST APIs in {self.region}: {e}")
|
|
86
|
+
|
|
87
|
+
return resources
|
|
88
|
+
|
|
89
|
+
def _collect_v2_apis(self) -> List[Resource]:
|
|
90
|
+
"""Collect HTTP and WebSocket APIs (API Gateway v2).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of v2 API resources
|
|
94
|
+
"""
|
|
95
|
+
resources = []
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
client = self._create_client("apigatewayv2")
|
|
99
|
+
|
|
100
|
+
paginator = client.get_paginator("get_apis")
|
|
101
|
+
for page in paginator.paginate():
|
|
102
|
+
for api in page.get("Items", []):
|
|
103
|
+
api_id = api["ApiId"]
|
|
104
|
+
api_name = api["Name"]
|
|
105
|
+
protocol_type = api["ProtocolType"] # HTTP or WEBSOCKET
|
|
106
|
+
|
|
107
|
+
# Get tags
|
|
108
|
+
tags = api.get("Tags", {})
|
|
109
|
+
|
|
110
|
+
# Build ARN
|
|
111
|
+
arn = f"arn:aws:apigateway:{self.region}::/apis/{api_id}"
|
|
112
|
+
|
|
113
|
+
# Extract creation date
|
|
114
|
+
created_at = api.get("CreatedDate")
|
|
115
|
+
|
|
116
|
+
# Determine resource type based on protocol
|
|
117
|
+
if protocol_type == "HTTP":
|
|
118
|
+
resource_type = "AWS::ApiGatewayV2::Api::HTTP"
|
|
119
|
+
elif protocol_type == "WEBSOCKET":
|
|
120
|
+
resource_type = "AWS::ApiGatewayV2::Api::WebSocket"
|
|
121
|
+
else:
|
|
122
|
+
resource_type = f"AWS::ApiGatewayV2::Api::{protocol_type}"
|
|
123
|
+
|
|
124
|
+
# Create resource
|
|
125
|
+
resource = Resource(
|
|
126
|
+
arn=arn,
|
|
127
|
+
resource_type=resource_type,
|
|
128
|
+
name=api_name,
|
|
129
|
+
region=self.region,
|
|
130
|
+
tags=tags,
|
|
131
|
+
config_hash=compute_config_hash(api),
|
|
132
|
+
created_at=created_at,
|
|
133
|
+
raw_config=api,
|
|
134
|
+
)
|
|
135
|
+
resources.append(resource)
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.logger.error(f"Error collecting API Gateway v2 APIs in {self.region}: {e}")
|
|
139
|
+
|
|
140
|
+
return resources
|