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.
Files changed (152) hide show
  1. aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
  2. aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
  3. aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
  4. aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
  5. aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
  6. aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
  7. src/__init__.py +3 -0
  8. src/aws/__init__.py +11 -0
  9. src/aws/client.py +128 -0
  10. src/aws/credentials.py +191 -0
  11. src/aws/rate_limiter.py +177 -0
  12. src/cli/__init__.py +12 -0
  13. src/cli/config.py +130 -0
  14. src/cli/main.py +4046 -0
  15. src/cloudtrail/__init__.py +5 -0
  16. src/cloudtrail/query.py +642 -0
  17. src/config_service/__init__.py +21 -0
  18. src/config_service/collector.py +346 -0
  19. src/config_service/detector.py +256 -0
  20. src/config_service/resource_type_mapping.py +328 -0
  21. src/cost/__init__.py +5 -0
  22. src/cost/analyzer.py +226 -0
  23. src/cost/explorer.py +209 -0
  24. src/cost/reporter.py +237 -0
  25. src/delta/__init__.py +5 -0
  26. src/delta/calculator.py +206 -0
  27. src/delta/differ.py +185 -0
  28. src/delta/formatters.py +272 -0
  29. src/delta/models.py +154 -0
  30. src/delta/reporter.py +234 -0
  31. src/matching/__init__.py +6 -0
  32. src/matching/config.py +52 -0
  33. src/matching/normalizer.py +450 -0
  34. src/matching/prompts.py +33 -0
  35. src/models/__init__.py +21 -0
  36. src/models/config_diff.py +135 -0
  37. src/models/cost_report.py +87 -0
  38. src/models/deletion_operation.py +104 -0
  39. src/models/deletion_record.py +97 -0
  40. src/models/delta_report.py +122 -0
  41. src/models/efs_resource.py +80 -0
  42. src/models/elasticache_resource.py +90 -0
  43. src/models/group.py +318 -0
  44. src/models/inventory.py +133 -0
  45. src/models/protection_rule.py +123 -0
  46. src/models/report.py +288 -0
  47. src/models/resource.py +111 -0
  48. src/models/security_finding.py +102 -0
  49. src/models/snapshot.py +122 -0
  50. src/restore/__init__.py +20 -0
  51. src/restore/audit.py +175 -0
  52. src/restore/cleaner.py +461 -0
  53. src/restore/config.py +209 -0
  54. src/restore/deleter.py +976 -0
  55. src/restore/dependency.py +254 -0
  56. src/restore/safety.py +115 -0
  57. src/security/__init__.py +0 -0
  58. src/security/checks/__init__.py +0 -0
  59. src/security/checks/base.py +56 -0
  60. src/security/checks/ec2_checks.py +88 -0
  61. src/security/checks/elasticache_checks.py +149 -0
  62. src/security/checks/iam_checks.py +102 -0
  63. src/security/checks/rds_checks.py +140 -0
  64. src/security/checks/s3_checks.py +95 -0
  65. src/security/checks/secrets_checks.py +96 -0
  66. src/security/checks/sg_checks.py +142 -0
  67. src/security/cis_mapper.py +97 -0
  68. src/security/models.py +53 -0
  69. src/security/reporter.py +174 -0
  70. src/security/scanner.py +87 -0
  71. src/snapshot/__init__.py +6 -0
  72. src/snapshot/capturer.py +453 -0
  73. src/snapshot/filter.py +259 -0
  74. src/snapshot/inventory_storage.py +236 -0
  75. src/snapshot/report_formatter.py +250 -0
  76. src/snapshot/reporter.py +189 -0
  77. src/snapshot/resource_collectors/__init__.py +5 -0
  78. src/snapshot/resource_collectors/apigateway.py +140 -0
  79. src/snapshot/resource_collectors/backup.py +136 -0
  80. src/snapshot/resource_collectors/base.py +81 -0
  81. src/snapshot/resource_collectors/cloudformation.py +55 -0
  82. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  83. src/snapshot/resource_collectors/codebuild.py +69 -0
  84. src/snapshot/resource_collectors/codepipeline.py +82 -0
  85. src/snapshot/resource_collectors/dynamodb.py +65 -0
  86. src/snapshot/resource_collectors/ec2.py +240 -0
  87. src/snapshot/resource_collectors/ecs.py +215 -0
  88. src/snapshot/resource_collectors/efs_collector.py +102 -0
  89. src/snapshot/resource_collectors/eks.py +200 -0
  90. src/snapshot/resource_collectors/elasticache_collector.py +79 -0
  91. src/snapshot/resource_collectors/elb.py +126 -0
  92. src/snapshot/resource_collectors/eventbridge.py +156 -0
  93. src/snapshot/resource_collectors/glue.py +199 -0
  94. src/snapshot/resource_collectors/iam.py +188 -0
  95. src/snapshot/resource_collectors/kms.py +111 -0
  96. src/snapshot/resource_collectors/lambda_func.py +139 -0
  97. src/snapshot/resource_collectors/rds.py +109 -0
  98. src/snapshot/resource_collectors/route53.py +86 -0
  99. src/snapshot/resource_collectors/s3.py +105 -0
  100. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  101. src/snapshot/resource_collectors/sns.py +68 -0
  102. src/snapshot/resource_collectors/sqs.py +82 -0
  103. src/snapshot/resource_collectors/ssm.py +160 -0
  104. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  105. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  106. src/snapshot/resource_collectors/waf.py +159 -0
  107. src/snapshot/storage.py +351 -0
  108. src/storage/__init__.py +21 -0
  109. src/storage/audit_store.py +419 -0
  110. src/storage/database.py +294 -0
  111. src/storage/group_store.py +763 -0
  112. src/storage/inventory_store.py +320 -0
  113. src/storage/resource_store.py +416 -0
  114. src/storage/schema.py +339 -0
  115. src/storage/snapshot_store.py +363 -0
  116. src/utils/__init__.py +12 -0
  117. src/utils/export.py +305 -0
  118. src/utils/hash.py +60 -0
  119. src/utils/logging.py +63 -0
  120. src/utils/pagination.py +41 -0
  121. src/utils/paths.py +51 -0
  122. src/utils/progress.py +41 -0
  123. src/utils/unsupported_resources.py +306 -0
  124. src/web/__init__.py +5 -0
  125. src/web/app.py +97 -0
  126. src/web/dependencies.py +69 -0
  127. src/web/routes/__init__.py +1 -0
  128. src/web/routes/api/__init__.py +18 -0
  129. src/web/routes/api/charts.py +156 -0
  130. src/web/routes/api/cleanup.py +186 -0
  131. src/web/routes/api/filters.py +253 -0
  132. src/web/routes/api/groups.py +305 -0
  133. src/web/routes/api/inventories.py +80 -0
  134. src/web/routes/api/queries.py +202 -0
  135. src/web/routes/api/resources.py +393 -0
  136. src/web/routes/api/snapshots.py +314 -0
  137. src/web/routes/api/views.py +260 -0
  138. src/web/routes/pages.py +198 -0
  139. src/web/services/__init__.py +1 -0
  140. src/web/templates/base.html +955 -0
  141. src/web/templates/components/navbar.html +31 -0
  142. src/web/templates/components/sidebar.html +104 -0
  143. src/web/templates/pages/audit_logs.html +86 -0
  144. src/web/templates/pages/cleanup.html +279 -0
  145. src/web/templates/pages/dashboard.html +227 -0
  146. src/web/templates/pages/diff.html +175 -0
  147. src/web/templates/pages/error.html +30 -0
  148. src/web/templates/pages/groups.html +721 -0
  149. src/web/templates/pages/queries.html +246 -0
  150. src/web/templates/pages/resources.html +2429 -0
  151. src/web/templates/pages/snapshot_detail.html +271 -0
  152. 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
@@ -0,0 +1,5 @@
1
+ """AWS Inventory Browser - Web UI module."""
2
+
3
+ from .app import create_app
4
+
5
+ __all__ = ["create_app"]
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
@@ -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
+ }