runbooks 0.1.7__py3-none-any.whl → 0.1.8__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 +1 -1
- runbooks/aws/__init__.py +58 -0
- runbooks/aws/dynamodb_operations.py +231 -0
- runbooks/aws/ec2_copy_image_cross-region.py +195 -0
- runbooks/aws/ec2_describe_instances.py +202 -0
- runbooks/aws/ec2_ebs_snapshots_delete.py +186 -0
- runbooks/aws/ec2_run_instances.py +207 -0
- runbooks/aws/ec2_start_stop_instances.py +199 -0
- runbooks/aws/ec2_terminate_instances.py +143 -0
- runbooks/aws/ec2_unused_eips.py +196 -0
- runbooks/aws/ec2_unused_volumes.py +184 -0
- runbooks/aws/s3_create_bucket.py +140 -0
- runbooks/aws/s3_list_buckets.py +152 -0
- runbooks/aws/s3_list_objects.py +151 -0
- runbooks/aws/s3_object_operations.py +183 -0
- runbooks/aws/tagging_lambda_handler.py +172 -0
- runbooks/python101/calculator.py +34 -0
- runbooks/python101/config.py +1 -0
- runbooks/python101/exceptions.py +16 -0
- runbooks/python101/file_manager.py +218 -0
- runbooks/python101/toolkit.py +153 -0
- runbooks/security_baseline/__init__.py +0 -0
- runbooks/security_baseline/checklist/__init__.py +17 -0
- runbooks/security_baseline/checklist/account_level_bucket_public_access.py +86 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +65 -0
- runbooks/security_baseline/checklist/bucket_public_access.py +82 -0
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +66 -0
- runbooks/security_baseline/checklist/direct_attached_policy.py +69 -0
- runbooks/security_baseline/checklist/guardduty_enabled.py +71 -0
- runbooks/security_baseline/checklist/iam_password_policy.py +43 -0
- runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +55 -0
- runbooks/security_baseline/checklist/multi_region_trail.py +64 -0
- runbooks/security_baseline/checklist/root_access_key.py +72 -0
- runbooks/security_baseline/checklist/root_mfa.py +39 -0
- runbooks/security_baseline/checklist/root_usage.py +128 -0
- runbooks/security_baseline/checklist/trail_enabled.py +68 -0
- runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
- runbooks/security_baseline/report_generator.py +149 -0
- runbooks/security_baseline/run_script.py +76 -0
- runbooks/security_baseline/security_baseline_tester.py +179 -0
- runbooks/security_baseline/utils/__init__.py +1 -0
- runbooks/security_baseline/utils/common.py +109 -0
- runbooks/security_baseline/utils/enums.py +44 -0
- runbooks/security_baseline/utils/language.py +762 -0
- runbooks/security_baseline/utils/level_const.py +5 -0
- runbooks/security_baseline/utils/permission_list.py +26 -0
- runbooks/utils/__init__.py +0 -0
- runbooks/utils/logger.py +36 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/METADATA +2 -2
- runbooks-0.1.8.dist-info/RECORD +54 -0
- runbooks-0.1.7.dist-info/RECORD +0 -6
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/WHEEL +0 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/entry_points.txt +0 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
import logging
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3
|
+
|
4
|
+
import botocore.exceptions
|
5
|
+
from utils import common
|
6
|
+
from utils import level_const as level
|
7
|
+
|
8
|
+
|
9
|
+
def get_cloudwatch_alarms(client, region):
|
10
|
+
try:
|
11
|
+
alarms = client.describe_alarms()["MetricAlarms"]
|
12
|
+
return region, alarms
|
13
|
+
except (client.exceptions.InvalidNextToken, botocore.exceptions.ClientError) as e:
|
14
|
+
logging.error(f"Error getting CloudWatch alarms for region {region}: {str(e)}", exc_info=True)
|
15
|
+
return region, "ERR"
|
16
|
+
|
17
|
+
|
18
|
+
def check_cloudwatch_alarm_configuration(session, translator) -> common.CheckResult:
|
19
|
+
logging.info(translator.translate("checking"))
|
20
|
+
|
21
|
+
ret = common.CheckResult()
|
22
|
+
ret.title = translator.translate("title")
|
23
|
+
ret.result_cols = ["Region", "Name"]
|
24
|
+
|
25
|
+
ec2_client = session.client("ec2")
|
26
|
+
|
27
|
+
try:
|
28
|
+
regions = common.get_opted_in_regions(ec2_client)
|
29
|
+
except botocore.exceptions.ClientError as e:
|
30
|
+
logging.error(f"Error getting opted-in regions: {str(e)}", exc_info=True)
|
31
|
+
ret.level = level.error
|
32
|
+
ret.msg = translator.translate("unexpected_error")
|
33
|
+
ret.result_rows.append(["ERR", "ERR"])
|
34
|
+
ret.error_message = str(e)
|
35
|
+
return ret
|
36
|
+
|
37
|
+
with ThreadPoolExecutor() as thread_executor:
|
38
|
+
futures = [
|
39
|
+
thread_executor.submit(get_cloudwatch_alarms, session.client("cloudwatch", region_name=region), region)
|
40
|
+
for region in regions
|
41
|
+
]
|
42
|
+
|
43
|
+
is_alarm_exist = False
|
44
|
+
ret.result_rows = []
|
45
|
+
error_occurred = False
|
46
|
+
|
47
|
+
for future in as_completed(futures):
|
48
|
+
region, alarms = future.result()
|
49
|
+
if alarms == "ERR":
|
50
|
+
error_occurred = True
|
51
|
+
ret.result_rows.append([region, "ERR"])
|
52
|
+
elif alarms:
|
53
|
+
is_alarm_exist = True
|
54
|
+
ret.result_rows.extend([region, alarm["AlarmName"]] for alarm in alarms)
|
55
|
+
|
56
|
+
if error_occurred:
|
57
|
+
ret.level = level.error
|
58
|
+
ret.msg = translator.translate("retrieval_error")
|
59
|
+
elif is_alarm_exist:
|
60
|
+
ret.level = level.success
|
61
|
+
ret.msg = translator.translate("alarm_exist")
|
62
|
+
else:
|
63
|
+
ret.level = level.warning
|
64
|
+
ret.msg = translator.translate("alarm_not_exist")
|
65
|
+
|
66
|
+
return ret
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import botocore.exceptions
|
4
|
+
from utils import common
|
5
|
+
from utils import level_const as level
|
6
|
+
|
7
|
+
|
8
|
+
def get_user_policies(client, user_name):
|
9
|
+
try:
|
10
|
+
direct_attached = len(client.list_attached_user_policies(UserName=user_name)["AttachedPolicies"])
|
11
|
+
inline = len(client.list_user_policies(UserName=user_name)["PolicyNames"])
|
12
|
+
return str(direct_attached), str(inline)
|
13
|
+
except botocore.exceptions.ClientError as e:
|
14
|
+
logging.error(f"Error getting policies for user {user_name}: {str(e)}", exc_info=True)
|
15
|
+
return "ERR", "ERR"
|
16
|
+
|
17
|
+
|
18
|
+
def check_iam_direct_attached_policy(session, translator) -> common.CheckResult:
|
19
|
+
logging.info(translator.translate("checking"))
|
20
|
+
|
21
|
+
client = session.client("iam")
|
22
|
+
ret = common.CheckResult()
|
23
|
+
ret.title = translator.translate("title")
|
24
|
+
ret.result_cols = ["IAM User", "Direct Attached Managed Policy", "Inline Policy"]
|
25
|
+
|
26
|
+
try:
|
27
|
+
user_list = client.list_users()["Users"]
|
28
|
+
except client.exceptions.NoSuchEntityException:
|
29
|
+
ret.level = level.warning
|
30
|
+
ret.msg = translator.translate("no_user")
|
31
|
+
ret.result_rows.append(["-", "-", "-"])
|
32
|
+
return ret
|
33
|
+
except client.exceptions.ServiceFailureException as e:
|
34
|
+
ret.level = level.error
|
35
|
+
ret.msg = translator.translate("service_failure")
|
36
|
+
ret.result_rows.append(["ERR", "ERR", "ERR"])
|
37
|
+
ret.error_message = str(e)
|
38
|
+
logging.error(f"Service Failure: {str(e)}", exc_info=True)
|
39
|
+
return ret
|
40
|
+
except botocore.exceptions.ClientError as e:
|
41
|
+
ret.level = level.error
|
42
|
+
ret.msg = translator.translate("unexpected_error")
|
43
|
+
ret.result_rows.append(["ERR", "ERR", "ERR"])
|
44
|
+
ret.error_message = str(e)
|
45
|
+
logging.error(f"Unexpected Error: {str(e)}", exc_info=True)
|
46
|
+
return ret
|
47
|
+
|
48
|
+
if not user_list:
|
49
|
+
ret.level = level.warning
|
50
|
+
ret.msg = translator.translate("no_user")
|
51
|
+
return ret
|
52
|
+
|
53
|
+
ret.level = level.success
|
54
|
+
ret.msg = translator.translate("success")
|
55
|
+
|
56
|
+
for user in user_list:
|
57
|
+
user_name = user["UserName"]
|
58
|
+
direct_attached, inline = get_user_policies(client, user_name)
|
59
|
+
|
60
|
+
if direct_attached == "ERR" or inline == "ERR":
|
61
|
+
ret.level = level.error
|
62
|
+
ret.msg = translator.translate("unexpected_error")
|
63
|
+
else:
|
64
|
+
ret.result_rows.append([user_name, direct_attached, inline])
|
65
|
+
if int(direct_attached) > 0 or int(inline) > 0:
|
66
|
+
ret.level = level.warning
|
67
|
+
ret.msg = translator.translate("warning")
|
68
|
+
|
69
|
+
return ret
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import logging
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3
|
+
|
4
|
+
import botocore.exceptions
|
5
|
+
from utils import common
|
6
|
+
from utils import level_const as level
|
7
|
+
|
8
|
+
|
9
|
+
def get_guard_duty_configuration(client, region):
|
10
|
+
try:
|
11
|
+
detectors = client.list_detectors()["DetectorIds"]
|
12
|
+
return region, len(detectors)
|
13
|
+
except (
|
14
|
+
client.exceptions.BadRequestException,
|
15
|
+
client.exceptions.InternalServerErrorException,
|
16
|
+
botocore.exceptions.ClientError,
|
17
|
+
) as e:
|
18
|
+
logging.error(f"Error getting GuardDuty configuration for region {region}: {str(e)}", exc_info=True)
|
19
|
+
return region, "ERR"
|
20
|
+
|
21
|
+
|
22
|
+
def check_guard_duty_enabled(session, translator) -> common.CheckResult:
|
23
|
+
logging.info(translator.translate("checking"))
|
24
|
+
|
25
|
+
ret = common.CheckResult()
|
26
|
+
ret.title = translator.translate("title")
|
27
|
+
ret.result_cols = ["Region", "GuardDuty Setting"]
|
28
|
+
|
29
|
+
ec2_client = session.client("ec2")
|
30
|
+
|
31
|
+
try:
|
32
|
+
regions = common.get_opted_in_regions(ec2_client)
|
33
|
+
except botocore.exceptions.ClientError as e:
|
34
|
+
logging.error(f"Error getting opted-in regions: {str(e)}", exc_info=True)
|
35
|
+
ret.level = level.error
|
36
|
+
ret.msg = translator.translate("unexpected_error")
|
37
|
+
ret.result_rows.append(["ERR", "ERR"])
|
38
|
+
ret.error_message = str(e)
|
39
|
+
return ret
|
40
|
+
|
41
|
+
with ThreadPoolExecutor() as executor:
|
42
|
+
futures = [
|
43
|
+
executor.submit(get_guard_duty_configuration, session.client("guardduty", region_name=region), region)
|
44
|
+
for region in regions
|
45
|
+
]
|
46
|
+
|
47
|
+
nums_of_guardduty_enabled = 0
|
48
|
+
error_occurred = False
|
49
|
+
|
50
|
+
for future in as_completed(futures):
|
51
|
+
region, number_of_detectors = future.result()
|
52
|
+
if number_of_detectors == "ERR":
|
53
|
+
error_occurred = True
|
54
|
+
ret.result_rows.append([region, "ERR"])
|
55
|
+
elif number_of_detectors > 0:
|
56
|
+
nums_of_guardduty_enabled += 1
|
57
|
+
ret.result_rows.append([region, "Activated"])
|
58
|
+
else:
|
59
|
+
ret.result_rows.append([region, "Inactivated"])
|
60
|
+
|
61
|
+
if error_occurred:
|
62
|
+
ret.level = level.error
|
63
|
+
ret.msg = translator.translate("retrieval_error")
|
64
|
+
else:
|
65
|
+
ret.level = level.info
|
66
|
+
if nums_of_guardduty_enabled > 0:
|
67
|
+
ret.msg = translator.translate("is_activated")
|
68
|
+
else:
|
69
|
+
ret.msg = translator.translate("is_not_activated")
|
70
|
+
|
71
|
+
return ret
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import botocore.exceptions
|
4
|
+
from utils import common
|
5
|
+
from utils import level_const as level
|
6
|
+
|
7
|
+
|
8
|
+
def check_iam_password_policy(session, translator) -> common.CheckResult:
|
9
|
+
logging.info(translator.translate("checking"))
|
10
|
+
|
11
|
+
client = session.client("iam")
|
12
|
+
ret = common.CheckResult()
|
13
|
+
ret.title = translator.translate("title")
|
14
|
+
ret.result_cols = ["Password Policy"]
|
15
|
+
|
16
|
+
try:
|
17
|
+
client.get_account_password_policy()
|
18
|
+
ret.level = level.success
|
19
|
+
ret.msg = translator.translate("success")
|
20
|
+
except client.exceptions.NoSuchEntityException:
|
21
|
+
ret.level = level.warning
|
22
|
+
ret.msg = translator.translate("warning")
|
23
|
+
ret.result_rows.append(["Not Set"])
|
24
|
+
except client.exceptions.ServiceFailureException as e:
|
25
|
+
logging.error(f"Service Failure: {str(e)}", exc_info=True)
|
26
|
+
ret.level = level.error
|
27
|
+
ret.msg = translator.translate("service_failure")
|
28
|
+
ret.result_rows.append(["ERR"])
|
29
|
+
ret.error_message = str(e)
|
30
|
+
except botocore.exceptions.ClientError as e:
|
31
|
+
logging.error(f"Unexpected ClientError: {str(e)}", exc_info=True)
|
32
|
+
ret.level = level.error
|
33
|
+
ret.msg = translator.translate("unexpected_error")
|
34
|
+
ret.result_rows.append(["ERR"])
|
35
|
+
ret.error_message = str(e)
|
36
|
+
except Exception as e:
|
37
|
+
logging.error(f"Unexpected error: {str(e)}", exc_info=True)
|
38
|
+
ret.level = level.error
|
39
|
+
ret.msg = translator.translate("unexpected_error")
|
40
|
+
ret.result_rows.append(["ERR"])
|
41
|
+
ret.error_message = str(e)
|
42
|
+
|
43
|
+
return ret
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from utils import common
|
4
|
+
from utils import level_const as level
|
5
|
+
|
6
|
+
|
7
|
+
def check_iam_user_mfa_setting(session, translator, credential_report) -> common.CheckResult:
|
8
|
+
logging.info(translator.translate("checking"))
|
9
|
+
|
10
|
+
ret = common.CheckResult()
|
11
|
+
ret.title = translator.translate("title")
|
12
|
+
ret.result_cols = ["IAM User", "MFA Enabled"]
|
13
|
+
|
14
|
+
if not credential_report:
|
15
|
+
ret.level = level.error
|
16
|
+
ret.msg = translator.translate("credential_report_error")
|
17
|
+
ret.result_rows.append(["ERR", "ERR"])
|
18
|
+
return ret
|
19
|
+
|
20
|
+
user_info_list = common.get_iam_credential_report(credential_report)
|
21
|
+
|
22
|
+
if not user_info_list:
|
23
|
+
ret.level = level.warning
|
24
|
+
ret.msg = translator.translate("no_iam_user")
|
25
|
+
return ret
|
26
|
+
|
27
|
+
ret.level = level.success
|
28
|
+
ret.msg = translator.translate("success")
|
29
|
+
|
30
|
+
for user_info in user_info_list:
|
31
|
+
user_name = user_info[common.CREDENTIAL_REPORT_COLS.USER.value]
|
32
|
+
mfa_status = user_info[common.CREDENTIAL_REPORT_COLS.MFA_ACTIVE.value].upper()
|
33
|
+
ret.result_rows.append([user_name, mfa_status])
|
34
|
+
|
35
|
+
if mfa_status == "FALSE":
|
36
|
+
ret.level = level.danger
|
37
|
+
ret.msg = translator.translate("danger")
|
38
|
+
|
39
|
+
return ret
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import logging
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3
|
+
|
4
|
+
import botocore.exceptions
|
5
|
+
from utils import common
|
6
|
+
from utils import level_const as level
|
7
|
+
|
8
|
+
|
9
|
+
def get_instance_usage_by_region(client, region):
|
10
|
+
try:
|
11
|
+
number_of_instances = sum(
|
12
|
+
len(reservation["Instances"]) for reservation in client.describe_instances()["Reservations"]
|
13
|
+
)
|
14
|
+
return region, str(number_of_instances)
|
15
|
+
except botocore.exceptions.ClientError as e:
|
16
|
+
logging.error(f"Error getting instance usage for region {region}: {str(e)}", exc_info=True)
|
17
|
+
return region, "ERR"
|
18
|
+
|
19
|
+
|
20
|
+
def check_multiregion_instance_usage(session, translator) -> common.CheckResult:
|
21
|
+
logging.info(translator.translate("checking"))
|
22
|
+
|
23
|
+
ret = common.CheckResult()
|
24
|
+
ret.title = translator.translate("title")
|
25
|
+
ret.result_cols = ["Region", "Instance Usage"]
|
26
|
+
|
27
|
+
ec2_client = session.client("ec2")
|
28
|
+
|
29
|
+
try:
|
30
|
+
regions = common.get_opted_in_regions(ec2_client)
|
31
|
+
except botocore.exceptions.ClientError as e:
|
32
|
+
logging.error(f"Error getting opted-in regions: {str(e)}", exc_info=True)
|
33
|
+
ret.level = level.error
|
34
|
+
ret.msg = translator.translate("unexpected_error")
|
35
|
+
ret.result_rows.append(["ERR", "ERR"])
|
36
|
+
ret.error_message = str(e)
|
37
|
+
return ret
|
38
|
+
|
39
|
+
with ThreadPoolExecutor() as executor:
|
40
|
+
futures = [
|
41
|
+
executor.submit(get_instance_usage_by_region, session.client("ec2", region_name=region), region)
|
42
|
+
for region in regions
|
43
|
+
]
|
44
|
+
|
45
|
+
ret.level = level.info
|
46
|
+
ret.msg = translator.translate("info_msg")
|
47
|
+
|
48
|
+
for future in as_completed(futures):
|
49
|
+
region, number_of_instances = future.result()
|
50
|
+
if number_of_instances == "ERR":
|
51
|
+
ret.level = level.error
|
52
|
+
ret.msg = translator.translate("retrieval_error")
|
53
|
+
ret.result_rows.append([region, number_of_instances])
|
54
|
+
|
55
|
+
return ret
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import botocore.exceptions
|
4
|
+
from utils import common
|
5
|
+
from utils import level_const as level
|
6
|
+
|
7
|
+
|
8
|
+
def check_multi_region_trail_enabled(session, translator) -> common.CheckResult:
|
9
|
+
logging.info(translator.translate("checking"))
|
10
|
+
|
11
|
+
ret = common.CheckResult()
|
12
|
+
ret.title = translator.translate("title")
|
13
|
+
ret.result_cols = ["Trail", "Multi-Region Logging"]
|
14
|
+
|
15
|
+
client = session.client("cloudtrail")
|
16
|
+
|
17
|
+
try:
|
18
|
+
trails = client.describe_trails()["trailList"]
|
19
|
+
except (
|
20
|
+
client.exceptions.UnsupportedOperationException,
|
21
|
+
client.exceptions.OperationNotPermittedException,
|
22
|
+
client.exceptions.NoManagementAccountSLRExistsException,
|
23
|
+
botocore.exceptions.ClientError,
|
24
|
+
) as e:
|
25
|
+
logging.error(f"Error describing trails: {str(e)}", exc_info=True)
|
26
|
+
ret.level = level.error
|
27
|
+
ret.msg = translator.translate("describe_trails_error")
|
28
|
+
ret.result_rows.append(["ERR", "ERR"])
|
29
|
+
ret.error_message = str(e)
|
30
|
+
return ret
|
31
|
+
|
32
|
+
if not trails:
|
33
|
+
ret.level = level.danger
|
34
|
+
ret.msg = translator.translate("no_trail")
|
35
|
+
ret.result_rows.append(["-", "-"])
|
36
|
+
return ret
|
37
|
+
|
38
|
+
logging_disabled_counter = 0
|
39
|
+
for trail in trails:
|
40
|
+
trail_arn = trail["TrailARN"]
|
41
|
+
try:
|
42
|
+
is_multi_region = client.get_trail(Name=trail_arn)["Trail"]["IsMultiRegionTrail"]
|
43
|
+
ret.result_rows.append([trail_arn, str(is_multi_region)])
|
44
|
+
if not is_multi_region:
|
45
|
+
logging_disabled_counter += 1
|
46
|
+
except botocore.exceptions.ClientError as e:
|
47
|
+
logging.error(f"Error getting trail {trail_arn}: {str(e)}", exc_info=True)
|
48
|
+
ret.result_rows.append([trail_arn, "ERR"])
|
49
|
+
ret.level = level.error
|
50
|
+
ret.msg = translator.translate("get_trail_error")
|
51
|
+
ret.error_message = str(e)
|
52
|
+
|
53
|
+
if ret.level != level.error:
|
54
|
+
if logging_disabled_counter == len(trails):
|
55
|
+
ret.level = level.danger
|
56
|
+
ret.msg = translator.translate("danger")
|
57
|
+
elif logging_disabled_counter > 0:
|
58
|
+
ret.level = level.warning
|
59
|
+
ret.msg = translator.translate("warning")
|
60
|
+
else:
|
61
|
+
ret.level = level.success
|
62
|
+
ret.msg = translator.translate("success")
|
63
|
+
|
64
|
+
return ret
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import logging
|
2
|
+
from datetime import datetime, timedelta, timezone
|
3
|
+
|
4
|
+
from utils import common
|
5
|
+
from utils import level_const as level
|
6
|
+
|
7
|
+
|
8
|
+
def get_last_used_days(date):
|
9
|
+
if date in ("N/A", "no_information"):
|
10
|
+
return timedelta.max
|
11
|
+
return datetime.now(timezone.utc) - datetime.fromisoformat(date[:-6])
|
12
|
+
|
13
|
+
|
14
|
+
def format_last_used(last_used_days):
|
15
|
+
if last_used_days == timedelta.max:
|
16
|
+
return "N/A"
|
17
|
+
if last_used_days.days == 0:
|
18
|
+
return "Today"
|
19
|
+
return f"{last_used_days.days} days before"
|
20
|
+
|
21
|
+
|
22
|
+
def check_access_key(root_credential_report, key_number):
|
23
|
+
key_active = root_credential_report[
|
24
|
+
getattr(common.CREDENTIAL_REPORT_COLS, f"ACCESS_KEY_{key_number}_ACTIVE").value
|
25
|
+
].upper()
|
26
|
+
key_last_used = root_credential_report[
|
27
|
+
getattr(common.CREDENTIAL_REPORT_COLS, f"ACCESS_KEY_{key_number}_LAST_USED_DATE").value
|
28
|
+
]
|
29
|
+
|
30
|
+
if key_active == "TRUE":
|
31
|
+
last_used_days = get_last_used_days(key_last_used)
|
32
|
+
return f"AccessKey{key_number}", "In Use", format_last_used(last_used_days), True
|
33
|
+
return f"AccessKey{key_number}", "Not In Use", "N/A", False
|
34
|
+
|
35
|
+
|
36
|
+
def check_root_accesskey_usage(session, translator, credential_report) -> common.CheckResult:
|
37
|
+
logging.info(translator.translate("checking"))
|
38
|
+
|
39
|
+
ret = common.CheckResult()
|
40
|
+
ret.title = translator.translate("title")
|
41
|
+
ret.result_cols = ["Access Key", "Status", "Last Used Date"]
|
42
|
+
|
43
|
+
if not credential_report:
|
44
|
+
ret.level = level.error
|
45
|
+
ret.msg = translator.translate("credential_report_error")
|
46
|
+
ret.result_rows.append(["ERR", "ERR", "ERR"])
|
47
|
+
return ret
|
48
|
+
|
49
|
+
try:
|
50
|
+
root_credential_report = common.get_root_credential_report(credential_report)
|
51
|
+
|
52
|
+
key_in_use = False
|
53
|
+
for key_number in [1, 2]:
|
54
|
+
key, status, last_used, is_active = check_access_key(root_credential_report, key_number)
|
55
|
+
ret.result_rows.append([key, status, last_used])
|
56
|
+
key_in_use = key_in_use or is_active
|
57
|
+
|
58
|
+
if key_in_use:
|
59
|
+
ret.level = level.danger
|
60
|
+
ret.msg = translator.translate("danger")
|
61
|
+
else:
|
62
|
+
ret.level = level.success
|
63
|
+
ret.msg = translator.translate("success")
|
64
|
+
|
65
|
+
except Exception as e:
|
66
|
+
logging.error(f"Error processing root access key check: {str(e)}", exc_info=True)
|
67
|
+
ret.level = level.error
|
68
|
+
ret.msg = translator.translate("processing_error")
|
69
|
+
ret.result_rows.append(["ERR", "ERR", "ERR"])
|
70
|
+
ret.error_message = str(e)
|
71
|
+
|
72
|
+
return ret
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from utils import common
|
4
|
+
from utils import level_const as level
|
5
|
+
|
6
|
+
|
7
|
+
def check_root_mfa_setting(session, translator, credential_report) -> common.CheckResult:
|
8
|
+
logging.info(translator.translate("checking"))
|
9
|
+
|
10
|
+
ret = common.CheckResult()
|
11
|
+
ret.title = translator.translate("title")
|
12
|
+
ret.result_cols = ["MFA Setting"]
|
13
|
+
|
14
|
+
if not credential_report:
|
15
|
+
ret.level = level.error
|
16
|
+
ret.msg = translator.translate("credential_report_error")
|
17
|
+
ret.result_rows.append(["ERR"])
|
18
|
+
return ret
|
19
|
+
|
20
|
+
try:
|
21
|
+
root_credential_report = common.get_root_credential_report(credential_report)
|
22
|
+
mfa_status = root_credential_report[common.CREDENTIAL_REPORT_COLS.MFA_ACTIVE.value].upper()
|
23
|
+
except Exception as e:
|
24
|
+
logging.error(f"Error processing root credential report: {str(e)}", exc_info=True)
|
25
|
+
ret.level = level.error
|
26
|
+
ret.msg = translator.translate("processing_error")
|
27
|
+
ret.result_rows.append(["ERR"])
|
28
|
+
ret.error_message = str(e)
|
29
|
+
return ret
|
30
|
+
|
31
|
+
if mfa_status == "TRUE":
|
32
|
+
ret.level = level.success
|
33
|
+
ret.msg = translator.translate("success")
|
34
|
+
else:
|
35
|
+
ret.level = level.danger
|
36
|
+
ret.msg = translator.translate("danger")
|
37
|
+
|
38
|
+
ret.result_rows.append([mfa_status])
|
39
|
+
return ret
|
@@ -0,0 +1,128 @@
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
|
+
|
3
|
+
from utils import common
|
4
|
+
from utils import level_const as level
|
5
|
+
|
6
|
+
from runbooks.utils.logger import configure_logger
|
7
|
+
|
8
|
+
logger = configure_logger(__name__) ## ✅ Configure Logger
|
9
|
+
|
10
|
+
## Define the standard threshold for root account access
|
11
|
+
ROOT_ACCESS_DAYS_STANDARD = 7
|
12
|
+
|
13
|
+
|
14
|
+
## @depreciated def get_root_access_days(date):
|
15
|
+
def get_root_access_days(date: str) -> timedelta:
|
16
|
+
"""
|
17
|
+
Calculates the timedelta between a given ISO datetime string and now, with robust error handling.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
date (str): The ISO format string to parse.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
timedelta: The time difference between the given date and now,
|
24
|
+
or timedelta.max for invalid or missing dates.
|
25
|
+
"""
|
26
|
+
if not date or date in ("N/A", "no_information"):
|
27
|
+
return timedelta.max
|
28
|
+
|
29
|
+
try:
|
30
|
+
parsed_date = datetime.fromisoformat(date)
|
31
|
+
except ValueError:
|
32
|
+
try:
|
33
|
+
## Strips the last 6 characters in the timezone offset (e.g., +00:00).
|
34
|
+
parsed_date = datetime.fromisoformat(date[:-6])
|
35
|
+
except ValueError:
|
36
|
+
logger.warning(f"Invalid date format encountered: {date}")
|
37
|
+
return timedelta.max
|
38
|
+
|
39
|
+
return datetime.now(timezone.utc) - parsed_date
|
40
|
+
|
41
|
+
|
42
|
+
def format_last_access_message(last_used_timedelta):
|
43
|
+
"""
|
44
|
+
Formats a user-friendly message for last access time.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
last_used_timedelta (timedelta): Time since last access.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
str: A descriptive message.
|
51
|
+
"""
|
52
|
+
if last_used_timedelta == timedelta.max:
|
53
|
+
return "No history"
|
54
|
+
if last_used_timedelta.days == 0:
|
55
|
+
return "Today"
|
56
|
+
return f"{last_used_timedelta.days} days ago"
|
57
|
+
|
58
|
+
|
59
|
+
def check_root_usage(session, translator, credential_report) -> common.CheckResult:
|
60
|
+
"""
|
61
|
+
Performs a security check on root account usage.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
session: AWS session object (reserved for future extensions).
|
65
|
+
translator: Translator for multi-language support.
|
66
|
+
credential_report: Credential report from AWS.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
common.CheckResult: The result of the root usage security check.
|
70
|
+
"""
|
71
|
+
logger.info(translator.translate("checking"))
|
72
|
+
|
73
|
+
result = common.CheckResult()
|
74
|
+
result.title = translator.translate("title")
|
75
|
+
result.result_cols = ["Credential Type", "Last Access Date"]
|
76
|
+
|
77
|
+
## Handle missing credential report
|
78
|
+
if not credential_report:
|
79
|
+
result.level = level.error
|
80
|
+
result.msg = translator.translate("credential_report_error")
|
81
|
+
result.result_rows.append(["ERR", "No Credential Report Available"])
|
82
|
+
return result
|
83
|
+
|
84
|
+
try:
|
85
|
+
## Retrieve root credential details
|
86
|
+
root_credential_report = common.get_root_credential_report(credential_report)
|
87
|
+
|
88
|
+
## Calculate days since last usage for each credential type
|
89
|
+
password_last_used = get_root_access_days(
|
90
|
+
root_credential_report[common.CREDENTIAL_REPORT_COLS.PASSWORD_LAST_USED.value]
|
91
|
+
)
|
92
|
+
access_key1_last_used = get_root_access_days(
|
93
|
+
root_credential_report[common.CREDENTIAL_REPORT_COLS.ACCESS_KEY_1_LAST_USED_DATE.value]
|
94
|
+
)
|
95
|
+
access_key2_last_used = get_root_access_days(
|
96
|
+
root_credential_report[common.CREDENTIAL_REPORT_COLS.ACCESS_KEY_2_LAST_USED_DATE.value]
|
97
|
+
)
|
98
|
+
|
99
|
+
## Determine the most recent access across all credentials
|
100
|
+
last_access_days = min(password_last_used, access_key1_last_used, access_key2_last_used)
|
101
|
+
|
102
|
+
if last_access_days > timedelta(ROOT_ACCESS_DAYS_STANDARD):
|
103
|
+
result.level = level.success
|
104
|
+
result.msg = translator.translate("success").format(ROOT_ACCESS_DAYS_STANDARD)
|
105
|
+
elif last_access_days.days == 0:
|
106
|
+
result.level = level.danger
|
107
|
+
result.msg = translator.translate("access_today")
|
108
|
+
else:
|
109
|
+
result.level = level.danger
|
110
|
+
result.msg = translator.translate("danger").format(last_access_days.days)
|
111
|
+
|
112
|
+
## Add detailed result rows for each credential type
|
113
|
+
result.result_rows.extend(
|
114
|
+
[
|
115
|
+
["PASSWORD", format_last_access_message(password_last_used)],
|
116
|
+
["ACCESS KEY1", format_last_access_message(access_key1_last_used)],
|
117
|
+
["ACCESS KEY2", format_last_access_message(access_key2_last_used)],
|
118
|
+
]
|
119
|
+
)
|
120
|
+
|
121
|
+
except Exception as e:
|
122
|
+
logger.error(f"Error processing root usage check: {str(e)}", exc_info=True)
|
123
|
+
result.level = level.error
|
124
|
+
result.msg = translator.translate("processing_error")
|
125
|
+
result.result_rows.append(["ERR", "Processing Error"])
|
126
|
+
result.error_message = str(e)
|
127
|
+
|
128
|
+
return result
|