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,306 @@
|
|
|
1
|
+
"""Utility for detecting unsupported AWS resources.
|
|
2
|
+
|
|
3
|
+
This module helps identify AWS resources that exist in an account but are not
|
|
4
|
+
covered by any of our collectors. This is important for:
|
|
5
|
+
- Ensuring complete inventory coverage
|
|
6
|
+
- Identifying when new AWS services need collectors
|
|
7
|
+
- Alerting users about resources that won't be included in snapshots
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
13
|
+
|
|
14
|
+
import boto3
|
|
15
|
+
from botocore.exceptions import ClientError
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Mapping of AWS resource type prefixes to our collector service names
|
|
21
|
+
# This maps from AWS Resource Groups Tagging API format to our collectors
|
|
22
|
+
SUPPORTED_RESOURCE_TYPE_PREFIXES: Dict[str, str] = {
|
|
23
|
+
# IAM
|
|
24
|
+
"iam:": "iam",
|
|
25
|
+
# Compute
|
|
26
|
+
"ec2:": "ec2",
|
|
27
|
+
"lambda:": "lambda",
|
|
28
|
+
"ecs:": "ecs",
|
|
29
|
+
"eks:": "eks",
|
|
30
|
+
# Storage
|
|
31
|
+
"s3:": "s3",
|
|
32
|
+
"dynamodb:": "dynamodb",
|
|
33
|
+
"elasticache:": "elasticache",
|
|
34
|
+
"rds:": "rds",
|
|
35
|
+
"elasticfilesystem:": "efs",
|
|
36
|
+
"backup:": "backup",
|
|
37
|
+
# Networking
|
|
38
|
+
"elasticloadbalancing:": "elb",
|
|
39
|
+
"route53:": "route53",
|
|
40
|
+
# Messaging
|
|
41
|
+
"sns:": "sns",
|
|
42
|
+
"sqs:": "sqs",
|
|
43
|
+
# Security
|
|
44
|
+
"secretsmanager:": "secretsmanager",
|
|
45
|
+
"kms:": "kms",
|
|
46
|
+
"wafv2:": "waf",
|
|
47
|
+
# Monitoring & Logging
|
|
48
|
+
"cloudwatch:": "cloudwatch",
|
|
49
|
+
"logs:": "cloudwatch", # CloudWatch Logs is part of CloudWatch collector
|
|
50
|
+
# Integration & Orchestration
|
|
51
|
+
"states:": "stepfunctions",
|
|
52
|
+
"events:": "eventbridge",
|
|
53
|
+
"apigateway:": "apigateway",
|
|
54
|
+
"codepipeline:": "codepipeline",
|
|
55
|
+
"codebuild:": "codebuild",
|
|
56
|
+
# Management
|
|
57
|
+
"cloudformation:": "cloudformation",
|
|
58
|
+
"ssm:": "ssm",
|
|
59
|
+
# VPC
|
|
60
|
+
"ec2:vpc-endpoint": "vpcendpoints",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class UnsupportedResource:
|
|
66
|
+
"""Represents an AWS resource that is not covered by any collector."""
|
|
67
|
+
|
|
68
|
+
resource_arn: str
|
|
69
|
+
resource_type: str
|
|
70
|
+
tags: Dict[str, str]
|
|
71
|
+
region: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class UnsupportedResourceReport:
|
|
76
|
+
"""Report of unsupported resources found in an AWS account."""
|
|
77
|
+
|
|
78
|
+
unsupported_resources: List[UnsupportedResource]
|
|
79
|
+
unsupported_types: Set[str]
|
|
80
|
+
supported_types: Set[str]
|
|
81
|
+
total_resources_scanned: int
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_all_tagged_resources(
|
|
85
|
+
session: Optional[boto3.Session] = None,
|
|
86
|
+
regions: Optional[List[str]] = None,
|
|
87
|
+
) -> List[Tuple[str, str, Dict[str, str], str]]:
|
|
88
|
+
"""Get all tagged resources across all services using Resource Groups Tagging API.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
session: Optional boto3 session (uses default if not provided)
|
|
92
|
+
regions: Optional list of regions to scan (uses all regions if not provided)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of tuples (arn, resource_type, tags, region)
|
|
96
|
+
"""
|
|
97
|
+
if session is None:
|
|
98
|
+
session = boto3.Session()
|
|
99
|
+
|
|
100
|
+
if regions is None:
|
|
101
|
+
# Get all available regions
|
|
102
|
+
ec2 = session.client("ec2", region_name="us-east-1")
|
|
103
|
+
try:
|
|
104
|
+
response = ec2.describe_regions(AllRegions=False)
|
|
105
|
+
regions = [r["RegionName"] for r in response["Regions"]]
|
|
106
|
+
except ClientError as e:
|
|
107
|
+
logger.warning(f"Could not get regions, using default list: {e}")
|
|
108
|
+
regions = ["us-east-1", "us-west-2", "eu-west-1"]
|
|
109
|
+
|
|
110
|
+
all_resources: List[Tuple[str, str, Dict[str, str], str]] = []
|
|
111
|
+
|
|
112
|
+
for region in regions:
|
|
113
|
+
try:
|
|
114
|
+
tagging = session.client("resourcegroupstaggingapi", region_name=region)
|
|
115
|
+
paginator = tagging.get_paginator("get_resources")
|
|
116
|
+
|
|
117
|
+
for page in paginator.paginate():
|
|
118
|
+
for resource in page.get("ResourceTagMappingList", []):
|
|
119
|
+
arn = resource["ResourceARN"]
|
|
120
|
+
tags = {t["Key"]: t["Value"] for t in resource.get("Tags", [])}
|
|
121
|
+
|
|
122
|
+
# Extract resource type from ARN
|
|
123
|
+
# ARN format: arn:partition:service:region:account:resource-type/resource-id
|
|
124
|
+
parts = arn.split(":")
|
|
125
|
+
if len(parts) >= 6:
|
|
126
|
+
service = parts[2]
|
|
127
|
+
resource_part = parts[5] if len(parts) > 5 else ""
|
|
128
|
+
|
|
129
|
+
# Handle resource types like "bucket/name" or "function:name"
|
|
130
|
+
if "/" in resource_part:
|
|
131
|
+
resource_type = f"{service}:{resource_part.split('/')[0]}"
|
|
132
|
+
elif ":" in resource_part:
|
|
133
|
+
resource_type = f"{service}:{resource_part.split(':')[0]}"
|
|
134
|
+
else:
|
|
135
|
+
resource_type = f"{service}:{resource_part}"
|
|
136
|
+
|
|
137
|
+
all_resources.append((arn, resource_type, tags, region))
|
|
138
|
+
|
|
139
|
+
except ClientError as e:
|
|
140
|
+
logger.debug(f"Could not scan region {region}: {e}")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Error scanning region {region}: {e}")
|
|
143
|
+
|
|
144
|
+
return all_resources
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_resource_type_supported(resource_type: str) -> bool:
|
|
148
|
+
"""Check if a resource type is supported by any collector.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
resource_type: The AWS resource type (e.g., "s3:bucket", "lambda:function")
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if the resource type is supported
|
|
155
|
+
"""
|
|
156
|
+
resource_type_lower = resource_type.lower()
|
|
157
|
+
|
|
158
|
+
for prefix in SUPPORTED_RESOURCE_TYPE_PREFIXES:
|
|
159
|
+
if resource_type_lower.startswith(prefix):
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect_unsupported_resources(
|
|
166
|
+
session: Optional[boto3.Session] = None,
|
|
167
|
+
regions: Optional[List[str]] = None,
|
|
168
|
+
include_untagged_warning: bool = True,
|
|
169
|
+
) -> UnsupportedResourceReport:
|
|
170
|
+
"""Detect AWS resources that are not supported by any collector.
|
|
171
|
+
|
|
172
|
+
This function uses the Resource Groups Tagging API to find all tagged resources
|
|
173
|
+
and compares them against our supported resource types.
|
|
174
|
+
|
|
175
|
+
Note: This only detects TAGGED resources. Resources without tags will not be
|
|
176
|
+
detected by this method. Use AWS Config for more comprehensive detection.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
session: Optional boto3 session
|
|
180
|
+
regions: Optional list of regions to scan
|
|
181
|
+
include_untagged_warning: Whether to log a warning about untagged resources
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
UnsupportedResourceReport with details about unsupported resources
|
|
185
|
+
"""
|
|
186
|
+
if include_untagged_warning:
|
|
187
|
+
logger.info(
|
|
188
|
+
"Note: This detection only covers TAGGED resources. "
|
|
189
|
+
"Untagged resources will not be detected. "
|
|
190
|
+
"Consider enabling AWS Config for comprehensive resource tracking."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Get all tagged resources
|
|
194
|
+
all_resources = get_all_tagged_resources(session, regions)
|
|
195
|
+
|
|
196
|
+
unsupported_resources: List[UnsupportedResource] = []
|
|
197
|
+
unsupported_types: Set[str] = set()
|
|
198
|
+
supported_types: Set[str] = set()
|
|
199
|
+
|
|
200
|
+
for arn, resource_type, tags, region in all_resources:
|
|
201
|
+
if is_resource_type_supported(resource_type):
|
|
202
|
+
supported_types.add(resource_type)
|
|
203
|
+
else:
|
|
204
|
+
unsupported_types.add(resource_type)
|
|
205
|
+
unsupported_resources.append(
|
|
206
|
+
UnsupportedResource(
|
|
207
|
+
resource_arn=arn,
|
|
208
|
+
resource_type=resource_type,
|
|
209
|
+
tags=tags,
|
|
210
|
+
region=region,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return UnsupportedResourceReport(
|
|
215
|
+
unsupported_resources=unsupported_resources,
|
|
216
|
+
unsupported_types=unsupported_types,
|
|
217
|
+
supported_types=supported_types,
|
|
218
|
+
total_resources_scanned=len(all_resources),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_unsupported_resource_summary(report: UnsupportedResourceReport) -> str:
|
|
223
|
+
"""Generate a human-readable summary of unsupported resources.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
report: The unsupported resource report
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Formatted string summary
|
|
230
|
+
"""
|
|
231
|
+
lines = [
|
|
232
|
+
f"Unsupported Resource Detection Report",
|
|
233
|
+
"=" * 40,
|
|
234
|
+
f"Total resources scanned: {report.total_resources_scanned}",
|
|
235
|
+
f"Supported resource types found: {len(report.supported_types)}",
|
|
236
|
+
f"Unsupported resource types found: {len(report.unsupported_types)}",
|
|
237
|
+
f"Total unsupported resources: {len(report.unsupported_resources)}",
|
|
238
|
+
"",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
if report.unsupported_types:
|
|
242
|
+
lines.append("Unsupported Resource Types:")
|
|
243
|
+
lines.append("-" * 30)
|
|
244
|
+
for resource_type in sorted(report.unsupported_types):
|
|
245
|
+
count = sum(
|
|
246
|
+
1
|
|
247
|
+
for r in report.unsupported_resources
|
|
248
|
+
if r.resource_type == resource_type
|
|
249
|
+
)
|
|
250
|
+
lines.append(f" {resource_type}: {count} resource(s)")
|
|
251
|
+
|
|
252
|
+
lines.append("")
|
|
253
|
+
lines.append("Consider adding collectors for these services to ensure")
|
|
254
|
+
lines.append("complete inventory coverage and safe cleanup operations.")
|
|
255
|
+
|
|
256
|
+
return "\n".join(lines)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def check_for_unsupported_resources_quick(
|
|
260
|
+
session: Optional[boto3.Session] = None,
|
|
261
|
+
region: str = "us-east-1",
|
|
262
|
+
limit: int = 100,
|
|
263
|
+
) -> Tuple[bool, Set[str]]:
|
|
264
|
+
"""Quick check for unsupported resources in a single region.
|
|
265
|
+
|
|
266
|
+
This is a faster alternative to full detection, useful for CLI warnings.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
session: Optional boto3 session
|
|
270
|
+
region: Region to check
|
|
271
|
+
limit: Maximum number of resources to check
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Tuple of (has_unsupported, unsupported_types)
|
|
275
|
+
"""
|
|
276
|
+
if session is None:
|
|
277
|
+
session = boto3.Session()
|
|
278
|
+
|
|
279
|
+
unsupported_types: Set[str] = set()
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
tagging = session.client("resourcegroupstaggingapi", region_name=region)
|
|
283
|
+
|
|
284
|
+
response = tagging.get_resources(ResourcesPerPage=limit)
|
|
285
|
+
|
|
286
|
+
for resource in response.get("ResourceTagMappingList", []):
|
|
287
|
+
arn = resource["ResourceARN"]
|
|
288
|
+
parts = arn.split(":")
|
|
289
|
+
if len(parts) >= 6:
|
|
290
|
+
service = parts[2]
|
|
291
|
+
resource_part = parts[5] if len(parts) > 5 else ""
|
|
292
|
+
|
|
293
|
+
if "/" in resource_part:
|
|
294
|
+
resource_type = f"{service}:{resource_part.split('/')[0]}"
|
|
295
|
+
elif ":" in resource_part:
|
|
296
|
+
resource_type = f"{service}:{resource_part.split(':')[0]}"
|
|
297
|
+
else:
|
|
298
|
+
resource_type = f"{service}:{resource_part}"
|
|
299
|
+
|
|
300
|
+
if not is_resource_type_supported(resource_type):
|
|
301
|
+
unsupported_types.add(resource_type)
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.debug(f"Quick unsupported resource check failed: {e}")
|
|
305
|
+
|
|
306
|
+
return bool(unsupported_types), unsupported_types
|
src/web/__init__.py
ADDED
src/web/app.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""FastAPI application factory for AWS Inventory Browser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
11
|
+
from fastapi.templating import Jinja2Templates
|
|
12
|
+
|
|
13
|
+
from .dependencies import init_database
|
|
14
|
+
|
|
15
|
+
# Get the directory where this module is located
|
|
16
|
+
MODULE_DIR = Path(__file__).parent
|
|
17
|
+
TEMPLATES_DIR = MODULE_DIR / "templates"
|
|
18
|
+
STATIC_DIR = MODULE_DIR / "static"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_templates() -> Jinja2Templates:
|
|
22
|
+
"""Get configured Jinja2Templates instance."""
|
|
23
|
+
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
24
|
+
|
|
25
|
+
# Add custom filters
|
|
26
|
+
def format_number(value: int) -> str:
|
|
27
|
+
"""Format number with commas."""
|
|
28
|
+
return f"{value:,}"
|
|
29
|
+
|
|
30
|
+
def truncate_arn(arn: str, max_length: int = 50) -> str:
|
|
31
|
+
"""Truncate ARN for display."""
|
|
32
|
+
if len(arn) <= max_length:
|
|
33
|
+
return arn
|
|
34
|
+
return arn[:max_length - 3] + "..."
|
|
35
|
+
|
|
36
|
+
def service_from_type(resource_type: str) -> str:
|
|
37
|
+
"""Extract service name from resource type."""
|
|
38
|
+
if ":" in resource_type:
|
|
39
|
+
return resource_type.split(":")[0]
|
|
40
|
+
return resource_type
|
|
41
|
+
|
|
42
|
+
templates.env.filters["format_number"] = format_number
|
|
43
|
+
templates.env.filters["truncate_arn"] = truncate_arn
|
|
44
|
+
templates.env.filters["service"] = service_from_type
|
|
45
|
+
|
|
46
|
+
return templates
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@asynccontextmanager
|
|
50
|
+
async def lifespan(app: FastAPI):
|
|
51
|
+
"""Manage application lifecycle."""
|
|
52
|
+
# Startup: Initialize database
|
|
53
|
+
storage_path = getattr(app.state, "storage_path", None)
|
|
54
|
+
init_database(storage_path)
|
|
55
|
+
yield
|
|
56
|
+
# Shutdown: Nothing to clean up
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_app(storage_path: Optional[str] = None) -> FastAPI:
|
|
60
|
+
"""Create and configure the FastAPI application.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
storage_path: Optional path to storage directory.
|
|
64
|
+
If not provided, uses default from Config.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Configured FastAPI application instance.
|
|
68
|
+
"""
|
|
69
|
+
app = FastAPI(
|
|
70
|
+
title="AWS Inventory Browser",
|
|
71
|
+
description="Browse and analyze AWS resource inventory",
|
|
72
|
+
version="1.0.0",
|
|
73
|
+
lifespan=lifespan,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Store config for lifespan access
|
|
77
|
+
app.state.storage_path = storage_path
|
|
78
|
+
|
|
79
|
+
# Mount static files if directory exists
|
|
80
|
+
if STATIC_DIR.exists():
|
|
81
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
82
|
+
|
|
83
|
+
# Get templates
|
|
84
|
+
templates = get_templates()
|
|
85
|
+
|
|
86
|
+
# Include API routes
|
|
87
|
+
from .routes.api import router as api_router
|
|
88
|
+
app.include_router(api_router, prefix="/api")
|
|
89
|
+
|
|
90
|
+
# Include page routes
|
|
91
|
+
from .routes import pages
|
|
92
|
+
app.include_router(pages.router)
|
|
93
|
+
|
|
94
|
+
# Add templates to app state for access in routes
|
|
95
|
+
app.state.templates = templates
|
|
96
|
+
|
|
97
|
+
return app
|
src/web/dependencies.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Dependency injection for FastAPI routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..storage import AuditStore, Database, GroupStore, InventoryStore, ResourceStore, SnapshotStore
|
|
9
|
+
|
|
10
|
+
# Global instances (initialized at startup)
|
|
11
|
+
_db: Optional[Database] = None
|
|
12
|
+
_storage_path: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init_database(storage_path: Optional[str] = None) -> None:
|
|
16
|
+
"""Initialize the database connection.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
storage_path: Optional path to storage directory.
|
|
20
|
+
If not provided, uses default from Config.
|
|
21
|
+
"""
|
|
22
|
+
global _db, _storage_path
|
|
23
|
+
_storage_path = storage_path
|
|
24
|
+
|
|
25
|
+
if storage_path:
|
|
26
|
+
db_path = Path(storage_path) / "inventory.db"
|
|
27
|
+
_db = Database(db_path=db_path)
|
|
28
|
+
else:
|
|
29
|
+
_db = Database()
|
|
30
|
+
|
|
31
|
+
_db.ensure_schema()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_database() -> Database:
|
|
35
|
+
"""Get the database instance."""
|
|
36
|
+
global _db
|
|
37
|
+
if _db is None:
|
|
38
|
+
init_database(_storage_path)
|
|
39
|
+
return _db # type: ignore
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_snapshot_store() -> SnapshotStore:
|
|
43
|
+
"""Get a SnapshotStore instance."""
|
|
44
|
+
return SnapshotStore(get_database())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_resource_store() -> ResourceStore:
|
|
48
|
+
"""Get a ResourceStore instance."""
|
|
49
|
+
return ResourceStore(get_database())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_inventory_store() -> InventoryStore:
|
|
53
|
+
"""Get an InventoryStore instance."""
|
|
54
|
+
return InventoryStore(get_database())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_audit_store() -> AuditStore:
|
|
58
|
+
"""Get an AuditStore instance."""
|
|
59
|
+
return AuditStore(get_database())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_group_store() -> GroupStore:
|
|
63
|
+
"""Get a GroupStore instance."""
|
|
64
|
+
return GroupStore(get_database())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_storage_path() -> Optional[str]:
|
|
68
|
+
"""Get the configured storage path."""
|
|
69
|
+
return _storage_path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Web routes package."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""API routes package."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from . import charts, cleanup, filters, groups, inventories, queries, resources, snapshots, views
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
router.include_router(snapshots.router, tags=["snapshots"])
|
|
9
|
+
router.include_router(resources.router, tags=["resources"])
|
|
10
|
+
router.include_router(queries.router, tags=["queries"])
|
|
11
|
+
router.include_router(charts.router, tags=["charts"])
|
|
12
|
+
router.include_router(cleanup.router, tags=["cleanup"])
|
|
13
|
+
router.include_router(filters.router, tags=["filters"])
|
|
14
|
+
router.include_router(views.router, tags=["views"])
|
|
15
|
+
router.include_router(groups.router, tags=["groups"])
|
|
16
|
+
router.include_router(inventories.router, tags=["inventories"])
|
|
17
|
+
|
|
18
|
+
__all__ = ["router"]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Chart data API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Query
|
|
8
|
+
|
|
9
|
+
from ...dependencies import get_resource_store, get_snapshot_store
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/charts")
|
|
12
|
+
|
|
13
|
+
# Chart.js color palette
|
|
14
|
+
CHART_COLORS = [
|
|
15
|
+
"#3B82F6", # Blue
|
|
16
|
+
"#10B981", # Green
|
|
17
|
+
"#F59E0B", # Amber
|
|
18
|
+
"#EF4444", # Red
|
|
19
|
+
"#8B5CF6", # Purple
|
|
20
|
+
"#EC4899", # Pink
|
|
21
|
+
"#06B6D4", # Cyan
|
|
22
|
+
"#84CC16", # Lime
|
|
23
|
+
"#F97316", # Orange
|
|
24
|
+
"#6366F1", # Indigo
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.get("/resources-by-type")
|
|
29
|
+
async def chart_resources_by_type(
|
|
30
|
+
snapshot: Optional[str] = Query(None, description="Limit to specific snapshot"),
|
|
31
|
+
limit: int = Query(10, le=20),
|
|
32
|
+
):
|
|
33
|
+
"""Get Chart.js data for resources by type."""
|
|
34
|
+
store = get_resource_store()
|
|
35
|
+
stats = store.get_stats(snapshot_name=snapshot, group_by="type")[:limit]
|
|
36
|
+
|
|
37
|
+
labels = [s["group_key"] for s in stats]
|
|
38
|
+
data = [s["count"] for s in stats]
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
"labels": labels,
|
|
42
|
+
"datasets": [
|
|
43
|
+
{
|
|
44
|
+
"data": data,
|
|
45
|
+
"backgroundColor": CHART_COLORS[:len(data)],
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get("/resources-by-region")
|
|
52
|
+
async def chart_resources_by_region(
|
|
53
|
+
snapshot: Optional[str] = Query(None, description="Limit to specific snapshot"),
|
|
54
|
+
):
|
|
55
|
+
"""Get Chart.js data for resources by region."""
|
|
56
|
+
store = get_resource_store()
|
|
57
|
+
stats = store.get_stats(snapshot_name=snapshot, group_by="region")
|
|
58
|
+
|
|
59
|
+
labels = [s["group_key"] for s in stats]
|
|
60
|
+
data = [s["count"] for s in stats]
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"labels": labels,
|
|
64
|
+
"datasets": [
|
|
65
|
+
{
|
|
66
|
+
"label": "Resources",
|
|
67
|
+
"data": data,
|
|
68
|
+
"backgroundColor": "#3B82F6",
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/resources-by-service")
|
|
75
|
+
async def chart_resources_by_service(
|
|
76
|
+
snapshot: Optional[str] = Query(None, description="Limit to specific snapshot"),
|
|
77
|
+
limit: int = Query(10, le=20),
|
|
78
|
+
):
|
|
79
|
+
"""Get Chart.js data for resources by service."""
|
|
80
|
+
store = get_resource_store()
|
|
81
|
+
stats = store.get_stats(snapshot_name=snapshot, group_by="service")[:limit]
|
|
82
|
+
|
|
83
|
+
labels = [s["group_key"] for s in stats]
|
|
84
|
+
data = [s["count"] for s in stats]
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"labels": labels,
|
|
88
|
+
"datasets": [
|
|
89
|
+
{
|
|
90
|
+
"data": data,
|
|
91
|
+
"backgroundColor": CHART_COLORS[:len(data)],
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.get("/snapshot-trend")
|
|
98
|
+
async def chart_snapshot_trend():
|
|
99
|
+
"""Get Chart.js data for snapshot resource count over time."""
|
|
100
|
+
store = get_snapshot_store()
|
|
101
|
+
snapshots = store.list_all()
|
|
102
|
+
|
|
103
|
+
# Sort by created_at
|
|
104
|
+
sorted_snapshots = sorted(snapshots, key=lambda s: s.get("created_at", ""))
|
|
105
|
+
|
|
106
|
+
# Take last 10 snapshots for trend
|
|
107
|
+
recent = sorted_snapshots[-10:] if len(sorted_snapshots) > 10 else sorted_snapshots
|
|
108
|
+
|
|
109
|
+
labels = [s["name"][:20] for s in recent] # Truncate long names
|
|
110
|
+
data = [s.get("resource_count", 0) for s in recent]
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"labels": labels,
|
|
114
|
+
"datasets": [
|
|
115
|
+
{
|
|
116
|
+
"label": "Resource Count",
|
|
117
|
+
"data": data,
|
|
118
|
+
"borderColor": "#3B82F6",
|
|
119
|
+
"backgroundColor": "rgba(59, 130, 246, 0.1)",
|
|
120
|
+
"fill": True,
|
|
121
|
+
"tension": 0.3,
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.get("/tag-coverage")
|
|
128
|
+
async def chart_tag_coverage(
|
|
129
|
+
snapshot: Optional[str] = Query(None, description="Limit to specific snapshot"),
|
|
130
|
+
):
|
|
131
|
+
"""Get Chart.js data for tag coverage analysis."""
|
|
132
|
+
resource_store = get_resource_store()
|
|
133
|
+
|
|
134
|
+
# Get all resources
|
|
135
|
+
resources = resource_store.search(snapshot_name=snapshot, limit=10000)
|
|
136
|
+
|
|
137
|
+
# Count tagged vs untagged
|
|
138
|
+
tagged = 0
|
|
139
|
+
untagged = 0
|
|
140
|
+
|
|
141
|
+
for r in resources:
|
|
142
|
+
tags = r.get("tags", {})
|
|
143
|
+
if tags and len(tags) > 0:
|
|
144
|
+
tagged += 1
|
|
145
|
+
else:
|
|
146
|
+
untagged += 1
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"labels": ["Tagged", "Untagged"],
|
|
150
|
+
"datasets": [
|
|
151
|
+
{
|
|
152
|
+
"data": [tagged, untagged],
|
|
153
|
+
"backgroundColor": ["#10B981", "#EF4444"],
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
}
|