sraverify 0.1.0__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.
- sraverify/__init__.py +36 -0
- sraverify/checks/__init__.py +56 -0
- sraverify/checks/accessanalyzer/SRA_IAA_1.py +188 -0
- sraverify/checks/accessanalyzer/SRA_IAA_2.py +162 -0
- sraverify/checks/accessanalyzer/SRA_IAA_3.py +260 -0
- sraverify/checks/accessanalyzer/SRA_IAA_4.py +207 -0
- sraverify/checks/accessanalyzer/__init__.py +3 -0
- sraverify/checks/cloudtrail/SRA-CT-1.py +220 -0
- sraverify/checks/cloudtrail/SRA-CT-10.py +229 -0
- sraverify/checks/cloudtrail/SRA-CT-11.py +242 -0
- sraverify/checks/cloudtrail/SRA-CT-12.py +163 -0
- sraverify/checks/cloudtrail/SRA-CT-13.py +279 -0
- sraverify/checks/cloudtrail/SRA-CT-2.py +218 -0
- sraverify/checks/cloudtrail/SRA-CT-3.py +196 -0
- sraverify/checks/cloudtrail/SRA-CT-4.py +161 -0
- sraverify/checks/cloudtrail/SRA-CT-5.py +200 -0
- sraverify/checks/cloudtrail/SRA-CT-6.py +161 -0
- sraverify/checks/cloudtrail/SRA-CT-7.py +194 -0
- sraverify/checks/cloudtrail/SRA-CT-8.py +226 -0
- sraverify/checks/cloudtrail/SRA-CT-9.py +226 -0
- sraverify/checks/cloudtrail/__init__.py +3 -0
- sraverify/checks/config/SRA-CONFIG-1.py +197 -0
- sraverify/checks/config/__init__.py +3 -0
- sraverify/core/__init__.py +3 -0
- sraverify/core/check.py +227 -0
- sraverify/core/logging.py +37 -0
- sraverify/core/session.py +47 -0
- sraverify/lib/__init__.py +4 -0
- sraverify/lib/audit_info.py +37 -0
- sraverify/lib/banner.py +42 -0
- sraverify/lib/check_loader.py +80 -0
- sraverify/lib/org_mgmt_checker.py +86 -0
- sraverify/lib/outputs.py +46 -0
- sraverify/lib/progress.py +75 -0
- sraverify/lib/regions.py +27 -0
- sraverify/lib/session.py +23 -0
- sraverify/main.py +350 -0
- sraverify/services/__init__.py +3 -0
- sraverify/services/accessanalyzer/__init__.py +15 -0
- sraverify/services/accessanalyzer/base.py +123 -0
- sraverify/services/accessanalyzer/checks/__init__.py +3 -0
- sraverify/services/accessanalyzer/checks/sra_accessanalyzer_01.py +82 -0
- sraverify/services/accessanalyzer/checks/sra_accessanalyzer_02.py +82 -0
- sraverify/services/accessanalyzer/checks/sra_accessanalyzer_03.py +103 -0
- sraverify/services/accessanalyzer/checks/sra_accessanalyzer_04.py +139 -0
- sraverify/services/accessanalyzer/client.py +123 -0
- sraverify/services/account/__init__.py +9 -0
- sraverify/services/account/base.py +56 -0
- sraverify/services/account/checks/__init__.py +1 -0
- sraverify/services/account/checks/sra_account_01.py +65 -0
- sraverify/services/account/checks/sra_account_02.py +63 -0
- sraverify/services/account/checks/sra_account_03.py +63 -0
- sraverify/services/account/client.py +51 -0
- sraverify/services/auditmanager/__init__.py +10 -0
- sraverify/services/auditmanager/base.py +72 -0
- sraverify/services/auditmanager/checks/__init__.py +1 -0
- sraverify/services/auditmanager/checks/sra_auditmanager_01.py +58 -0
- sraverify/services/auditmanager/checks/sra_auditmanager_02.py +80 -0
- sraverify/services/auditmanager/client.py +58 -0
- sraverify/services/cloudtrail/__init__.py +33 -0
- sraverify/services/cloudtrail/base.py +167 -0
- sraverify/services/cloudtrail/checks/__init__.py +1 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_01.py +83 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_02.py +99 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_03.py +94 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_04.py +92 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_05.py +106 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_06.py +93 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_07.py +96 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_08.py +145 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_09.py +167 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_10.py +162 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_11.py +178 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_12.py +77 -0
- sraverify/services/cloudtrail/checks/sra_cloudtrail_13.py +120 -0
- sraverify/services/cloudtrail/client.py +118 -0
- sraverify/services/config/__init__.py +25 -0
- sraverify/services/config/base.py +249 -0
- sraverify/services/config/checks/__init__.py +1 -0
- sraverify/services/config/checks/sra_config_01.py +123 -0
- sraverify/services/config/checks/sra_config_02.py +156 -0
- sraverify/services/config/checks/sra_config_03.py +149 -0
- sraverify/services/config/checks/sra_config_04.py +104 -0
- sraverify/services/config/checks/sra_config_05.py +104 -0
- sraverify/services/config/checks/sra_config_06.py +194 -0
- sraverify/services/config/checks/sra_config_07.py +162 -0
- sraverify/services/config/checks/sra_config_08.py +185 -0
- sraverify/services/config/checks/sra_config_09.py +177 -0
- sraverify/services/config/client.py +264 -0
- sraverify/services/ec2/__init__.py +8 -0
- sraverify/services/ec2/base.py +75 -0
- sraverify/services/ec2/checks/__init__.py +1 -0
- sraverify/services/ec2/checks/sra_ec2_01.py +83 -0
- sraverify/services/ec2/client.py +63 -0
- sraverify/services/firewallmanager/__init__.py +23 -0
- sraverify/services/firewallmanager/base.py +48 -0
- sraverify/services/firewallmanager/checks/__init__.py +1 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_01.py +75 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_02.py +57 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_03.py +51 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_04.py +51 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_05.py +51 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_06.py +51 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_07.py +51 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_08.py +61 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_09.py +61 -0
- sraverify/services/firewallmanager/checks/sra_firewallmanager_10.py +71 -0
- sraverify/services/firewallmanager/client.py +40 -0
- sraverify/services/guardduty/__init__.py +58 -0
- sraverify/services/guardduty/base.py +207 -0
- sraverify/services/guardduty/checks/__init__.py +3 -0
- sraverify/services/guardduty/checks/sra_guardduty_01.py +51 -0
- sraverify/services/guardduty/checks/sra_guardduty_02.py +80 -0
- sraverify/services/guardduty/checks/sra_guardduty_03.py +77 -0
- sraverify/services/guardduty/checks/sra_guardduty_04.py +84 -0
- sraverify/services/guardduty/checks/sra_guardduty_05.py +84 -0
- sraverify/services/guardduty/checks/sra_guardduty_06.py +84 -0
- sraverify/services/guardduty/checks/sra_guardduty_07.py +85 -0
- sraverify/services/guardduty/checks/sra_guardduty_08.py +83 -0
- sraverify/services/guardduty/checks/sra_guardduty_09.py +84 -0
- sraverify/services/guardduty/checks/sra_guardduty_10.py +83 -0
- sraverify/services/guardduty/checks/sra_guardduty_11.py +93 -0
- sraverify/services/guardduty/checks/sra_guardduty_12.py +83 -0
- sraverify/services/guardduty/checks/sra_guardduty_13.py +90 -0
- sraverify/services/guardduty/checks/sra_guardduty_14.py +136 -0
- sraverify/services/guardduty/checks/sra_guardduty_15.py +94 -0
- sraverify/services/guardduty/checks/sra_guardduty_16.py +94 -0
- sraverify/services/guardduty/checks/sra_guardduty_17.py +91 -0
- sraverify/services/guardduty/checks/sra_guardduty_18.py +91 -0
- sraverify/services/guardduty/checks/sra_guardduty_19.py +91 -0
- sraverify/services/guardduty/checks/sra_guardduty_20.py +111 -0
- sraverify/services/guardduty/checks/sra_guardduty_21.py +112 -0
- sraverify/services/guardduty/checks/sra_guardduty_22.py +111 -0
- sraverify/services/guardduty/checks/sra_guardduty_23.py +154 -0
- sraverify/services/guardduty/checks/sra_guardduty_24.py +111 -0
- sraverify/services/guardduty/checks/sra_guardduty_25.py +111 -0
- sraverify/services/guardduty/client.py +107 -0
- sraverify/services/inspector/__init__.py +29 -0
- sraverify/services/inspector/base.py +233 -0
- sraverify/services/inspector/checks/__init__.py +3 -0
- sraverify/services/inspector/checks/sra_inspector_01.py +69 -0
- sraverify/services/inspector/checks/sra_inspector_02.py +68 -0
- sraverify/services/inspector/checks/sra_inspector_03.py +68 -0
- sraverify/services/inspector/checks/sra_inspector_04.py +70 -0
- sraverify/services/inspector/checks/sra_inspector_05.py +69 -0
- sraverify/services/inspector/checks/sra_inspector_06.py +115 -0
- sraverify/services/inspector/checks/sra_inspector_07.py +109 -0
- sraverify/services/inspector/checks/sra_inspector_08.py +69 -0
- sraverify/services/inspector/checks/sra_inspector_09.py +69 -0
- sraverify/services/inspector/checks/sra_inspector_10.py +69 -0
- sraverify/services/inspector/checks/sra_inspector_11.py +69 -0
- sraverify/services/inspector/client.py +99 -0
- sraverify/services/macie/__init__.py +27 -0
- sraverify/services/macie/base.py +271 -0
- sraverify/services/macie/checks/__init__.py +1 -0
- sraverify/services/macie/checks/sra_macie_01.py +100 -0
- sraverify/services/macie/checks/sra_macie_02.py +102 -0
- sraverify/services/macie/checks/sra_macie_03.py +152 -0
- sraverify/services/macie/checks/sra_macie_04.py +120 -0
- sraverify/services/macie/checks/sra_macie_05.py +85 -0
- sraverify/services/macie/checks/sra_macie_06.py +124 -0
- sraverify/services/macie/checks/sra_macie_07.py +138 -0
- sraverify/services/macie/checks/sra_macie_08.py +82 -0
- sraverify/services/macie/checks/sra_macie_09.py +103 -0
- sraverify/services/macie/checks/sra_macie_10.py +81 -0
- sraverify/services/macie/client.py +220 -0
- sraverify/services/s3/__init__.py +16 -0
- sraverify/services/s3/base.py +69 -0
- sraverify/services/s3/checks/__init__.py +1 -0
- sraverify/services/s3/checks/sra_s3_01.py +89 -0
- sraverify/services/s3/checks/sra_s3_02.py +89 -0
- sraverify/services/s3/checks/sra_s3_03.py +88 -0
- sraverify/services/s3/checks/sra_s3_04.py +88 -0
- sraverify/services/s3/client.py +52 -0
- sraverify/services/securityhub/__init__.py +27 -0
- sraverify/services/securityhub/base.py +349 -0
- sraverify/services/securityhub/checks/__init__.py +1 -0
- sraverify/services/securityhub/checks/sra_securityhub_01.py +115 -0
- sraverify/services/securityhub/checks/sra_securityhub_02.py +114 -0
- sraverify/services/securityhub/checks/sra_securityhub_03.py +136 -0
- sraverify/services/securityhub/checks/sra_securityhub_04.py +75 -0
- sraverify/services/securityhub/checks/sra_securityhub_05.py +102 -0
- sraverify/services/securityhub/checks/sra_securityhub_06.py +113 -0
- sraverify/services/securityhub/checks/sra_securityhub_07.py +121 -0
- sraverify/services/securityhub/checks/sra_securityhub_08.py +113 -0
- sraverify/services/securityhub/checks/sra_securityhub_09.py +100 -0
- sraverify/services/securityhub/checks/sra_securityhub_10.py +94 -0
- sraverify/services/securityhub/checks/sra_securityhub_11.py +73 -0
- sraverify/services/securityhub/client.py +249 -0
- sraverify/services/securityincidentresponse/__init__.py +13 -0
- sraverify/services/securityincidentresponse/base.py +95 -0
- sraverify/services/securityincidentresponse/checks/__init__.py +1 -0
- sraverify/services/securityincidentresponse/checks/sra_securityincidentresponse_01.py +77 -0
- sraverify/services/securityincidentresponse/checks/sra_securityincidentresponse_02.py +72 -0
- sraverify/services/securityincidentresponse/checks/sra_securityincidentresponse_03.py +86 -0
- sraverify/services/securityincidentresponse/checks/sra_securityincidentresponse_04.py +117 -0
- sraverify/services/securityincidentresponse/checks/sra_securityincidentresponse_05.py +55 -0
- sraverify/services/securityincidentresponse/client.py +71 -0
- sraverify/services/securitylake/__init__.py +39 -0
- sraverify/services/securitylake/base.py +461 -0
- sraverify/services/securitylake/checks/__init__.py +1 -0
- sraverify/services/securitylake/checks/sra_securitylake_01.py +98 -0
- sraverify/services/securitylake/checks/sra_securitylake_02.py +133 -0
- sraverify/services/securitylake/checks/sra_securitylake_03.py +116 -0
- sraverify/services/securitylake/checks/sra_securitylake_04.py +72 -0
- sraverify/services/securitylake/checks/sra_securitylake_05.py +116 -0
- sraverify/services/securitylake/checks/sra_securitylake_06.py +104 -0
- sraverify/services/securitylake/checks/sra_securitylake_07.py +108 -0
- sraverify/services/securitylake/checks/sra_securitylake_08.py +107 -0
- sraverify/services/securitylake/checks/sra_securitylake_09.py +107 -0
- sraverify/services/securitylake/checks/sra_securitylake_10.py +106 -0
- sraverify/services/securitylake/checks/sra_securitylake_11.py +109 -0
- sraverify/services/securitylake/checks/sra_securitylake_12.py +108 -0
- sraverify/services/securitylake/checks/sra_securitylake_13.py +108 -0
- sraverify/services/securitylake/checks/sra_securitylake_14.py +72 -0
- sraverify/services/securitylake/checks/sra_securitylake_15.py +120 -0
- sraverify/services/securitylake/checks/sra_securitylake_16.py +104 -0
- sraverify/services/securitylake/checks/sra_securitylake_17.py +103 -0
- sraverify/services/securitylake/client.py +247 -0
- sraverify/services/shield/__init__.py +33 -0
- sraverify/services/shield/base.py +199 -0
- sraverify/services/shield/checks/__init__.py +1 -0
- sraverify/services/shield/checks/sra_shield_01.py +68 -0
- sraverify/services/shield/checks/sra_shield_02.py +77 -0
- sraverify/services/shield/checks/sra_shield_03.py +84 -0
- sraverify/services/shield/checks/sra_shield_04.py +84 -0
- sraverify/services/shield/checks/sra_shield_05.py +84 -0
- sraverify/services/shield/checks/sra_shield_06.py +84 -0
- sraverify/services/shield/checks/sra_shield_07.py +84 -0
- sraverify/services/shield/checks/sra_shield_08.py +69 -0
- sraverify/services/shield/checks/sra_shield_09.py +86 -0
- sraverify/services/shield/checks/sra_shield_10.py +100 -0
- sraverify/services/shield/checks/sra_shield_11.py +71 -0
- sraverify/services/shield/checks/sra_shield_12.py +130 -0
- sraverify/services/shield/checks/sra_shield_13.py +112 -0
- sraverify/services/shield/checks/sra_shield_14.py +111 -0
- sraverify/services/shield/client.py +214 -0
- sraverify/services/waf/__init__.py +21 -0
- sraverify/services/waf/base.py +100 -0
- sraverify/services/waf/checks/__init__.py +1 -0
- sraverify/services/waf/checks/sra_waf_01.py +63 -0
- sraverify/services/waf/checks/sra_waf_02.py +82 -0
- sraverify/services/waf/checks/sra_waf_03.py +123 -0
- sraverify/services/waf/checks/sra_waf_04.py +94 -0
- sraverify/services/waf/checks/sra_waf_05.py +94 -0
- sraverify/services/waf/checks/sra_waf_06.py +91 -0
- sraverify/services/waf/checks/sra_waf_07.py +94 -0
- sraverify/services/waf/checks/sra_waf_08.py +66 -0
- sraverify/services/waf/checks/sra_waf_09.py +95 -0
- sraverify/services/waf/client.py +109 -0
- sraverify/utils/__init__.py +3 -0
- sraverify/utils/banner.py +65 -0
- sraverify/utils/outputs.py +57 -0
- sraverify/utils/progress.py +97 -0
- sraverify-0.1.0.dist-info/LICENSE +175 -0
- sraverify-0.1.0.dist-info/METADATA +516 -0
- sraverify-0.1.0.dist-info/NOTICE +1 -0
- sraverify-0.1.0.dist-info/RECORD +261 -0
- sraverify-0.1.0.dist-info/WHEEL +5 -0
- sraverify-0.1.0.dist-info/entry_points.txt +2 -0
- sraverify-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from typing import Dict, List, Any
|
|
2
|
+
from sraverify.checks import SecurityCheck
|
|
3
|
+
from botocore.exceptions import ClientError
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
class SRACT9(SecurityCheck):
|
|
8
|
+
"""SRA-CT-9: Organization Trail CloudWatch Logs Delivery Status"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, check_type="organization"):
|
|
11
|
+
"""Initialize the check with organization type"""
|
|
12
|
+
super().__init__(check_type=check_type)
|
|
13
|
+
self.check_id = "SRA-CT-9"
|
|
14
|
+
self.check_name = "Organization trail is publishing logs to CloudWatch Logs"
|
|
15
|
+
self.description = ('This check verifies that last attempt to send CloudTrail logs to CloudWatch Logs was successful. '
|
|
16
|
+
'Successful delivery of CloudTrails logs to CloudWatch ensures later availability for monitoring. '
|
|
17
|
+
'CloudTrail requires right permission to send log events to CloudWatch Logs.')
|
|
18
|
+
self.service = "CloudTrail"
|
|
19
|
+
self.severity = "HIGH"
|
|
20
|
+
self.check_type = check_type
|
|
21
|
+
self.check_logic = ('1. Verify execution from Organization Management Account | '
|
|
22
|
+
'2. List CloudTrail trails in current region | '
|
|
23
|
+
'3. Check for organization trail with IsOrganizationTrail=true | '
|
|
24
|
+
'4. Verify CloudWatch Logs configuration and successful delivery by checking: '
|
|
25
|
+
'a) CloudWatch Logs group is configured, b) IAM role is configured, '
|
|
26
|
+
'c) No delivery errors exist, d) Latest delivery was successful within 24 hours')
|
|
27
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
28
|
+
self.findings = []
|
|
29
|
+
|
|
30
|
+
def get_findings(self) -> List[Dict[str, Any]]:
|
|
31
|
+
"""Return the findings"""
|
|
32
|
+
return self.findings
|
|
33
|
+
|
|
34
|
+
def check_cloudwatch_delivery_status(self, cloudtrail_client, trail_name: str) -> tuple:
|
|
35
|
+
"""Check CloudWatch Logs delivery status and return status details"""
|
|
36
|
+
try:
|
|
37
|
+
status = cloudtrail_client.get_trail_status(Name=trail_name)
|
|
38
|
+
latest_delivery_time = status.get('LatestCloudWatchLogsDeliveryTime')
|
|
39
|
+
latest_delivery_error = status.get('LatestCloudWatchLogsDeliveryError', '')
|
|
40
|
+
is_logging = status.get('IsLogging', False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Step 1: Verify we're in management account using org_mgmt_checker
|
|
44
|
+
is_management, error_message = self.org_checker.verify_org_management()
|
|
45
|
+
if not is_management:
|
|
46
|
+
finding = {
|
|
47
|
+
'CheckId': self.check_id,
|
|
48
|
+
'Status': 'ERROR',
|
|
49
|
+
'Region': region,
|
|
50
|
+
"Severity": self.severity,
|
|
51
|
+
'Title': f"{self.check_id} {self.check_name}",
|
|
52
|
+
'Description': self.description,
|
|
53
|
+
'ResourceId': account_id,
|
|
54
|
+
'ResourceType': 'AWS::Organizations::Account',
|
|
55
|
+
'AccountId': account_id,
|
|
56
|
+
'CheckedValue': 'Management Account Access',
|
|
57
|
+
'ActualValue': error_message if error_message else 'Not running from management account',
|
|
58
|
+
'Remediation': 'Run this check from the Organization Management Account',
|
|
59
|
+
'Service': self.service,
|
|
60
|
+
'CheckLogic': self.check_logic,
|
|
61
|
+
'CheckType': self.check_type
|
|
62
|
+
}
|
|
63
|
+
self.findings.append(finding)
|
|
64
|
+
return self.findings
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Check if delivery is recent (within 24 hours)
|
|
68
|
+
current_time = datetime.now(timezone.utc)
|
|
69
|
+
is_recent = False
|
|
70
|
+
if latest_delivery_time:
|
|
71
|
+
time_difference = current_time - latest_delivery_time
|
|
72
|
+
is_recent = time_difference.total_seconds() < 86400 # 24 hours
|
|
73
|
+
|
|
74
|
+
return is_logging, is_recent, latest_delivery_error
|
|
75
|
+
|
|
76
|
+
except ClientError as e:
|
|
77
|
+
self.logger.error(f"Error getting trail status for {trail_name}: {str(e)}")
|
|
78
|
+
return False, False, str(e)
|
|
79
|
+
|
|
80
|
+
def run(self, session) -> None:
|
|
81
|
+
"""Run the security check"""
|
|
82
|
+
try:
|
|
83
|
+
# Get account information
|
|
84
|
+
sts_client = session.client('sts')
|
|
85
|
+
account_id = sts_client.get_caller_identity()['Account']
|
|
86
|
+
region = session.region_name
|
|
87
|
+
self.logger.debug(f"Running check for account: {account_id} in region: {region}")
|
|
88
|
+
|
|
89
|
+
# Initialize CloudTrail client
|
|
90
|
+
cloudtrail_client = session.client('cloudtrail')
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# List trails and find organization trails
|
|
94
|
+
trails = cloudtrail_client.describe_trails(includeShadowTrails=True)
|
|
95
|
+
org_trails = [t for t in trails['trailList'] if t.get('IsOrganizationTrail')]
|
|
96
|
+
self.logger.debug(f"Found {len(org_trails)} organization trails")
|
|
97
|
+
|
|
98
|
+
if not org_trails:
|
|
99
|
+
self.findings.append({
|
|
100
|
+
"CheckId": self.check_id,
|
|
101
|
+
"Status": "FAIL",
|
|
102
|
+
"Region": region,
|
|
103
|
+
"Severity": self.severity,
|
|
104
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
105
|
+
"Description": self.description,
|
|
106
|
+
"ResourceId": "organization-trail",
|
|
107
|
+
"ResourceType": "AWS::CloudTrail::Trail",
|
|
108
|
+
"AccountId": account_id,
|
|
109
|
+
"CheckedValue": "Organization Trail Configuration",
|
|
110
|
+
"ActualValue": "No organization trail found",
|
|
111
|
+
"Remediation": "Create an organization trail with CloudWatch Logs configuration",
|
|
112
|
+
"Service": self.service,
|
|
113
|
+
"CheckLogic": self.check_logic,
|
|
114
|
+
"CheckType": self.check_type
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Check each organization trail for CloudWatch Logs delivery status
|
|
119
|
+
valid_trail = None
|
|
120
|
+
for trail in org_trails:
|
|
121
|
+
trail_name = trail['Name']
|
|
122
|
+
cloudwatch_logs_group = trail.get('CloudWatchLogsLogGroupArn')
|
|
123
|
+
cloudwatch_logs_role = trail.get('CloudWatchLogsRoleArn')
|
|
124
|
+
|
|
125
|
+
# Skip if CloudWatch Logs is not configured
|
|
126
|
+
if not (cloudwatch_logs_group and cloudwatch_logs_role):
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
is_logging, is_recent, delivery_error = self.check_cloudwatch_delivery_status(cloudtrail_client, trail_name)
|
|
130
|
+
|
|
131
|
+
if is_logging and is_recent and not delivery_error:
|
|
132
|
+
valid_trail = trail
|
|
133
|
+
self.logger.debug(f"Found valid trail with successful CloudWatch Logs delivery: {trail_name}")
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
# Create finding based on CloudWatch Logs delivery status
|
|
137
|
+
if valid_trail:
|
|
138
|
+
self.findings.append({
|
|
139
|
+
"CheckId": self.check_id,
|
|
140
|
+
"Status": "PASS",
|
|
141
|
+
"Region": region,
|
|
142
|
+
"Severity": self.severity,
|
|
143
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
144
|
+
"Description": self.description,
|
|
145
|
+
"ResourceId": valid_trail['TrailARN'],
|
|
146
|
+
"ResourceType": "AWS::CloudTrail::Trail",
|
|
147
|
+
"AccountId": account_id,
|
|
148
|
+
"CheckedValue": "CloudWatch Logs Delivery Status",
|
|
149
|
+
"ActualValue": f"Organization trail {valid_trail['Name']} is successfully delivering logs to CloudWatch Logs group",
|
|
150
|
+
"Remediation": "None required",
|
|
151
|
+
"Service": self.service,
|
|
152
|
+
"CheckLogic": self.check_logic,
|
|
153
|
+
"CheckType": self.check_type
|
|
154
|
+
})
|
|
155
|
+
else:
|
|
156
|
+
actual_value = "No organization trail found with successful recent CloudWatch Logs delivery"
|
|
157
|
+
remediation = "Verify CloudWatch Logs configuration and permissions"
|
|
158
|
+
if org_trails:
|
|
159
|
+
trail = org_trails[0]
|
|
160
|
+
if not (trail.get('CloudWatchLogsLogGroupArn') and trail.get('CloudWatchLogsRoleArn')):
|
|
161
|
+
actual_value = f"Trail {trail['Name']} has incomplete CloudWatch Logs configuration"
|
|
162
|
+
remediation = "Configure CloudWatch Logs group and IAM role for the organization trail"
|
|
163
|
+
elif delivery_error:
|
|
164
|
+
actual_value = f"Trail {trail['Name']} has delivery error: {delivery_error}"
|
|
165
|
+
remediation = "Resolve CloudWatch Logs delivery errors (check IAM role permissions)"
|
|
166
|
+
elif not is_recent:
|
|
167
|
+
actual_value = f"Trail {trail['Name']} has no recent CloudWatch Logs delivery"
|
|
168
|
+
remediation = "Verify trail logging is enabled and check CloudWatch Logs permissions"
|
|
169
|
+
|
|
170
|
+
self.findings.append({
|
|
171
|
+
"CheckId": self.check_id,
|
|
172
|
+
"Status": "FAIL",
|
|
173
|
+
"Region": region,
|
|
174
|
+
"Severity": self.severity,
|
|
175
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
176
|
+
"Description": self.description,
|
|
177
|
+
"ResourceId": (valid_trail or org_trails[0])['TrailARN'],
|
|
178
|
+
"ResourceType": "AWS::CloudTrail::Trail",
|
|
179
|
+
"AccountId": account_id,
|
|
180
|
+
"CheckedValue": "CloudWatch Logs Delivery Status",
|
|
181
|
+
"ActualValue": actual_value,
|
|
182
|
+
"Remediation": remediation,
|
|
183
|
+
"Service": self.service,
|
|
184
|
+
"CheckLogic": self.check_logic,
|
|
185
|
+
"CheckType": self.check_type
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
except ClientError as e:
|
|
189
|
+
self.logger.error(f"Error accessing CloudTrail: {str(e)}")
|
|
190
|
+
self.findings.append({
|
|
191
|
+
"CheckId": self.check_id,
|
|
192
|
+
"Status": "ERROR",
|
|
193
|
+
"Region": region,
|
|
194
|
+
"Severity": self.severity,
|
|
195
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
196
|
+
"Description": self.description,
|
|
197
|
+
"ResourceId": "cloudtrail",
|
|
198
|
+
"ResourceType": "AWS::CloudTrail::Trail",
|
|
199
|
+
"AccountId": account_id,
|
|
200
|
+
"CheckedValue": "CloudTrail API Access",
|
|
201
|
+
"ActualValue": f"Error accessing CloudTrail: {str(e)}",
|
|
202
|
+
"Remediation": "Verify CloudTrail permissions",
|
|
203
|
+
"Service": self.service,
|
|
204
|
+
"CheckLogic": self.check_logic,
|
|
205
|
+
"CheckType": self.check_type
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
self.logger.error(f"Unexpected error in check: {str(e)}")
|
|
210
|
+
self.findings.append({
|
|
211
|
+
"CheckId": self.check_id,
|
|
212
|
+
"Status": "ERROR",
|
|
213
|
+
"Region": region if 'region' in locals() else session.region_name,
|
|
214
|
+
"Severity": self.severity,
|
|
215
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
216
|
+
"Description": self.description,
|
|
217
|
+
"ResourceId": "check-execution",
|
|
218
|
+
"ResourceType": "AWS::CloudTrail::Trail",
|
|
219
|
+
"AccountId": account_id if 'account_id' in locals() else "unknown",
|
|
220
|
+
"CheckedValue": "Check Execution",
|
|
221
|
+
"ActualValue": f"Unexpected error: {str(e)}",
|
|
222
|
+
"Remediation": "Contact support team",
|
|
223
|
+
"Service": self.service,
|
|
224
|
+
"CheckLogic": self.check_logic,
|
|
225
|
+
"CheckType": self.check_type
|
|
226
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from typing import Dict, List, Any, Optional
|
|
2
|
+
from botocore.exceptions import ClientError
|
|
3
|
+
import logging
|
|
4
|
+
from sraverify.lib.check_loader import SecurityCheck
|
|
5
|
+
|
|
6
|
+
class SRACONFIG1(SecurityCheck):
|
|
7
|
+
"""SRA-CONFIG-1: AWS Config recorder configuration check"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, check_type="account"):
|
|
10
|
+
"""Initialize the check with account type"""
|
|
11
|
+
super().__init__(check_type=check_type)
|
|
12
|
+
self.check_id = "SRA-CONFIG-1"
|
|
13
|
+
self.check_name = "AWS Config recorder is configured in this region"
|
|
14
|
+
self.description = ('This check verifies that a configuration recorders exists in the AWS Region. '
|
|
15
|
+
'AWS Config uses the configuration recorder to detect changes in your resource '
|
|
16
|
+
'configurations and capture these changes as configuration items. You must create '
|
|
17
|
+
'a configuration recorder in every AWS Region for AWS Config can track your '
|
|
18
|
+
'resource configurations in the region.')
|
|
19
|
+
self.service = "Config"
|
|
20
|
+
self.severity = "HIGH"
|
|
21
|
+
self.check_type = check_type
|
|
22
|
+
self.check_logic = ('1. List Config recorders in current region | '
|
|
23
|
+
'2. Verify at least one recorder exists | '
|
|
24
|
+
'3. Check recorder configuration status')
|
|
25
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
26
|
+
self.findings = []
|
|
27
|
+
self._regions = None
|
|
28
|
+
|
|
29
|
+
def initialize(self, regions: Optional[List[str]] = None):
|
|
30
|
+
"""Initialize check with optional regions"""
|
|
31
|
+
self._regions = regions
|
|
32
|
+
|
|
33
|
+
def get_findings(self) -> List[Dict[str, Any]]:
|
|
34
|
+
"""Return the findings"""
|
|
35
|
+
return self.findings
|
|
36
|
+
|
|
37
|
+
def _create_finding(self, status: str, region: str, account_id: str,
|
|
38
|
+
resource_id: str, actual_value: str,
|
|
39
|
+
remediation: str) -> Dict[str, Any]:
|
|
40
|
+
"""Create a standardized finding"""
|
|
41
|
+
return {
|
|
42
|
+
"CheckId": self.check_id,
|
|
43
|
+
"Status": status,
|
|
44
|
+
"Region": region,
|
|
45
|
+
"Severity": self.severity,
|
|
46
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
47
|
+
"Description": self.description,
|
|
48
|
+
"ResourceId": resource_id,
|
|
49
|
+
"ResourceType": "AWS::Config::ConfigurationRecorder",
|
|
50
|
+
"AccountId": account_id,
|
|
51
|
+
"CheckedValue": "Configuration Recorder Status",
|
|
52
|
+
"ActualValue": actual_value,
|
|
53
|
+
"Remediation": remediation,
|
|
54
|
+
"Service": self.service,
|
|
55
|
+
"CheckLogic": self.check_logic,
|
|
56
|
+
"CheckType": self.check_type
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def check_recorder_status(self, config_client, recorder_name: str) -> tuple:
|
|
60
|
+
"""Check Config recorder status and return details"""
|
|
61
|
+
try:
|
|
62
|
+
status = config_client.describe_configuration_recorder_status(
|
|
63
|
+
ConfigurationRecorderNames=[recorder_name]
|
|
64
|
+
)
|
|
65
|
+
if status['ConfigurationRecordersStatus']:
|
|
66
|
+
recorder_status = status['ConfigurationRecordersStatus'][0]
|
|
67
|
+
return (
|
|
68
|
+
recorder_status.get('recording', False),
|
|
69
|
+
recorder_status.get('lastStatus', 'ERROR'),
|
|
70
|
+
recorder_status.get('lastErrorCode', ''),
|
|
71
|
+
recorder_status.get('lastErrorMessage', '')
|
|
72
|
+
)
|
|
73
|
+
return False, 'ERROR', 'NO_STATUS', 'No recorder status found'
|
|
74
|
+
|
|
75
|
+
except ClientError as e:
|
|
76
|
+
self.logger.error(f"Error getting recorder status for {recorder_name}: {str(e)}")
|
|
77
|
+
return False, 'ERROR', str(e), 'Error getting recorder status'
|
|
78
|
+
|
|
79
|
+
def _check_region(self, session, region: str) -> Optional[Dict[str, Any]]:
|
|
80
|
+
"""Check Config configuration in a specific region"""
|
|
81
|
+
try:
|
|
82
|
+
account_id = session.client('sts').get_caller_identity()['Account']
|
|
83
|
+
self.logger.debug(f"Checking region: {region}")
|
|
84
|
+
|
|
85
|
+
# Initialize Config client for the specific region
|
|
86
|
+
config_client = session.client('config', region_name=region)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# List configuration recorders
|
|
90
|
+
recorders = config_client.describe_configuration_recorders()
|
|
91
|
+
if not recorders['ConfigurationRecorders']:
|
|
92
|
+
return self._create_finding(
|
|
93
|
+
status="FAIL",
|
|
94
|
+
region=region,
|
|
95
|
+
account_id=account_id,
|
|
96
|
+
resource_id="config-recorder",
|
|
97
|
+
actual_value="No configuration recorder found in region",
|
|
98
|
+
remediation="Create a configuration recorder in this region"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Check each recorder's status
|
|
102
|
+
valid_recorder = None
|
|
103
|
+
for recorder in recorders['ConfigurationRecorders']:
|
|
104
|
+
recorder_name = recorder['name']
|
|
105
|
+
is_recording, last_status, error_code, error_message = self.check_recorder_status(
|
|
106
|
+
config_client,
|
|
107
|
+
recorder_name
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if is_recording and last_status == 'SUCCESS':
|
|
111
|
+
valid_recorder = recorder
|
|
112
|
+
self.logger.debug(f"Found active recorder: {recorder_name}")
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if valid_recorder:
|
|
116
|
+
return self._create_finding(
|
|
117
|
+
status="PASS",
|
|
118
|
+
region=region,
|
|
119
|
+
account_id=account_id,
|
|
120
|
+
resource_id=valid_recorder['name'],
|
|
121
|
+
actual_value=f"Configuration recorder {valid_recorder['name']} is active and recording",
|
|
122
|
+
remediation="None required"
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
recorder = recorders['ConfigurationRecorders'][0]
|
|
126
|
+
actual_value = f"Configuration recorder {recorder['name']} exists but "
|
|
127
|
+
if not is_recording:
|
|
128
|
+
actual_value += "is not recording"
|
|
129
|
+
else:
|
|
130
|
+
actual_value += f"has status: {last_status}"
|
|
131
|
+
if error_code:
|
|
132
|
+
actual_value += f" (Error: {error_code} - {error_message})"
|
|
133
|
+
|
|
134
|
+
return self._create_finding(
|
|
135
|
+
status="FAIL",
|
|
136
|
+
region=region,
|
|
137
|
+
account_id=account_id,
|
|
138
|
+
resource_id=recorder['name'],
|
|
139
|
+
actual_value=actual_value,
|
|
140
|
+
remediation="Start the configuration recorder or fix configuration errors"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
except ClientError as e:
|
|
144
|
+
return self._create_finding(
|
|
145
|
+
status="ERROR",
|
|
146
|
+
region=region,
|
|
147
|
+
account_id=account_id,
|
|
148
|
+
resource_id="config",
|
|
149
|
+
actual_value=f"Error accessing Config: {str(e)}",
|
|
150
|
+
remediation="Verify Config permissions"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return self._create_finding(
|
|
155
|
+
status="ERROR",
|
|
156
|
+
region=region,
|
|
157
|
+
account_id="Unknown",
|
|
158
|
+
resource_id="check-execution",
|
|
159
|
+
actual_value=f"Error: {str(e)}",
|
|
160
|
+
remediation="Check logs for more details"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def run(self, session) -> None:
|
|
164
|
+
"""Run the security check"""
|
|
165
|
+
try:
|
|
166
|
+
# Get regions to check
|
|
167
|
+
regions_to_check = self._regions if self._regions else [session.region_name]
|
|
168
|
+
|
|
169
|
+
# Check each region
|
|
170
|
+
for region in regions_to_check:
|
|
171
|
+
try:
|
|
172
|
+
finding = self._check_region(session, region)
|
|
173
|
+
if finding:
|
|
174
|
+
self.findings.append(finding)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self.findings.append(
|
|
177
|
+
self._create_finding(
|
|
178
|
+
status="ERROR",
|
|
179
|
+
region=region,
|
|
180
|
+
account_id="Unknown",
|
|
181
|
+
resource_id="Unknown",
|
|
182
|
+
actual_value=f"Region check failed: {str(e)}",
|
|
183
|
+
remediation="Check regional access and permissions"
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
self.findings.append(
|
|
189
|
+
self._create_finding(
|
|
190
|
+
status="ERROR",
|
|
191
|
+
region="Unknown",
|
|
192
|
+
account_id="Unknown",
|
|
193
|
+
resource_id="Unknown",
|
|
194
|
+
actual_value=f"Check execution failed: {str(e)}",
|
|
195
|
+
remediation="Check logs for more details"
|
|
196
|
+
)
|
|
197
|
+
)
|
sraverify/core/check.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for security checks.
|
|
3
|
+
"""
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
|
+
import boto3
|
|
6
|
+
from sraverify.core.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SecurityCheck:
|
|
10
|
+
"""Base class for all security checks."""
|
|
11
|
+
|
|
12
|
+
# Class-level cache for account information shared across all instances
|
|
13
|
+
_account_info_cache = {}
|
|
14
|
+
|
|
15
|
+
def __init__(self, account_type="application", service=None, resource_type=None):
|
|
16
|
+
"""
|
|
17
|
+
Initialize security check.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
account_type: Type of account (application, audit, log-archive, management)
|
|
21
|
+
service: AWS service name
|
|
22
|
+
resource_type: AWS resource type for findings
|
|
23
|
+
"""
|
|
24
|
+
self.account_type = account_type
|
|
25
|
+
self.service = service
|
|
26
|
+
self.resource_type = resource_type
|
|
27
|
+
self.check_id = None
|
|
28
|
+
self.check_name = None
|
|
29
|
+
self.description = None
|
|
30
|
+
self.rationale = None
|
|
31
|
+
self.remediation = None
|
|
32
|
+
self.severity = "Unknown"
|
|
33
|
+
self.check_logic = None
|
|
34
|
+
self.findings = []
|
|
35
|
+
self.regions = []
|
|
36
|
+
self.session = None
|
|
37
|
+
self._clients = {}
|
|
38
|
+
self.account_info = None # Will hold {'account_id': str, 'account_name': str}
|
|
39
|
+
|
|
40
|
+
def initialize(self, session: boto3.Session, regions: Optional[List[str]] = None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize check with AWS session and optional regions.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
session: AWS session to use for the check
|
|
46
|
+
regions: List of AWS regions to check. If not provided, enabled regions will be detected.
|
|
47
|
+
"""
|
|
48
|
+
logger.debug(f"Initializing {self.__class__.__name__} check")
|
|
49
|
+
self.session = session
|
|
50
|
+
# All account types need regions, so we'll get them regardless of account type
|
|
51
|
+
self.regions = regions if regions else self._get_enabled_regions()
|
|
52
|
+
logger.debug(f"Check will run in regions: {', '.join(self.regions)}")
|
|
53
|
+
|
|
54
|
+
# Get account info once during initialization
|
|
55
|
+
self.account_info = self._get_account_info()
|
|
56
|
+
logger.debug(f"Check initialized for account: {self.account_info['account_name']} ({self.account_info['account_id']})")
|
|
57
|
+
|
|
58
|
+
self._setup_clients()
|
|
59
|
+
|
|
60
|
+
def _get_enabled_regions(self) -> List[str]:
|
|
61
|
+
"""
|
|
62
|
+
Get all enabled regions in the AWS account.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of enabled region names
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
logger.debug("Getting enabled AWS regions")
|
|
69
|
+
session = boto3.Session()
|
|
70
|
+
ec2_client = session.client('ec2', region_name='us-east-1')
|
|
71
|
+
response = ec2_client.describe_regions(AllRegions=False)
|
|
72
|
+
regions = [region['RegionName'] for region in response['Regions']]
|
|
73
|
+
logger.debug(f"Found {len(regions)} enabled regions")
|
|
74
|
+
return regions
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Failed to get enabled regions: {str(e)}")
|
|
77
|
+
raise Exception(f"Failed to get enabled regions: {str(e)}")
|
|
78
|
+
|
|
79
|
+
def _setup_clients(self):
|
|
80
|
+
"""
|
|
81
|
+
Set up clients for each region. Must be implemented by subclasses.
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError("Subclasses must implement _setup_clients method")
|
|
84
|
+
|
|
85
|
+
def get_client(self, region: str) -> Optional[Any]:
|
|
86
|
+
"""
|
|
87
|
+
Get client for a specific region.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
region: AWS region name
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Client for the region or None if not available
|
|
94
|
+
"""
|
|
95
|
+
return self._clients.get(region)
|
|
96
|
+
|
|
97
|
+
def create_finding(self, status: str, region: str, resource_id: str,
|
|
98
|
+
actual_value: str, remediation: str,
|
|
99
|
+
checked_value: Optional[str] = None) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Create a standardized finding.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
status: Check status (PASS/FAIL/ERROR)
|
|
105
|
+
region: AWS region
|
|
106
|
+
resource_id: Resource identifier
|
|
107
|
+
actual_value: Actual value found
|
|
108
|
+
remediation: Remediation steps
|
|
109
|
+
checked_value: Value that was checked (defaults to service name + " Configuration")
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Finding dictionary
|
|
113
|
+
|
|
114
|
+
Note: account_id and account_name are automatically populated from initialization.
|
|
115
|
+
"""
|
|
116
|
+
if checked_value is None:
|
|
117
|
+
checked_value = f"{self.service} Configuration"
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"CheckId": self.check_id,
|
|
121
|
+
"Status": status,
|
|
122
|
+
"Region": region,
|
|
123
|
+
"Severity": self.severity,
|
|
124
|
+
"Title": f"{self.check_id} {self.check_name}",
|
|
125
|
+
"Description": self.description,
|
|
126
|
+
"ResourceId": resource_id,
|
|
127
|
+
"ResourceType": self.resource_type,
|
|
128
|
+
"AccountId": self.account_id,
|
|
129
|
+
"AccountName": self.account_name,
|
|
130
|
+
"CheckedValue": checked_value,
|
|
131
|
+
"ActualValue": actual_value,
|
|
132
|
+
"Remediation": remediation,
|
|
133
|
+
"Service": self.service,
|
|
134
|
+
"CheckLogic": self.check_logic,
|
|
135
|
+
"AccountType": self.account_type
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
139
|
+
"""
|
|
140
|
+
Execute the check. Must be implemented by subclasses.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of findings
|
|
144
|
+
"""
|
|
145
|
+
raise NotImplementedError("Subclasses must implement execute method")
|
|
146
|
+
|
|
147
|
+
def get_findings(self) -> List[Dict[str, Any]]:
|
|
148
|
+
"""
|
|
149
|
+
Get findings from the check.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of findings
|
|
153
|
+
"""
|
|
154
|
+
return self.findings
|
|
155
|
+
|
|
156
|
+
def _get_account_info(self) -> Dict[str, str]:
|
|
157
|
+
"""
|
|
158
|
+
Get account ID and name from AWS Account API with caching.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dictionary with 'account_id' and 'account_name' keys
|
|
162
|
+
"""
|
|
163
|
+
# Get account ID from STS first (reliable, high rate limits)
|
|
164
|
+
try:
|
|
165
|
+
sts_client = self.session.client("sts")
|
|
166
|
+
response = sts_client.get_caller_identity()
|
|
167
|
+
account_id = response["Account"]
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Failed to get account ID from STS: {str(e)}")
|
|
170
|
+
raise Exception(f"Failed to get account ID: {str(e)}")
|
|
171
|
+
|
|
172
|
+
# Check class-level cache
|
|
173
|
+
if account_id in SecurityCheck._account_info_cache:
|
|
174
|
+
logger.debug(f"Using cached account information for {account_id}")
|
|
175
|
+
return SecurityCheck._account_info_cache[account_id]
|
|
176
|
+
|
|
177
|
+
# Try to get account name from Account API (low rate limits)
|
|
178
|
+
try:
|
|
179
|
+
logger.debug("Getting AWS account name from Account API")
|
|
180
|
+
account_client = self.session.client("account")
|
|
181
|
+
response = account_client.get_account_information()
|
|
182
|
+
account_name = response['AccountName']
|
|
183
|
+
logger.debug(f"Retrieved account name: {account_name}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.warning(f"Failed to get account name from Account API: {str(e)}")
|
|
186
|
+
account_name = "" # Blank account name when Account API fails
|
|
187
|
+
|
|
188
|
+
# Create and cache account info
|
|
189
|
+
account_info = {
|
|
190
|
+
'account_id': account_id,
|
|
191
|
+
'account_name': account_name
|
|
192
|
+
}
|
|
193
|
+
SecurityCheck._account_info_cache[account_id] = account_info
|
|
194
|
+
logger.debug(f"Cached account information for {account_id}")
|
|
195
|
+
|
|
196
|
+
return account_info
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def account_id(self) -> str:
|
|
200
|
+
"""Get current account ID."""
|
|
201
|
+
return self.account_info['account_id'] if self.account_info else None
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def account_name(self) -> str:
|
|
205
|
+
"""Get current account name."""
|
|
206
|
+
return self.account_info['account_name'] if self.account_info else None
|
|
207
|
+
|
|
208
|
+
def get_management_accountId(self, session: boto3.Session) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Get AWS management account ID from the session.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
session: AWS session
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
AWS management account ID
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
logger.debug("Getting AWS management account ID")
|
|
220
|
+
org_client = session.client("organizations")
|
|
221
|
+
response = org_client.describe_organization()
|
|
222
|
+
management_account_id = response["Organization"]["MasterAccountId"]
|
|
223
|
+
logger.debug(f"Management account ID: {management_account_id}")
|
|
224
|
+
return management_account_id
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Failed to get AWS management account ID: {str(e)}")
|
|
227
|
+
raise Exception(f"Failed to get AWS management account ID: {str(e)}")
|