runbooks 0.7.0__py3-none-any.whl → 0.7.6__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.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,294 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
import logging
|
4
|
-
import sys
|
5
|
-
from os.path import split
|
6
|
-
from queue import Queue
|
7
|
-
from threading import Thread
|
8
|
-
from time import time
|
9
|
-
|
10
|
-
from ArgumentsClass import CommonArguments
|
11
|
-
from botocore.exceptions import ClientError
|
12
|
-
from colorama import Fore, init
|
13
|
-
from Inventory_Modules import display_results, find_cw_groups_retention2, get_all_credentials
|
14
|
-
from tqdm.auto import tqdm
|
15
|
-
|
16
|
-
init()
|
17
|
-
__version__ = "2024.05.10"
|
18
|
-
ERASE_LINE = "\x1b[2K"
|
19
|
-
begin_time = time()
|
20
|
-
|
21
|
-
|
22
|
-
##################
|
23
|
-
# Functions
|
24
|
-
##################
|
25
|
-
def parse_args(f_arguments):
|
26
|
-
script_path, script_name = split(sys.argv[0])
|
27
|
-
parser = CommonArguments()
|
28
|
-
parser.multiprofile()
|
29
|
-
parser.multiregion()
|
30
|
-
parser.extendedargs()
|
31
|
-
parser.rootOnly()
|
32
|
-
parser.rolestouse()
|
33
|
-
parser.save_to_file()
|
34
|
-
parser.verbosity()
|
35
|
-
parser.timing()
|
36
|
-
parser.version(__version__)
|
37
|
-
local = parser.my_parser.add_argument_group(script_name, "Parameters specific to this script")
|
38
|
-
|
39
|
-
local.add_argument(
|
40
|
-
"+R",
|
41
|
-
"--ReplaceRetention",
|
42
|
-
help="The retention you want to update to on all groups that match.",
|
43
|
-
default=None,
|
44
|
-
metavar="retention days",
|
45
|
-
type=int,
|
46
|
-
choices=[0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 2192, 2557, 2922, 3288, 3653],
|
47
|
-
dest="pRetentionDays",
|
48
|
-
)
|
49
|
-
local.add_argument(
|
50
|
-
"-o",
|
51
|
-
"--OldRetention",
|
52
|
-
help="The retention you want to change on all groups that match. Use '0' for 'Never'",
|
53
|
-
default=None,
|
54
|
-
metavar="retention days",
|
55
|
-
type=int,
|
56
|
-
choices=[0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 2192, 2557, 2922, 3288, 3653],
|
57
|
-
dest="pOldRetentionDays",
|
58
|
-
)
|
59
|
-
return parser.my_parser.parse_args(f_arguments)
|
60
|
-
|
61
|
-
|
62
|
-
def check_cw_groups_retention(f_all_credential_list: list) -> list:
|
63
|
-
"""
|
64
|
-
This function will check the retention on all CW Groups in all accounts.
|
65
|
-
@param f_all_credential_list: Listing of all credentials for accounts we'll look into
|
66
|
-
@return: Returns a list of all CW Groups in all accounts provided by the credentials submitted.
|
67
|
-
"""
|
68
|
-
|
69
|
-
class FindCWGroups(Thread):
|
70
|
-
def __init__(self, queue):
|
71
|
-
Thread.__init__(self)
|
72
|
-
self.queue = queue
|
73
|
-
|
74
|
-
def run(self):
|
75
|
-
while True:
|
76
|
-
# Get the work from the queue and expand the tuple
|
77
|
-
c_account_credentials = self.queue.get()
|
78
|
-
pbar.update()
|
79
|
-
logging.info(f"De-queued info for account number {c_account_credentials['AccountId']}")
|
80
|
-
try:
|
81
|
-
CW_Groups = find_cw_groups_retention2(c_account_credentials)
|
82
|
-
if len(CW_Groups) > 0:
|
83
|
-
for logGroup in CW_Groups:
|
84
|
-
if "retentionInDays" in logGroup.keys():
|
85
|
-
logGroup["Retention"] = logGroup["retentionInDays"]
|
86
|
-
else:
|
87
|
-
logGroup["Retention"] = "Never"
|
88
|
-
logGroup["Name"] = logGroup["logGroupName"]
|
89
|
-
logGroup["Size"] = logGroup["storedBytes"]
|
90
|
-
logGroup["AccessKeyId"] = c_account_credentials["AccessKeyId"]
|
91
|
-
logGroup["SecretAccessKey"] = c_account_credentials["SecretAccessKey"]
|
92
|
-
logGroup["SessionToken"] = c_account_credentials["SessionToken"]
|
93
|
-
logGroup["ParentProfile"] = (
|
94
|
-
c_account_credentials["Profile"]
|
95
|
-
if c_account_credentials["Profile"] is not None
|
96
|
-
else "default"
|
97
|
-
)
|
98
|
-
logGroup["MgmtAccount"] = c_account_credentials["MgmtAccount"]
|
99
|
-
logGroup["AccountId"] = c_account_credentials["AccountId"]
|
100
|
-
logGroup["Region"] = c_account_credentials["Region"]
|
101
|
-
AllCWLogGroups.extend(CW_Groups)
|
102
|
-
|
103
|
-
except KeyError as my_Error:
|
104
|
-
logging.error(f"Account Access failed - trying to access {c_account_credentials['AccountId']}")
|
105
|
-
logging.info(f"Actual Error: {my_Error}")
|
106
|
-
pass
|
107
|
-
except AttributeError as my_Error:
|
108
|
-
logging.error(f"Error: Likely that one of the supplied profiles was wrong")
|
109
|
-
logging.warning(my_Error)
|
110
|
-
continue
|
111
|
-
except ClientError as my_Error:
|
112
|
-
if "AuthFailure" in str(my_Error):
|
113
|
-
logging.error(
|
114
|
-
f"Authorization Failure accessing account {c_account_credentials['AccountId']} in {c_account_credentials['Region']} region"
|
115
|
-
)
|
116
|
-
logging.warning(
|
117
|
-
f"It's possible that the region {c_account_credentials['Region']} hasn't been opted-into"
|
118
|
-
)
|
119
|
-
continue
|
120
|
-
else:
|
121
|
-
logging.error(f"Error: Likely throttling errors from too much activity")
|
122
|
-
logging.warning(my_Error)
|
123
|
-
continue
|
124
|
-
finally:
|
125
|
-
self.queue.task_done()
|
126
|
-
|
127
|
-
checkqueue = Queue()
|
128
|
-
|
129
|
-
AllCWLogGroups = []
|
130
|
-
WorkerThreads = min(len(f_all_credential_list), 25)
|
131
|
-
WorkerThreads = min(len(f_all_credential_list), 1)
|
132
|
-
|
133
|
-
pbar = tqdm(
|
134
|
-
desc=f"Finding CloudWatch log groups from {len(f_all_credential_list)} accounts / regions",
|
135
|
-
total=len(f_all_credential_list),
|
136
|
-
unit=" locations",
|
137
|
-
)
|
138
|
-
|
139
|
-
for x in range(WorkerThreads):
|
140
|
-
worker = FindCWGroups(checkqueue)
|
141
|
-
# Setting daemon to True will let the main thread exit even though the workers are blocking
|
142
|
-
worker.daemon = True
|
143
|
-
worker.start()
|
144
|
-
|
145
|
-
for credential in f_all_credential_list:
|
146
|
-
logging.info(f"Beginning to queue data - starting with {credential['AccountId']}")
|
147
|
-
try:
|
148
|
-
# While double parens are necessary below, if you're queuing multiple values, we're only queuing one right now.
|
149
|
-
# But if/ when we add fragment finding, I'm leaving this comment here to remind myself of that.
|
150
|
-
# checkqueue.put((credential))
|
151
|
-
checkqueue.put(credential)
|
152
|
-
except ClientError as my_Error:
|
153
|
-
if "AuthFailure" in str(my_Error):
|
154
|
-
logging.error(
|
155
|
-
f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
|
156
|
-
)
|
157
|
-
logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
|
158
|
-
pass
|
159
|
-
checkqueue.join()
|
160
|
-
pbar.close()
|
161
|
-
return AllCWLogGroups
|
162
|
-
|
163
|
-
|
164
|
-
def update_cw_groups_retention(fCWGroups: list, fOldRetentionDays: int = None, fRetentionDays: int = None):
|
165
|
-
import boto3
|
166
|
-
|
167
|
-
if fOldRetentionDays is None:
|
168
|
-
fOldRetentionDays = 0
|
169
|
-
Success = True
|
170
|
-
for item in fCWGroups:
|
171
|
-
cw_session = boto3.Session(
|
172
|
-
aws_access_key_id=item["AccessKeyId"],
|
173
|
-
aws_secret_access_key=item["SecretAccessKey"],
|
174
|
-
aws_session_token=item["SessionToken"],
|
175
|
-
region_name=item["Region"],
|
176
|
-
)
|
177
|
-
cw_client = cw_session.client("logs")
|
178
|
-
logging.info(f"Connecting to account {item['AccountId']}")
|
179
|
-
try:
|
180
|
-
print(
|
181
|
-
f"{ERASE_LINE}Updating log group {item['logGroupName']} account {item['AccountId']} in region {item['Region']}",
|
182
|
-
end="\r",
|
183
|
-
)
|
184
|
-
if "retentionInDays" not in item.keys():
|
185
|
-
retentionPeriod = "Never"
|
186
|
-
else:
|
187
|
-
retentionPeriod = item["retentionInDays"]
|
188
|
-
if (
|
189
|
-
fOldRetentionDays == 0 and "retentionInDays" not in item.keys()
|
190
|
-
) or retentionPeriod == fOldRetentionDays:
|
191
|
-
result = cw_client.put_retention_policy(
|
192
|
-
logGroupName=item["logGroupName"], retentionInDays=fRetentionDays
|
193
|
-
)
|
194
|
-
print(
|
195
|
-
f"Account: {item['AccountId']} in Region: {item['Region']} updated {item['logGroupName']} from {retentionPeriod} to {fRetentionDays} days"
|
196
|
-
)
|
197
|
-
Updated = True
|
198
|
-
else:
|
199
|
-
Updated = False
|
200
|
-
logging.info(
|
201
|
-
f"Skipped {item['logGroupName']} in account: {item['AccountId']} in Region: {item['Region']} as it didn't match criteria"
|
202
|
-
)
|
203
|
-
Success = True
|
204
|
-
except ClientError as my_Error:
|
205
|
-
logging.error(my_Error)
|
206
|
-
Success = False
|
207
|
-
return Success
|
208
|
-
return Success
|
209
|
-
|
210
|
-
|
211
|
-
##################
|
212
|
-
# Main
|
213
|
-
##################
|
214
|
-
|
215
|
-
if __name__ == "__main__":
|
216
|
-
args = parse_args(sys.argv[1:])
|
217
|
-
|
218
|
-
pProfiles = args.Profiles
|
219
|
-
pRegionList = args.Regions
|
220
|
-
pAccounts = args.Accounts
|
221
|
-
pSkipAccounts = args.SkipAccounts
|
222
|
-
pSkipProfiles = args.SkipProfiles
|
223
|
-
pAccessRoles = args.AccessRoles
|
224
|
-
pRetentionDays = args.pRetentionDays
|
225
|
-
pOldRetentionDays = args.pOldRetentionDays
|
226
|
-
pRootOnly = args.RootOnly
|
227
|
-
pFilename = args.Filename
|
228
|
-
pTiming = args.Time
|
229
|
-
verbose = args.loglevel
|
230
|
-
logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
|
231
|
-
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
232
|
-
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
233
|
-
logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
|
234
|
-
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
235
|
-
|
236
|
-
print()
|
237
|
-
print(f"Checking for CW Log Groups... ")
|
238
|
-
print()
|
239
|
-
|
240
|
-
CredentialList = get_all_credentials(
|
241
|
-
pProfiles, pTiming, pSkipProfiles, pSkipAccounts, pRootOnly, pAccounts, pRegionList, pAccessRoles
|
242
|
-
)
|
243
|
-
SuccessfulAccountAccesses = [x for x in CredentialList if x["Success"]]
|
244
|
-
AllChildAccounts = list(set([(x["MgmtAccount"], x["AccountId"]) for x in SuccessfulAccountAccesses]))
|
245
|
-
RegionList = list(set([x["Region"] for x in SuccessfulAccountAccesses]))
|
246
|
-
|
247
|
-
display_dict = {
|
248
|
-
# 'ParentProfile': {'DisplayOrder': 1, 'Heading': 'Parent Profile'},
|
249
|
-
"MgmtAccount": {"DisplayOrder": 2, "Heading": "Mgmt Acct"},
|
250
|
-
"AccountId": {"DisplayOrder": 3, "Heading": "Acct Number"},
|
251
|
-
"Region": {"DisplayOrder": 4, "Heading": "Region"},
|
252
|
-
"Retention": {"DisplayOrder": 5, "Heading": "Days Retention", "Condition": ["Never"]},
|
253
|
-
"Name": {"DisplayOrder": 7, "Heading": "CW Log Name"},
|
254
|
-
"Size": {"DisplayOrder": 6, "Heading": "Size (Bytes)"},
|
255
|
-
}
|
256
|
-
|
257
|
-
CWGroups = check_cw_groups_retention(CredentialList)
|
258
|
-
sorted_CWGroups = sorted(CWGroups, key=lambda k: (k["MgmtAccount"], k["AccountId"], k["Region"], k["Name"]))
|
259
|
-
|
260
|
-
display_results(sorted_CWGroups, display_dict, None, pFilename)
|
261
|
-
|
262
|
-
print(ERASE_LINE)
|
263
|
-
totalspace = 0
|
264
|
-
for i in CWGroups:
|
265
|
-
totalspace += i["storedBytes"]
|
266
|
-
print(
|
267
|
-
f"Found {len(CWGroups)} log groups across {len(AllChildAccounts)} accounts across {len(RegionList)} regions, representing {totalspace / 1024 / 1024 / 1024:,.3f} GB"
|
268
|
-
)
|
269
|
-
print(f"To give you a small idea - in us-east-1 - it costs $0.03 per GB per month to store (after 5GB).")
|
270
|
-
if totalspace / 1024 / 1024 / 1024 <= 5.0:
|
271
|
-
print("Which means this is essentially free for you...")
|
272
|
-
else:
|
273
|
-
print(
|
274
|
-
f"This means you're paying about ${((totalspace / 1024 / 1024 / 1024) - 5) * 0.03:,.2f} per month in CW storage charges"
|
275
|
-
)
|
276
|
-
|
277
|
-
if pRetentionDays is not None:
|
278
|
-
print(f"As per your request - updating ALL retention periods to {pRetentionDays} days")
|
279
|
-
print(f"")
|
280
|
-
UpdateAllRetention = input(
|
281
|
-
f"This is definitely an intrusive command, so please confirm you want to do this (y/n): "
|
282
|
-
) in ["Y", "y"]
|
283
|
-
if UpdateAllRetention:
|
284
|
-
print(f"Updating all log groups to have a {pRetentionDays} retention period")
|
285
|
-
update_cw_groups_retention(CWGroups, pOldRetentionDays, pRetentionDays)
|
286
|
-
else:
|
287
|
-
print(f"No changes made")
|
288
|
-
print()
|
289
|
-
if pTiming:
|
290
|
-
print(ERASE_LINE)
|
291
|
-
print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
|
292
|
-
print()
|
293
|
-
print("Thank you for using this script")
|
294
|
-
print()
|