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
src/aws/describe_sso.py
CHANGED
|
@@ -1,176 +1,203 @@
|
|
|
1
1
|
"""Describe SSO."""
|
|
2
2
|
import logging
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
3
4
|
|
|
4
|
-
from
|
|
5
|
+
from .client_manager import get_client_manager
|
|
6
|
+
from .exceptions import AWSServiceError
|
|
7
|
+
from ..utils.progress import track_operation
|
|
8
|
+
from ..config import get_config
|
|
5
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
6
11
|
|
|
7
|
-
def list_instances(region: str):
|
|
8
|
-
"""
|
|
9
|
-
List all instances in the region.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
:return:
|
|
13
|
+
def list_instances(region: str) -> List[Dict[str, Any]]:
|
|
13
14
|
"""
|
|
14
|
-
|
|
15
|
-
response = sso_client.list_instances()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return response["Instances"]
|
|
15
|
+
List all SSO instances in the region.
|
|
19
16
|
|
|
17
|
+
Args:
|
|
18
|
+
region: AWS region name
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
instance_arn, account_id, permission_set_arn, region, next_token
|
|
24
|
-
):
|
|
25
|
-
"""
|
|
26
|
-
List all account assignments.
|
|
27
|
-
|
|
28
|
-
:param instance_arn:
|
|
29
|
-
:param account_id:
|
|
30
|
-
:param permission_set_arn:
|
|
31
|
-
:param region:
|
|
32
|
-
:return:
|
|
20
|
+
Returns:
|
|
21
|
+
List of SSO instances
|
|
33
22
|
|
|
23
|
+
Raises:
|
|
24
|
+
AWSServiceError: If the API call fails
|
|
34
25
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"MaxItems": 1000,
|
|
43
|
-
"PageSize": 20,
|
|
44
|
-
"StartingToken": next_token,
|
|
45
|
-
},
|
|
46
|
-
)
|
|
47
|
-
return response_iterator["AccountAssignments"]
|
|
26
|
+
client_manager = get_client_manager(region=region)
|
|
27
|
+
|
|
28
|
+
with track_operation("Listing SSO instances") as task_id:
|
|
29
|
+
response = client_manager.call_api("sso-admin", "list_instances")
|
|
30
|
+
instances = response.get("Instances", [])
|
|
31
|
+
logger.info(f"Found {len(instances)} SSO instances")
|
|
32
|
+
return instances
|
|
48
33
|
|
|
49
34
|
|
|
50
35
|
def list_account_assignments(
|
|
51
|
-
instance_arn
|
|
52
|
-
|
|
36
|
+
instance_arn: str,
|
|
37
|
+
account_id: str,
|
|
38
|
+
permission_set_arn: str,
|
|
39
|
+
region: str
|
|
40
|
+
) -> List[Dict[str, Any]]:
|
|
53
41
|
"""
|
|
54
|
-
List all account assignments.
|
|
42
|
+
List all account assignments for a permission set.
|
|
55
43
|
|
|
56
|
-
:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
:return:
|
|
44
|
+
Args:
|
|
45
|
+
instance_arn: SSO instance ARN
|
|
46
|
+
account_id: AWS account ID
|
|
47
|
+
permission_set_arn: Permission set ARN
|
|
48
|
+
region: AWS region name
|
|
62
49
|
|
|
50
|
+
Returns:
|
|
51
|
+
List of account assignments
|
|
63
52
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
53
|
+
client_manager = get_client_manager(region=region)
|
|
54
|
+
config = get_config()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Use paginated API call for better handling of large result sets
|
|
58
|
+
assignments = client_manager.paginate_api_call(
|
|
59
|
+
"sso-admin",
|
|
60
|
+
"list_account_assignments",
|
|
61
|
+
"AccountAssignments",
|
|
62
|
+
InstanceArn=instance_arn,
|
|
63
|
+
AccountId=account_id,
|
|
64
|
+
PermissionSetArn=permission_set_arn,
|
|
65
|
+
PaginationConfig={
|
|
66
|
+
'MaxItems': config.pagination.max_items,
|
|
67
|
+
'PageSize': config.pagination.default_page_size
|
|
68
|
+
}
|
|
79
69
|
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
|
|
71
|
+
logger.debug(f"Retrieved {len(assignments)} account assignments for account {account_id}")
|
|
72
|
+
return assignments
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Failed to list account assignments for account {account_id}: {e}")
|
|
76
|
+
raise AWSServiceError(f"Failed to list account assignments: {e}")
|
|
85
77
|
|
|
86
78
|
|
|
87
|
-
def
|
|
79
|
+
def list_permissions_set(instance_arn: str, region: str) -> List[str]:
|
|
88
80
|
"""
|
|
89
|
-
List all permission
|
|
81
|
+
List all permission sets in an SSO instance.
|
|
90
82
|
|
|
91
|
-
:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
:return:
|
|
95
|
-
"""
|
|
96
|
-
sso_client = client("sso-admin", region_name=region)
|
|
97
|
-
paginator = sso_client.get_paginator("list_permission_sets")
|
|
98
|
-
response_iterator = paginator.paginate(
|
|
99
|
-
InstanceArn=instance_arn,
|
|
100
|
-
PaginationConfig={
|
|
101
|
-
"MaxItems": 1000,
|
|
102
|
-
"PageSize": 20,
|
|
103
|
-
"StartingToken": next_token,
|
|
104
|
-
},
|
|
105
|
-
)
|
|
106
|
-
response_iterator = response_iterator.build_full_result()
|
|
107
|
-
return response_iterator["PermissionSets"]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def list_permissions_set(instance_arn, region):
|
|
111
|
-
"""
|
|
112
|
-
List all permission set in a region.
|
|
83
|
+
Args:
|
|
84
|
+
instance_arn: SSO instance ARN
|
|
85
|
+
region: AWS region name
|
|
113
86
|
|
|
114
|
-
:
|
|
115
|
-
|
|
116
|
-
:return:
|
|
87
|
+
Returns:
|
|
88
|
+
List of permission set ARNs
|
|
117
89
|
"""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
90
|
+
client_manager = get_client_manager(region=region)
|
|
91
|
+
config = get_config()
|
|
92
|
+
|
|
93
|
+
with track_operation("Listing permission sets") as task_id:
|
|
94
|
+
try:
|
|
95
|
+
permission_sets = client_manager.paginate_api_call(
|
|
96
|
+
"sso-admin",
|
|
97
|
+
"list_permission_sets",
|
|
98
|
+
"PermissionSets",
|
|
99
|
+
InstanceArn=instance_arn,
|
|
100
|
+
PaginationConfig={
|
|
101
|
+
'MaxItems': config.pagination.max_items,
|
|
102
|
+
'PageSize': config.pagination.default_page_size
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
logger.info(f"Found {len(permission_sets)} permission sets")
|
|
107
|
+
return permission_sets
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to list permission sets: {e}")
|
|
111
|
+
raise AWSServiceError(f"Failed to list permission sets: {e}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def list_permission_provisioned(account_id: str, instance_arn: str, region: str) -> List[str]:
|
|
115
|
+
"""
|
|
116
|
+
List permission sets provisioned to an account.
|
|
137
117
|
|
|
118
|
+
Args:
|
|
119
|
+
account_id: AWS account ID
|
|
120
|
+
instance_arn: SSO instance ARN
|
|
121
|
+
region: AWS region name
|
|
138
122
|
|
|
139
|
-
|
|
123
|
+
Returns:
|
|
124
|
+
List of provisioned permission set ARNs
|
|
140
125
|
"""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
client_manager = get_client_manager(region=region)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
response = client_manager.call_api(
|
|
130
|
+
"sso-admin",
|
|
131
|
+
"list_permission_sets_provisioned_to_account",
|
|
132
|
+
InstanceArn=instance_arn,
|
|
133
|
+
AccountId=account_id
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
permission_sets = response.get("PermissionSets", [])
|
|
137
|
+
logger.debug(f"Found {len(permission_sets)} provisioned permission sets for account {account_id}")
|
|
138
|
+
return permission_sets
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Failed to list provisioned permission sets for account {account_id}: {e}")
|
|
142
|
+
raise AWSServiceError(f"Failed to list provisioned permission sets: {e}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def extends_permissions_set(
|
|
146
|
+
permissions_sets: List[str],
|
|
147
|
+
store_arn: str,
|
|
148
|
+
region: str
|
|
149
|
+
) -> Dict[str, str]:
|
|
147
150
|
"""
|
|
148
|
-
|
|
149
|
-
response = l_client.list_permission_sets_provisioned_to_account(
|
|
150
|
-
InstanceArn=instance_arn,
|
|
151
|
-
AccountId=account_id,
|
|
152
|
-
)
|
|
153
|
-
logging.debug(response)
|
|
154
|
-
return response["PermissionSets"]
|
|
151
|
+
Get detailed information for permission sets.
|
|
155
152
|
|
|
153
|
+
Args:
|
|
154
|
+
permissions_sets: List of permission set ARNs
|
|
155
|
+
store_arn: SSO instance ARN
|
|
156
|
+
region: AWS region name
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary mapping permission set ARN to name
|
|
158
160
|
"""
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
client_manager = get_client_manager(region=region)
|
|
162
|
+
permissions_map = {}
|
|
163
|
+
|
|
164
|
+
with track_operation(f"Getting details for {len(permissions_sets)} permission sets") as task_id:
|
|
165
|
+
for i, permission_set_arn in enumerate(permissions_sets):
|
|
166
|
+
try:
|
|
167
|
+
response = client_manager.call_api(
|
|
168
|
+
"sso-admin",
|
|
169
|
+
"describe_permission_set",
|
|
170
|
+
InstanceArn=store_arn,
|
|
171
|
+
PermissionSetArn=permission_set_arn
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
permission_set = response.get("PermissionSet", {})
|
|
175
|
+
permissions_map[permission_set_arn] = permission_set.get("Name", "Unknown")
|
|
176
|
+
|
|
177
|
+
# Update progress
|
|
178
|
+
if i % 5 == 0: # Update every 5 items to avoid too frequent updates
|
|
179
|
+
client_manager._progress_tracker.update_progress(task_id, advance=5)
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.warning(f"Failed to describe permission set {permission_set_arn}: {e}")
|
|
183
|
+
permissions_map[permission_set_arn] = "Unknown"
|
|
184
|
+
|
|
185
|
+
logger.info(f"Retrieved details for {len(permissions_map)} permission sets")
|
|
186
|
+
return permissions_map
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Backward compatibility function
|
|
190
|
+
def client(service_name: str, region_name: str, profile: Optional[str] = None):
|
|
165
191
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
Backward compatibility function for existing code.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
service_name: AWS service name
|
|
196
|
+
region_name: AWS region
|
|
197
|
+
profile: AWS profile (optional)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Boto3 client instance
|
|
201
|
+
"""
|
|
202
|
+
from .client_manager import client as new_client
|
|
203
|
+
return new_client(service_name, region_name, profile)
|
src/aws/exceptions.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Custom exceptions for AWS operations."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AWSError(Exception):
|
|
5
|
+
"""Base exception for AWS-related errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AWSCredentialsError(AWSError):
|
|
10
|
+
"""Raised when AWS credentials are invalid or missing."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AWSPermissionError(AWSError):
|
|
15
|
+
"""Raised when AWS permissions are insufficient."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AWSServiceError(AWSError):
|
|
20
|
+
"""Raised when AWS service calls fail."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AWSResourceNotFoundError(AWSError):
|
|
25
|
+
"""Raised when AWS resources are not found."""
|
|
26
|
+
pass
|
src/banner/banner.py
CHANGED
|
@@ -1,48 +1,51 @@
|
|
|
1
1
|
"""Create Banner."""
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
banner = (
|
|
6
|
-
Fore.GREEN
|
|
7
|
-
+ """
|
|
8
|
-
.----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------.
|
|
9
|
-
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |
|
|
10
|
-
| | _______ | || | _________ | || | ____ ____ | || | _________ | || | _______ | || | _______ | || | _________ | |
|
|
11
|
-
| | |_ __ \ | || | |_ ___ | | || ||_ _| |_ _| | || | |_ ___ | | || | |_ __ \ | || | / ___ | | || | |_ ___ | | |
|
|
12
|
-
| | | |__) | | || | | |_ \_| | || | \ \ / / | || | | |_ \_| | || | | |__) | | || | | (__ \_| | || | | |_ \_| | |
|
|
13
|
-
| | | __ / | || | | _| _ | || | \ \ / / | || | | _| _ | || | | __ / | || | '.___`-. | || | | _| _ | |
|
|
14
|
-
| | _| | \ \_ | || | _| |___/ | | || | \ ' / | || | _| |___/ | | || | _| | \ \_ | || | |`\____) | | || | _| |___/ | | |
|
|
15
|
-
| | |____| |___| | || | |_________| | || | \_/ | || | |_________| | || | |____| |___| | || | |_______.' | || | |_________| | |
|
|
16
|
-
| | | || | | || | | || | | || | | || | | || | | |
|
|
17
|
-
| '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |
|
|
18
|
-
'----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------'
|
|
19
|
-
.----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------.
|
|
20
|
-
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |
|
|
21
|
-
| | ________ | || | _____ | || | __ | || | ______ | || | _______ | || | __ | || | ____ ____ | || | _______ | |
|
|
22
|
-
| | |_ ___ `. | || | |_ _| | || | / \ | || | .' ___ | | || | |_ __ \ | || | / \ | || ||_ \ / _|| || | / ___ | | |
|
|
23
|
-
| | | | `. \ | || | | | | || | / /\ \ | || | / .' \_| | || | | |__) | | || | / /\ \ | || | | \/ | | || | | (__ \_| | |
|
|
24
|
-
| | | | | | | || | | | | || | / ____ \ | || | | | ____ | || | | __ / | || | / ____ \ | || | | |\ /| | | || | '.___`-. | |
|
|
25
|
-
| | _| |___.' / | || | _| |_ | || | _/ / \ \_ | || | \ `.___] _| | || | _| | \ \_ | || | _/ / \ \_ | || | _| |_\/_| |_ | || | |`\____) | | |
|
|
26
|
-
| | |________.' | || | |_____| | || ||____| |____|| || | `._____.' | || | |____| |___| | || ||____| |____|| || ||_____||_____|| || | |_______.' | |
|
|
27
|
-
| | | || | | || | | || | | || | | || | | || | | || | | |
|
|
28
|
-
| '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |
|
|
29
|
-
'----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Power by - Avelez- 👤
|
|
33
|
-
"""
|
|
34
|
-
)
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.text import Text
|
|
35
4
|
|
|
36
5
|
|
|
37
6
|
def get_version(version):
|
|
38
7
|
"""
|
|
39
|
-
|
|
8
|
+
Display professional banner with version.
|
|
40
9
|
|
|
41
|
-
:param version:
|
|
42
|
-
:return:
|
|
10
|
+
:param version: Version string
|
|
11
|
+
:return: Version string
|
|
43
12
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# Create the banner with a clean, modern design
|
|
16
|
+
banner_lines = [
|
|
17
|
+
"",
|
|
18
|
+
"╔═══════════════════════════════════════════════════════════════╗",
|
|
19
|
+
"║ ║",
|
|
20
|
+
"║ 🔄 REVERSE DIAGRAMS ║",
|
|
21
|
+
"║ ║",
|
|
22
|
+
"║ AWS Infrastructure Documentation as Code ║",
|
|
23
|
+
"║ ║",
|
|
24
|
+
"╚═══════════════════════════════════════════════════════════════╝",
|
|
25
|
+
""
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Print banner with colors
|
|
29
|
+
for line in banner_lines:
|
|
30
|
+
if "REVERSE DIAGRAMS" in line:
|
|
31
|
+
text = Text(line, style="bold cyan")
|
|
32
|
+
elif "AWS Infrastructure" in line:
|
|
33
|
+
text = Text(line, style="green")
|
|
34
|
+
elif "═" in line or "║" in line:
|
|
35
|
+
text = Text(line, style="cyan")
|
|
36
|
+
else:
|
|
37
|
+
text = Text(line)
|
|
38
|
+
console.print(text, justify="center")
|
|
39
|
+
|
|
40
|
+
# Print version info
|
|
41
|
+
version_text = Text()
|
|
42
|
+
version_text.append(" Version ", style="dim")
|
|
43
|
+
version_text.append(version, style="bold yellow")
|
|
44
|
+
version_text.append(" • ", style="dim")
|
|
45
|
+
version_text.append("github.com/velez94/reverse_diagrams", style="dim cyan underline")
|
|
46
|
+
version_text.append(" ", style="dim")
|
|
47
|
+
|
|
48
|
+
console.print(version_text, justify="center")
|
|
49
|
+
console.print()
|
|
50
|
+
|
|
48
51
|
return version
|
src/config.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Configuration management for Reverse Diagrams."""
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class PaginationConfig:
|
|
11
|
+
"""Configuration for AWS API pagination."""
|
|
12
|
+
default_page_size: int = 20
|
|
13
|
+
max_page_size: int = 1000
|
|
14
|
+
max_items: int = 10000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AWSConfig:
|
|
19
|
+
"""Configuration for AWS operations."""
|
|
20
|
+
timeout: int = 30
|
|
21
|
+
max_retries: int = 3
|
|
22
|
+
backoff_factor: float = 2.0
|
|
23
|
+
|
|
24
|
+
# Required permissions documentation
|
|
25
|
+
required_permissions: Dict[str, list] = field(default_factory=lambda: {
|
|
26
|
+
'organizations': [
|
|
27
|
+
'organizations:DescribeOrganization',
|
|
28
|
+
'organizations:ListRoots',
|
|
29
|
+
'organizations:ListOrganizationalUnitsForParent',
|
|
30
|
+
'organizations:ListAccounts',
|
|
31
|
+
'organizations:ListParents'
|
|
32
|
+
],
|
|
33
|
+
'sso-admin': [
|
|
34
|
+
'sso:ListInstances',
|
|
35
|
+
'sso:ListPermissionSets',
|
|
36
|
+
'sso:DescribePermissionSet',
|
|
37
|
+
'sso:ListAccountAssignments'
|
|
38
|
+
],
|
|
39
|
+
'identitystore': [
|
|
40
|
+
'identitystore:ListGroups',
|
|
41
|
+
'identitystore:ListUsers',
|
|
42
|
+
'identitystore:ListGroupMemberships'
|
|
43
|
+
]
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OutputConfig:
|
|
49
|
+
"""Configuration for output generation."""
|
|
50
|
+
default_output_dir: str = "diagrams"
|
|
51
|
+
json_indent: int = 4
|
|
52
|
+
create_directories: bool = True
|
|
53
|
+
file_permissions: int = 0o644
|
|
54
|
+
|
|
55
|
+
# Supported output formats
|
|
56
|
+
supported_formats: list = field(default_factory=lambda: ['png', 'svg', 'pdf'])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class LoggingConfig:
|
|
61
|
+
"""Configuration for logging."""
|
|
62
|
+
level: str = "INFO"
|
|
63
|
+
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
64
|
+
file_path: Optional[Path] = None
|
|
65
|
+
max_file_size: int = 10 * 1024 * 1024 # 10MB
|
|
66
|
+
backup_count: int = 5
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Config:
|
|
71
|
+
"""Main configuration class."""
|
|
72
|
+
pagination: PaginationConfig = field(default_factory=PaginationConfig)
|
|
73
|
+
aws: AWSConfig = field(default_factory=AWSConfig)
|
|
74
|
+
output: OutputConfig = field(default_factory=OutputConfig)
|
|
75
|
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
76
|
+
|
|
77
|
+
# Performance settings
|
|
78
|
+
max_concurrent_workers: int = 5
|
|
79
|
+
enable_caching: bool = True
|
|
80
|
+
cache_ttl_hours: int = 1
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_env(cls) -> 'Config':
|
|
84
|
+
"""Create configuration from environment variables."""
|
|
85
|
+
config = cls()
|
|
86
|
+
|
|
87
|
+
# AWS configuration from environment
|
|
88
|
+
if os.getenv('AWS_TIMEOUT'):
|
|
89
|
+
config.aws.timeout = int(os.getenv('AWS_TIMEOUT'))
|
|
90
|
+
if os.getenv('AWS_MAX_RETRIES'):
|
|
91
|
+
config.aws.max_retries = int(os.getenv('AWS_MAX_RETRIES'))
|
|
92
|
+
|
|
93
|
+
# Output configuration from environment
|
|
94
|
+
if os.getenv('REVERSE_DIAGRAMS_OUTPUT_DIR'):
|
|
95
|
+
config.output.default_output_dir = os.getenv('REVERSE_DIAGRAMS_OUTPUT_DIR')
|
|
96
|
+
|
|
97
|
+
# Logging configuration from environment
|
|
98
|
+
if os.getenv('LOG_LEVEL'):
|
|
99
|
+
config.logging.level = os.getenv('LOG_LEVEL').upper()
|
|
100
|
+
if os.getenv('LOG_FILE'):
|
|
101
|
+
config.logging.file_path = Path(os.getenv('LOG_FILE'))
|
|
102
|
+
|
|
103
|
+
# Performance settings from environment
|
|
104
|
+
if os.getenv('MAX_WORKERS'):
|
|
105
|
+
config.max_concurrent_workers = int(os.getenv('MAX_WORKERS'))
|
|
106
|
+
if os.getenv('ENABLE_CACHING'):
|
|
107
|
+
config.enable_caching = os.getenv('ENABLE_CACHING').lower() == 'true'
|
|
108
|
+
|
|
109
|
+
return config
|
|
110
|
+
|
|
111
|
+
def setup_logging(self):
|
|
112
|
+
"""Setup logging based on configuration."""
|
|
113
|
+
handlers = [logging.StreamHandler()]
|
|
114
|
+
|
|
115
|
+
if self.logging.file_path:
|
|
116
|
+
from logging.handlers import RotatingFileHandler
|
|
117
|
+
file_handler = RotatingFileHandler(
|
|
118
|
+
self.logging.file_path,
|
|
119
|
+
maxBytes=self.logging.max_file_size,
|
|
120
|
+
backupCount=self.logging.backup_count
|
|
121
|
+
)
|
|
122
|
+
handlers.append(file_handler)
|
|
123
|
+
|
|
124
|
+
logging.basicConfig(
|
|
125
|
+
level=getattr(logging, self.logging.level),
|
|
126
|
+
format=self.logging.format,
|
|
127
|
+
handlers=handlers
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Reduce noise from AWS SDK
|
|
131
|
+
logging.getLogger('boto3').setLevel(logging.WARNING)
|
|
132
|
+
logging.getLogger('botocore').setLevel(logging.WARNING)
|
|
133
|
+
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Global configuration instance
|
|
137
|
+
_config: Optional[Config] = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_config() -> Config:
|
|
141
|
+
"""Get global configuration instance."""
|
|
142
|
+
global _config
|
|
143
|
+
if _config is None:
|
|
144
|
+
_config = Config.from_env()
|
|
145
|
+
_config.setup_logging()
|
|
146
|
+
return _config
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def set_config(config: Config):
|
|
150
|
+
"""Set global configuration instance."""
|
|
151
|
+
global _config
|
|
152
|
+
_config = config
|
|
153
|
+
_config.setup_logging()
|