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,260 @@
1
+ """Saved views API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from typing import List, Optional
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+ from pydantic import BaseModel
11
+
12
+ from ...dependencies import get_database
13
+
14
+ router = APIRouter(prefix="/views")
15
+
16
+
17
+ class ColumnConfig(BaseModel):
18
+ """Column configuration model."""
19
+
20
+ field: str
21
+ label: str
22
+ visible: bool = True
23
+ width: Optional[int] = None
24
+
25
+
26
+ class ViewConfig(BaseModel):
27
+ """View configuration model."""
28
+
29
+ columns: List[ColumnConfig]
30
+ sort_by: Optional[str] = None
31
+ sort_order: str = "asc"
32
+ filters: Optional[dict] = None
33
+
34
+
35
+ class SavedView(BaseModel):
36
+ """Saved view model."""
37
+
38
+ id: Optional[int] = None
39
+ name: str
40
+ description: Optional[str] = None
41
+ view_config: ViewConfig
42
+ is_default: bool = False
43
+ is_favorite: bool = False
44
+
45
+
46
+ # Define available columns for resources
47
+ AVAILABLE_COLUMNS = [
48
+ {"field": "name", "label": "Name", "default": True},
49
+ {"field": "arn", "label": "ARN", "default": True},
50
+ {"field": "resource_type", "label": "Type", "default": True},
51
+ {"field": "region", "label": "Region", "default": True},
52
+ {"field": "snapshot_name", "label": "Snapshot", "default": True},
53
+ {"field": "tags", "label": "Tags", "default": False},
54
+ {"field": "created_at", "label": "Created", "default": False},
55
+ {"field": "config_hash", "label": "Config Hash", "default": False},
56
+ ]
57
+
58
+
59
+ @router.get("/columns")
60
+ async def get_available_columns():
61
+ """Get list of available columns for customization."""
62
+ return {"columns": AVAILABLE_COLUMNS}
63
+
64
+
65
+ @router.get("")
66
+ async def list_saved_views(favorites_only: bool = False):
67
+ """List saved views."""
68
+ db = get_database()
69
+
70
+ sql = "SELECT * FROM saved_views WHERE 1=1"
71
+ params: List = []
72
+
73
+ if favorites_only:
74
+ sql += " AND is_favorite = 1"
75
+
76
+ sql += " ORDER BY is_default DESC, is_favorite DESC, last_used_at DESC NULLS LAST, name"
77
+
78
+ rows = db.fetchall(sql, tuple(params))
79
+
80
+ views = []
81
+ for row in rows:
82
+ row_dict = dict(row)
83
+ if row_dict.get("view_config"):
84
+ row_dict["view_config"] = json.loads(row_dict["view_config"])
85
+ views.append(row_dict)
86
+
87
+ return {"views": views}
88
+
89
+
90
+ @router.get("/default")
91
+ async def get_default_view():
92
+ """Get the default view configuration."""
93
+ db = get_database()
94
+
95
+ row = db.fetchone("SELECT * FROM saved_views WHERE is_default = 1")
96
+
97
+ if row:
98
+ row_dict = dict(row)
99
+ if row_dict.get("view_config"):
100
+ row_dict["view_config"] = json.loads(row_dict["view_config"])
101
+ return row_dict
102
+
103
+ # Return a default configuration if none is set
104
+ return {
105
+ "id": None,
106
+ "name": "Default View",
107
+ "view_config": {
108
+ "columns": [
109
+ {"field": c["field"], "label": c["label"], "visible": c["default"]}
110
+ for c in AVAILABLE_COLUMNS
111
+ ],
112
+ "sort_by": "name",
113
+ "sort_order": "asc",
114
+ },
115
+ }
116
+
117
+
118
+ @router.post("")
119
+ async def create_saved_view(view: SavedView):
120
+ """Save a new view."""
121
+ db = get_database()
122
+
123
+ try:
124
+ # If this is set as default, unset other defaults
125
+ if view.is_default:
126
+ db.execute("UPDATE saved_views SET is_default = 0 WHERE is_default = 1")
127
+
128
+ config_json = json.dumps(view.view_config.model_dump())
129
+ cursor = db.execute(
130
+ """
131
+ INSERT INTO saved_views (name, description, view_config, is_default, is_favorite, created_at)
132
+ VALUES (?, ?, ?, ?, ?, ?)
133
+ """,
134
+ (
135
+ view.name,
136
+ view.description,
137
+ config_json,
138
+ view.is_default,
139
+ view.is_favorite,
140
+ datetime.utcnow().isoformat(),
141
+ ),
142
+ )
143
+ db._conn.commit() # type: ignore
144
+ return {"id": cursor.lastrowid, "message": "View saved"}
145
+ except Exception as e:
146
+ if "UNIQUE constraint" in str(e):
147
+ raise HTTPException(status_code=400, detail=f"View with name '{view.name}' already exists")
148
+ raise HTTPException(status_code=500, detail=str(e))
149
+
150
+
151
+ @router.get("/{view_id}")
152
+ async def get_saved_view(view_id: int):
153
+ """Get a saved view by ID."""
154
+ db = get_database()
155
+ row = db.fetchone("SELECT * FROM saved_views WHERE id = ?", (view_id,))
156
+
157
+ if not row:
158
+ raise HTTPException(status_code=404, detail="View not found")
159
+
160
+ row_dict = dict(row)
161
+ if row_dict.get("view_config"):
162
+ row_dict["view_config"] = json.loads(row_dict["view_config"])
163
+ return row_dict
164
+
165
+
166
+ @router.put("/{view_id}")
167
+ async def update_saved_view(view_id: int, view: SavedView):
168
+ """Update a saved view."""
169
+ db = get_database()
170
+
171
+ existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
172
+ if not existing:
173
+ raise HTTPException(status_code=404, detail="View not found")
174
+
175
+ # If this is set as default, unset other defaults
176
+ if view.is_default:
177
+ db.execute("UPDATE saved_views SET is_default = 0 WHERE is_default = 1 AND id != ?", (view_id,))
178
+
179
+ config_json = json.dumps(view.view_config.model_dump())
180
+ db.execute(
181
+ """
182
+ UPDATE saved_views
183
+ SET name = ?, description = ?, view_config = ?, is_default = ?, is_favorite = ?
184
+ WHERE id = ?
185
+ """,
186
+ (view.name, view.description, config_json, view.is_default, view.is_favorite, view_id),
187
+ )
188
+ db._conn.commit() # type: ignore
189
+ return {"message": "View updated"}
190
+
191
+
192
+ @router.delete("/{view_id}")
193
+ async def delete_saved_view(view_id: int):
194
+ """Delete a saved view."""
195
+ db = get_database()
196
+
197
+ existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
198
+ if not existing:
199
+ raise HTTPException(status_code=404, detail="View not found")
200
+
201
+ db.execute("DELETE FROM saved_views WHERE id = ?", (view_id,))
202
+ db._conn.commit() # type: ignore
203
+ return {"message": "View deleted"}
204
+
205
+
206
+ @router.post("/{view_id}/use")
207
+ async def mark_view_used(view_id: int):
208
+ """Mark a view as used (updates last_used_at and use_count)."""
209
+ db = get_database()
210
+
211
+ existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
212
+ if not existing:
213
+ raise HTTPException(status_code=404, detail="View not found")
214
+
215
+ db.execute(
216
+ """
217
+ UPDATE saved_views
218
+ SET last_used_at = ?, use_count = use_count + 1
219
+ WHERE id = ?
220
+ """,
221
+ (datetime.utcnow().isoformat(), view_id),
222
+ )
223
+ db._conn.commit() # type: ignore
224
+ return {"message": "View marked as used"}
225
+
226
+
227
+ @router.post("/{view_id}/set-default")
228
+ async def set_default_view(view_id: int):
229
+ """Set a view as the default."""
230
+ db = get_database()
231
+
232
+ existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
233
+ if not existing:
234
+ raise HTTPException(status_code=404, detail="View not found")
235
+
236
+ # Unset current default
237
+ db.execute("UPDATE saved_views SET is_default = 0 WHERE is_default = 1")
238
+
239
+ # Set new default
240
+ db.execute("UPDATE saved_views SET is_default = 1 WHERE id = ?", (view_id,))
241
+ db._conn.commit() # type: ignore
242
+ return {"message": "View set as default"}
243
+
244
+
245
+ @router.post("/{view_id}/favorite")
246
+ async def toggle_view_favorite(view_id: int):
247
+ """Toggle the favorite status of a view."""
248
+ db = get_database()
249
+
250
+ existing = db.fetchone("SELECT id, is_favorite FROM saved_views WHERE id = ?", (view_id,))
251
+ if not existing:
252
+ raise HTTPException(status_code=404, detail="View not found")
253
+
254
+ new_favorite = not existing["is_favorite"]
255
+ db.execute(
256
+ "UPDATE saved_views SET is_favorite = ? WHERE id = ?",
257
+ (new_favorite, view_id),
258
+ )
259
+ db._conn.commit() # type: ignore
260
+ return {"message": "Favorite toggled", "is_favorite": new_favorite}
@@ -0,0 +1,198 @@
1
+ """HTML page routes for the web UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Request
6
+ from fastapi.responses import HTMLResponse
7
+
8
+ from ..dependencies import get_audit_store, get_group_store, get_resource_store, get_snapshot_store
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.get("/", response_class=HTMLResponse)
14
+ async def dashboard(request: Request):
15
+ """Render the main dashboard page."""
16
+ templates = request.app.state.templates
17
+
18
+ # Get dashboard data
19
+ snapshot_store = get_snapshot_store()
20
+ resource_store = get_resource_store()
21
+
22
+ snapshots = snapshot_store.list_all()
23
+ total_snapshots = len(snapshots)
24
+ total_resources = snapshot_store.get_resource_count()
25
+ active_snapshot = snapshot_store.get_active()
26
+
27
+ # Get stats for charts
28
+ stats_by_type = resource_store.get_stats(group_by="type")[:10]
29
+ stats_by_region = resource_store.get_stats(group_by="region")
30
+
31
+ return templates.TemplateResponse(
32
+ "pages/dashboard.html",
33
+ {
34
+ "request": request,
35
+ "total_snapshots": total_snapshots,
36
+ "total_resources": total_resources,
37
+ "active_snapshot": active_snapshot,
38
+ "recent_snapshots": snapshots[:5],
39
+ "stats_by_type": stats_by_type,
40
+ "stats_by_region": stats_by_region,
41
+ },
42
+ )
43
+
44
+
45
+ @router.get("/snapshots", response_class=HTMLResponse)
46
+ async def snapshots_list(request: Request):
47
+ """Render the snapshots list page."""
48
+ templates = request.app.state.templates
49
+ snapshot_store = get_snapshot_store()
50
+
51
+ snapshots = snapshot_store.list_all()
52
+ active_snapshot = snapshot_store.get_active()
53
+
54
+ return templates.TemplateResponse(
55
+ "pages/snapshots.html",
56
+ {
57
+ "request": request,
58
+ "snapshots": snapshots,
59
+ "active_snapshot": active_snapshot,
60
+ },
61
+ )
62
+
63
+
64
+ @router.get("/snapshots/{name}", response_class=HTMLResponse)
65
+ async def snapshot_detail(request: Request, name: str):
66
+ """Render snapshot detail page."""
67
+ templates = request.app.state.templates
68
+ snapshot_store = get_snapshot_store()
69
+ resource_store = get_resource_store()
70
+
71
+ snapshot = snapshot_store.load(name)
72
+ if not snapshot:
73
+ return templates.TemplateResponse(
74
+ "pages/error.html",
75
+ {"request": request, "error": f"Snapshot '{name}' not found"},
76
+ status_code=404,
77
+ )
78
+
79
+ # Get stats for this snapshot
80
+ stats_by_type = resource_store.get_stats(snapshot_name=name, group_by="type")
81
+ stats_by_region = resource_store.get_stats(snapshot_name=name, group_by="region")
82
+
83
+ return templates.TemplateResponse(
84
+ "pages/snapshot_detail.html",
85
+ {
86
+ "request": request,
87
+ "snapshot": snapshot,
88
+ "stats_by_type": stats_by_type,
89
+ "stats_by_region": stats_by_region,
90
+ },
91
+ )
92
+
93
+
94
+ @router.get("/resources", response_class=HTMLResponse)
95
+ async def resources_list(request: Request):
96
+ """Render the resource explorer page."""
97
+ templates = request.app.state.templates
98
+ resource_store = get_resource_store()
99
+ snapshot_store = get_snapshot_store()
100
+
101
+ # Get filter options
102
+ resource_types = resource_store.get_unique_resource_types()
103
+ regions = resource_store.get_unique_regions()
104
+ snapshots = snapshot_store.list_all()
105
+
106
+ return templates.TemplateResponse(
107
+ "pages/resources.html",
108
+ {
109
+ "request": request,
110
+ "resource_types": resource_types,
111
+ "regions": regions,
112
+ "snapshots": snapshots,
113
+ },
114
+ )
115
+
116
+
117
+ @router.get("/diff", response_class=HTMLResponse)
118
+ async def diff_page(request: Request):
119
+ """Render the diff viewer page."""
120
+ templates = request.app.state.templates
121
+ snapshot_store = get_snapshot_store()
122
+
123
+ snapshots = snapshot_store.list_all()
124
+
125
+ return templates.TemplateResponse(
126
+ "pages/diff.html",
127
+ {
128
+ "request": request,
129
+ "snapshots": snapshots,
130
+ },
131
+ )
132
+
133
+
134
+ @router.get("/queries", response_class=HTMLResponse)
135
+ async def queries_page(request: Request):
136
+ """Render the SQL query editor page."""
137
+ templates = request.app.state.templates
138
+
139
+ return templates.TemplateResponse(
140
+ "pages/queries.html",
141
+ {
142
+ "request": request,
143
+ },
144
+ )
145
+
146
+
147
+ @router.get("/cleanup", response_class=HTMLResponse)
148
+ async def cleanup_page(request: Request):
149
+ """Render the cleanup operations page."""
150
+ templates = request.app.state.templates
151
+ snapshot_store = get_snapshot_store()
152
+
153
+ snapshots = snapshot_store.list_all()
154
+
155
+ return templates.TemplateResponse(
156
+ "pages/cleanup.html",
157
+ {
158
+ "request": request,
159
+ "snapshots": snapshots,
160
+ },
161
+ )
162
+
163
+
164
+ @router.get("/audit", response_class=HTMLResponse)
165
+ async def audit_logs_page(request: Request):
166
+ """Render the audit logs page."""
167
+ templates = request.app.state.templates
168
+ audit_store = get_audit_store()
169
+
170
+ operations = audit_store.list_operations(limit=50)
171
+
172
+ return templates.TemplateResponse(
173
+ "pages/audit_logs.html",
174
+ {
175
+ "request": request,
176
+ "operations": operations,
177
+ },
178
+ )
179
+
180
+
181
+ @router.get("/groups", response_class=HTMLResponse)
182
+ async def groups_page(request: Request):
183
+ """Render the resource groups page."""
184
+ templates = request.app.state.templates
185
+ group_store = get_group_store()
186
+ snapshot_store = get_snapshot_store()
187
+
188
+ groups = group_store.list_all()
189
+ snapshots = snapshot_store.list_all()
190
+
191
+ return templates.TemplateResponse(
192
+ "pages/groups.html",
193
+ {
194
+ "request": request,
195
+ "groups": groups,
196
+ "snapshots": snapshots,
197
+ },
198
+ )
@@ -0,0 +1 @@
1
+ """Web services package."""