runbooks 0.1.7__py3-none-any.whl → 0.1.9__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 (55) 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 +18 -0
  24. runbooks/security_baseline/checklist/account_level_bucket_public_access.py +87 -0
  25. runbooks/security_baseline/checklist/alternate_contacts.py +66 -0
  26. runbooks/security_baseline/checklist/bucket_public_access.py +83 -0
  27. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +67 -0
  28. runbooks/security_baseline/checklist/direct_attached_policy.py +70 -0
  29. runbooks/security_baseline/checklist/guardduty_enabled.py +72 -0
  30. runbooks/security_baseline/checklist/iam_password_policy.py +44 -0
  31. runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
  32. runbooks/security_baseline/checklist/multi_region_instance_usage.py +56 -0
  33. runbooks/security_baseline/checklist/multi_region_trail.py +65 -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 +69 -0
  38. runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
  39. runbooks/security_baseline/report_generator.py +150 -0
  40. runbooks/security_baseline/run_script.py +76 -0
  41. runbooks/security_baseline/security_baseline_tester.py +184 -0
  42. runbooks/security_baseline/utils/__init__.py +2 -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.7.dist-info → runbooks-0.1.9.dist-info}/METADATA +2 -2
  51. runbooks-0.1.9.dist-info/RECORD +54 -0
  52. runbooks-0.1.7.dist-info/RECORD +0 -6
  53. {runbooks-0.1.7.dist-info → runbooks-0.1.9.dist-info}/WHEEL +0 -0
  54. {runbooks-0.1.7.dist-info → runbooks-0.1.9.dist-info}/entry_points.txt +0 -0
  55. {runbooks-0.1.7.dist-info → runbooks-0.1.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,69 @@
1
+ import logging
2
+
3
+ import botocore.exceptions
4
+
5
+ from ..utils import common
6
+ from ..utils import level_const as level
7
+
8
+
9
+ def get_trail_status(client, trail_arn):
10
+ try:
11
+ return client.get_trail_status(Name=trail_arn)["IsLogging"]
12
+ except botocore.exceptions.ClientError as e:
13
+ logging.error(f"Error getting trail status for {trail_arn}: {str(e)}", exc_info=True)
14
+ return "ERR"
15
+
16
+
17
+ def check_trail_enabled(session, translator) -> common.CheckResult:
18
+ logging.info(translator.translate("checking"))
19
+
20
+ ret = common.CheckResult()
21
+ ret.title = translator.translate("title")
22
+ ret.result_cols = ["Trail", "Is Logging"]
23
+
24
+ client = session.client("cloudtrail")
25
+
26
+ try:
27
+ trails = client.describe_trails()["trailList"]
28
+ except (
29
+ client.exceptions.UnsupportedOperationException,
30
+ client.exceptions.OperationNotPermittedException,
31
+ client.exceptions.NoManagementAccountSLRExistsException,
32
+ botocore.exceptions.ClientError,
33
+ ) as e:
34
+ logging.error(f"Error describing trails: {str(e)}", exc_info=True)
35
+ ret.level = level.error
36
+ ret.msg = translator.translate("describe_trails_error")
37
+ ret.result_rows.append(["ERR", "ERR"])
38
+ ret.error_message = str(e)
39
+ return ret
40
+
41
+ if not trails:
42
+ ret.level = level.danger
43
+ ret.msg = translator.translate("no_trail")
44
+ ret.result_rows.append(["-", "-"])
45
+ return ret
46
+
47
+ logging_disabled_counter = 0
48
+ for trail in trails:
49
+ trail_arn = trail["TrailARN"]
50
+ is_logging = get_trail_status(client, trail_arn)
51
+ ret.result_rows.append([trail_arn, str(is_logging)])
52
+ if is_logging == "ERR":
53
+ ret.level = level.error
54
+ ret.msg = translator.translate("get_trail_status_error")
55
+ elif not is_logging:
56
+ logging_disabled_counter += 1
57
+
58
+ if ret.level != level.error:
59
+ if logging_disabled_counter == len(trails):
60
+ ret.level = level.danger
61
+ ret.msg = translator.translate("danger")
62
+ elif logging_disabled_counter > 0:
63
+ ret.level = level.warning
64
+ ret.msg = translator.translate("warning")
65
+ else:
66
+ ret.level = level.success
67
+ ret.msg = translator.translate("success")
68
+
69
+ 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,150 @@
1
+ import datetime
2
+ import os
3
+ from pathlib import Path
4
+ from string import Template
5
+
6
+ from jinja2 import Template
7
+
8
+ from runbooks.utils.logger import configure_logger
9
+
10
+ from .utils import language, level_const
11
+
12
+ ## ✅ Configure Logger
13
+ logger = configure_logger(__name__)
14
+
15
+
16
+ class ReportGenerator:
17
+ def __init__(self, account_id, results, language_code):
18
+ self.account_id = account_id
19
+ self.results = results
20
+ self.language = language_code
21
+ self.generated_at = datetime.datetime.now().strftime("(UTC) %Y-%m-%d %H:%M:%S")
22
+
23
+ def create_html_report(self):
24
+ template = self._load_template()
25
+ context = self._prepare_context()
26
+ return template.render(context)
27
+
28
+ def _load_template(self):
29
+ """
30
+ Load the appropriate HTML template based on self.language.
31
+
32
+ Supported languages: KR, JP, VN, EN (default).
33
+ Falls back to English template if self.language is unrecognized.
34
+ """
35
+ ## Normalize user input like "en", "En", "EN" -> "EN"
36
+ lang = self.language.upper() if self.language else "EN"
37
+
38
+ ## Map language codes to template filenames
39
+ template_map = {
40
+ "KR": "report_template_kr.html",
41
+ "JP": "report_template_jp.html",
42
+ "VN": "report_template_vn.html",
43
+ "EN": "report_template_en.html",
44
+ }
45
+
46
+ ## Fall back to English if language code not recognized
47
+ template_filename = template_map.get(lang, "report_template_en.html")
48
+
49
+ # Always resolve paths relative to this file’s directory
50
+ script_dir = Path(__file__).resolve().parent
51
+ template_path = script_dir / template_filename
52
+
53
+ ## Attempt to read the template file
54
+ if not template_path.is_file():
55
+ logger.error("Template file '%s' for language '%s' not found at %s", template_filename, lang, template_path)
56
+ raise FileNotFoundError(f"Could not find the template '{template_filename}' for language '{lang}'.")
57
+
58
+ with template_path.open("r", encoding="utf-8") as file:
59
+ return Template(file.read())
60
+
61
+ # if self.language == "KR":
62
+ # with open("report_template_kr.html", "r") as file:
63
+ # return Template(file.read())
64
+ # elif self.language == "JP":
65
+ # with open("report_template_jp.html", "r") as file:
66
+ # return Template(file.read())
67
+ # elif self.language == "VN":
68
+ # with open("report_template_vn.html", "r") as file:
69
+ # return Template(file.read())
70
+ # else:
71
+ # ## Get the absolute directory where *this script* is located
72
+ # report_dir = os.path.dirname(os.path.abspath(__file__))
73
+ # report_path = os.path.join(report_dir, "report_template_en.html")
74
+ # ## with open("report_template_en.html", "r") as file:
75
+ # with open(report_path, "r") as file:
76
+ # return Template(file.read())
77
+
78
+ def _prepare_context(self):
79
+ return {
80
+ "account_id": self.account_id,
81
+ "generated_at": self.generated_at,
82
+ "overview": self._generate_overview(),
83
+ "result_sections": self._generate_result_sections(),
84
+ "language": self.language,
85
+ }
86
+
87
+ def _generate_overview(self):
88
+ filtered_results = {level: len(results) for level, results in self.results.items() if isinstance(results, list)}
89
+ desired_levels = ["Danger", "Warning", "Success", "Info", "Error"]
90
+ sorted_result = [(level, filtered_results[level]) for level in desired_levels]
91
+
92
+ return sorted_result
93
+
94
+ def _generate_result_sections(self):
95
+ sections = []
96
+ for level, results in self.results.items():
97
+ if isinstance(results, list):
98
+ if len(results) == 0:
99
+ ## This condition only applies when there are no 'Error' items at all.
100
+ formatted_results = [
101
+ {
102
+ "title": "All inspection items were successfully checked.",
103
+ "message": "No results available for this section. All checks were successful.",
104
+ "table": [],
105
+ }
106
+ ]
107
+ else:
108
+ formatted_results = self._format_results(results, level)
109
+
110
+ sections.append({"level": level, "result_items": formatted_results})
111
+
112
+ sort_order = {"Danger": 0, "Warning": 1, "Success": 2, "Info": 3, "Error": 4}
113
+
114
+ sections.sort(key=lambda x: sort_order.get(x["level"], len(sort_order)))
115
+ return sections
116
+
117
+ def _format_results(self, results, level):
118
+ formatted_results = []
119
+ for result in results:
120
+ if isinstance(result, dict): # if type(result) is dict
121
+ formatted_result = {
122
+ "title": result.get("title", "Unknown"),
123
+ "message": result.get("msg", "No message"),
124
+ "table": self._format_table(result.get("result_cols", []), result.get("result_rows", [])),
125
+ }
126
+ if level == level_const.error:
127
+ formatted_result["error_message"] = result.get("error_message", "Unknown error")
128
+ formatted_results.append(formatted_result)
129
+ elif hasattr(result, "to_dict"): # if the result object has 'to_dict' attribute
130
+ result_dict = result.to_dict()
131
+ formatted_result = {
132
+ "title": result_dict.get("title", "Unknown"),
133
+ "message": result_dict.get("msg", "No message"),
134
+ "table": self._format_table(result_dict.get("result_cols", []), result_dict.get("result_rows", [])),
135
+ }
136
+ if level == level_const.error:
137
+ formatted_result["error_message"] = result_dict.get("error_message", "Unknown error")
138
+ formatted_results.append(formatted_result)
139
+
140
+ return formatted_results
141
+
142
+ def _format_table(self, cols, rows):
143
+ if not rows:
144
+ return None
145
+ return {"headers": cols, "rows": rows}
146
+
147
+
148
+ def generate_html_report(account_id_str, result_sort_by_level, language_code):
149
+ generator = ReportGenerator(account_id_str, result_sort_by_level, language_code)
150
+ 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 runbooks.utils.logger import configure_logger
19
+
20
+ from .security_baseline_tester import SecurityBaselineTester
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,184 @@
1
+ import datetime
2
+ import importlib
3
+ import json
4
+ import logging
5
+ import os
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+ from pathlib import Path
8
+
9
+ import boto3
10
+ import botocore
11
+
12
+ from . import report_generator
13
+ from .checklist import * # noqa: F403
14
+ from .utils import common, language, level_const
15
+
16
+ # from .utils.language import get_translator
17
+
18
+
19
+ class SecurityBaselineTester:
20
+ def __init__(self, profile, lang_code):
21
+ self.profile = profile
22
+ self.language = lang_code
23
+ self.session = self._create_session()
24
+ self.config = self._load_config()
25
+ ## Call module 'language' and pass the string 'lang_code'
26
+ self.translator = language.get_translator("main", lang_code)
27
+
28
+ def _create_session(self):
29
+ if self.profile == "default":
30
+ return boto3.Session()
31
+ return boto3.Session(profile_name=self.profile)
32
+
33
+ def _load_config(self):
34
+ ## Get the absolute directory where *this script* is located
35
+ script_dir = os.path.dirname(os.path.abspath(__file__))
36
+ config_path = os.path.join(script_dir, "config.json")
37
+
38
+ try:
39
+ # with open("./config.json", "r") as file:
40
+ with open(config_path, "r") as file:
41
+ return json.load(file)
42
+ except FileNotFoundError:
43
+ logging.error("config.json file not found. Please ensure it exists in the same directory as this script.")
44
+ raise
45
+ except json.JSONDecodeError:
46
+ logging.error("Error parsing config.json. Please ensure it is a valid JSON file.")
47
+ raise
48
+
49
+ def run(self):
50
+ try:
51
+ self._validate_session()
52
+ caller_identity = self._get_caller_identity()
53
+ self._print_auditor_info(caller_identity)
54
+
55
+ logging.info(self.translator.translate("start_test"))
56
+
57
+ account_id, results = self._execute_tests()
58
+ self._generate_report(account_id, results)
59
+
60
+ logging.info(self.translator.translate("test_completed"))
61
+ except Exception as e:
62
+ logging.error(f"An error occurred during the security baseline test: {str(e)}", exc_info=True)
63
+
64
+ def _validate_session(self):
65
+ if self.session.region_name is None:
66
+ raise ValueError('AWS region is not specified. Run "aws configure" to set it.')
67
+
68
+ def _get_caller_identity(self):
69
+ try:
70
+ return self.session.client("sts").get_caller_identity()
71
+ except botocore.exceptions.ClientError as e:
72
+ logging.error(f"Failed to get caller identity: {str(e)}")
73
+ raise
74
+
75
+ def _print_auditor_info(self, caller_identity):
76
+ logging.info("==================== AUDITOR INFO ====================")
77
+ logging.info(f"USER ID : {caller_identity['UserId']}")
78
+ logging.info(f"ACCOUNT : {caller_identity['Account']}")
79
+ logging.info(f"ARN : {caller_identity['Arn']}")
80
+ logging.info("=====================================================")
81
+
82
+ def _execute_tests(self):
83
+ iam_client = self.session.client("iam")
84
+ sts_client = self.session.client("sts")
85
+
86
+ account_id = common.get_account_id(sts_client)
87
+ logging.info(self.translator.translate("request_credential_report"))
88
+ credential_report = common.generate_credential_report(iam_client)
89
+
90
+ with ThreadPoolExecutor(max_workers=self.config.get("max_workers", 5)) as executor:
91
+ futures = {
92
+ executor.submit(self._run_check, check_name, credential_report): check_name
93
+ for check_name in self.config.get("checks", [])
94
+ }
95
+
96
+ results = {
97
+ level: [] for level in ["Success", "Warning", "Danger", "Error", "Info"] if isinstance(level, str)
98
+ }
99
+ for future in as_completed(futures):
100
+ result = future.result()
101
+ results[result.level].append(result)
102
+
103
+ return account_id, results
104
+
105
+ def _run_check(self, check_name, credential_report):
106
+ # check_module = __import__(f"checklist.{check_name}", fromlist=[check_name])
107
+ check_module = importlib.import_module(f"runbooks.security_baseline.checklist.{check_name}")
108
+ check_method = getattr(check_module, self.config["checks"][check_name])
109
+ translator = language.get_translator(check_name, self.language)
110
+
111
+ if check_name in [
112
+ "alternate_contacts",
113
+ "account_level_bucket_public_access",
114
+ "bucket_public_access",
115
+ "cloudwatch_alarm_configuration",
116
+ "direct_attached_policy",
117
+ "guardduty_enabled",
118
+ "multi_region_instance_usage",
119
+ "multi_region_trail",
120
+ "trail_enabled",
121
+ "iam_password_policy",
122
+ ]:
123
+ return check_method(self.session, translator)
124
+ elif check_name in ["root_mfa", "root_usage", "root_access_key", "iam_user_mfa"]:
125
+ return check_method(self.session, translator, credential_report)
126
+ elif check_name == "trusted_advisor":
127
+ return check_method(translator)
128
+ else:
129
+ raise ValueError(f"Unknown check method: {check_name}")
130
+
131
+ def _check_result_directory(self):
132
+ """
133
+ Ensures that the 'results' directory (located next to this script) exists.
134
+
135
+ :return: A Path object pointing to the results directory.
136
+ """
137
+
138
+ # directory_name = "./results"
139
+ # if not os.path.exists(directory_name):
140
+ # os.makedirs(directory_name)
141
+ # logging.info(self.translator.translate("results_folder_created"))
142
+ # else:
143
+ # logging.info(self.translator.translate("results_folder_already_exists"))
144
+
145
+ script_dir = Path(__file__).resolve().parent
146
+ results_dir = script_dir / "results"
147
+
148
+ if not results_dir.exists():
149
+ results_dir.mkdir(parents=True, exist_ok=True)
150
+ logging.info(self.translator.translate("results_folder_created"))
151
+ else:
152
+ logging.info(self.translator.translate("results_folder_already_exists"))
153
+
154
+ return results_dir
155
+
156
+ def _generate_report(self, account_id, results):
157
+ """
158
+ Generates an HTML security report and writes it to the 'results' directory.
159
+
160
+ :param account_id: The AWS account ID or similar identifier.
161
+ :param results: A dictionary containing the security baseline results.
162
+ """
163
+ html_report = report_generator.generate_html_report(account_id, results, self.language)
164
+
165
+ current_time = datetime.datetime.now().strftime("%y%m%d-%H%M%S")
166
+ short_account_id = account_id[-4:]
167
+
168
+ ## Ensure the results directory exists
169
+ results_dir = self._check_result_directory()
170
+
171
+ ## Build the report filename
172
+ report_filename = f"security-report-{short_account_id}-{current_time}.html"
173
+ report_path = results_dir / report_filename
174
+
175
+ ## Get the absolute directory where *this script* is located
176
+ # test_report_dir = os.path.dirname(os.path.abspath(__file__))
177
+ # test_report_path = os.path.join(test_report_dir, report_filename)
178
+
179
+ # with open(test_report_path, "w") as file:
180
+ ## Write the report to disk
181
+ with report_path.open("w", encoding="utf-8") as file:
182
+ file.write(html_report)
183
+
184
+ logging.info(self.translator.translate("generate_result_report"))
@@ -0,0 +1,2 @@
1
+ ## security_baseline/utils/__init__.py
2
+ __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