reverse-diagrams 0.2.6__py3-none-any.whl → 1.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-0.2.6.dist-info → reverse_diagrams-1.0.0.dist-info}/METADATA +36 -23
- reverse_diagrams-1.0.0.dist-info/RECORD +20 -0
- reverse_diagrams-1.0.0.dist-info/licenses/LICENSE +13 -0
- src/aws/describe_identity_store.py +131 -66
- src/aws/describe_organization.py +91 -36
- src/aws/describe_sso.py +133 -20
- src/banner/banner.py +7 -2
- src/dgms/graph_mapper.py +75 -31
- src/dgms/graph_template.py +4 -4
- src/export_report/export_csv.py +3 -2
- src/reports/__init__.py +0 -0
- src/reports/save_results.py +24 -0
- src/reverse_diagrams.py +325 -169
- src/version.py +2 -0
- reverse_diagrams-0.2.6.dist-info/RECORD +0 -17
- reverse_diagrams-0.2.6.dist-info/licenses/LICENSE +0 -21
- {reverse_diagrams-0.2.6.dist-info → reverse_diagrams-1.0.0.dist-info}/WHEEL +0 -0
- {reverse_diagrams-0.2.6.dist-info → reverse_diagrams-1.0.0.dist-info}/entry_points.txt +0 -0
src/aws/describe_sso.py
CHANGED
|
@@ -1,53 +1,166 @@
|
|
|
1
|
-
import boto3
|
|
2
1
|
import logging
|
|
3
2
|
|
|
3
|
+
from boto3 import client
|
|
4
4
|
|
|
5
|
-
def list_instances(client=boto3.client('sso-admin', region_name="us-east-2")):
|
|
6
|
-
response = client.list_instances(
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
def list_instances(region: str):
|
|
7
|
+
"""
|
|
8
|
+
List all instances in the region.
|
|
9
|
+
|
|
10
|
+
:param region:
|
|
11
|
+
:return:
|
|
12
|
+
"""
|
|
13
|
+
sso_client = client("sso-admin", region_name=region)
|
|
14
|
+
response = sso_client.list_instances()
|
|
9
15
|
|
|
10
16
|
return response["Instances"]
|
|
11
17
|
|
|
12
18
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
# def list account assignments with pagination
|
|
20
|
+
def list_account_assignments_pag(
|
|
21
|
+
instance_arn, account_id, permission_set_arn, region, next_token
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
List all account assignments.
|
|
25
|
+
|
|
26
|
+
:param instance_arn:
|
|
27
|
+
:param account_id:
|
|
28
|
+
:param permission_set_arn:
|
|
29
|
+
:param region:
|
|
30
|
+
:return:
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
sso_client = client("sso-admin", region_name=region)
|
|
34
|
+
paginator = sso_client.get_paginator("list_account_assignments")
|
|
35
|
+
response_iterator = paginator.paginate(
|
|
17
36
|
InstanceArn=instance_arn,
|
|
18
37
|
AccountId=account_id,
|
|
19
38
|
PermissionSetArn=permission_set_arn,
|
|
20
|
-
|
|
39
|
+
PaginationConfig={
|
|
40
|
+
"MaxItems": 1000,
|
|
41
|
+
"PageSize": 20,
|
|
42
|
+
"StartingToken": next_token,
|
|
43
|
+
},
|
|
21
44
|
)
|
|
45
|
+
return response_iterator["AccountAssignments"]
|
|
46
|
+
|
|
22
47
|
|
|
23
|
-
|
|
48
|
+
def list_account_assignments(instance_arn, account_id, permission_set_arn, region):
|
|
49
|
+
"""
|
|
50
|
+
List all account assignments.
|
|
24
51
|
|
|
52
|
+
:param instance_arn:
|
|
53
|
+
:param account_id:
|
|
54
|
+
:param permission_set_arn:
|
|
55
|
+
:param region:
|
|
56
|
+
:return:
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
|
|
58
|
+
"""
|
|
59
|
+
sso_client = client("sso-admin", region_name=region)
|
|
60
|
+
response = sso_client.list_account_assignments(
|
|
61
|
+
InstanceArn=instance_arn,
|
|
62
|
+
AccountId=account_id,
|
|
63
|
+
PermissionSetArn=permission_set_arn,
|
|
64
|
+
)
|
|
65
|
+
account_assignments = response["AccountAssignments"]
|
|
66
|
+
if len(response["AccountAssignments"]) >= 20:
|
|
67
|
+
logging.info("Paginating ...")
|
|
68
|
+
response_iterator = list_account_assignments_pag(
|
|
69
|
+
instance_arn,
|
|
70
|
+
account_id,
|
|
71
|
+
permission_set_arn,
|
|
72
|
+
region,
|
|
73
|
+
next_token=response["NextToken"],
|
|
74
|
+
)
|
|
75
|
+
for response in response_iterator:
|
|
76
|
+
logging.debug(response)
|
|
77
|
+
account_assignments.append(response["AccountAssignments"])
|
|
78
|
+
logging.info(response["AccountAssignments"])
|
|
79
|
+
return account_assignments
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def list_permissions_set_pag(instance_arn, region, next_token):
|
|
83
|
+
"""
|
|
84
|
+
List all permission set in a region.
|
|
85
|
+
|
|
86
|
+
:param instance_arn:
|
|
87
|
+
:param region:
|
|
88
|
+
:return:
|
|
89
|
+
"""
|
|
90
|
+
sso_client = client("sso-admin", region_name=region)
|
|
91
|
+
paginator = sso_client.get_paginator("list_permission_sets")
|
|
92
|
+
response_iterator = paginator.paginate(
|
|
28
93
|
InstanceArn=instance_arn,
|
|
94
|
+
PaginationConfig={
|
|
95
|
+
"MaxItems": 1000,
|
|
96
|
+
"PageSize": 20,
|
|
97
|
+
"StartingToken": next_token,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
return response_iterator["PermissionSets"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def list_permissions_set(instance_arn, region):
|
|
104
|
+
"""
|
|
105
|
+
List all permission set in a region.
|
|
29
106
|
|
|
107
|
+
:param instance_arn:
|
|
108
|
+
:param region:
|
|
109
|
+
:return:
|
|
110
|
+
"""
|
|
111
|
+
sso_client = client(
|
|
112
|
+
"sso-admin",
|
|
113
|
+
region_name=region,
|
|
30
114
|
)
|
|
115
|
+
response = sso_client.list_permission_sets(InstanceArn=instance_arn, MaxResults=20)
|
|
31
116
|
logging.debug(response)
|
|
32
|
-
|
|
117
|
+
permissions_set = response["PermissionSets"]
|
|
118
|
+
|
|
119
|
+
if len(response["PermissionSets"]) >= 20:
|
|
120
|
+
logging.info("Paginating ...")
|
|
121
|
+
response_iterator = list_permissions_set_pag(
|
|
122
|
+
instance_arn, region, next_token=response["NextToken"]
|
|
123
|
+
)
|
|
124
|
+
for response in response_iterator:
|
|
125
|
+
logging.debug(response)
|
|
126
|
+
permissions_set.append(response["PermissionSets"])
|
|
127
|
+
logging.info(response["PermissionSets"])
|
|
128
|
+
|
|
129
|
+
return permissions_set
|
|
130
|
+
|
|
33
131
|
|
|
132
|
+
def list_permission_provisioned(account_id, instance_arn, region):
|
|
133
|
+
"""
|
|
134
|
+
List permission provisioned.
|
|
34
135
|
|
|
35
|
-
|
|
36
|
-
|
|
136
|
+
:param account_id:
|
|
137
|
+
:param instance_arn:
|
|
138
|
+
:param region:
|
|
139
|
+
:return:
|
|
140
|
+
"""
|
|
141
|
+
l_client = client("sso-admin", region_name=region)
|
|
142
|
+
response = l_client.list_permission_sets_provisioned_to_account(
|
|
37
143
|
InstanceArn=instance_arn,
|
|
38
144
|
AccountId=account_id,
|
|
39
|
-
|
|
40
145
|
)
|
|
41
146
|
logging.debug(response)
|
|
42
147
|
return response["PermissionSets"]
|
|
43
148
|
|
|
44
149
|
|
|
45
|
-
def extends_permissions_set(permissions_sets, store_arn,
|
|
150
|
+
def extends_permissions_set(permissions_sets, store_arn, region):
|
|
151
|
+
"""
|
|
152
|
+
List all permission set in a region
|
|
153
|
+
|
|
154
|
+
:param permissions_sets:
|
|
155
|
+
:param store_arn:
|
|
156
|
+
:param region:
|
|
157
|
+
:return:
|
|
158
|
+
"""
|
|
159
|
+
sso_client = client("sso-admin", region_name=region)
|
|
46
160
|
l_permissions_set_arn_name = {}
|
|
47
161
|
for p in permissions_sets:
|
|
48
|
-
response =
|
|
49
|
-
InstanceArn=store_arn,
|
|
50
|
-
PermissionSetArn=p
|
|
162
|
+
response = sso_client.describe_permission_set(
|
|
163
|
+
InstanceArn=store_arn, PermissionSetArn=p
|
|
51
164
|
)
|
|
52
165
|
|
|
53
166
|
l_permissions_set_arn_name[p] = response["PermissionSet"]["Name"]
|
src/banner/banner.py
CHANGED
|
@@ -2,7 +2,6 @@ from colorama import Fore
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
banner = """Reverse Diagrams
|
|
5
|
-
|
|
6
5
|
##### ###### # # ###### ##### #### ######
|
|
7
6
|
# # # # # # # # # #
|
|
8
7
|
# # ##### # # ##### # # #### #####
|
|
@@ -21,7 +20,13 @@ banner = """Reverse Diagrams
|
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
def get_version(version):
|
|
24
|
-
|
|
23
|
+
"""
|
|
24
|
+
Get package version.
|
|
25
|
+
|
|
26
|
+
:param version:
|
|
27
|
+
:return:
|
|
28
|
+
"""
|
|
29
|
+
print(Fore.BLUE + banner)
|
|
25
30
|
|
|
26
31
|
print(version + Fore.RESET)
|
|
27
32
|
|
src/dgms/graph_mapper.py
CHANGED
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os.path
|
|
3
3
|
import re
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def format_name_string(a_string, action=None):
|
|
7
8
|
"""
|
|
9
|
+
Format name strings to avoid no allowed characters.
|
|
8
10
|
|
|
9
11
|
:param a_string:
|
|
10
12
|
:param action:
|
|
11
13
|
:return: sring --terragrunt-json-out
|
|
12
14
|
"""
|
|
13
|
-
if action ==
|
|
15
|
+
if action == "split":
|
|
14
16
|
if len(a_string) > 17:
|
|
15
17
|
a_string = a_string[:16] + "\\n" + a_string[16:]
|
|
16
|
-
elif action ==
|
|
17
|
-
|
|
18
|
-
a_string =
|
|
19
|
-
a_string = a_string.replace(" ", '')
|
|
18
|
+
elif action == "format":
|
|
19
|
+
a_string = re.sub("[-!?@.+]", "", a_string)
|
|
20
|
+
a_string = a_string.replace(" ", "")
|
|
20
21
|
return a_string
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def create_sso_mapper_complete(template_file, acc_assignments, d_groups):
|
|
25
|
+
"""
|
|
26
|
+
Create sso mapper.
|
|
27
|
+
|
|
28
|
+
:param template_file: Template file
|
|
29
|
+
:param acc_assignments:
|
|
30
|
+
:param d_groups:
|
|
31
|
+
:return:
|
|
32
|
+
"""
|
|
24
33
|
|
|
25
|
-
with open(template_file,
|
|
34
|
+
with open(template_file, "a") as f:
|
|
26
35
|
ident = " "
|
|
27
36
|
|
|
28
37
|
for key, value in acc_assignments.items():
|
|
29
|
-
|
|
30
38
|
if len(value) > 0:
|
|
31
39
|
print(f"\n with Cluster('Account: {key}'):", file=f)
|
|
32
40
|
for m in value:
|
|
33
41
|
logging.debug(m)
|
|
34
42
|
|
|
35
43
|
if "GroupName" in m.keys():
|
|
36
|
-
print(
|
|
44
|
+
print(
|
|
45
|
+
f"\n{ident}with Cluster('Group: {m['GroupName']}'):", file=f
|
|
46
|
+
)
|
|
37
47
|
print(
|
|
38
48
|
f"\n{ident}{ident}gg_{format_name_string(m['GroupName'], 'format')}=Users(\"{format_name_string(m['GroupName'], 'split')}\")\n"
|
|
39
49
|
f"{ident}{ident}gg_{format_name_string(m['GroupName'], 'format')} \\\n"
|
|
@@ -43,31 +53,39 @@ def create_sso_mapper_complete(template_file, acc_assignments, d_groups):
|
|
|
43
53
|
f"{ident}{ident}gg_{format_name_string(m['GroupName'], 'format')} \\\n"
|
|
44
54
|
f"{ident}{ident}{ident}- Edge(color=\"darkgreen\", style=\"dotted\", label=\"Member\") \\\n"
|
|
45
55
|
f"{ident}{ident}{ident}- mm_{format_name_string(m['GroupName'], 'format')} \n",
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
file=f,
|
|
57
|
+
)
|
|
48
58
|
|
|
49
59
|
if "UserName" in m.keys():
|
|
50
|
-
print(
|
|
60
|
+
print(
|
|
61
|
+
f"\n{ident}with Cluster('User: {m['UserName']}'):", file=f
|
|
62
|
+
)
|
|
51
63
|
print(
|
|
52
64
|
f"\n{ident}{ident}uu_{format_name_string(m['UserName'], 'format')}=User(\"{format_name_string(m['UserName'], 'split')}\")\n"
|
|
53
65
|
f"{ident}{ident}uu_{format_name_string(m['UserName'], 'format')} \\\n"
|
|
54
66
|
f"{ident}{ident}{ident}- Edge(color=\"brown\", style=\"dotted\") \\\n"
|
|
55
67
|
f"{ident}{ident}{ident}- IAMPermissions(\"{format_name_string(m['PermissionSetName'], 'split')}\")",
|
|
56
|
-
file=f
|
|
68
|
+
file=f,
|
|
69
|
+
)
|
|
57
70
|
# print(f"\n{ident}ou >> uu_{format_name_string(m['UserName'])}\n", file=f)
|
|
58
71
|
f.close()
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
def create_file(template_content, file_name, directory_path="."):
|
|
62
75
|
"""
|
|
76
|
+
Create file into directory.
|
|
63
77
|
|
|
64
78
|
:param template_content:
|
|
65
79
|
:param file_name:
|
|
66
80
|
:param directory_path:
|
|
67
81
|
:return:
|
|
68
82
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
# create directory if not exists and is different from .
|
|
84
|
+
if not os.path.exists(directory_path):
|
|
85
|
+
os.makedirs(directory_path)
|
|
86
|
+
logging.debug(f"Directory {directory_path} created")
|
|
87
|
+
f_path = Path(os.path.join(directory_path, file_name))
|
|
88
|
+
with open(f_path, "w") as f:
|
|
71
89
|
f.write(template_content)
|
|
72
90
|
f.close()
|
|
73
91
|
|
|
@@ -79,44 +97,62 @@ def find_ou_name(ous, search_id):
|
|
|
79
97
|
|
|
80
98
|
|
|
81
99
|
def create_mapper(template_file, org, root_id, list_ous, list_accounts):
|
|
82
|
-
with open(template_file,
|
|
100
|
+
with open(template_file, "a") as f:
|
|
83
101
|
ident = " "
|
|
84
|
-
print(
|
|
85
|
-
print(
|
|
102
|
+
print("\n with Cluster('Organizations'):", file=f)
|
|
103
|
+
print(
|
|
104
|
+
f"\n{ident}oo = Organizations('{org['Id']}\\n{org['MasterAccountId']}\\n{root_id}')",
|
|
105
|
+
file=f,
|
|
106
|
+
)
|
|
86
107
|
for a, i in zip(list_ous, range(len(list_ous))):
|
|
87
108
|
print(
|
|
88
109
|
f"\n{ident}ou_{format_name_string(a['Name'], 'format')}= OrganizationsOrganizationalUnit(\"{a['Id']}\\n{a['Name']}\")",
|
|
89
|
-
file=f
|
|
110
|
+
file=f,
|
|
111
|
+
)
|
|
90
112
|
|
|
91
113
|
for p in a["Parents"]:
|
|
92
|
-
if p[
|
|
93
|
-
print(
|
|
94
|
-
|
|
114
|
+
if p["Type"] == "ROOT":
|
|
115
|
+
print(
|
|
116
|
+
f"\n{ident}oo>> ou_{format_name_string(a['Name'], 'format')}",
|
|
117
|
+
file=f,
|
|
118
|
+
)
|
|
119
|
+
if p["Type"] == "ORGANIZATIONAL_UNIT":
|
|
95
120
|
print(
|
|
96
121
|
f"\n{ident}ou_{format_name_string(find_ou_name(list_ous, p['Id']), 'format')}>> ou_{format_name_string(a['Name'], 'format')}",
|
|
97
|
-
file=f
|
|
122
|
+
file=f,
|
|
123
|
+
)
|
|
98
124
|
|
|
99
125
|
for c, i in zip(list_accounts, range(len(list_accounts))):
|
|
100
126
|
# print(f"\n aa_{i}= OrganizationsAccount(\"{c['account']}\")", file=f)
|
|
101
127
|
for p in c["parents"]:
|
|
102
|
-
if p[
|
|
103
|
-
print(
|
|
128
|
+
if p["Type"] == "ROOT":
|
|
129
|
+
print(
|
|
130
|
+
f"\n{ident}oo >> OrganizationsAccount(\"{c['account']}\\n{c['name']}\")",
|
|
131
|
+
file=f,
|
|
132
|
+
)
|
|
104
133
|
|
|
105
134
|
for o, j in zip(list_ous, range(len(list_ous))):
|
|
106
|
-
if p[
|
|
135
|
+
if p["Id"] == o["Id"] and p["Type"] == "ORGANIZATIONAL_UNIT":
|
|
107
136
|
print(
|
|
108
137
|
f"\n{ident}ou_{format_name_string(o['Name'], 'format')}>> OrganizationsAccount(\"{c['account']}\\n{format_name_string(c['name'], action='split')}\")",
|
|
109
|
-
file=f
|
|
138
|
+
file=f,
|
|
139
|
+
)
|
|
110
140
|
|
|
111
141
|
f.close()
|
|
112
142
|
|
|
113
143
|
|
|
114
144
|
def create_sso_mapper(template_file, group_and_members):
|
|
115
|
-
|
|
145
|
+
"""
|
|
146
|
+
Create sso mapper.
|
|
147
|
+
|
|
148
|
+
:param template_file:
|
|
149
|
+
:param group_and_members:
|
|
150
|
+
:return:
|
|
151
|
+
"""
|
|
152
|
+
with open(template_file, "a") as f:
|
|
116
153
|
ident = " "
|
|
117
|
-
print(
|
|
154
|
+
print("\n with Cluster('Groups'):", file=f)
|
|
118
155
|
for g, l in zip(group_and_members, range(len(group_and_members))):
|
|
119
|
-
|
|
120
156
|
if len(g["members"]) > 0:
|
|
121
157
|
print(f"\n{ident}with Cluster(\"{g['group_name']}\"):", file=f)
|
|
122
158
|
users = "["
|
|
@@ -127,12 +163,20 @@ def create_sso_mapper(template_file, group_and_members):
|
|
|
127
163
|
users += "]"
|
|
128
164
|
print(f"\n{ident}{ident}gg_{l}= {users}", file=f)
|
|
129
165
|
else:
|
|
130
|
-
print(
|
|
166
|
+
print(
|
|
167
|
+
f"\n{ident}gg_{l}= Users(\"{format_name_string(g['group_name'], 'split')}\")",
|
|
168
|
+
file=f,
|
|
169
|
+
)
|
|
131
170
|
|
|
132
171
|
|
|
133
172
|
def create_users_men(members):
|
|
134
|
-
|
|
173
|
+
"""
|
|
174
|
+
Create member users.
|
|
135
175
|
|
|
176
|
+
:param members:
|
|
177
|
+
:return:
|
|
178
|
+
"""
|
|
179
|
+
if len(members) > 0:
|
|
136
180
|
users = "["
|
|
137
181
|
for m in members:
|
|
138
182
|
user_name = m["MemberId"]["UserName"]
|
src/dgms/graph_template.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
graph_template= """
|
|
1
|
+
graph_template = """
|
|
2
2
|
from diagrams import Diagram, Cluster
|
|
3
3
|
|
|
4
4
|
from diagrams.aws.management import Organizations, OrganizationsAccount, OrganizationsOrganizationalUnit
|
|
@@ -7,7 +7,7 @@ with Diagram("Organizations-State", show=False, direction="TB"):
|
|
|
7
7
|
ou = OrganizationsOrganizationalUnit("OU")
|
|
8
8
|
oa = OrganizationsAccount("Account")
|
|
9
9
|
"""
|
|
10
|
-
graph_template_sso= """
|
|
10
|
+
graph_template_sso = """
|
|
11
11
|
from diagrams import Diagram, Cluster
|
|
12
12
|
|
|
13
13
|
from diagrams.aws.management import Organizations, OrganizationsAccount, OrganizationsOrganizationalUnit
|
|
@@ -18,7 +18,7 @@ with Diagram("SSO-State", show=False, direction="TB"):
|
|
|
18
18
|
uu= User("User")
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
graph_template_sso_complete="""
|
|
21
|
+
graph_template_sso_complete = """
|
|
22
22
|
from diagrams import Diagram, Cluster, Edge
|
|
23
23
|
|
|
24
24
|
from diagrams.aws.management import Organizations, OrganizationsAccount, OrganizationsOrganizationalUnit
|
|
@@ -29,4 +29,4 @@ with Diagram("IAM Identity Center", show=False, direction="LR"):
|
|
|
29
29
|
uu = User("User")
|
|
30
30
|
pp= IAMPermissions("PermissionsSet")
|
|
31
31
|
ou = OrganizationsOrganizationalUnit("PermissionsAssignments")
|
|
32
|
-
"""
|
|
32
|
+
"""
|
src/export_report/export_csv.py
CHANGED
src/reports/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Save results."""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from colorama import Fore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def save_results(results, filename, directory_path="."):
|
|
10
|
+
"""
|
|
11
|
+
Save results to a file.
|
|
12
|
+
:param results:
|
|
13
|
+
:param filename:
|
|
14
|
+
|
|
15
|
+
:return: None. Saves results to a file.
|
|
16
|
+
"""
|
|
17
|
+
if not Path.exists(Path(directory_path)):
|
|
18
|
+
Path.mkdir(Path(directory_path))
|
|
19
|
+
logging.debug(f"Directory {directory_path} created")
|
|
20
|
+
with open(f"{directory_path}/{filename}", "w") as f:
|
|
21
|
+
json.dump(results, fp=f, indent=4)
|
|
22
|
+
print(
|
|
23
|
+
f"{Fore.YELLOW}ℹ️ The accounts are stored in {directory_path}/{filename} {Fore.RESET}"
|
|
24
|
+
)
|