reverse-diagrams 1.3.3__py3-none-any.whl → 1.3.5__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.
- reverse_diagrams-1.3.5.dist-info/METADATA +706 -0
- reverse_diagrams-1.3.5.dist-info/RECORD +35 -0
- {reverse_diagrams-1.3.3.dist-info → reverse_diagrams-1.3.5.dist-info}/WHEEL +1 -1
- src/aws/client_manager.py +217 -0
- src/aws/describe_identity_store.py +8 -0
- src/aws/describe_organization.py +324 -445
- src/aws/describe_sso.py +170 -142
- src/aws/exceptions.py +26 -0
- src/config.py +153 -0
- src/models.py +242 -0
- src/plugins/__init__.py +12 -0
- src/plugins/base.py +292 -0
- src/plugins/builtin/__init__.py +12 -0
- src/plugins/builtin/ec2_plugin.py +228 -0
- src/plugins/builtin/identity_center_plugin.py +496 -0
- src/plugins/builtin/organizations_plugin.py +376 -0
- src/plugins/registry.py +126 -0
- src/reports/console_view.py +57 -19
- src/reports/save_results.py +210 -15
- src/reverse_diagrams.py +332 -39
- src/utils/__init__.py +1 -0
- src/utils/cache.py +274 -0
- src/utils/concurrent.py +361 -0
- src/utils/progress.py +257 -0
- src/version.py +1 -1
- reverse_diagrams-1.3.3.dist-info/METADATA +0 -247
- reverse_diagrams-1.3.3.dist-info/RECORD +0 -21
- src/reports/tes.py +0 -366
- {reverse_diagrams-1.3.3.dist-info → reverse_diagrams-1.3.5.dist-info}/entry_points.txt +0 -0
- {reverse_diagrams-1.3.3.dist-info → reverse_diagrams-1.3.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""AWS Organizations plugin for generating organizational structure diagrams."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from src.plugins.base import AWSServicePlugin, PluginMetadata
|
|
7
|
+
from src.aws.client_manager import AWSClientManager
|
|
8
|
+
from src.models import DiagramConfig
|
|
9
|
+
from src.utils.concurrent import get_concurrent_processor
|
|
10
|
+
from src.utils.progress import get_progress_tracker
|
|
11
|
+
from src.dgms.graph_mapper import create_mapper, find_ou_name
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrganizationsPlugin(AWSServicePlugin):
|
|
17
|
+
"""Plugin for AWS Organizations service diagram generation."""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def metadata(self) -> PluginMetadata:
|
|
21
|
+
"""Get plugin metadata."""
|
|
22
|
+
return PluginMetadata(
|
|
23
|
+
name="organizations",
|
|
24
|
+
version="1.0.0",
|
|
25
|
+
description="Generate diagrams for AWS Organizations structure, accounts, and OUs",
|
|
26
|
+
author="Reverse Diagrams Team",
|
|
27
|
+
aws_services=["organizations"],
|
|
28
|
+
dependencies=[]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def collect_data(self, client_manager: AWSClientManager, region: str, **kwargs) -> Dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Collect AWS Organizations data.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
client_manager: AWS client manager
|
|
37
|
+
region: AWS region
|
|
38
|
+
**kwargs: Additional parameters
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dictionary containing Organizations data
|
|
42
|
+
"""
|
|
43
|
+
logger.debug(f"Collecting AWS Organizations data from region {region}")
|
|
44
|
+
progress = get_progress_tracker()
|
|
45
|
+
|
|
46
|
+
data = {
|
|
47
|
+
"region": region,
|
|
48
|
+
"organization": {},
|
|
49
|
+
"roots": [],
|
|
50
|
+
"organizational_units": [],
|
|
51
|
+
"accounts": [],
|
|
52
|
+
"organizations_complete": {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Get organization details
|
|
57
|
+
progress.show_success("🏢 Getting Organization Info")
|
|
58
|
+
org_response = client_manager.call_api("organizations", "describe_organization")
|
|
59
|
+
data["organization"] = org_response.get("Organization", {})
|
|
60
|
+
logger.debug(f"Retrieved organization: {data['organization'].get('Id', 'Unknown')}")
|
|
61
|
+
|
|
62
|
+
# Get roots
|
|
63
|
+
roots_response = client_manager.call_api("organizations", "list_roots")
|
|
64
|
+
data["roots"] = roots_response.get("Roots", [])
|
|
65
|
+
logger.debug(f"Found {len(data['roots'])} organization roots")
|
|
66
|
+
|
|
67
|
+
if not data["roots"]:
|
|
68
|
+
raise ValueError("No organization roots found")
|
|
69
|
+
|
|
70
|
+
root_id = data["roots"][0]["Id"]
|
|
71
|
+
|
|
72
|
+
# Get organizational units recursively
|
|
73
|
+
progress.show_success("📋 Listing Organizational Units")
|
|
74
|
+
data["organizational_units"] = self._list_organizational_units_recursive(
|
|
75
|
+
client_manager, root_id, region
|
|
76
|
+
)
|
|
77
|
+
logger.debug(f"Found {len(data['organizational_units'])} organizational units")
|
|
78
|
+
|
|
79
|
+
# Get accounts with parent information
|
|
80
|
+
progress.show_success("👥 Getting Account Information")
|
|
81
|
+
data["accounts"] = self._list_accounts_with_parents(client_manager, region)
|
|
82
|
+
logger.debug(f"Found {len(data['accounts'])} accounts")
|
|
83
|
+
|
|
84
|
+
# Create complete organization map
|
|
85
|
+
data["organizations_complete"] = self._create_organization_complete_map(
|
|
86
|
+
root_id,
|
|
87
|
+
data["organization"],
|
|
88
|
+
data["organizational_units"],
|
|
89
|
+
data["accounts"]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
progress.show_summary(
|
|
93
|
+
"Organization Summary",
|
|
94
|
+
[
|
|
95
|
+
f"Organization ID: {data['organization'].get('Id', 'Unknown')}",
|
|
96
|
+
f"Master Account: {data['organization'].get('MasterAccountId', 'Unknown')}",
|
|
97
|
+
f"Organizational Units: {len(data['organizational_units'])}",
|
|
98
|
+
f"Accounts: {len(data['accounts'])}"
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Failed to collect Organizations data: {e}")
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
def _list_organizational_units_recursive(
|
|
109
|
+
self,
|
|
110
|
+
client_manager: AWSClientManager,
|
|
111
|
+
parent_id: str,
|
|
112
|
+
region: str,
|
|
113
|
+
depth: int = 0
|
|
114
|
+
) -> List[Dict[str, Any]]:
|
|
115
|
+
"""
|
|
116
|
+
List organizational units recursively.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
client_manager: AWS client manager
|
|
120
|
+
parent_id: Parent ID to start from
|
|
121
|
+
region: AWS region
|
|
122
|
+
depth: Current recursion depth
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of all organizational units
|
|
126
|
+
"""
|
|
127
|
+
if depth > 10: # Prevent infinite recursion
|
|
128
|
+
logger.warning(f"Maximum recursion depth reached for parent {parent_id}")
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
all_ous = []
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Get OUs for current parent using pagination
|
|
135
|
+
ous = client_manager.paginate_api_call(
|
|
136
|
+
"organizations",
|
|
137
|
+
"list_organizational_units_for_parent",
|
|
138
|
+
"OrganizationalUnits",
|
|
139
|
+
ParentId=parent_id
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for ou in ous:
|
|
143
|
+
# Add parent information
|
|
144
|
+
try:
|
|
145
|
+
parents_response = client_manager.call_api(
|
|
146
|
+
"organizations",
|
|
147
|
+
"list_parents",
|
|
148
|
+
ChildId=ou["Id"]
|
|
149
|
+
)
|
|
150
|
+
ou["Parents"] = parents_response.get("Parents", [])
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.warning(f"Failed to get parents for OU {ou['Id']}: {e}")
|
|
153
|
+
ou["Parents"] = []
|
|
154
|
+
|
|
155
|
+
all_ous.append(ou)
|
|
156
|
+
|
|
157
|
+
# Recursively get child OUs
|
|
158
|
+
child_ous = self._list_organizational_units_recursive(
|
|
159
|
+
client_manager, ou["Id"], region, depth + 1
|
|
160
|
+
)
|
|
161
|
+
all_ous.extend(child_ous)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning(f"Failed to get OUs for parent {parent_id}: {e}")
|
|
165
|
+
|
|
166
|
+
return all_ous
|
|
167
|
+
|
|
168
|
+
def _list_accounts_with_parents(
|
|
169
|
+
self,
|
|
170
|
+
client_manager: AWSClientManager,
|
|
171
|
+
region: str
|
|
172
|
+
) -> List[Dict[str, Any]]:
|
|
173
|
+
"""
|
|
174
|
+
List all accounts with parent information.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
client_manager: AWS client manager
|
|
178
|
+
region: AWS region
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of accounts with parent information
|
|
182
|
+
"""
|
|
183
|
+
progress = get_progress_tracker()
|
|
184
|
+
|
|
185
|
+
# Get all accounts using pagination
|
|
186
|
+
accounts = client_manager.paginate_api_call(
|
|
187
|
+
"organizations",
|
|
188
|
+
"list_accounts",
|
|
189
|
+
"Accounts"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
logger.debug(f"Found {len(accounts)} accounts in organization")
|
|
193
|
+
|
|
194
|
+
# Add parent information for each account
|
|
195
|
+
indexed_accounts = []
|
|
196
|
+
|
|
197
|
+
with progress.track_operation(f"Getting parent info for {len(accounts)} accounts", total=len(accounts)) as task_id:
|
|
198
|
+
for i, account in enumerate(accounts):
|
|
199
|
+
try:
|
|
200
|
+
parents_response = client_manager.call_api(
|
|
201
|
+
"organizations",
|
|
202
|
+
"list_parents",
|
|
203
|
+
ChildId=account["Id"]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
indexed_accounts.append({
|
|
207
|
+
"account": account["Id"],
|
|
208
|
+
"name": account["Name"],
|
|
209
|
+
"email": account.get("Email", ""),
|
|
210
|
+
"status": account.get("Status", "ACTIVE"),
|
|
211
|
+
"parents": parents_response.get("Parents", [])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
progress.update_progress(task_id)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.warning(f"Failed to get parents for account {account['Id']}: {e}")
|
|
218
|
+
indexed_accounts.append({
|
|
219
|
+
"account": account["Id"],
|
|
220
|
+
"name": account["Name"],
|
|
221
|
+
"email": account.get("Email", ""),
|
|
222
|
+
"status": account.get("Status", "ACTIVE"),
|
|
223
|
+
"parents": []
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return indexed_accounts
|
|
227
|
+
|
|
228
|
+
def _create_organization_complete_map(
|
|
229
|
+
self,
|
|
230
|
+
root_id: str,
|
|
231
|
+
organization: Dict[str, Any],
|
|
232
|
+
organizational_units: List[Dict[str, Any]],
|
|
233
|
+
accounts: List[Dict[str, Any]]
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Create a complete organization map with nested structure.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
root_id: Organization root ID
|
|
240
|
+
organization: Organization details
|
|
241
|
+
organizational_units: List of OUs
|
|
242
|
+
accounts: List of accounts
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Complete organization structure
|
|
246
|
+
"""
|
|
247
|
+
organizations_complete = {
|
|
248
|
+
"rootId": root_id,
|
|
249
|
+
"masterAccountId": organization.get("MasterAccountId", ""),
|
|
250
|
+
"noOutAccounts": [],
|
|
251
|
+
"organizationalUnits": {},
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Build OU hierarchy
|
|
255
|
+
for ou in organizational_units:
|
|
256
|
+
for parent in ou.get("Parents", []):
|
|
257
|
+
if parent["Type"] == "ROOT":
|
|
258
|
+
organizations_complete["organizationalUnits"][ou["Name"]] = {
|
|
259
|
+
"Id": ou["Id"],
|
|
260
|
+
"Name": ou["Name"],
|
|
261
|
+
"accounts": {},
|
|
262
|
+
"nestedOus": {},
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Add accounts to appropriate OUs or root
|
|
266
|
+
for account in accounts:
|
|
267
|
+
for parent in account.get("parents", []):
|
|
268
|
+
if parent["Type"] == "ROOT":
|
|
269
|
+
organizations_complete["noOutAccounts"].append({
|
|
270
|
+
"account": account["account"],
|
|
271
|
+
"name": account["name"]
|
|
272
|
+
})
|
|
273
|
+
elif parent["Type"] == "ORGANIZATIONAL_UNIT":
|
|
274
|
+
# Find the OU name
|
|
275
|
+
ou_name = find_ou_name(organizational_units, parent["Id"])
|
|
276
|
+
if ou_name and ou_name in organizations_complete["organizationalUnits"]:
|
|
277
|
+
organizations_complete["organizationalUnits"][ou_name]["accounts"][account["name"]] = {
|
|
278
|
+
"account": account["account"],
|
|
279
|
+
"name": account["name"]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return organizations_complete
|
|
283
|
+
|
|
284
|
+
def generate_diagram_code(self, data: Dict[str, Any], config: DiagramConfig) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Generate diagram code for AWS Organizations.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
data: Organizations data collected from AWS
|
|
290
|
+
config: Diagram configuration
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Python code for generating Organizations diagram
|
|
294
|
+
"""
|
|
295
|
+
logger.debug("Generating AWS Organizations diagram code")
|
|
296
|
+
|
|
297
|
+
from src.dgms.graph_template import graph_template
|
|
298
|
+
|
|
299
|
+
# Start with the template
|
|
300
|
+
code_lines = [
|
|
301
|
+
"from diagrams import Diagram, Cluster",
|
|
302
|
+
"from diagrams.aws.management import Organizations, OrganizationsAccount, OrganizationsOrganizationalUnit",
|
|
303
|
+
"",
|
|
304
|
+
f'with Diagram("{config.title}", show=False, direction="{config.direction}"):'
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
# Add organization root
|
|
308
|
+
organization = data.get("organization", {})
|
|
309
|
+
roots = data.get("roots", [])
|
|
310
|
+
organizational_units = data.get("organizational_units", [])
|
|
311
|
+
accounts = data.get("accounts", [])
|
|
312
|
+
|
|
313
|
+
if roots:
|
|
314
|
+
root_id = roots[0]["Id"]
|
|
315
|
+
code_lines.extend([
|
|
316
|
+
" with Cluster('Organizations'):",
|
|
317
|
+
f" oo = Organizations('{organization.get('Id', 'Unknown')}\\n{organization.get('MasterAccountId', 'Unknown')}\\n{root_id}')"
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
# Add organizational units
|
|
321
|
+
for ou in organizational_units:
|
|
322
|
+
ou_name_safe = self._format_name_for_code(ou["Name"])
|
|
323
|
+
code_lines.append(
|
|
324
|
+
f" ou_{ou_name_safe} = OrganizationsOrganizationalUnit(\"{ou['Id']}\\n{ou['Name']}\")"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Add relationships
|
|
328
|
+
for parent in ou.get("Parents", []):
|
|
329
|
+
if parent["Type"] == "ROOT":
|
|
330
|
+
code_lines.append(f" oo >> ou_{ou_name_safe}")
|
|
331
|
+
elif parent["Type"] == "ORGANIZATIONAL_UNIT":
|
|
332
|
+
parent_name = find_ou_name(organizational_units, parent["Id"])
|
|
333
|
+
if parent_name:
|
|
334
|
+
parent_name_safe = self._format_name_for_code(parent_name)
|
|
335
|
+
code_lines.append(f" ou_{parent_name_safe} >> ou_{ou_name_safe}")
|
|
336
|
+
|
|
337
|
+
# Add accounts
|
|
338
|
+
for account in accounts:
|
|
339
|
+
account_name_safe = self._format_name_for_code(account["name"])
|
|
340
|
+
for parent in account.get("parents", []):
|
|
341
|
+
if parent["Type"] == "ROOT":
|
|
342
|
+
code_lines.append(
|
|
343
|
+
f" oo >> OrganizationsAccount(\"{account['account']}\\n{self._split_long_name(account['name'])}\")"
|
|
344
|
+
)
|
|
345
|
+
elif parent["Type"] == "ORGANIZATIONAL_UNIT":
|
|
346
|
+
ou_name = find_ou_name(organizational_units, parent["Id"])
|
|
347
|
+
if ou_name:
|
|
348
|
+
ou_name_safe = self._format_name_for_code(ou_name)
|
|
349
|
+
code_lines.append(
|
|
350
|
+
f" ou_{ou_name_safe} >> OrganizationsAccount(\"{account['account']}\\n{self._split_long_name(account['name'])}\")"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return "\n".join(code_lines)
|
|
354
|
+
|
|
355
|
+
def _format_name_for_code(self, name: str) -> str:
|
|
356
|
+
"""Format name for use in Python code."""
|
|
357
|
+
import re
|
|
358
|
+
# Remove special characters and spaces
|
|
359
|
+
formatted = re.sub(r'[^\w]', '', name)
|
|
360
|
+
return formatted if formatted else "Unknown"
|
|
361
|
+
|
|
362
|
+
def _split_long_name(self, name: str) -> str:
|
|
363
|
+
"""Split long names for better display."""
|
|
364
|
+
if len(name) > 17:
|
|
365
|
+
return name[:16] + "\\n" + name[16:]
|
|
366
|
+
return name
|
|
367
|
+
|
|
368
|
+
def get_required_permissions(self) -> List[str]:
|
|
369
|
+
"""Get required AWS permissions for Organizations plugin."""
|
|
370
|
+
return [
|
|
371
|
+
"organizations:DescribeOrganization",
|
|
372
|
+
"organizations:ListRoots",
|
|
373
|
+
"organizations:ListOrganizationalUnitsForParent",
|
|
374
|
+
"organizations:ListAccounts",
|
|
375
|
+
"organizations:ListParents"
|
|
376
|
+
]
|
src/plugins/registry.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Plugin registry and discovery system."""
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Type, List, Optional
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .base import AWSServicePlugin, PluginManager, discover_plugins_in_directory
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Global plugin manager instance
|
|
12
|
+
_plugin_manager: Optional[PluginManager] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_plugin_manager() -> PluginManager:
|
|
16
|
+
"""Get or create global plugin manager instance."""
|
|
17
|
+
global _plugin_manager
|
|
18
|
+
if _plugin_manager is None:
|
|
19
|
+
_plugin_manager = PluginManager()
|
|
20
|
+
# Auto-discover built-in plugins
|
|
21
|
+
discover_plugins()
|
|
22
|
+
return _plugin_manager
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register_plugin(plugin_class: Type[AWSServicePlugin]) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Register a plugin class with the global manager.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
plugin_class: Plugin class to register
|
|
31
|
+
"""
|
|
32
|
+
manager = get_plugin_manager()
|
|
33
|
+
manager.register_plugin(plugin_class)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def discover_plugins() -> List[Type[AWSServicePlugin]]:
|
|
37
|
+
"""
|
|
38
|
+
Discover and register plugins from standard locations.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of discovered plugin classes
|
|
42
|
+
"""
|
|
43
|
+
manager = get_plugin_manager()
|
|
44
|
+
all_discovered = []
|
|
45
|
+
|
|
46
|
+
# Standard plugin directories
|
|
47
|
+
plugin_directories = [
|
|
48
|
+
Path(__file__).parent / "builtin", # Built-in plugins
|
|
49
|
+
Path.home() / ".reverse_diagrams" / "plugins", # User plugins
|
|
50
|
+
Path.cwd() / "plugins", # Local plugins
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Add plugins from environment variable
|
|
54
|
+
env_plugin_dirs = os.environ.get("REVERSE_DIAGRAMS_PLUGIN_DIRS", "")
|
|
55
|
+
if env_plugin_dirs:
|
|
56
|
+
for dir_path in env_plugin_dirs.split(":"):
|
|
57
|
+
if dir_path.strip():
|
|
58
|
+
plugin_directories.append(Path(dir_path.strip()))
|
|
59
|
+
|
|
60
|
+
# Discover plugins in each directory
|
|
61
|
+
for plugin_dir in plugin_directories:
|
|
62
|
+
try:
|
|
63
|
+
discovered = discover_plugins_in_directory(plugin_dir)
|
|
64
|
+
for plugin_class in discovered:
|
|
65
|
+
manager.register_plugin(plugin_class)
|
|
66
|
+
all_discovered.append(plugin_class)
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.warning(f"Failed to discover plugins in {plugin_dir}: {e}")
|
|
70
|
+
|
|
71
|
+
logger.debug(f"Discovered and registered {len(all_discovered)} plugins")
|
|
72
|
+
return all_discovered
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def list_available_plugins() -> List[str]:
|
|
76
|
+
"""
|
|
77
|
+
List names of all available plugins.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of plugin names
|
|
81
|
+
"""
|
|
82
|
+
manager = get_plugin_manager()
|
|
83
|
+
metadata_list = manager.list_available_plugins()
|
|
84
|
+
return [metadata.name for metadata in metadata_list]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_plugin_info(plugin_name: str) -> Optional[dict]:
|
|
88
|
+
"""
|
|
89
|
+
Get information about a specific plugin.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
plugin_name: Name of the plugin
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Plugin information dictionary or None if not found
|
|
96
|
+
"""
|
|
97
|
+
manager = get_plugin_manager()
|
|
98
|
+
|
|
99
|
+
# Try to get from loaded plugins first
|
|
100
|
+
plugin = manager.get_plugin(plugin_name)
|
|
101
|
+
if plugin:
|
|
102
|
+
metadata = plugin.metadata
|
|
103
|
+
return {
|
|
104
|
+
"name": metadata.name,
|
|
105
|
+
"version": metadata.version,
|
|
106
|
+
"description": metadata.description,
|
|
107
|
+
"author": metadata.author,
|
|
108
|
+
"aws_services": metadata.aws_services,
|
|
109
|
+
"dependencies": metadata.dependencies,
|
|
110
|
+
"status": "loaded"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Check available plugins
|
|
114
|
+
for metadata in manager.list_available_plugins():
|
|
115
|
+
if metadata.name == plugin_name:
|
|
116
|
+
return {
|
|
117
|
+
"name": metadata.name,
|
|
118
|
+
"version": metadata.version,
|
|
119
|
+
"description": metadata.description,
|
|
120
|
+
"author": metadata.author,
|
|
121
|
+
"aws_services": metadata.aws_services,
|
|
122
|
+
"dependencies": metadata.dependencies,
|
|
123
|
+
"status": "available"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return None
|
src/reports/console_view.py
CHANGED
|
@@ -23,15 +23,38 @@ def get_members(group):
|
|
|
23
23
|
"""
|
|
24
24
|
Get members name.
|
|
25
25
|
|
|
26
|
-
:param group:
|
|
27
|
-
:return:
|
|
26
|
+
:param group: Can be either a list of group dicts or a dict with numeric keys
|
|
27
|
+
:return: Dictionary mapping group names to member lists
|
|
28
28
|
"""
|
|
29
29
|
members_groups = {}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
|
|
31
|
+
# Handle new format (list of dicts with group_id, group_name, members)
|
|
32
|
+
if isinstance(group, list):
|
|
33
|
+
for g in group:
|
|
34
|
+
group_name = g.get("group_name", "Unknown Group")
|
|
35
|
+
members_groups[group_name] = []
|
|
36
|
+
for member in g.get("members", []):
|
|
37
|
+
# Handle both old and new member formats
|
|
38
|
+
if isinstance(member, dict):
|
|
39
|
+
if "MemberId" in member and "UserName" in member["MemberId"]:
|
|
40
|
+
members_groups[group_name].append(member["MemberId"]["UserName"])
|
|
41
|
+
elif "UserName" in member:
|
|
42
|
+
members_groups[group_name].append(member["UserName"])
|
|
43
|
+
|
|
44
|
+
# Handle old format (dict with numeric keys)
|
|
45
|
+
elif isinstance(group, dict):
|
|
46
|
+
for key in group:
|
|
47
|
+
g = group[key]
|
|
48
|
+
if isinstance(g, dict) and "group_name" in g:
|
|
49
|
+
group_name = g["group_name"]
|
|
50
|
+
members_groups[group_name] = []
|
|
51
|
+
for member in g.get("members", []):
|
|
52
|
+
if isinstance(member, dict):
|
|
53
|
+
if "MemberId" in member and "UserName" in member["MemberId"]:
|
|
54
|
+
members_groups[group_name].append(member["MemberId"]["UserName"])
|
|
55
|
+
elif "UserName" in member:
|
|
56
|
+
members_groups[group_name].append(member["UserName"])
|
|
57
|
+
|
|
35
58
|
return members_groups
|
|
36
59
|
|
|
37
60
|
|
|
@@ -53,17 +76,17 @@ def create_group_console_view(groups):
|
|
|
53
76
|
"""
|
|
54
77
|
Create tree.
|
|
55
78
|
|
|
56
|
-
:param groups:
|
|
79
|
+
:param groups: Can be either a list of group dicts or a dict with numeric keys
|
|
57
80
|
:return:
|
|
58
81
|
"""
|
|
59
82
|
members = get_members(groups)
|
|
60
83
|
console = Console()
|
|
61
84
|
c = [
|
|
62
85
|
Panel(
|
|
63
|
-
f"[b][green]{
|
|
86
|
+
f"[b][green]{group_name}[/b]\n[yellow]{pretty_members(members[group_name])}",
|
|
64
87
|
expand=True,
|
|
65
88
|
)
|
|
66
|
-
for
|
|
89
|
+
for group_name in members.keys()
|
|
67
90
|
]
|
|
68
91
|
console.print(Columns(c))
|
|
69
92
|
|
|
@@ -227,12 +250,27 @@ def watch_on_demand(
|
|
|
227
250
|
:param args:
|
|
228
251
|
:return:
|
|
229
252
|
"""
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
253
|
+
try:
|
|
254
|
+
if args.watch_graph_organization:
|
|
255
|
+
# create_console_view(file_path=f"{diagrams_path}/json/organizations.json")
|
|
256
|
+
print("Not available yet")
|
|
257
|
+
if args.watch_graph_accounts_assignments:
|
|
258
|
+
assign = load_json(args.watch_graph_accounts_assignments)
|
|
259
|
+
create_account_assignments_view(assign=assign)
|
|
260
|
+
if args.watch_graph_identity:
|
|
261
|
+
c = load_json(args.watch_graph_identity)
|
|
262
|
+
create_group_console_view(groups=c)
|
|
263
|
+
except FileNotFoundError as e:
|
|
264
|
+
print(f"❌ Error: File not found - {e}")
|
|
265
|
+
print("💡 Make sure you've generated the diagrams first using:")
|
|
266
|
+
print(" reverse_diagrams -o -i -p <profile> -r <region>")
|
|
267
|
+
except json.JSONDecodeError as e:
|
|
268
|
+
print(f"❌ Error: Invalid JSON file - {e}")
|
|
269
|
+
print("💡 The file may be corrupted. Try regenerating it.")
|
|
270
|
+
except KeyError as e:
|
|
271
|
+
print(f"❌ Error: Missing expected data in file - {e}")
|
|
272
|
+
print("💡 The file format may be outdated. Try regenerating it.")
|
|
273
|
+
except Exception as e:
|
|
274
|
+
print(f"❌ Unexpected error: {e}")
|
|
275
|
+
import traceback
|
|
276
|
+
traceback.print_exc()
|