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,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if IAM Access Analyzer delegated admin is the Audit account.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.accessanalyzer.base import AccessAnalyzerCheck
|
|
6
|
+
from sraverify.core.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SRA_ACCESSANALYZER_03(AccessAnalyzerCheck):
|
|
10
|
+
"""Check if IAM Access Analyzer delegated admin is the Audit account."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
"""Initialize IAM Access Analyzer check."""
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.check_id = "SRA-ACCESSANALYZER-03"
|
|
16
|
+
self.check_name = "IAM Access Analyzer Delegated Admin is the Audit Account"
|
|
17
|
+
self.description = ("This check verifies whether IAA delegated admin account is the "
|
|
18
|
+
"audit account of your AWS organization. Audit account is "
|
|
19
|
+
"dedicated to operating security services, monitoring AWS accounts, and "
|
|
20
|
+
"automating security alerting and response. IAA helps monitor resources "
|
|
21
|
+
"shared outside zone of trust.")
|
|
22
|
+
self.severity = "HIGH"
|
|
23
|
+
self.account_type = "management"
|
|
24
|
+
self.check_logic = ("Check if the delegated administrator account matches any of the specified Audit account IDs")
|
|
25
|
+
|
|
26
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
27
|
+
"""Execute the check."""
|
|
28
|
+
findings = []
|
|
29
|
+
|
|
30
|
+
delegated_admin = self.get_delegated_admin()
|
|
31
|
+
|
|
32
|
+
if not delegated_admin:
|
|
33
|
+
findings.append(
|
|
34
|
+
self.create_finding(
|
|
35
|
+
status="FAIL",
|
|
36
|
+
region="global",
|
|
37
|
+
resource_id=f"organization/{self.account_id}",
|
|
38
|
+
actual_value="No delegated administrator configured for IAM Access Analyzer",
|
|
39
|
+
remediation="Configure a delegated administrator for IAM Access Analyzer first"
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return findings
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Get the delegated admin account ID
|
|
46
|
+
delegated_admin_id = delegated_admin.get('Id')
|
|
47
|
+
|
|
48
|
+
# Check if audit_accounts is provided via _audit_accounts (new attribute name)
|
|
49
|
+
audit_accounts = []
|
|
50
|
+
if hasattr(self, '_audit_accounts') and self._audit_accounts:
|
|
51
|
+
audit_accounts = self._audit_accounts
|
|
52
|
+
# For backward compatibility, also check the old attribute name
|
|
53
|
+
elif hasattr(self, 'audit_accounts') and self.audit_accounts:
|
|
54
|
+
audit_accounts = self.audit_accounts
|
|
55
|
+
|
|
56
|
+
if not audit_accounts:
|
|
57
|
+
findings.append(
|
|
58
|
+
self.create_finding(
|
|
59
|
+
status="ERROR",
|
|
60
|
+
region="global",
|
|
61
|
+
resource_id=delegated_admin_id,
|
|
62
|
+
actual_value="Audit Account ID not provided",
|
|
63
|
+
remediation="Provide the Audit account IDs using --audit-account flag"
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return findings
|
|
67
|
+
|
|
68
|
+
# Check if delegated admin matches any of the specified Audit accounts
|
|
69
|
+
if delegated_admin_id in audit_accounts:
|
|
70
|
+
findings.append(
|
|
71
|
+
self.create_finding(
|
|
72
|
+
status="PASS",
|
|
73
|
+
region="global",
|
|
74
|
+
resource_id=delegated_admin_id,
|
|
75
|
+
actual_value=f"IAM Access Analyzer delegated administrator (Account: {delegated_admin_id}) "
|
|
76
|
+
f"matches one of the specified Audit accounts {', '.join(audit_accounts)}",
|
|
77
|
+
remediation="No remediation needed"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
findings.append(
|
|
82
|
+
self.create_finding(
|
|
83
|
+
status="FAIL",
|
|
84
|
+
region="global",
|
|
85
|
+
resource_id=delegated_admin_id,
|
|
86
|
+
actual_value=f"IAM Access Analyzer delegated administrator (Account: {delegated_admin_id}) "
|
|
87
|
+
f"does not match any of the specified Audit accounts ({', '.join(audit_accounts)})",
|
|
88
|
+
remediation=f"Update the delegated administrator to be one of the Audit accounts ({', '.join(audit_accounts)})"
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
findings.append(
|
|
94
|
+
self.create_finding(
|
|
95
|
+
status="ERROR",
|
|
96
|
+
region="global",
|
|
97
|
+
resource_id=delegated_admin_id if 'delegated_admin_id' in locals() else f"organization/{self.account_id}",
|
|
98
|
+
actual_value=f"Error checking delegated administrator: {str(e)}",
|
|
99
|
+
remediation="Ensure proper permissions to check Organizations structure"
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return findings
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if IAM Access Analyzer external access analyzer is configured with Organization zone of trust in every region.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.accessanalyzer.base import AccessAnalyzerCheck
|
|
6
|
+
from sraverify.core.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SRA_ACCESSANALYZER_04(AccessAnalyzerCheck):
|
|
10
|
+
"""Check if IAM Access Analyzer has an analyzer with Organization zone of trust in every region."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
"""Initialize IAM Access Analyzer check."""
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.check_id = "SRA-ACCESSANALYZER-04"
|
|
16
|
+
self.check_name = "IAM Access Analyzer external access analyzer is configured with Organization zone of trust in every region"
|
|
17
|
+
self.description = ("This check verifies whether IAA external access analyzer is configured with a zone of trust "
|
|
18
|
+
"of your AWS organization in every available region. IAM Access Analyzer generates a finding for each instance of a "
|
|
19
|
+
"resource-based policy that grants access to a resource within your zone of trust to a "
|
|
20
|
+
"principal that is not within your zone of trust. When you configure an organization as the "
|
|
21
|
+
"zone of trust for an analyzer- IAA generates findings or each instance of a resource-based "
|
|
22
|
+
"policy that grants access to a resource within your AWS organization to a principal that is "
|
|
23
|
+
"not within your AWS organization.")
|
|
24
|
+
self.severity = "HIGH"
|
|
25
|
+
self.account_type = "audit"
|
|
26
|
+
self.check_logic = ("Check if an IAM Access Analyzer with Organization zone of trust exists in each region, created by the audit account")
|
|
27
|
+
self._analyzer_details_cache = {}
|
|
28
|
+
self._audit_accounts = []
|
|
29
|
+
|
|
30
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
31
|
+
"""Execute the check for each region."""
|
|
32
|
+
findings = []
|
|
33
|
+
# First, verify this check is running from an audit account - silently add to findings without logging warnings
|
|
34
|
+
if hasattr(self, '_audit_accounts') and self._audit_accounts:
|
|
35
|
+
if self.account_id not in self._audit_accounts:
|
|
36
|
+
# Don't log a warning, just add to findings
|
|
37
|
+
findings.append(
|
|
38
|
+
self.create_finding(
|
|
39
|
+
status="ERROR",
|
|
40
|
+
region="global",
|
|
41
|
+
resource_id="accessanalyzer:account-validation",
|
|
42
|
+
actual_value=f"Invalid account for IAM Access Analyzer check: Account {self.account_id} is not an audit account",
|
|
43
|
+
remediation=f"This check must be run from an audit account ({', '.join(self._audit_accounts)}). Either run this check from one of the designated audit accounts or update your configuration to specify the correct audit account(s) using the --account-type parameter."
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
return findings
|
|
47
|
+
|
|
48
|
+
# If no regions have Access Analyzer available, return a single error
|
|
49
|
+
if not self._clients:
|
|
50
|
+
findings.append(
|
|
51
|
+
self.create_finding(
|
|
52
|
+
status="ERROR",
|
|
53
|
+
region="global",
|
|
54
|
+
resource_id=f"accessanalyzer:global",
|
|
55
|
+
actual_value="IAM Access Analyzer not available in any specified region",
|
|
56
|
+
remediation="Ensure IAM Access Analyzer service is available in at least one region and you have proper permissions"
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
return findings
|
|
60
|
+
|
|
61
|
+
# Check if any analyzers exist across all regions
|
|
62
|
+
total_analyzers = 0
|
|
63
|
+
for region, client in self._clients.items():
|
|
64
|
+
analyzers = self.get_analyzers(region)
|
|
65
|
+
total_analyzers += len(analyzers)
|
|
66
|
+
|
|
67
|
+
# If no analyzers exist at all, return a single global finding
|
|
68
|
+
if total_analyzers == 0:
|
|
69
|
+
findings.append(
|
|
70
|
+
self.create_finding(
|
|
71
|
+
status="FAIL",
|
|
72
|
+
region="global",
|
|
73
|
+
resource_id="accessanalyzer:global",
|
|
74
|
+
actual_value="No IAM Access Analyzers found in any region",
|
|
75
|
+
remediation=(
|
|
76
|
+
"Create IAM Access Analyzers with Organization zone of trust in each region using the AWS CLI command: "
|
|
77
|
+
"aws accessanalyzer create-analyzer --analyzer-name org-analyzer --type ORGANIZATION --region <region>"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return findings
|
|
82
|
+
|
|
83
|
+
# Track if we found organization analyzers in any region
|
|
84
|
+
found_org_analyzers = False
|
|
85
|
+
all_regions_checked = True
|
|
86
|
+
|
|
87
|
+
# Check each region where Access Analyzer is available
|
|
88
|
+
for region, client in self._clients.items():
|
|
89
|
+
try:
|
|
90
|
+
# Get analyzers for this region using the base class method that handles caching
|
|
91
|
+
analyzers = self.get_analyzers(region)
|
|
92
|
+
|
|
93
|
+
# Look for analyzers with organization zone of trust in this specific region
|
|
94
|
+
# that are created by this account
|
|
95
|
+
org_analyzers = [
|
|
96
|
+
a for a in analyzers
|
|
97
|
+
if a.get('type') == 'ORGANIZATION'
|
|
98
|
+
and a.get('status') == 'ACTIVE'
|
|
99
|
+
and a.get('arn', '').split(':')[4] == self.account_id
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if org_analyzers:
|
|
103
|
+
# If we found organization analyzers in this region created by this account, report a PASS
|
|
104
|
+
found_org_analyzers = True
|
|
105
|
+
analyzer_names = [a.get('name', 'Unknown') for a in org_analyzers]
|
|
106
|
+
analyzer_arn = org_analyzers[0].get('arn', f"arn:aws:access-analyzer:{region}:{self.account_id}:analyzer/{region}")
|
|
107
|
+
|
|
108
|
+
findings.append(
|
|
109
|
+
self.create_finding(
|
|
110
|
+
status="PASS",
|
|
111
|
+
region=region,
|
|
112
|
+
resource_id=analyzer_arn,
|
|
113
|
+
actual_value=f"Found IAM Access Analyzer with Organization zone of trust in {region}: {', '.join(analyzer_names)}",
|
|
114
|
+
remediation="No remediation needed"
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# If no organization analyzers in this region created by this account
|
|
119
|
+
findings.append(
|
|
120
|
+
self.create_finding(
|
|
121
|
+
status="FAIL",
|
|
122
|
+
region=region,
|
|
123
|
+
resource_id="No Organization analyzer found in this region",
|
|
124
|
+
actual_value=f"No IAM Access Analyzer with Organization zone of trust found in {region}",
|
|
125
|
+
remediation=f"Create an IAM Access Analyzer with Organization zone of trust in {region} using the AWS CLI command: aws accessanalyzer create-analyzer --analyzer-name org-analyzer --type ORGANIZATION --region {region}"
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
all_regions_checked = False
|
|
130
|
+
findings.append(
|
|
131
|
+
self.create_finding(
|
|
132
|
+
status="ERROR",
|
|
133
|
+
region=region, resource_id="error",
|
|
134
|
+
actual_value=f"Error checking IAM Access Analyzer in {region}: {str(e)}",
|
|
135
|
+
remediation="Ensure you have proper permissions to list IAM Access Analyzers and that the service is available in this region"
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return findings
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IAM Access Analyzer client for interacting with AWS IAM Access Analyzer service.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Optional, Any
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import ClientError, EndpointConnectionError
|
|
7
|
+
from sraverify.core.logging import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AccessAnalyzerClient:
|
|
11
|
+
"""Client for interacting with AWS IAM Access Analyzer service."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, region: str, session: Optional[boto3.Session] = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize IAM Access Analyzer client for a specific region.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
region: AWS region name
|
|
19
|
+
session: AWS session to use (if None, a new session will be created)
|
|
20
|
+
"""
|
|
21
|
+
self.region = region
|
|
22
|
+
self.session = session or boto3.Session()
|
|
23
|
+
self.client = self.session.client('accessanalyzer', region_name=region)
|
|
24
|
+
self.org_client = self.session.client('organizations', region_name=region)
|
|
25
|
+
logger.debug(f"Initialized AccessAnalyzerClient for region {region}")
|
|
26
|
+
|
|
27
|
+
def is_access_analyzer_available(self) -> bool:
|
|
28
|
+
"""Check if Access Analyzer is available in the region."""
|
|
29
|
+
try:
|
|
30
|
+
logger.debug(f"Checking if Access Analyzer is available in {self.region}")
|
|
31
|
+
self.client.list_analyzers(maxResults=1)
|
|
32
|
+
logger.debug(f"Access Analyzer is available in {self.region}")
|
|
33
|
+
return True
|
|
34
|
+
except ClientError as e:
|
|
35
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
36
|
+
if error_code == 'AccessDeniedException':
|
|
37
|
+
# If we get an access denied, the service exists but we don't have permissions
|
|
38
|
+
logger.debug(f"Access Analyzer exists in {self.region} but access is denied")
|
|
39
|
+
return True
|
|
40
|
+
logger.debug(f"Access Analyzer not available in {self.region}: {error_code}")
|
|
41
|
+
return False
|
|
42
|
+
except EndpointConnectionError:
|
|
43
|
+
# Service not available in this region
|
|
44
|
+
logger.debug(f"Access Analyzer endpoint not available in {self.region}")
|
|
45
|
+
return False
|
|
46
|
+
except Exception as e:
|
|
47
|
+
# Any other error, assume service is not available
|
|
48
|
+
logger.debug(f"Error checking Access Analyzer availability in {self.region}: {str(e)}")
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def list_analyzers(self) -> List[Dict[str, Any]]:
|
|
52
|
+
"""
|
|
53
|
+
List all analyzers in the region.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of analyzer details
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
analyzers = []
|
|
60
|
+
paginator = self.client.get_paginator('list_analyzers')
|
|
61
|
+
|
|
62
|
+
logger.debug(f"Listing analyzers in {self.region}")
|
|
63
|
+
for page in paginator.paginate():
|
|
64
|
+
analyzers.extend(page.get('analyzers', []))
|
|
65
|
+
|
|
66
|
+
logger.debug(f"Found {len(analyzers)} analyzers in {self.region}")
|
|
67
|
+
return analyzers
|
|
68
|
+
except ClientError as e:
|
|
69
|
+
logger.warning(f"Error listing analyzers in {self.region}: {e}")
|
|
70
|
+
return []
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.warning(f"Unexpected error listing analyzers in {self.region}: {e}")
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
def get_analyzer_details(self, analyzer_arn: str) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Get details for a specific analyzer.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
analyzer_arn: ARN of the analyzer
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Analyzer details
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
logger.debug(f"Getting details for analyzer {analyzer_arn}")
|
|
87
|
+
response = self.client.get_analyzer(analyzerArn=analyzer_arn)
|
|
88
|
+
return response
|
|
89
|
+
except ClientError as e:
|
|
90
|
+
logger.warning(f"Error getting analyzer details for {analyzer_arn}: {e}")
|
|
91
|
+
return {}
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.warning(f"Unexpected error getting analyzer details for {analyzer_arn}: {e}")
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
def get_delegated_admin(self) -> Dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
Get the delegated administrator for IAM Access Analyzer.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dictionary containing delegated administrator details or empty dict if none
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
logger.debug("Getting delegated administrator for IAM Access Analyzer")
|
|
105
|
+
response = self.org_client.list_delegated_administrators(ServicePrincipal='access-analyzer.amazonaws.com')
|
|
106
|
+
delegated_admins = response.get('DelegatedAdministrators', [])
|
|
107
|
+
|
|
108
|
+
if delegated_admins:
|
|
109
|
+
logger.debug(f"Found delegated administrator for IAM Access Analyzer: {delegated_admins[0].get('Id')}")
|
|
110
|
+
return delegated_admins[0]
|
|
111
|
+
else:
|
|
112
|
+
logger.debug("No delegated administrator found for IAM Access Analyzer")
|
|
113
|
+
return {}
|
|
114
|
+
except ClientError as e:
|
|
115
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
116
|
+
if error_code == 'AWSOrganizationsNotInUseException':
|
|
117
|
+
logger.debug("AWS Organizations not in use")
|
|
118
|
+
else:
|
|
119
|
+
logger.warning(f"Error getting delegated administrator: {e}")
|
|
120
|
+
return {}
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"Unexpected error getting delegated administrator: {e}")
|
|
123
|
+
return {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from sraverify.services.account.checks.sra_account_01 import SRA_ACCOUNT_01
|
|
2
|
+
from sraverify.services.account.checks.sra_account_02 import SRA_ACCOUNT_02
|
|
3
|
+
from sraverify.services.account.checks.sra_account_03 import SRA_ACCOUNT_03
|
|
4
|
+
|
|
5
|
+
CHECKS = {
|
|
6
|
+
"SRA-ACCOUNT-01": SRA_ACCOUNT_01,
|
|
7
|
+
"SRA-ACCOUNT-02": SRA_ACCOUNT_02,
|
|
8
|
+
"SRA-ACCOUNT-03": SRA_ACCOUNT_03,
|
|
9
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for Account security checks.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
from sraverify.core.check import SecurityCheck
|
|
6
|
+
from sraverify.services.account.client import AccountClient
|
|
7
|
+
from sraverify.core.logging import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AccountCheck(SecurityCheck):
|
|
11
|
+
"""Base class for all Account security checks."""
|
|
12
|
+
|
|
13
|
+
# Class-level cache shared across all instances
|
|
14
|
+
_contact_cache = {}
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize Account base check."""
|
|
18
|
+
super().__init__(
|
|
19
|
+
account_type="application",
|
|
20
|
+
service="Account",
|
|
21
|
+
resource_type="AWS::Account::AlternateContact"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _setup_clients(self):
|
|
25
|
+
"""Set up Account clients for each region."""
|
|
26
|
+
self._clients.clear()
|
|
27
|
+
if hasattr(self, 'regions') and self.regions:
|
|
28
|
+
for region in self.regions:
|
|
29
|
+
self._clients[region] = AccountClient(region, session=self.session)
|
|
30
|
+
|
|
31
|
+
def get_alternate_contact(self, region: str, contact_type: str, account_id: str = None) -> Dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Get alternate contact information with caching.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
region: AWS region name
|
|
37
|
+
contact_type: Type of contact (BILLING, OPERATIONS, or SECURITY)
|
|
38
|
+
account_id: Optional account ID
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dictionary containing contact details or empty dict if not available
|
|
42
|
+
"""
|
|
43
|
+
cache_key = f"{self.account_id}:{region}:{contact_type}"
|
|
44
|
+
if cache_key in AccountCheck._contact_cache:
|
|
45
|
+
logger.debug(f"Account: Using cached {contact_type} contact for {region}")
|
|
46
|
+
return AccountCheck._contact_cache[cache_key]
|
|
47
|
+
|
|
48
|
+
client = self.get_client(region)
|
|
49
|
+
if not client:
|
|
50
|
+
logger.warning(f"Account: No Account client available for region {region}")
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
contact_info = client.get_alternate_contact(contact_type, account_id)
|
|
54
|
+
AccountCheck._contact_cache[cache_key] = contact_info
|
|
55
|
+
|
|
56
|
+
return contact_info
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Account checks module
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SRA-ACCOUNT-01: Verify security alternate contact is configured
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.account.base import AccountCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ACCOUNT_01(AccountCheck):
|
|
9
|
+
"""Check if security alternate contact is configured for the AWS account."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.check_id = "SRA-ACCOUNT-01"
|
|
14
|
+
self.check_name = "Security alternate contact configured"
|
|
15
|
+
self.description = "Verifies that a security alternate contact is configured for the AWS account"
|
|
16
|
+
self.severity = "MEDIUM"
|
|
17
|
+
self.check_logic = "Uses GetAlternateContact API to verify security contact exists and has required fields"
|
|
18
|
+
|
|
19
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
20
|
+
"""Execute the security alternate contact check."""
|
|
21
|
+
account_id = self.account_id
|
|
22
|
+
|
|
23
|
+
# Account-level check only needs to run once, use first region
|
|
24
|
+
region = self.regions[0] if self.regions else "us-east-1"
|
|
25
|
+
|
|
26
|
+
contact_info = self.get_alternate_contact(region, "SECURITY")
|
|
27
|
+
|
|
28
|
+
if "Error" in contact_info:
|
|
29
|
+
error_code = contact_info["Error"].get("Code", "")
|
|
30
|
+
if error_code == "ResourceNotFoundException":
|
|
31
|
+
self.findings.append(self.create_finding(
|
|
32
|
+
status="FAIL",
|
|
33
|
+
region=region,
|
|
34
|
+
resource_id=f"account-{account_id}",
|
|
35
|
+
actual_value="No security alternate contact configured",
|
|
36
|
+
remediation="Configure a security alternate contact using AWS Console > Account Settings > Alternate contacts or AWS CLI: aws account put-alternate-contact --alternate-contact-type SECURITY --email-address <email> --name <name> --phone-number <phone> --title <title>"
|
|
37
|
+
))
|
|
38
|
+
else:
|
|
39
|
+
self.findings.append(self.create_finding(
|
|
40
|
+
status="ERROR",
|
|
41
|
+
region=region,
|
|
42
|
+
resource_id=f"account-{account_id}",
|
|
43
|
+
actual_value=contact_info["Error"].get("Message", "Unknown error"),
|
|
44
|
+
remediation="Check IAM permissions for Account Management API access"
|
|
45
|
+
))
|
|
46
|
+
else:
|
|
47
|
+
contact = contact_info.get("AlternateContact", {})
|
|
48
|
+
if contact and contact.get("EmailAddress") and contact.get("Name"):
|
|
49
|
+
self.findings.append(self.create_finding(
|
|
50
|
+
status="PASS",
|
|
51
|
+
region=region,
|
|
52
|
+
resource_id=f"account-{account_id}",
|
|
53
|
+
actual_value=f"Security contact configured: {contact.get('Name')} ({contact.get('EmailAddress')})",
|
|
54
|
+
remediation="No remediation needed"
|
|
55
|
+
))
|
|
56
|
+
else:
|
|
57
|
+
self.findings.append(self.create_finding(
|
|
58
|
+
status="FAIL",
|
|
59
|
+
region=region,
|
|
60
|
+
resource_id=f"account-{account_id}",
|
|
61
|
+
actual_value="Security alternate contact exists but missing required fields",
|
|
62
|
+
remediation="Update security alternate contact to include name and email address using AWS Console > Account Settings > Alternate contacts"
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
return self.findings
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SRA-ACCOUNT-02: Verify billing alternate contact is configured
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.account.base import AccountCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ACCOUNT_02(AccountCheck):
|
|
9
|
+
"""Check if billing alternate contact is configured for the AWS account."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.check_id = "SRA-ACCOUNT-02"
|
|
14
|
+
self.check_name = "Billing alternate contact configured"
|
|
15
|
+
self.description = "Verifies that a billing alternate contact is configured for the AWS account"
|
|
16
|
+
self.severity = "MEDIUM"
|
|
17
|
+
self.check_logic = "Uses GetAlternateContact API to verify billing contact exists and has required fields"
|
|
18
|
+
|
|
19
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
20
|
+
"""Execute the billing alternate contact check."""
|
|
21
|
+
account_id = self.account_id
|
|
22
|
+
region = self.regions[0] if self.regions else "us-east-1"
|
|
23
|
+
|
|
24
|
+
contact_info = self.get_alternate_contact(region, "BILLING")
|
|
25
|
+
|
|
26
|
+
if "Error" in contact_info:
|
|
27
|
+
error_code = contact_info["Error"].get("Code", "")
|
|
28
|
+
if error_code == "ResourceNotFoundException":
|
|
29
|
+
self.findings.append(self.create_finding(
|
|
30
|
+
status="FAIL",
|
|
31
|
+
region=region,
|
|
32
|
+
resource_id=f"account-{account_id}",
|
|
33
|
+
actual_value="No billing alternate contact configured",
|
|
34
|
+
remediation="Configure a billing alternate contact using AWS Console > Account Settings > Alternate contacts or AWS CLI: aws account put-alternate-contact --alternate-contact-type BILLING --email-address <email> --name <name> --phone-number <phone> --title <title>"
|
|
35
|
+
))
|
|
36
|
+
else:
|
|
37
|
+
self.findings.append(self.create_finding(
|
|
38
|
+
status="ERROR",
|
|
39
|
+
region=region,
|
|
40
|
+
resource_id=f"account-{account_id}",
|
|
41
|
+
actual_value=contact_info["Error"].get("Message", "Unknown error"),
|
|
42
|
+
remediation="Check IAM permissions for Account Management API access"
|
|
43
|
+
))
|
|
44
|
+
else:
|
|
45
|
+
contact = contact_info.get("AlternateContact", {})
|
|
46
|
+
if contact and contact.get("EmailAddress") and contact.get("Name"):
|
|
47
|
+
self.findings.append(self.create_finding(
|
|
48
|
+
status="PASS",
|
|
49
|
+
region=region,
|
|
50
|
+
resource_id=f"account-{account_id}",
|
|
51
|
+
actual_value=f"Billing contact configured: {contact.get('Name')} ({contact.get('EmailAddress')})",
|
|
52
|
+
remediation="No remediation needed"
|
|
53
|
+
))
|
|
54
|
+
else:
|
|
55
|
+
self.findings.append(self.create_finding(
|
|
56
|
+
status="FAIL",
|
|
57
|
+
region=region,
|
|
58
|
+
resource_id=f"account-{account_id}",
|
|
59
|
+
actual_value="Billing alternate contact exists but missing required fields",
|
|
60
|
+
remediation="Update billing alternate contact to include name and email address using AWS Console > Account Settings > Alternate contacts"
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
return self.findings
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SRA-ACCOUNT-03: Verify operations alternate contact is configured
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.account.base import AccountCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ACCOUNT_03(AccountCheck):
|
|
9
|
+
"""Check if operations alternate contact is configured for the AWS account."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.check_id = "SRA-ACCOUNT-03"
|
|
14
|
+
self.check_name = "Operations alternate contact configured"
|
|
15
|
+
self.description = "Verifies that an operations alternate contact is configured for the AWS account"
|
|
16
|
+
self.severity = "MEDIUM"
|
|
17
|
+
self.check_logic = "Uses GetAlternateContact API to verify operations contact exists and has required fields"
|
|
18
|
+
|
|
19
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
20
|
+
"""Execute the operations alternate contact check."""
|
|
21
|
+
account_id = self.account_id
|
|
22
|
+
region = self.regions[0] if self.regions else "us-east-1"
|
|
23
|
+
|
|
24
|
+
contact_info = self.get_alternate_contact(region, "OPERATIONS")
|
|
25
|
+
|
|
26
|
+
if "Error" in contact_info:
|
|
27
|
+
error_code = contact_info["Error"].get("Code", "")
|
|
28
|
+
if error_code == "ResourceNotFoundException":
|
|
29
|
+
self.findings.append(self.create_finding(
|
|
30
|
+
status="FAIL",
|
|
31
|
+
region=region,
|
|
32
|
+
resource_id=f"account-{account_id}",
|
|
33
|
+
actual_value="No operations alternate contact configured",
|
|
34
|
+
remediation="Configure an operations alternate contact using AWS Console > Account Settings > Alternate contacts or AWS CLI: aws account put-alternate-contact --alternate-contact-type OPERATIONS --email-address <email> --name <name> --phone-number <phone> --title <title>"
|
|
35
|
+
))
|
|
36
|
+
else:
|
|
37
|
+
self.findings.append(self.create_finding(
|
|
38
|
+
status="ERROR",
|
|
39
|
+
region=region,
|
|
40
|
+
resource_id=f"account-{account_id}",
|
|
41
|
+
actual_value=contact_info["Error"].get("Message", "Unknown error"),
|
|
42
|
+
remediation="Check IAM permissions for Account Management API access"
|
|
43
|
+
))
|
|
44
|
+
else:
|
|
45
|
+
contact = contact_info.get("AlternateContact", {})
|
|
46
|
+
if contact and contact.get("EmailAddress") and contact.get("Name"):
|
|
47
|
+
self.findings.append(self.create_finding(
|
|
48
|
+
status="PASS",
|
|
49
|
+
region=region,
|
|
50
|
+
resource_id=f"account-{account_id}",
|
|
51
|
+
actual_value=f"Operations contact configured: {contact.get('Name')} ({contact.get('EmailAddress')})",
|
|
52
|
+
remediation="No remediation needed"
|
|
53
|
+
))
|
|
54
|
+
else:
|
|
55
|
+
self.findings.append(self.create_finding(
|
|
56
|
+
status="FAIL",
|
|
57
|
+
region=region,
|
|
58
|
+
resource_id=f"account-{account_id}",
|
|
59
|
+
actual_value="Operations alternate contact exists but missing required fields",
|
|
60
|
+
remediation="Update operations alternate contact to include name and email address using AWS Console > Account Settings > Alternate contacts"
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
return self.findings
|