runbooks 0.1.6__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.6.dist-info → runbooks-0.1.8.dist-info}/METADATA +9 -1
- runbooks-0.1.8.dist-info/RECORD +54 -0
- runbooks-0.1.8.dist-info/entry_points.txt +3 -0
- runbooks-0.1.6.dist-info/RECORD +0 -6
- runbooks-0.1.6.dist-info/entry_points.txt +0 -2
- {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/WHEEL +0 -0
- {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,68 @@
|
|
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_trail_status(client, trail_arn):
|
9
|
+
try:
|
10
|
+
return client.get_trail_status(Name=trail_arn)["IsLogging"]
|
11
|
+
except botocore.exceptions.ClientError as e:
|
12
|
+
logging.error(f"Error getting trail status for {trail_arn}: {str(e)}", exc_info=True)
|
13
|
+
return "ERR"
|
14
|
+
|
15
|
+
|
16
|
+
def check_trail_enabled(session, translator) -> common.CheckResult:
|
17
|
+
logging.info(translator.translate("checking"))
|
18
|
+
|
19
|
+
ret = common.CheckResult()
|
20
|
+
ret.title = translator.translate("title")
|
21
|
+
ret.result_cols = ["Trail", "Is Logging"]
|
22
|
+
|
23
|
+
client = session.client("cloudtrail")
|
24
|
+
|
25
|
+
try:
|
26
|
+
trails = client.describe_trails()["trailList"]
|
27
|
+
except (
|
28
|
+
client.exceptions.UnsupportedOperationException,
|
29
|
+
client.exceptions.OperationNotPermittedException,
|
30
|
+
client.exceptions.NoManagementAccountSLRExistsException,
|
31
|
+
botocore.exceptions.ClientError,
|
32
|
+
) as e:
|
33
|
+
logging.error(f"Error describing trails: {str(e)}", exc_info=True)
|
34
|
+
ret.level = level.error
|
35
|
+
ret.msg = translator.translate("describe_trails_error")
|
36
|
+
ret.result_rows.append(["ERR", "ERR"])
|
37
|
+
ret.error_message = str(e)
|
38
|
+
return ret
|
39
|
+
|
40
|
+
if not trails:
|
41
|
+
ret.level = level.danger
|
42
|
+
ret.msg = translator.translate("no_trail")
|
43
|
+
ret.result_rows.append(["-", "-"])
|
44
|
+
return ret
|
45
|
+
|
46
|
+
logging_disabled_counter = 0
|
47
|
+
for trail in trails:
|
48
|
+
trail_arn = trail["TrailARN"]
|
49
|
+
is_logging = get_trail_status(client, trail_arn)
|
50
|
+
ret.result_rows.append([trail_arn, str(is_logging)])
|
51
|
+
if is_logging == "ERR":
|
52
|
+
ret.level = level.error
|
53
|
+
ret.msg = translator.translate("get_trail_status_error")
|
54
|
+
elif not is_logging:
|
55
|
+
logging_disabled_counter += 1
|
56
|
+
|
57
|
+
if ret.level != level.error:
|
58
|
+
if logging_disabled_counter == len(trails):
|
59
|
+
ret.level = level.danger
|
60
|
+
ret.msg = translator.translate("danger")
|
61
|
+
elif logging_disabled_counter > 0:
|
62
|
+
ret.level = level.warning
|
63
|
+
ret.msg = translator.translate("warning")
|
64
|
+
else:
|
65
|
+
ret.level = level.success
|
66
|
+
ret.msg = translator.translate("success")
|
67
|
+
|
68
|
+
return ret
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from utils import common
|
4
|
+
from utils import level_const as level
|
5
|
+
|
6
|
+
|
7
|
+
def check_trust_advisor_configuration(translator) -> common.CheckResult:
|
8
|
+
logging.info(translator.translate("checking"))
|
9
|
+
|
10
|
+
ret = common.CheckResult()
|
11
|
+
ret.title = translator.translate("title")
|
12
|
+
ret.result_cols = []
|
13
|
+
|
14
|
+
try:
|
15
|
+
ret.level = level.info
|
16
|
+
ret.msg = translator.translate("info")
|
17
|
+
ret.result_rows.append([])
|
18
|
+
except Exception as e:
|
19
|
+
logging.error(f"Error in Trust Advisor check: {str(e)}", exc_info=True)
|
20
|
+
ret.level = level.error
|
21
|
+
ret.msg = translator.translate("error")
|
22
|
+
ret.error_message = str(e)
|
23
|
+
|
24
|
+
return ret
|
@@ -0,0 +1,149 @@
|
|
1
|
+
import datetime
|
2
|
+
import os
|
3
|
+
from pathlib import Path
|
4
|
+
from string import Template
|
5
|
+
|
6
|
+
from jinja2 import Template
|
7
|
+
from utils import language, level_const
|
8
|
+
|
9
|
+
from runbooks.utils.logger import configure_logger
|
10
|
+
|
11
|
+
## ✅ Configure Logger
|
12
|
+
logger = configure_logger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class ReportGenerator:
|
16
|
+
def __init__(self, account_id, results, language_code):
|
17
|
+
self.account_id = account_id
|
18
|
+
self.results = results
|
19
|
+
self.language = language_code
|
20
|
+
self.generated_at = datetime.datetime.now().strftime("(UTC) %Y-%m-%d %H:%M:%S")
|
21
|
+
|
22
|
+
def create_html_report(self):
|
23
|
+
template = self._load_template()
|
24
|
+
context = self._prepare_context()
|
25
|
+
return template.render(context)
|
26
|
+
|
27
|
+
def _load_template(self):
|
28
|
+
"""
|
29
|
+
Load the appropriate HTML template based on self.language.
|
30
|
+
|
31
|
+
Supported languages: KR, JP, VN, EN (default).
|
32
|
+
Falls back to English template if self.language is unrecognized.
|
33
|
+
"""
|
34
|
+
## Normalize user input like "en", "En", "EN" -> "EN"
|
35
|
+
lang = self.language.upper() if self.language else "EN"
|
36
|
+
|
37
|
+
## Map language codes to template filenames
|
38
|
+
template_map = {
|
39
|
+
"KR": "report_template_kr.html",
|
40
|
+
"JP": "report_template_jp.html",
|
41
|
+
"VN": "report_template_vn.html",
|
42
|
+
"EN": "report_template_en.html",
|
43
|
+
}
|
44
|
+
|
45
|
+
## Fall back to English if language code not recognized
|
46
|
+
template_filename = template_map.get(lang, "report_template_en.html")
|
47
|
+
|
48
|
+
# Always resolve paths relative to this file’s directory
|
49
|
+
script_dir = Path(__file__).resolve().parent
|
50
|
+
template_path = script_dir / template_filename
|
51
|
+
|
52
|
+
## Attempt to read the template file
|
53
|
+
if not template_path.is_file():
|
54
|
+
logger.error("Template file '%s' for language '%s' not found at %s", template_filename, lang, template_path)
|
55
|
+
raise FileNotFoundError(f"Could not find the template '{template_filename}' for language '{lang}'.")
|
56
|
+
|
57
|
+
with template_path.open("r", encoding="utf-8") as file:
|
58
|
+
return Template(file.read())
|
59
|
+
|
60
|
+
# if self.language == "KR":
|
61
|
+
# with open("report_template_kr.html", "r") as file:
|
62
|
+
# return Template(file.read())
|
63
|
+
# elif self.language == "JP":
|
64
|
+
# with open("report_template_jp.html", "r") as file:
|
65
|
+
# return Template(file.read())
|
66
|
+
# elif self.language == "VN":
|
67
|
+
# with open("report_template_vn.html", "r") as file:
|
68
|
+
# return Template(file.read())
|
69
|
+
# else:
|
70
|
+
# ## Get the absolute directory where *this script* is located
|
71
|
+
# report_dir = os.path.dirname(os.path.abspath(__file__))
|
72
|
+
# report_path = os.path.join(report_dir, "report_template_en.html")
|
73
|
+
# ## with open("report_template_en.html", "r") as file:
|
74
|
+
# with open(report_path, "r") as file:
|
75
|
+
# return Template(file.read())
|
76
|
+
|
77
|
+
def _prepare_context(self):
|
78
|
+
return {
|
79
|
+
"account_id": self.account_id,
|
80
|
+
"generated_at": self.generated_at,
|
81
|
+
"overview": self._generate_overview(),
|
82
|
+
"result_sections": self._generate_result_sections(),
|
83
|
+
"language": self.language,
|
84
|
+
}
|
85
|
+
|
86
|
+
def _generate_overview(self):
|
87
|
+
filtered_results = {level: len(results) for level, results in self.results.items() if isinstance(results, list)}
|
88
|
+
desired_levels = ["Danger", "Warning", "Success", "Info", "Error"]
|
89
|
+
sorted_result = [(level, filtered_results[level]) for level in desired_levels]
|
90
|
+
|
91
|
+
return sorted_result
|
92
|
+
|
93
|
+
def _generate_result_sections(self):
|
94
|
+
sections = []
|
95
|
+
for level, results in self.results.items():
|
96
|
+
if isinstance(results, list):
|
97
|
+
if len(results) == 0:
|
98
|
+
## This condition only applies when there are no 'Error' items at all.
|
99
|
+
formatted_results = [
|
100
|
+
{
|
101
|
+
"title": "All inspection items were successfully checked.",
|
102
|
+
"message": "No results available for this section. All checks were successful.",
|
103
|
+
"table": [],
|
104
|
+
}
|
105
|
+
]
|
106
|
+
else:
|
107
|
+
formatted_results = self._format_results(results, level)
|
108
|
+
|
109
|
+
sections.append({"level": level, "result_items": formatted_results})
|
110
|
+
|
111
|
+
sort_order = {"Danger": 0, "Warning": 1, "Success": 2, "Info": 3, "Error": 4}
|
112
|
+
|
113
|
+
sections.sort(key=lambda x: sort_order.get(x["level"], len(sort_order)))
|
114
|
+
return sections
|
115
|
+
|
116
|
+
def _format_results(self, results, level):
|
117
|
+
formatted_results = []
|
118
|
+
for result in results:
|
119
|
+
if isinstance(result, dict): # if type(result) is dict
|
120
|
+
formatted_result = {
|
121
|
+
"title": result.get("title", "Unknown"),
|
122
|
+
"message": result.get("msg", "No message"),
|
123
|
+
"table": self._format_table(result.get("result_cols", []), result.get("result_rows", [])),
|
124
|
+
}
|
125
|
+
if level == level_const.error:
|
126
|
+
formatted_result["error_message"] = result.get("error_message", "Unknown error")
|
127
|
+
formatted_results.append(formatted_result)
|
128
|
+
elif hasattr(result, "to_dict"): # if the result object has 'to_dict' attribute
|
129
|
+
result_dict = result.to_dict()
|
130
|
+
formatted_result = {
|
131
|
+
"title": result_dict.get("title", "Unknown"),
|
132
|
+
"message": result_dict.get("msg", "No message"),
|
133
|
+
"table": self._format_table(result_dict.get("result_cols", []), result_dict.get("result_rows", [])),
|
134
|
+
}
|
135
|
+
if level == level_const.error:
|
136
|
+
formatted_result["error_message"] = result_dict.get("error_message", "Unknown error")
|
137
|
+
formatted_results.append(formatted_result)
|
138
|
+
|
139
|
+
return formatted_results
|
140
|
+
|
141
|
+
def _format_table(self, cols, rows):
|
142
|
+
if not rows:
|
143
|
+
return None
|
144
|
+
return {"headers": cols, "rows": rows}
|
145
|
+
|
146
|
+
|
147
|
+
def generate_html_report(account_id_str, result_sort_by_level, language_code):
|
148
|
+
generator = ReportGenerator(account_id_str, result_sort_by_level, language_code)
|
149
|
+
return generator.create_html_report()
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
AWS Security Baseline Tester Script
|
5
|
+
|
6
|
+
Date: 2025-01-10
|
7
|
+
Version: 1.1.0
|
8
|
+
|
9
|
+
This script evaluates AWS account security configurations against a baseline checklist
|
10
|
+
and generates a multilingual report in HTML format.
|
11
|
+
|
12
|
+
Compatible with both local (via pip or Docker) and AWS Lambda environments.
|
13
|
+
"""
|
14
|
+
|
15
|
+
import argparse
|
16
|
+
import sys
|
17
|
+
|
18
|
+
from security_baseline_tester import SecurityBaselineTester
|
19
|
+
|
20
|
+
from runbooks.utils.logger import configure_logger
|
21
|
+
|
22
|
+
## ✅ Configure Logger
|
23
|
+
logger = configure_logger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
# ==============================
|
27
|
+
# Parse Command-Line Arguments
|
28
|
+
# ==============================
|
29
|
+
def parse_arguments():
|
30
|
+
"""
|
31
|
+
Parses command-line arguments for the security baseline tester.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
argparse.Namespace: Parsed arguments.
|
35
|
+
"""
|
36
|
+
parser = argparse.ArgumentParser(
|
37
|
+
description="AWS Security Baseline Tester - Evaluate your AWS account's security configuration."
|
38
|
+
)
|
39
|
+
parser.add_argument(
|
40
|
+
"--profile", default="default", help="AWS IAM profile to use for authentication (default: 'default')."
|
41
|
+
)
|
42
|
+
parser.add_argument(
|
43
|
+
"--language",
|
44
|
+
choices=["EN", "JP", "KR", "VN"],
|
45
|
+
default="EN",
|
46
|
+
help="Language for the Security Baseline Report (default: 'EN').",
|
47
|
+
)
|
48
|
+
return parser.parse_args()
|
49
|
+
|
50
|
+
|
51
|
+
# ==============================
|
52
|
+
# Main Function
|
53
|
+
# ==============================
|
54
|
+
def main():
|
55
|
+
"""
|
56
|
+
Main entry point for the AWS Security Baseline Tester.
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
args = parse_arguments()
|
60
|
+
|
61
|
+
logger.info("Starting AWS Security Baseline Tester...")
|
62
|
+
logger.info(f"Using AWS profile: {args.profile}")
|
63
|
+
logger.info(f"Report language: {args.language}")
|
64
|
+
|
65
|
+
## Instantiate and run the Security Baseline Tester
|
66
|
+
tester = SecurityBaselineTester(args.profile, args.language)
|
67
|
+
tester.run()
|
68
|
+
|
69
|
+
logger.info("AWS Security Baseline testing completed successfully.")
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
72
|
+
sys.exit(1)
|
73
|
+
|
74
|
+
|
75
|
+
if __name__ == "__main__":
|
76
|
+
main()
|
@@ -0,0 +1,179 @@
|
|
1
|
+
import datetime
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
import boto3
|
9
|
+
import botocore
|
10
|
+
import report_generator
|
11
|
+
from checklist import * ## noqa: F403
|
12
|
+
from utils import common, level_const
|
13
|
+
from utils.language import get_translator
|
14
|
+
|
15
|
+
|
16
|
+
class SecurityBaselineTester:
|
17
|
+
def __init__(self, profile, language):
|
18
|
+
self.profile = profile
|
19
|
+
self.language = language
|
20
|
+
self.session = self._create_session()
|
21
|
+
self.config = self._load_config()
|
22
|
+
self.translator = get_translator("main", language)
|
23
|
+
|
24
|
+
def _create_session(self):
|
25
|
+
if self.profile == "default":
|
26
|
+
return boto3.Session()
|
27
|
+
return boto3.Session(profile_name=self.profile)
|
28
|
+
|
29
|
+
def _load_config(self):
|
30
|
+
## Get the absolute directory where *this script* is located
|
31
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
32
|
+
config_path = os.path.join(script_dir, "config.json")
|
33
|
+
|
34
|
+
try:
|
35
|
+
# with open("./config.json", "r") as file:
|
36
|
+
with open(config_path, "r") as file:
|
37
|
+
return json.load(file)
|
38
|
+
except FileNotFoundError:
|
39
|
+
logging.error("config.json file not found. Please ensure it exists in the same directory as this script.")
|
40
|
+
raise
|
41
|
+
except json.JSONDecodeError:
|
42
|
+
logging.error("Error parsing config.json. Please ensure it is a valid JSON file.")
|
43
|
+
raise
|
44
|
+
|
45
|
+
def run(self):
|
46
|
+
try:
|
47
|
+
self._validate_session()
|
48
|
+
caller_identity = self._get_caller_identity()
|
49
|
+
self._print_auditor_info(caller_identity)
|
50
|
+
|
51
|
+
logging.info(self.translator.translate("start_test"))
|
52
|
+
|
53
|
+
account_id, results = self._execute_tests()
|
54
|
+
self._generate_report(account_id, results)
|
55
|
+
|
56
|
+
logging.info(self.translator.translate("test_completed"))
|
57
|
+
except Exception as e:
|
58
|
+
logging.error(f"An error occurred during the security baseline test: {str(e)}", exc_info=True)
|
59
|
+
|
60
|
+
def _validate_session(self):
|
61
|
+
if self.session.region_name is None:
|
62
|
+
raise ValueError('AWS region is not specified. Run "aws configure" to set it.')
|
63
|
+
|
64
|
+
def _get_caller_identity(self):
|
65
|
+
try:
|
66
|
+
return self.session.client("sts").get_caller_identity()
|
67
|
+
except botocore.exceptions.ClientError as e:
|
68
|
+
logging.error(f"Failed to get caller identity: {str(e)}")
|
69
|
+
raise
|
70
|
+
|
71
|
+
def _print_auditor_info(self, caller_identity):
|
72
|
+
logging.info("==================== AUDITOR INFO ====================")
|
73
|
+
logging.info(f"USER ID : {caller_identity['UserId']}")
|
74
|
+
logging.info(f"ACCOUNT : {caller_identity['Account']}")
|
75
|
+
logging.info(f"ARN : {caller_identity['Arn']}")
|
76
|
+
logging.info("=====================================================")
|
77
|
+
|
78
|
+
def _execute_tests(self):
|
79
|
+
iam_client = self.session.client("iam")
|
80
|
+
sts_client = self.session.client("sts")
|
81
|
+
|
82
|
+
account_id = common.get_account_id(sts_client)
|
83
|
+
logging.info(self.translator.translate("request_credential_report"))
|
84
|
+
credential_report = common.generate_credential_report(iam_client)
|
85
|
+
|
86
|
+
with ThreadPoolExecutor(max_workers=self.config.get("max_workers", 5)) as executor:
|
87
|
+
futures = {
|
88
|
+
executor.submit(self._run_check, check_name, credential_report): check_name
|
89
|
+
for check_name in self.config.get("checks", [])
|
90
|
+
}
|
91
|
+
|
92
|
+
results = {
|
93
|
+
level: [] for level in ["Success", "Warning", "Danger", "Error", "Info"] if isinstance(level, str)
|
94
|
+
}
|
95
|
+
for future in as_completed(futures):
|
96
|
+
result = future.result()
|
97
|
+
results[result.level].append(result)
|
98
|
+
|
99
|
+
return account_id, results
|
100
|
+
|
101
|
+
def _run_check(self, check_name, credential_report):
|
102
|
+
check_module = __import__(f"checklist.{check_name}", fromlist=[check_name])
|
103
|
+
check_method = getattr(check_module, self.config["checks"][check_name])
|
104
|
+
translator = get_translator(check_name, self.language)
|
105
|
+
|
106
|
+
if check_name in [
|
107
|
+
"alternate_contacts",
|
108
|
+
"account_level_bucket_public_access",
|
109
|
+
"bucket_public_access",
|
110
|
+
"cloudwatch_alarm_configuration",
|
111
|
+
"direct_attached_policy",
|
112
|
+
"guardduty_enabled",
|
113
|
+
"multi_region_instance_usage",
|
114
|
+
"multi_region_trail",
|
115
|
+
"trail_enabled",
|
116
|
+
"iam_password_policy",
|
117
|
+
]:
|
118
|
+
return check_method(self.session, translator)
|
119
|
+
elif check_name in ["root_mfa", "root_usage", "root_access_key", "iam_user_mfa"]:
|
120
|
+
return check_method(self.session, translator, credential_report)
|
121
|
+
elif check_name == "trusted_advisor":
|
122
|
+
return check_method(translator)
|
123
|
+
else:
|
124
|
+
raise ValueError(f"Unknown check method: {check_name}")
|
125
|
+
|
126
|
+
def _check_result_directory(self):
|
127
|
+
"""
|
128
|
+
Ensures that the 'results' directory (located next to this script) exists.
|
129
|
+
|
130
|
+
:return: A Path object pointing to the results directory.
|
131
|
+
"""
|
132
|
+
|
133
|
+
# directory_name = "./results"
|
134
|
+
# if not os.path.exists(directory_name):
|
135
|
+
# os.makedirs(directory_name)
|
136
|
+
# logging.info(self.translator.translate("results_folder_created"))
|
137
|
+
# else:
|
138
|
+
# logging.info(self.translator.translate("results_folder_already_exists"))
|
139
|
+
|
140
|
+
script_dir = Path(__file__).resolve().parent
|
141
|
+
results_dir = script_dir / "results"
|
142
|
+
|
143
|
+
if not results_dir.exists():
|
144
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
145
|
+
logging.info(self.translator.translate("results_folder_created"))
|
146
|
+
else:
|
147
|
+
logging.info(self.translator.translate("results_folder_already_exists"))
|
148
|
+
|
149
|
+
return results_dir
|
150
|
+
|
151
|
+
def _generate_report(self, account_id, results):
|
152
|
+
"""
|
153
|
+
Generates an HTML security report and writes it to the 'results' directory.
|
154
|
+
|
155
|
+
:param account_id: The AWS account ID or similar identifier.
|
156
|
+
:param results: A dictionary containing the security baseline results.
|
157
|
+
"""
|
158
|
+
html_report = report_generator.generate_html_report(account_id, results, self.language)
|
159
|
+
|
160
|
+
current_time = datetime.datetime.now().strftime("%y%m%d-%H%M%S")
|
161
|
+
short_account_id = account_id[-4:]
|
162
|
+
|
163
|
+
## Ensure the results directory exists
|
164
|
+
results_dir = self._check_result_directory()
|
165
|
+
|
166
|
+
## Build the report filename
|
167
|
+
report_filename = f"security-report-{short_account_id}-{current_time}.html"
|
168
|
+
report_path = results_dir / report_filename
|
169
|
+
|
170
|
+
## Get the absolute directory where *this script* is located
|
171
|
+
# test_report_dir = os.path.dirname(os.path.abspath(__file__))
|
172
|
+
# test_report_path = os.path.join(test_report_dir, report_filename)
|
173
|
+
|
174
|
+
# with open(test_report_path, "w") as file:
|
175
|
+
## Write the report to disk
|
176
|
+
with report_path.open("w", encoding="utf-8") as file:
|
177
|
+
file.write(html_report)
|
178
|
+
|
179
|
+
logging.info(self.translator.translate("generate_result_report"))
|
@@ -0,0 +1 @@
|
|
1
|
+
__all__ = ["common", "level_const", "language", "enums"]
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import time
|
5
|
+
|
6
|
+
import botocore.exceptions
|
7
|
+
|
8
|
+
from .enums import CREDENTIAL_REPORT_COLS
|
9
|
+
|
10
|
+
|
11
|
+
class Ret:
|
12
|
+
def __init__(self) -> None:
|
13
|
+
self.status_code = 200
|
14
|
+
self.body = "success"
|
15
|
+
self.headers = {"Content-Type": "text/html;charset=UTF-8"}
|
16
|
+
|
17
|
+
def to_dict(self) -> dict:
|
18
|
+
return {"statusCode": self.status_code, "body": self.body, "headers": self.headers}
|
19
|
+
|
20
|
+
|
21
|
+
class CheckResult:
|
22
|
+
def __init__(self) -> None:
|
23
|
+
self.title = "UNKNOWN"
|
24
|
+
self.level = "UNKNOWN"
|
25
|
+
self.msg = "UNKNOWN"
|
26
|
+
self.result_rows = []
|
27
|
+
self.result_cols = []
|
28
|
+
self.error_message = None # 새로 추가된 필드
|
29
|
+
|
30
|
+
def to_dict(self) -> dict:
|
31
|
+
return {
|
32
|
+
"title": self.title,
|
33
|
+
"level": self.level,
|
34
|
+
"msg": self.msg,
|
35
|
+
"result_rows": self.result_rows,
|
36
|
+
"result_cols": self.result_cols,
|
37
|
+
"error_message": self.error_message,
|
38
|
+
}
|
39
|
+
|
40
|
+
def get_table(self) -> dict:
|
41
|
+
return {"cols": self.result_cols, "rows": self.result_rows}
|
42
|
+
|
43
|
+
|
44
|
+
def load_config():
|
45
|
+
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
|
46
|
+
try:
|
47
|
+
with open(config_path, "r") as file:
|
48
|
+
return json.load(file)
|
49
|
+
except FileNotFoundError:
|
50
|
+
logging.error(f"Config file not found: {config_path}")
|
51
|
+
raise
|
52
|
+
except json.JSONDecodeError:
|
53
|
+
logging.error(f"Invalid JSON in config file: {config_path}")
|
54
|
+
raise
|
55
|
+
|
56
|
+
|
57
|
+
config = load_config()
|
58
|
+
|
59
|
+
|
60
|
+
def get_iam_credential_report(credential_report) -> list:
|
61
|
+
first_iam_user_row_index = config["credential_report"]["first_iam_user_row_index"]
|
62
|
+
if len(credential_report) < first_iam_user_row_index + 1:
|
63
|
+
return []
|
64
|
+
else:
|
65
|
+
return list(map(lambda x: x.split(","), credential_report[first_iam_user_row_index:]))
|
66
|
+
|
67
|
+
|
68
|
+
def get_root_credential_report(credential_report) -> list:
|
69
|
+
root_row_index = config["credential_report"]["root_row_index"]
|
70
|
+
return credential_report[root_row_index].split(",")
|
71
|
+
|
72
|
+
|
73
|
+
def generate_credential_report(client) -> list:
|
74
|
+
try:
|
75
|
+
for trial in range(1, 5):
|
76
|
+
response = client.generate_credential_report()
|
77
|
+
logging.info(
|
78
|
+
f"Generating credentials report for your account...({str(trial)}) Current state is {str(response['State'])}."
|
79
|
+
)
|
80
|
+
|
81
|
+
if response["State"] == "COMPLETE":
|
82
|
+
return client.get_credential_report()["Content"].decode("ascii").split()
|
83
|
+
|
84
|
+
time.sleep(5)
|
85
|
+
|
86
|
+
raise TimeoutError("Time out")
|
87
|
+
|
88
|
+
except (TimeoutError, botocore.exceptions.ClientError) as e:
|
89
|
+
logging.error(f"Couldn't generate a credentials report for your account. (Error : {str(e)})")
|
90
|
+
return []
|
91
|
+
|
92
|
+
|
93
|
+
def get_account_id(client) -> str:
|
94
|
+
try:
|
95
|
+
return str(client.get_caller_identity()["Account"])
|
96
|
+
except botocore.exceptions.ClientError as e:
|
97
|
+
logging.error(f"Error getting account ID: {str(e)}")
|
98
|
+
return "ERR"
|
99
|
+
|
100
|
+
|
101
|
+
def get_opted_in_regions(ec2_client) -> list:
|
102
|
+
try:
|
103
|
+
response = ec2_client.describe_regions(
|
104
|
+
Filters=[{"Name": "opt-in-status", "Values": ["opted-in", "opt-in-not-required"]}]
|
105
|
+
)
|
106
|
+
return [region["RegionName"] for region in response["Regions"]]
|
107
|
+
except botocore.exceptions.ClientError as e:
|
108
|
+
logging.error(f"Error getting opted-in regions: {str(e)}")
|
109
|
+
raise
|
@@ -0,0 +1,44 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class CREDENTIAL_REPORT_COLS(Enum):
|
5
|
+
USER = 0
|
6
|
+
ARN = 1
|
7
|
+
USER_CREATION_TIME = 2
|
8
|
+
PASSWORD_ENABLED = 3
|
9
|
+
PASSWORD_LAST_USED = 4
|
10
|
+
PASSWORD_LAST_CHANGED = 5
|
11
|
+
PASSWORD_NEXT_ROTATION = 6
|
12
|
+
MFA_ACTIVE = 7
|
13
|
+
ACCESS_KEY_1_ACTIVE = 8
|
14
|
+
ACCESS_KEY_1_LAST_ROTATED = 9
|
15
|
+
ACCESS_KEY_1_LAST_USED_DATE = 10
|
16
|
+
ACCESS_KEY_1_LAST_USED_REGION = 11
|
17
|
+
ACCESS_KEY_1_LAST_USED_SERVICE = 12
|
18
|
+
ACCESS_KEY_2_ACTIVE = 13
|
19
|
+
ACCESS_KEY_2_LAST_ROTATED = 14
|
20
|
+
ACCESS_KEY_2_LAST_USED_DATE = 15
|
21
|
+
ACCESS_KEY_2_LAST_USED_REGION = 16
|
22
|
+
ACCESS_KEY_2_LAST_USED_SERVICE = 17
|
23
|
+
CERT_1_ACTIVE = 18
|
24
|
+
CERT_1_LAST_ROTATED = 19
|
25
|
+
CERT_2_ACTIVE = 20
|
26
|
+
CERT_2_LAST_ROTATED = 21
|
27
|
+
|
28
|
+
|
29
|
+
class CHECKLIST_INDEX_CONST(Enum):
|
30
|
+
ROOT_MFA_SETTING_CHECK = 0
|
31
|
+
ROOT_USAGE_CHECK = 1
|
32
|
+
ROOT_CREDENTIAL_CHECK = 2
|
33
|
+
IAM_MFA_SETTING_CHECK = 3
|
34
|
+
IAM_PASSWORD_POLICY_CHECK = 4
|
35
|
+
NON_GROUP_POLICY_CHECK = 5
|
36
|
+
ALTERNATE_CONTACT_CHECK = 6
|
37
|
+
TRAIL_ENABLED_CHECK = 7
|
38
|
+
MULTIREGION_TRAIL_CHECK = 8
|
39
|
+
ACCOUNT_LEVEL_BUCKET_PUBLIC_ACCESS_CHECK = 9
|
40
|
+
BUCKET_LEVEL_PUBLIC_ACCESS_CHECK = 10
|
41
|
+
CLOUDWATCH_ALARM_CONFIGURATION_CHECK = 11
|
42
|
+
MULTIREGION_INSTANCE_USAGE_CHECK = 12
|
43
|
+
GUARD_DUTY_ENABLED_CHECK = 13
|
44
|
+
TRUST_ADVISOR_CHECK = 14
|