reverse-diagrams 1.3.4__py3-none-any.whl → 2.0.0__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-2.0.0.dist-info/METADATA +706 -0
- reverse_diagrams-2.0.0.dist-info/RECORD +35 -0
- {reverse_diagrams-1.3.4.dist-info → reverse_diagrams-2.0.0.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 -143
- src/aws/exceptions.py +26 -0
- src/banner/banner.py +43 -40
- 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 +331 -38
- 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.4.dist-info/METADATA +0 -247
- reverse_diagrams-1.3.4.dist-info/RECORD +0 -21
- src/reports/tes.py +0 -366
- {reverse_diagrams-1.3.4.dist-info → reverse_diagrams-2.0.0.dist-info}/entry_points.txt +0 -0
- {reverse_diagrams-1.3.4.dist-info → reverse_diagrams-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
src/__init__.py,sha256=lOE8TAJF_WdxgxEBxVz9t8dC3s3t2c21VWYPrzfznkE,17
|
|
2
|
+
src/config.py,sha256=7KinPO7ybCc5IQKmz1OcT-oXF1TUQWlaIyNC2eW86bI,4980
|
|
3
|
+
src/models.py,sha256=vFDPdKGxjxwdBN3O_-bnnF9TIqM3I3KTK7n2n9PaktY,7608
|
|
4
|
+
src/reverse_diagrams.py,sha256=XFF3f1MoNDjtCYolj1SiDOYWlV5djD4Hq30NJw5gvbQ,16929
|
|
5
|
+
src/version.py,sha256=tJk7e8Z_LpxGMdTvSeAHQ45G-1AEWVWBBqAqDg_KC9w,48
|
|
6
|
+
src/aws/__init__.py,sha256=lOE8TAJF_WdxgxEBxVz9t8dC3s3t2c21VWYPrzfznkE,17
|
|
7
|
+
src/aws/client_manager.py,sha256=rmDoGDsxZj7q0tYf9xot5pxOrN5cBnHBkRgbiLrerqU,8456
|
|
8
|
+
src/aws/describe_identity_store.py,sha256=AA3GVz6UyKepImm6VL2nqkbyH13X8f40qn-zNhICEkw,15821
|
|
9
|
+
src/aws/describe_organization.py,sha256=tqImKyLtftk5upgVQmordFmBO7M1NeIQg3-KwwiQhOA,12743
|
|
10
|
+
src/aws/describe_sso.py,sha256=_wcyHr0xFLXicC6IJs7uaj26E6TXywiiwUjteStCI_M,6771
|
|
11
|
+
src/aws/exceptions.py,sha256=QJqO3i5Q1BiwnX15RDkUWQoDA-6MnYsNP5d8Q6SKqbE,562
|
|
12
|
+
src/banner/__init__.py,sha256=lOE8TAJF_WdxgxEBxVz9t8dC3s3t2c21VWYPrzfznkE,17
|
|
13
|
+
src/banner/banner.py,sha256=QOhJ_qI0_0MWI5wo-IMFv_-nrPN4RuJRJSqPUq_uPF0,2101
|
|
14
|
+
src/dgms/__init__.py,sha256=lOE8TAJF_WdxgxEBxVz9t8dC3s3t2c21VWYPrzfznkE,17
|
|
15
|
+
src/dgms/graph_mapper.py,sha256=xutxzxLCIGSBfHCAF-BnWXN21bjzusA43ZhKtXUir2E,7714
|
|
16
|
+
src/dgms/graph_template.py,sha256=IOEmTk9A0xqEhtdNnaFrVDJlBF0ggW0WpxosW4nbcQo,1217
|
|
17
|
+
src/plugins/__init__.py,sha256=Oq17hXr9-BOlY60ZcbKbQaeMf2cabqJc6APdtqJ7Ylg,312
|
|
18
|
+
src/plugins/base.py,sha256=wuLqqNibsAoJJacKI3pQwUFdzmhbXBm__U60_xyRDKA,9533
|
|
19
|
+
src/plugins/registry.py,sha256=R4IpEnis9wr5GUREAb3Rqi2X-4OBY41kvynGxg0doSo,3957
|
|
20
|
+
src/plugins/builtin/__init__.py,sha256=u06O_XAREbZ1BxvYp1d-2stD3iELpA_pTll3j5-b5Jw,360
|
|
21
|
+
src/plugins/builtin/ec2_plugin.py,sha256=BgLANn7sTN0m9Rs0gHnA-5mtPJL1V-tQmr7JnUtac38,8958
|
|
22
|
+
src/plugins/builtin/identity_center_plugin.py,sha256=9IKTEFAaluuePoED2jhwtLAL0_fjbcP72TEmRQCXpJ8,22141
|
|
23
|
+
src/plugins/builtin/organizations_plugin.py,sha256=4dYGCL4kvJ_ZAa9D_-6fRS6u0mqUr8hzalr-sRna1CE,15319
|
|
24
|
+
src/reports/__init__.py,sha256=lOE8TAJF_WdxgxEBxVz9t8dC3s3t2c21VWYPrzfznkE,17
|
|
25
|
+
src/reports/console_view.py,sha256=-zR8fd7h2lLwqjIIs8TMzl0r8Q1k3ZWt5xrtEhN9KNc,7997
|
|
26
|
+
src/reports/save_results.py,sha256=35KOP6R3t5DBWQDPdQy5agEODPQBTPnwe22M7-HfOJA,7004
|
|
27
|
+
src/utils/__init__.py,sha256=2qVZjBvUSXJngCvc68FnhAOJTA2RiW-AWc6HhxQX-ak,43
|
|
28
|
+
src/utils/cache.py,sha256=fZ5b0rgeETNeaJW19hHClkQwEiohF4hvLr1nxuPSqXQ,8711
|
|
29
|
+
src/utils/concurrent.py,sha256=6haOaH7IGeCLHyGlxkMrOYEuaA471k2ncHr3VtNXhNU,12230
|
|
30
|
+
src/utils/progress.py,sha256=j9e6-axDloIn8C5UL1CLVqlw4nQbKe7gK1Mjd_e0Wdk,7996
|
|
31
|
+
reverse_diagrams-2.0.0.dist-info/METADATA,sha256=L0RTnQsLw-WlLlEnHHjrS3FuZXvormP9aqwCr2yrxvg,23889
|
|
32
|
+
reverse_diagrams-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
+
reverse_diagrams-2.0.0.dist-info/entry_points.txt,sha256=VZNkrc7qUDbddTCH3pGd83EhUT3PHTx9MzpAk6bb6qc,63
|
|
34
|
+
reverse_diagrams-2.0.0.dist-info/licenses/LICENSE,sha256=o6nDaQ7M9xbR0L7HSJ3A-1JbBPoZro_zhVPO4-M5CAQ,571
|
|
35
|
+
reverse_diagrams-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""AWS Client Manager with proper error handling and validation."""
|
|
2
|
+
import logging
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from botocore.exceptions import (
|
|
9
|
+
ClientError,
|
|
10
|
+
NoCredentialsError,
|
|
11
|
+
PartialCredentialsError,
|
|
12
|
+
BotoCoreError
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
AWSCredentialsError,
|
|
17
|
+
AWSPermissionError,
|
|
18
|
+
AWSServiceError,
|
|
19
|
+
AWSResourceNotFoundError
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def retry_with_backoff(max_retries: int = 3, backoff_factor: float = 2.0):
|
|
26
|
+
"""Decorator to retry AWS API calls with exponential backoff."""
|
|
27
|
+
def decorator(func):
|
|
28
|
+
@wraps(func)
|
|
29
|
+
def wrapper(*args, **kwargs):
|
|
30
|
+
for attempt in range(max_retries):
|
|
31
|
+
try:
|
|
32
|
+
return func(*args, **kwargs)
|
|
33
|
+
except ClientError as e:
|
|
34
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
35
|
+
|
|
36
|
+
# Retry on throttling errors
|
|
37
|
+
if error_code in ['Throttling', 'TooManyRequestsException', 'RequestLimitExceeded']:
|
|
38
|
+
if attempt < max_retries - 1:
|
|
39
|
+
sleep_time = backoff_factor ** attempt
|
|
40
|
+
logger.warning(f"AWS API throttled, retrying in {sleep_time}s (attempt {attempt + 1}/{max_retries})")
|
|
41
|
+
time.sleep(sleep_time)
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Don't retry on other errors
|
|
45
|
+
raise AWSServiceError(f"AWS API error: {e}")
|
|
46
|
+
except BotoCoreError as e:
|
|
47
|
+
raise AWSServiceError(f"AWS service error: {e}")
|
|
48
|
+
|
|
49
|
+
# Final attempt without retry
|
|
50
|
+
return func(*args, **kwargs)
|
|
51
|
+
return wrapper
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AWSClientManager:
|
|
56
|
+
"""Manages AWS clients with proper error handling and validation."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, region: str, profile: Optional[str] = None):
|
|
59
|
+
"""
|
|
60
|
+
Initialize AWS client manager.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
region: AWS region name
|
|
64
|
+
profile: AWS CLI profile name (optional)
|
|
65
|
+
"""
|
|
66
|
+
self.region = region
|
|
67
|
+
self.profile = profile
|
|
68
|
+
self._session = None
|
|
69
|
+
self._clients: Dict[str, Any] = {}
|
|
70
|
+
|
|
71
|
+
self._setup_session()
|
|
72
|
+
self._validate_credentials()
|
|
73
|
+
|
|
74
|
+
def _setup_session(self):
|
|
75
|
+
"""Setup boto3 session with profile if provided."""
|
|
76
|
+
try:
|
|
77
|
+
if self.profile:
|
|
78
|
+
self._session = boto3.Session(profile_name=self.profile)
|
|
79
|
+
logger.info(f"Using AWS profile: {self.profile}")
|
|
80
|
+
else:
|
|
81
|
+
self._session = boto3.Session()
|
|
82
|
+
logger.info("Using default AWS credentials")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise AWSCredentialsError(f"Failed to setup AWS session: {e}")
|
|
85
|
+
|
|
86
|
+
def _validate_credentials(self):
|
|
87
|
+
"""Validate AWS credentials by making a test call."""
|
|
88
|
+
try:
|
|
89
|
+
sts_client = self.get_client('sts')
|
|
90
|
+
identity = sts_client.get_caller_identity()
|
|
91
|
+
logger.info(f"AWS credentials validated for account: {identity.get('Account')}")
|
|
92
|
+
except NoCredentialsError:
|
|
93
|
+
raise AWSCredentialsError("No AWS credentials found. Please configure AWS CLI or set environment variables.")
|
|
94
|
+
except PartialCredentialsError:
|
|
95
|
+
raise AWSCredentialsError("Incomplete AWS credentials. Please check your configuration.")
|
|
96
|
+
except ClientError as e:
|
|
97
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
98
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
99
|
+
raise AWSPermissionError(f"Insufficient AWS permissions: {e}")
|
|
100
|
+
raise AWSCredentialsError(f"AWS credential validation failed: {e}")
|
|
101
|
+
|
|
102
|
+
def get_client(self, service_name: str, **kwargs):
|
|
103
|
+
"""
|
|
104
|
+
Get AWS client for specified service with caching.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
service_name: AWS service name (e.g., 'organizations', 'sso-admin')
|
|
108
|
+
**kwargs: Additional client configuration
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Boto3 client instance
|
|
112
|
+
"""
|
|
113
|
+
client_key = f"{service_name}_{self.region}"
|
|
114
|
+
|
|
115
|
+
if client_key not in self._clients:
|
|
116
|
+
try:
|
|
117
|
+
self._clients[client_key] = self._session.client(
|
|
118
|
+
service_name,
|
|
119
|
+
region_name=self.region,
|
|
120
|
+
**kwargs
|
|
121
|
+
)
|
|
122
|
+
logger.debug(f"Created {service_name} client for region {self.region}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise AWSServiceError(f"Failed to create {service_name} client: {e}")
|
|
125
|
+
|
|
126
|
+
return self._clients[client_key]
|
|
127
|
+
|
|
128
|
+
@retry_with_backoff()
|
|
129
|
+
def call_api(self, service_name: str, method_name: str, **kwargs):
|
|
130
|
+
"""
|
|
131
|
+
Make AWS API call with error handling and retry logic.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
service_name: AWS service name
|
|
135
|
+
method_name: API method name
|
|
136
|
+
**kwargs: Method parameters
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
API response
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
client = self.get_client(service_name)
|
|
143
|
+
method = getattr(client, method_name)
|
|
144
|
+
response = method(**kwargs)
|
|
145
|
+
logger.debug(f"Successfully called {service_name}.{method_name}")
|
|
146
|
+
return response
|
|
147
|
+
except AttributeError:
|
|
148
|
+
raise AWSServiceError(f"Method {method_name} not found for service {service_name}")
|
|
149
|
+
except ClientError as e:
|
|
150
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
151
|
+
|
|
152
|
+
if error_code in ['ResourceNotFoundException', 'NoSuchEntity']:
|
|
153
|
+
raise AWSResourceNotFoundError(f"AWS resource not found: {e}")
|
|
154
|
+
elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
155
|
+
raise AWSPermissionError(f"Insufficient permissions for {service_name}.{method_name}: {e}")
|
|
156
|
+
else:
|
|
157
|
+
raise AWSServiceError(f"AWS API call failed {service_name}.{method_name}: {e}")
|
|
158
|
+
|
|
159
|
+
def paginate_api_call(self, service_name: str, method_name: str, result_key: str, **kwargs):
|
|
160
|
+
"""
|
|
161
|
+
Handle paginated AWS API calls.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
service_name: AWS service name
|
|
165
|
+
method_name: API method name
|
|
166
|
+
result_key: Key in response containing the results
|
|
167
|
+
**kwargs: Method parameters
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of all results from paginated calls
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
client = self.get_client(service_name)
|
|
174
|
+
paginator = client.get_paginator(method_name)
|
|
175
|
+
|
|
176
|
+
all_results = []
|
|
177
|
+
for page in paginator.paginate(**kwargs):
|
|
178
|
+
if result_key in page:
|
|
179
|
+
all_results.extend(page[result_key])
|
|
180
|
+
|
|
181
|
+
logger.info(f"Retrieved {len(all_results)} items from paginated {service_name}.{method_name}")
|
|
182
|
+
return all_results
|
|
183
|
+
|
|
184
|
+
except ClientError as e:
|
|
185
|
+
raise AWSServiceError(f"Paginated API call failed {service_name}.{method_name}: {e}")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise AWSServiceError(f"Pagination error for {service_name}.{method_name}: {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Global client manager instance
|
|
191
|
+
_client_manager: Optional[AWSClientManager] = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_client_manager(region: str, profile: Optional[str] = None) -> AWSClientManager:
|
|
195
|
+
"""Get or create global client manager instance."""
|
|
196
|
+
global _client_manager
|
|
197
|
+
|
|
198
|
+
if _client_manager is None or _client_manager.region != region or _client_manager.profile != profile:
|
|
199
|
+
_client_manager = AWSClientManager(region=region, profile=profile)
|
|
200
|
+
|
|
201
|
+
return _client_manager
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def client(service_name: str, region_name: str, profile: Optional[str] = None):
|
|
205
|
+
"""
|
|
206
|
+
Backward compatibility function for existing code.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
service_name: AWS service name
|
|
210
|
+
region_name: AWS region
|
|
211
|
+
profile: AWS profile (optional)
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Boto3 client instance
|
|
215
|
+
"""
|
|
216
|
+
manager = get_client_manager(region=region_name, profile=profile)
|
|
217
|
+
return manager.get_client(service_name)
|
|
@@ -406,6 +406,14 @@ def graph_identity_center(diagrams_path, region, auto):
|
|
|
406
406
|
+ emoji.emojize(":sparkle: Getting Identity store instance info" + Fore.RESET)
|
|
407
407
|
)
|
|
408
408
|
logging.debug(store_instances)
|
|
409
|
+
|
|
410
|
+
# Check if IAM Identity Center is enabled
|
|
411
|
+
if not store_instances or len(store_instances) == 0:
|
|
412
|
+
error_msg = "No IAM Identity Center (SSO) instances found. Please enable IAM Identity Center in your AWS account."
|
|
413
|
+
print(Fore.RED + f"❌ {error_msg}" + Fore.RESET)
|
|
414
|
+
logging.error(error_msg)
|
|
415
|
+
raise ValueError(error_msg)
|
|
416
|
+
|
|
409
417
|
store_id = store_instances[0]["IdentityStoreId"]
|
|
410
418
|
store_arn = store_instances[0]["InstanceArn"]
|
|
411
419
|
|