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.
Files changed (56) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/aws/__init__.py +58 -0
  3. runbooks/aws/dynamodb_operations.py +231 -0
  4. runbooks/aws/ec2_copy_image_cross-region.py +195 -0
  5. runbooks/aws/ec2_describe_instances.py +202 -0
  6. runbooks/aws/ec2_ebs_snapshots_delete.py +186 -0
  7. runbooks/aws/ec2_run_instances.py +207 -0
  8. runbooks/aws/ec2_start_stop_instances.py +199 -0
  9. runbooks/aws/ec2_terminate_instances.py +143 -0
  10. runbooks/aws/ec2_unused_eips.py +196 -0
  11. runbooks/aws/ec2_unused_volumes.py +184 -0
  12. runbooks/aws/s3_create_bucket.py +140 -0
  13. runbooks/aws/s3_list_buckets.py +152 -0
  14. runbooks/aws/s3_list_objects.py +151 -0
  15. runbooks/aws/s3_object_operations.py +183 -0
  16. runbooks/aws/tagging_lambda_handler.py +172 -0
  17. runbooks/python101/calculator.py +34 -0
  18. runbooks/python101/config.py +1 -0
  19. runbooks/python101/exceptions.py +16 -0
  20. runbooks/python101/file_manager.py +218 -0
  21. runbooks/python101/toolkit.py +153 -0
  22. runbooks/security_baseline/__init__.py +0 -0
  23. runbooks/security_baseline/checklist/__init__.py +17 -0
  24. runbooks/security_baseline/checklist/account_level_bucket_public_access.py +86 -0
  25. runbooks/security_baseline/checklist/alternate_contacts.py +65 -0
  26. runbooks/security_baseline/checklist/bucket_public_access.py +82 -0
  27. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +66 -0
  28. runbooks/security_baseline/checklist/direct_attached_policy.py +69 -0
  29. runbooks/security_baseline/checklist/guardduty_enabled.py +71 -0
  30. runbooks/security_baseline/checklist/iam_password_policy.py +43 -0
  31. runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
  32. runbooks/security_baseline/checklist/multi_region_instance_usage.py +55 -0
  33. runbooks/security_baseline/checklist/multi_region_trail.py +64 -0
  34. runbooks/security_baseline/checklist/root_access_key.py +72 -0
  35. runbooks/security_baseline/checklist/root_mfa.py +39 -0
  36. runbooks/security_baseline/checklist/root_usage.py +128 -0
  37. runbooks/security_baseline/checklist/trail_enabled.py +68 -0
  38. runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
  39. runbooks/security_baseline/report_generator.py +149 -0
  40. runbooks/security_baseline/run_script.py +76 -0
  41. runbooks/security_baseline/security_baseline_tester.py +179 -0
  42. runbooks/security_baseline/utils/__init__.py +1 -0
  43. runbooks/security_baseline/utils/common.py +109 -0
  44. runbooks/security_baseline/utils/enums.py +44 -0
  45. runbooks/security_baseline/utils/language.py +762 -0
  46. runbooks/security_baseline/utils/level_const.py +5 -0
  47. runbooks/security_baseline/utils/permission_list.py +26 -0
  48. runbooks/utils/__init__.py +0 -0
  49. runbooks/utils/logger.py +36 -0
  50. {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/METADATA +9 -1
  51. runbooks-0.1.8.dist-info/RECORD +54 -0
  52. runbooks-0.1.8.dist-info/entry_points.txt +3 -0
  53. runbooks-0.1.6.dist-info/RECORD +0 -6
  54. runbooks-0.1.6.dist-info/entry_points.txt +0 -2
  55. {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/WHEEL +0 -0
  56. {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