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,416 @@
1
+ """Resource query operations for SQLite backend."""
2
+
3
+ import logging
4
+ import re
5
+ from datetime import datetime
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+ from .database import Database, json_deserialize
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class ResourceStore:
14
+ """Query operations for resources across snapshots."""
15
+
16
+ def __init__(self, db: Database):
17
+ """Initialize resource store.
18
+
19
+ Args:
20
+ db: Database connection manager
21
+ """
22
+ self.db = db
23
+
24
+ def query_raw(self, sql: str, params: Tuple = ()) -> List[Dict[str, Any]]:
25
+ """Execute raw SQL query on the database.
26
+
27
+ Args:
28
+ sql: SQL query (should be SELECT only)
29
+ params: Query parameters
30
+
31
+ Returns:
32
+ List of result dictionaries
33
+
34
+ Raises:
35
+ ValueError: If query is not a SELECT statement
36
+ """
37
+ # Basic SQL injection prevention - only allow SELECT
38
+ sql_upper = sql.strip().upper()
39
+ if not sql_upper.startswith("SELECT"):
40
+ raise ValueError("Only SELECT queries are allowed")
41
+
42
+ # Block dangerous keywords
43
+ dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "CREATE", "TRUNCATE"]
44
+ for keyword in dangerous:
45
+ if re.search(rf"\b{keyword}\b", sql_upper):
46
+ raise ValueError(f"Query contains forbidden keyword: {keyword}")
47
+
48
+ return self.db.fetchall(sql, params)
49
+
50
+ def search(
51
+ self,
52
+ arn_pattern: Optional[str] = None,
53
+ resource_type: Optional[str] = None,
54
+ region: Optional[str] = None,
55
+ tag_key: Optional[str] = None,
56
+ tag_value: Optional[str] = None,
57
+ snapshot_name: Optional[str] = None,
58
+ created_before: Optional[datetime] = None,
59
+ created_after: Optional[datetime] = None,
60
+ limit: int = 100,
61
+ offset: int = 0,
62
+ ) -> List[Dict[str, Any]]:
63
+ """Search resources with filters.
64
+
65
+ Args:
66
+ arn_pattern: ARN pattern to match (supports % wildcard)
67
+ resource_type: Filter by resource type (exact or partial)
68
+ region: Filter by region
69
+ tag_key: Filter by tag key
70
+ tag_value: Filter by tag value (requires tag_key)
71
+ snapshot_name: Limit to specific snapshot
72
+ created_before: Resources created before this date
73
+ created_after: Resources created after this date
74
+ limit: Maximum results to return
75
+ offset: Offset for pagination
76
+
77
+ Returns:
78
+ List of matching resources with snapshot info
79
+ """
80
+ conditions = []
81
+ params: List[Any] = []
82
+
83
+ # Build query with joins
84
+ base_query = """
85
+ SELECT DISTINCT
86
+ r.arn,
87
+ r.resource_type,
88
+ r.name,
89
+ r.region,
90
+ r.config_hash,
91
+ r.created_at,
92
+ r.source,
93
+ r.canonical_name,
94
+ r.normalized_name,
95
+ r.normalization_method,
96
+ s.name as snapshot_name,
97
+ s.created_at as snapshot_created_at,
98
+ s.account_id
99
+ FROM resources r
100
+ JOIN snapshots s ON r.snapshot_id = s.id
101
+ """
102
+
103
+ # Tag join if filtering by tags
104
+ if tag_key:
105
+ base_query += " JOIN resource_tags t ON r.id = t.resource_id"
106
+ conditions.append("t.key = ?")
107
+ params.append(tag_key)
108
+ if tag_value:
109
+ conditions.append("t.value = ?")
110
+ params.append(tag_value)
111
+
112
+ # ARN filter
113
+ if arn_pattern:
114
+ if "%" in arn_pattern:
115
+ conditions.append("r.arn LIKE ?")
116
+ else:
117
+ conditions.append("r.arn LIKE ?")
118
+ arn_pattern = f"%{arn_pattern}%"
119
+ params.append(arn_pattern)
120
+
121
+ # Resource type filter
122
+ if resource_type:
123
+ if ":" in resource_type:
124
+ conditions.append("r.resource_type = ?")
125
+ else:
126
+ conditions.append("r.resource_type LIKE ?")
127
+ resource_type = f"%{resource_type}%"
128
+ params.append(resource_type)
129
+
130
+ # Region filter
131
+ if region:
132
+ conditions.append("r.region = ?")
133
+ params.append(region)
134
+
135
+ # Snapshot filter
136
+ if snapshot_name:
137
+ conditions.append("s.name = ?")
138
+ params.append(snapshot_name)
139
+
140
+ # Date filters
141
+ if created_before:
142
+ conditions.append("r.created_at < ?")
143
+ params.append(created_before.isoformat())
144
+
145
+ if created_after:
146
+ conditions.append("r.created_at >= ?")
147
+ params.append(created_after.isoformat())
148
+
149
+ # Build final query
150
+ if conditions:
151
+ base_query += " WHERE " + " AND ".join(conditions)
152
+
153
+ base_query += " ORDER BY s.created_at DESC, r.arn"
154
+ base_query += f" LIMIT {limit} OFFSET {offset}"
155
+
156
+ return self.db.fetchall(base_query, tuple(params))
157
+
158
+ def get_history(self, arn: str) -> List[Dict[str, Any]]:
159
+ """Get all snapshots containing a specific resource.
160
+
161
+ Args:
162
+ arn: Resource ARN
163
+
164
+ Returns:
165
+ List of snapshots with resource details, ordered by date
166
+ """
167
+ return self.db.fetchall(
168
+ """
169
+ SELECT
170
+ s.name as snapshot_name,
171
+ s.created_at as snapshot_created_at,
172
+ s.account_id,
173
+ r.config_hash,
174
+ r.created_at as resource_created_at,
175
+ r.source
176
+ FROM resources r
177
+ JOIN snapshots s ON r.snapshot_id = s.id
178
+ WHERE r.arn = ?
179
+ ORDER BY s.created_at DESC
180
+ """,
181
+ (arn,),
182
+ )
183
+
184
+ def get_stats(
185
+ self,
186
+ snapshot_name: Optional[str] = None,
187
+ group_by: str = "type",
188
+ ) -> List[Dict[str, Any]]:
189
+ """Get resource statistics.
190
+
191
+ Args:
192
+ snapshot_name: Limit to specific snapshot (None for all)
193
+ group_by: Grouping field - 'type', 'region', 'service', 'snapshot'
194
+
195
+ Returns:
196
+ List of statistics grouped by specified field
197
+ """
198
+ group_field = {
199
+ "type": "r.resource_type",
200
+ "region": "r.region",
201
+ "service": "SUBSTR(r.resource_type, 1, INSTR(r.resource_type, ':') - 1)",
202
+ "snapshot": "s.name",
203
+ }.get(group_by, "r.resource_type")
204
+
205
+ base_query = f"""
206
+ SELECT
207
+ {group_field} as group_key,
208
+ COUNT(*) as count
209
+ FROM resources r
210
+ JOIN snapshots s ON r.snapshot_id = s.id
211
+ """
212
+
213
+ params: List[str] = []
214
+ if snapshot_name:
215
+ base_query += " WHERE s.name = ?"
216
+ params.append(snapshot_name)
217
+
218
+ base_query += f" GROUP BY {group_field} ORDER BY count DESC"
219
+
220
+ return self.db.fetchall(base_query, tuple(params))
221
+
222
+ def compare_snapshots(
223
+ self,
224
+ snapshot1_name: str,
225
+ snapshot2_name: str,
226
+ ) -> Dict[str, List[Dict[str, Any]]]:
227
+ """Compare resources between two snapshots.
228
+
229
+ Args:
230
+ snapshot1_name: First (older) snapshot name
231
+ snapshot2_name: Second (newer) snapshot name
232
+
233
+ Returns:
234
+ Dict with 'added', 'removed', 'modified' resource lists
235
+ """
236
+ # Get resources from both snapshots indexed by ARN
237
+ snap1_resources = self.db.fetchall(
238
+ """
239
+ SELECT r.arn, r.resource_type, r.name, r.region, r.config_hash
240
+ FROM resources r
241
+ JOIN snapshots s ON r.snapshot_id = s.id
242
+ WHERE s.name = ?
243
+ """,
244
+ (snapshot1_name,),
245
+ )
246
+
247
+ snap2_resources = self.db.fetchall(
248
+ """
249
+ SELECT r.arn, r.resource_type, r.name, r.region, r.config_hash
250
+ FROM resources r
251
+ JOIN snapshots s ON r.snapshot_id = s.id
252
+ WHERE s.name = ?
253
+ """,
254
+ (snapshot2_name,),
255
+ )
256
+
257
+ snap1_by_arn = {r["arn"]: r for r in snap1_resources}
258
+ snap2_by_arn = {r["arn"]: r for r in snap2_resources}
259
+
260
+ snap1_arns = set(snap1_by_arn.keys())
261
+ snap2_arns = set(snap2_by_arn.keys())
262
+
263
+ added = [dict(snap2_by_arn[arn]) for arn in (snap2_arns - snap1_arns)]
264
+ removed = [dict(snap1_by_arn[arn]) for arn in (snap1_arns - snap2_arns)]
265
+
266
+ # Find modified (same ARN, different hash)
267
+ modified = []
268
+ for arn in snap1_arns & snap2_arns:
269
+ if snap1_by_arn[arn]["config_hash"] != snap2_by_arn[arn]["config_hash"]:
270
+ modified.append(
271
+ {
272
+ "arn": arn,
273
+ "resource_type": snap2_by_arn[arn]["resource_type"],
274
+ "name": snap2_by_arn[arn]["name"],
275
+ "region": snap2_by_arn[arn]["region"],
276
+ "old_hash": snap1_by_arn[arn]["config_hash"],
277
+ "new_hash": snap2_by_arn[arn]["config_hash"],
278
+ }
279
+ )
280
+
281
+ return {
282
+ "added": added,
283
+ "removed": removed,
284
+ "modified": modified,
285
+ "summary": {
286
+ "snapshot1": snapshot1_name,
287
+ "snapshot2": snapshot2_name,
288
+ "snapshot1_count": len(snap1_resources),
289
+ "snapshot2_count": len(snap2_resources),
290
+ "added_count": len(added),
291
+ "removed_count": len(removed),
292
+ "modified_count": len(modified),
293
+ },
294
+ }
295
+
296
+ def get_tags_for_resource(self, arn: str, snapshot_name: Optional[str] = None) -> Dict[str, str]:
297
+ """Get tags for a specific resource.
298
+
299
+ Args:
300
+ arn: Resource ARN
301
+ snapshot_name: Specific snapshot (uses most recent if None)
302
+
303
+ Returns:
304
+ Dict of tag key-value pairs
305
+ """
306
+ query = """
307
+ SELECT t.key, t.value
308
+ FROM resource_tags t
309
+ JOIN resources r ON t.resource_id = r.id
310
+ JOIN snapshots s ON r.snapshot_id = s.id
311
+ WHERE r.arn = ?
312
+ """
313
+ params: List[str] = [arn]
314
+
315
+ if snapshot_name:
316
+ query += " AND s.name = ?"
317
+ params.append(snapshot_name)
318
+ else:
319
+ query += " ORDER BY s.created_at DESC LIMIT 100"
320
+
321
+ rows = self.db.fetchall(query, tuple(params))
322
+ return {row["key"]: row["value"] for row in rows}
323
+
324
+ def find_by_tag(
325
+ self,
326
+ tag_key: str,
327
+ tag_value: Optional[str] = None,
328
+ snapshot_name: Optional[str] = None,
329
+ limit: int = 100,
330
+ ) -> List[Dict[str, Any]]:
331
+ """Find resources by tag.
332
+
333
+ Args:
334
+ tag_key: Tag key to search for
335
+ tag_value: Optional tag value to match
336
+ snapshot_name: Limit to specific snapshot
337
+ limit: Maximum results
338
+
339
+ Returns:
340
+ List of matching resources
341
+ """
342
+ query = """
343
+ SELECT DISTINCT
344
+ r.arn,
345
+ r.resource_type,
346
+ r.name,
347
+ r.region,
348
+ s.name as snapshot_name,
349
+ t.key as tag_key,
350
+ t.value as tag_value
351
+ FROM resources r
352
+ JOIN snapshots s ON r.snapshot_id = s.id
353
+ JOIN resource_tags t ON r.id = t.resource_id
354
+ WHERE t.key = ?
355
+ """
356
+ params: List[Any] = [tag_key]
357
+
358
+ if tag_value:
359
+ query += " AND t.value = ?"
360
+ params.append(tag_value)
361
+
362
+ if snapshot_name:
363
+ query += " AND s.name = ?"
364
+ params.append(snapshot_name)
365
+
366
+ query += f" ORDER BY s.created_at DESC LIMIT {limit}"
367
+
368
+ return self.db.fetchall(query, tuple(params))
369
+
370
+ def get_unique_resource_types(self, snapshot_name: Optional[str] = None) -> List[str]:
371
+ """Get list of unique resource types.
372
+
373
+ Args:
374
+ snapshot_name: Limit to specific snapshot
375
+
376
+ Returns:
377
+ List of resource type strings
378
+ """
379
+ query = """
380
+ SELECT DISTINCT r.resource_type
381
+ FROM resources r
382
+ """
383
+ params: List[str] = []
384
+
385
+ if snapshot_name:
386
+ query += " JOIN snapshots s ON r.snapshot_id = s.id WHERE s.name = ?"
387
+ params.append(snapshot_name)
388
+
389
+ query += " ORDER BY r.resource_type"
390
+
391
+ rows = self.db.fetchall(query, tuple(params))
392
+ return [row["resource_type"] for row in rows]
393
+
394
+ def get_unique_regions(self, snapshot_name: Optional[str] = None) -> List[str]:
395
+ """Get list of unique regions.
396
+
397
+ Args:
398
+ snapshot_name: Limit to specific snapshot
399
+
400
+ Returns:
401
+ List of region strings
402
+ """
403
+ query = """
404
+ SELECT DISTINCT r.region
405
+ FROM resources r
406
+ """
407
+ params: List[str] = []
408
+
409
+ if snapshot_name:
410
+ query += " JOIN snapshots s ON r.snapshot_id = s.id WHERE s.name = ?"
411
+ params.append(snapshot_name)
412
+
413
+ query += " ORDER BY r.region"
414
+
415
+ rows = self.db.fetchall(query, tuple(params))
416
+ return [row["region"] for row in rows]