sraverify 0.1.1__py3-none-any.whl → 0.1.3__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/core/logging.py +37 -3
- sraverify/main.py +3 -4
- sraverify/services/organizations/__init__.py +25 -0
- sraverify/services/organizations/base.py +176 -0
- sraverify/services/organizations/checks/__init__.py +3 -0
- sraverify/services/organizations/checks/sra_organizations_01.py +84 -0
- sraverify/services/organizations/checks/sra_organizations_02.py +123 -0
- sraverify/services/organizations/checks/sra_organizations_03.py +123 -0
- sraverify/services/organizations/checks/sra_organizations_04.py +123 -0
- sraverify/services/organizations/checks/sra_organizations_05.py +92 -0
- sraverify/services/organizations/checks/sra_organizations_06.py +125 -0
- sraverify/services/organizations/checks/sra_organizations_07.py +128 -0
- sraverify/services/organizations/checks/sra_organizations_08.py +167 -0
- sraverify/services/organizations/checks/sra_organizations_09.py +167 -0
- sraverify/services/organizations/client.py +153 -0
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/METADATA +1 -1
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/RECORD +22 -9
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/LICENSE +0 -0
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/NOTICE +0 -0
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/WHEEL +0 -0
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/entry_points.txt +0 -0
- {sraverify-0.1.1.dist-info → sraverify-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if organization has foundational OU - Workloads.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_04(OrganizationsCheck):
|
|
9
|
+
"""Check if organization has foundational OU - Workloads."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize Workloads OU check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::OrganizationalUnit")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-04"
|
|
15
|
+
self.check_name = "Organization has foundational OU - Workloads"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the organization has a Workloads organizational unit (OU) "
|
|
18
|
+
"directly under the root. The Workloads OU is a foundational OU recommended by AWS SRA "
|
|
19
|
+
"for organizing application workload accounts including production and non-production environments."
|
|
20
|
+
)
|
|
21
|
+
self.severity = "MEDIUM"
|
|
22
|
+
self.check_logic = (
|
|
23
|
+
"Retrieve the organization root using ListRoots API, then list all OUs under the root "
|
|
24
|
+
"using ListOrganizationalUnitsForParent API. Check passes if an OU named 'Workloads' "
|
|
25
|
+
"exists directly under the root."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
29
|
+
"""
|
|
30
|
+
Execute the check.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of findings
|
|
34
|
+
"""
|
|
35
|
+
# Organizations is a global service, use "global" as region
|
|
36
|
+
region = "global"
|
|
37
|
+
|
|
38
|
+
# Get organization roots
|
|
39
|
+
roots_response = self.get_roots()
|
|
40
|
+
|
|
41
|
+
# Check for errors getting roots
|
|
42
|
+
if "Error" in roots_response:
|
|
43
|
+
error_message = roots_response["Error"].get("Message", "Unknown error")
|
|
44
|
+
self.findings.append(self.create_finding(
|
|
45
|
+
status="ERROR",
|
|
46
|
+
region=region,
|
|
47
|
+
resource_id=None,
|
|
48
|
+
actual_value=f"Error: {error_message}",
|
|
49
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
50
|
+
checked_value="Workloads OU exists under root"
|
|
51
|
+
))
|
|
52
|
+
return self.findings
|
|
53
|
+
|
|
54
|
+
roots = roots_response.get("Roots", [])
|
|
55
|
+
if not roots:
|
|
56
|
+
self.findings.append(self.create_finding(
|
|
57
|
+
status="ERROR",
|
|
58
|
+
region=region,
|
|
59
|
+
resource_id=None,
|
|
60
|
+
actual_value="No organization root found",
|
|
61
|
+
remediation="Ensure AWS Organizations is enabled and properly configured",
|
|
62
|
+
checked_value="Workloads OU exists under root"
|
|
63
|
+
))
|
|
64
|
+
return self.findings
|
|
65
|
+
|
|
66
|
+
# Get the first root (organizations have only one root)
|
|
67
|
+
root = roots[0]
|
|
68
|
+
root_id = root.get("Id", "")
|
|
69
|
+
|
|
70
|
+
# Get OUs under the root
|
|
71
|
+
ous_response = self.get_ous_for_parent(root_id)
|
|
72
|
+
|
|
73
|
+
# Check for errors getting OUs
|
|
74
|
+
if "Error" in ous_response:
|
|
75
|
+
error_message = ous_response["Error"].get("Message", "Unknown error")
|
|
76
|
+
self.findings.append(self.create_finding(
|
|
77
|
+
status="ERROR",
|
|
78
|
+
region=region,
|
|
79
|
+
resource_id=root_id,
|
|
80
|
+
actual_value=f"Error: {error_message}",
|
|
81
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
82
|
+
checked_value="Workloads OU exists under root"
|
|
83
|
+
))
|
|
84
|
+
return self.findings
|
|
85
|
+
|
|
86
|
+
ous = ous_response.get("OrganizationalUnits", [])
|
|
87
|
+
|
|
88
|
+
# Look for Workloads OU
|
|
89
|
+
workloads_ou = None
|
|
90
|
+
for ou in ous:
|
|
91
|
+
if ou.get("Name") == "Workloads":
|
|
92
|
+
workloads_ou = ou
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if workloads_ou:
|
|
96
|
+
ou_id = workloads_ou.get("Id", "Unknown")
|
|
97
|
+
self.findings.append(self.create_finding(
|
|
98
|
+
status="PASS",
|
|
99
|
+
region=region,
|
|
100
|
+
resource_id=ou_id,
|
|
101
|
+
actual_value=f"Workloads OU exists: {ou_id}",
|
|
102
|
+
remediation="No remediation needed",
|
|
103
|
+
checked_value="Workloads OU exists under root"
|
|
104
|
+
))
|
|
105
|
+
else:
|
|
106
|
+
# List existing OUs for context
|
|
107
|
+
existing_ous = [ou.get("Name", "Unknown") for ou in ous]
|
|
108
|
+
existing_ous_str = ", ".join(existing_ous) if existing_ous else "None"
|
|
109
|
+
self.findings.append(self.create_finding(
|
|
110
|
+
status="FAIL",
|
|
111
|
+
region=region,
|
|
112
|
+
resource_id=root_id,
|
|
113
|
+
actual_value=f"Workloads OU not found. Existing OUs under root: {existing_ous_str}",
|
|
114
|
+
remediation=(
|
|
115
|
+
"Create a Workloads organizational unit under the organization root. "
|
|
116
|
+
"Navigate to AWS Organizations in the console, select the root, and create "
|
|
117
|
+
"a new OU named 'Workloads'. Alternatively, use the AWS CLI: "
|
|
118
|
+
f"aws organizations create-organizational-unit --parent-id {root_id} --name Workloads"
|
|
119
|
+
),
|
|
120
|
+
checked_value="Workloads OU exists under root"
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
return self.findings
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if organization has all features enabled.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_05(OrganizationsCheck):
|
|
9
|
+
"""Check if organization has all features enabled."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize all features enabled check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::Organization")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-05"
|
|
15
|
+
self.check_name = "Organization has all features enabled"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the organization has all features enabled. "
|
|
18
|
+
"All features mode enables full governance capabilities including Service Control Policies (SCPs), "
|
|
19
|
+
"tag policies, backup policies, and AI services opt-out policies. Organizations with only "
|
|
20
|
+
"consolidated billing have limited governance capabilities."
|
|
21
|
+
)
|
|
22
|
+
self.severity = "HIGH"
|
|
23
|
+
self.check_logic = (
|
|
24
|
+
"Call DescribeOrganization API to retrieve organization details. "
|
|
25
|
+
"Check passes if FeatureSet equals 'ALL', fails if FeatureSet equals 'CONSOLIDATED_BILLING'."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
29
|
+
"""
|
|
30
|
+
Execute the check.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of findings
|
|
34
|
+
"""
|
|
35
|
+
# Organizations is a global service, use "global" as region
|
|
36
|
+
region = "global"
|
|
37
|
+
|
|
38
|
+
# Get organization details
|
|
39
|
+
response = self.get_organization()
|
|
40
|
+
|
|
41
|
+
# Check for errors
|
|
42
|
+
if "Error" in response:
|
|
43
|
+
error_message = response["Error"].get("Message", "Unknown error")
|
|
44
|
+
self.findings.append(self.create_finding(
|
|
45
|
+
status="ERROR",
|
|
46
|
+
region=region,
|
|
47
|
+
resource_id=None,
|
|
48
|
+
actual_value=f"Error: {error_message}",
|
|
49
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
50
|
+
checked_value="Organization FeatureSet"
|
|
51
|
+
))
|
|
52
|
+
return self.findings
|
|
53
|
+
|
|
54
|
+
# Extract organization details
|
|
55
|
+
organization = response.get("Organization", {})
|
|
56
|
+
org_id = organization.get("Id", "Unknown")
|
|
57
|
+
feature_set = organization.get("FeatureSet", "Unknown")
|
|
58
|
+
|
|
59
|
+
if feature_set == "ALL":
|
|
60
|
+
self.findings.append(self.create_finding(
|
|
61
|
+
status="PASS",
|
|
62
|
+
region=region,
|
|
63
|
+
resource_id=org_id,
|
|
64
|
+
actual_value=f"FeatureSet: {feature_set}",
|
|
65
|
+
remediation="No remediation needed",
|
|
66
|
+
checked_value="Organization FeatureSet"
|
|
67
|
+
))
|
|
68
|
+
elif feature_set == "CONSOLIDATED_BILLING":
|
|
69
|
+
self.findings.append(self.create_finding(
|
|
70
|
+
status="FAIL",
|
|
71
|
+
region=region,
|
|
72
|
+
resource_id=org_id,
|
|
73
|
+
actual_value=f"FeatureSet: {feature_set}",
|
|
74
|
+
remediation=(
|
|
75
|
+
"Enable all features in AWS Organizations to gain full governance capabilities. "
|
|
76
|
+
"Navigate to AWS Organizations in the console and enable all features. "
|
|
77
|
+
"Note: This requires consent from all member accounts. "
|
|
78
|
+
"See: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_org_support-all-features.html"
|
|
79
|
+
),
|
|
80
|
+
checked_value="Organization FeatureSet"
|
|
81
|
+
))
|
|
82
|
+
else:
|
|
83
|
+
self.findings.append(self.create_finding(
|
|
84
|
+
status="ERROR",
|
|
85
|
+
region=region,
|
|
86
|
+
resource_id=org_id,
|
|
87
|
+
actual_value=f"Unknown FeatureSet: {feature_set}",
|
|
88
|
+
remediation="Verify organization configuration",
|
|
89
|
+
checked_value="Organization FeatureSet"
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
return self.findings
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if organization has Service Control Policies configured.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_06(OrganizationsCheck):
|
|
9
|
+
"""Check if organization has Service Control Policies configured."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize SCP check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::Policy")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-06"
|
|
15
|
+
self.check_name = "Organization has Service Control Policies configured"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the organization has at least one custom Service Control Policy (SCP) "
|
|
18
|
+
"configured beyond the default FullAWSAccess policy. SCPs are essential for implementing "
|
|
19
|
+
"permission guardrails across the organization to enforce security and compliance requirements."
|
|
20
|
+
)
|
|
21
|
+
self.severity = "HIGH"
|
|
22
|
+
self.check_logic = (
|
|
23
|
+
"Call ListPolicies API with filter for SERVICE_CONTROL_POLICY type. "
|
|
24
|
+
"Check passes if at least one custom SCP exists (AwsManaged=False), "
|
|
25
|
+
"fails if only the default FullAWSAccess policy exists or no SCPs are found."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
29
|
+
"""
|
|
30
|
+
Execute the check.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of findings
|
|
34
|
+
"""
|
|
35
|
+
# Organizations is a global service, use "global" as region
|
|
36
|
+
region = "global"
|
|
37
|
+
|
|
38
|
+
# Get organization details for org_id
|
|
39
|
+
org_response = self.get_organization()
|
|
40
|
+
org_id = None
|
|
41
|
+
if "Organization" in org_response:
|
|
42
|
+
org_id = org_response["Organization"].get("Id", "Unknown")
|
|
43
|
+
|
|
44
|
+
# Get SCPs
|
|
45
|
+
response = self.list_policies("SERVICE_CONTROL_POLICY")
|
|
46
|
+
|
|
47
|
+
# Check for errors
|
|
48
|
+
if "Error" in response:
|
|
49
|
+
error_code = response["Error"].get("Code", "")
|
|
50
|
+
error_message = response["Error"].get("Message", "Unknown error")
|
|
51
|
+
|
|
52
|
+
# PolicyTypeNotEnabledException means SCPs are not enabled
|
|
53
|
+
if error_code == "PolicyTypeNotEnabledException":
|
|
54
|
+
self.findings.append(self.create_finding(
|
|
55
|
+
status="FAIL",
|
|
56
|
+
region=region,
|
|
57
|
+
resource_id=org_id,
|
|
58
|
+
actual_value="Service Control Policies are not enabled",
|
|
59
|
+
remediation=(
|
|
60
|
+
"Enable Service Control Policies in AWS Organizations. "
|
|
61
|
+
"Navigate to AWS Organizations > Policies > Service control policies and enable SCPs. "
|
|
62
|
+
"Then create custom SCPs to implement permission guardrails."
|
|
63
|
+
),
|
|
64
|
+
checked_value="Custom SCPs configured"
|
|
65
|
+
))
|
|
66
|
+
else:
|
|
67
|
+
self.findings.append(self.create_finding(
|
|
68
|
+
status="ERROR",
|
|
69
|
+
region=region,
|
|
70
|
+
resource_id=org_id,
|
|
71
|
+
actual_value=f"Error: {error_message}",
|
|
72
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
73
|
+
checked_value="Custom SCPs configured"
|
|
74
|
+
))
|
|
75
|
+
return self.findings
|
|
76
|
+
|
|
77
|
+
policies = response.get("Policies", [])
|
|
78
|
+
|
|
79
|
+
# Count custom SCPs (not AWS managed)
|
|
80
|
+
custom_scps = [p for p in policies if not p.get("AwsManaged", False)]
|
|
81
|
+
custom_scp_count = len(custom_scps)
|
|
82
|
+
|
|
83
|
+
if not policies:
|
|
84
|
+
# No SCPs at all - SCPs might not be enabled
|
|
85
|
+
self.findings.append(self.create_finding(
|
|
86
|
+
status="FAIL",
|
|
87
|
+
region=region,
|
|
88
|
+
resource_id=org_id,
|
|
89
|
+
actual_value="No Service Control Policies found",
|
|
90
|
+
remediation=(
|
|
91
|
+
"Enable Service Control Policies in AWS Organizations and create custom SCPs. "
|
|
92
|
+
"Navigate to AWS Organizations > Policies > Service control policies and enable SCPs. "
|
|
93
|
+
"Then create custom SCPs to implement permission guardrails."
|
|
94
|
+
),
|
|
95
|
+
checked_value="Custom SCPs configured"
|
|
96
|
+
))
|
|
97
|
+
elif custom_scp_count == 0:
|
|
98
|
+
# Only AWS managed policies (FullAWSAccess)
|
|
99
|
+
policy_names = [p.get("Name", "Unknown") for p in policies]
|
|
100
|
+
self.findings.append(self.create_finding(
|
|
101
|
+
status="FAIL",
|
|
102
|
+
region=region,
|
|
103
|
+
resource_id=org_id,
|
|
104
|
+
actual_value=f"Only default policies found: {', '.join(policy_names)}",
|
|
105
|
+
remediation=(
|
|
106
|
+
"Create custom Service Control Policies to implement permission guardrails. "
|
|
107
|
+
"Navigate to AWS Organizations > Policies > Service control policies and create new SCPs. "
|
|
108
|
+
"Consider implementing SCPs for: denying root user actions, restricting regions, "
|
|
109
|
+
"preventing disabling of security services, and enforcing encryption."
|
|
110
|
+
),
|
|
111
|
+
checked_value="Custom SCPs configured"
|
|
112
|
+
))
|
|
113
|
+
else:
|
|
114
|
+
# Custom SCPs exist
|
|
115
|
+
custom_scp_names = [p.get("Name", "Unknown") for p in custom_scps]
|
|
116
|
+
self.findings.append(self.create_finding(
|
|
117
|
+
status="PASS",
|
|
118
|
+
region=region,
|
|
119
|
+
resource_id=org_id,
|
|
120
|
+
actual_value=f"{custom_scp_count} custom SCP(s) configured: {', '.join(custom_scp_names)}",
|
|
121
|
+
remediation="No remediation needed",
|
|
122
|
+
checked_value="Custom SCPs configured"
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
return self.findings
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if organization has Resource Control Policies configured.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_07(OrganizationsCheck):
|
|
9
|
+
"""Check if organization has Resource Control Policies configured."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize RCP check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::Policy")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-07"
|
|
15
|
+
self.check_name = "Organization has Resource Control Policies configured"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the organization has at least one custom Resource Control Policy (RCP) "
|
|
18
|
+
"configured beyond the default RCPFullAWSAccess policy. RCPs are a type of organization policy "
|
|
19
|
+
"that help you centrally establish data perimeter controls across AWS resources in your organization. "
|
|
20
|
+
"RCPs complement SCPs by controlling what resources can be accessed rather than what actions "
|
|
21
|
+
"principals can perform."
|
|
22
|
+
)
|
|
23
|
+
self.severity = "MEDIUM"
|
|
24
|
+
self.check_logic = (
|
|
25
|
+
"Call ListPolicies API with filter for RESOURCE_CONTROL_POLICY type. "
|
|
26
|
+
"Check passes if at least one custom RCP exists (AwsManaged=False), "
|
|
27
|
+
"fails if only the default RCPFullAWSAccess policy exists or no RCPs are found."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
31
|
+
"""
|
|
32
|
+
Execute the check.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of findings
|
|
36
|
+
"""
|
|
37
|
+
# Organizations is a global service, use "global" as region
|
|
38
|
+
region = "global"
|
|
39
|
+
|
|
40
|
+
# Get organization details for org_id
|
|
41
|
+
org_response = self.get_organization()
|
|
42
|
+
org_id = None
|
|
43
|
+
if "Organization" in org_response:
|
|
44
|
+
org_id = org_response["Organization"].get("Id", "Unknown")
|
|
45
|
+
|
|
46
|
+
# Get RCPs
|
|
47
|
+
response = self.list_policies("RESOURCE_CONTROL_POLICY")
|
|
48
|
+
|
|
49
|
+
# Check for errors
|
|
50
|
+
if "Error" in response:
|
|
51
|
+
error_code = response["Error"].get("Code", "")
|
|
52
|
+
error_message = response["Error"].get("Message", "Unknown error")
|
|
53
|
+
|
|
54
|
+
# PolicyTypeNotEnabledException means RCPs are not enabled
|
|
55
|
+
if error_code == "PolicyTypeNotEnabledException":
|
|
56
|
+
self.findings.append(self.create_finding(
|
|
57
|
+
status="FAIL",
|
|
58
|
+
region=region,
|
|
59
|
+
resource_id=org_id,
|
|
60
|
+
actual_value="Resource Control Policies are not enabled",
|
|
61
|
+
remediation=(
|
|
62
|
+
"Enable Resource Control Policies in AWS Organizations. "
|
|
63
|
+
"Navigate to AWS Organizations > Policies > Resource control policies and enable RCPs. "
|
|
64
|
+
"Then create RCPs to implement data perimeter controls across your organization."
|
|
65
|
+
),
|
|
66
|
+
checked_value="RCPs configured"
|
|
67
|
+
))
|
|
68
|
+
else:
|
|
69
|
+
self.findings.append(self.create_finding(
|
|
70
|
+
status="ERROR",
|
|
71
|
+
region=region,
|
|
72
|
+
resource_id=org_id,
|
|
73
|
+
actual_value=f"Error: {error_message}",
|
|
74
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
75
|
+
checked_value="RCPs configured"
|
|
76
|
+
))
|
|
77
|
+
return self.findings
|
|
78
|
+
|
|
79
|
+
policies = response.get("Policies", [])
|
|
80
|
+
|
|
81
|
+
# Count custom RCPs (not AWS managed - exclude RCPFullAWSAccess)
|
|
82
|
+
custom_rcps = [p for p in policies if not p.get("AwsManaged", False)]
|
|
83
|
+
custom_rcp_count = len(custom_rcps)
|
|
84
|
+
|
|
85
|
+
if not policies:
|
|
86
|
+
# No RCPs at all - RCPs might not be enabled
|
|
87
|
+
self.findings.append(self.create_finding(
|
|
88
|
+
status="FAIL",
|
|
89
|
+
region=region,
|
|
90
|
+
resource_id=org_id,
|
|
91
|
+
actual_value="No Resource Control Policies found",
|
|
92
|
+
remediation=(
|
|
93
|
+
"Enable Resource Control Policies in AWS Organizations and create RCPs. "
|
|
94
|
+
"Navigate to AWS Organizations > Policies > Resource control policies and enable RCPs. "
|
|
95
|
+
"Then create RCPs to implement data perimeter controls such as restricting access "
|
|
96
|
+
"to resources based on organization membership or network location."
|
|
97
|
+
),
|
|
98
|
+
checked_value="Custom RCPs configured"
|
|
99
|
+
))
|
|
100
|
+
elif custom_rcp_count == 0:
|
|
101
|
+
# Only AWS managed policies (RCPFullAWSAccess)
|
|
102
|
+
policy_names = [p.get("Name", "Unknown") for p in policies]
|
|
103
|
+
self.findings.append(self.create_finding(
|
|
104
|
+
status="FAIL",
|
|
105
|
+
region=region,
|
|
106
|
+
resource_id=org_id,
|
|
107
|
+
actual_value=f"Only default policies found: {', '.join(policy_names)}",
|
|
108
|
+
remediation=(
|
|
109
|
+
"Create custom Resource Control Policies to implement data perimeter controls. "
|
|
110
|
+
"Navigate to AWS Organizations > Policies > Resource control policies and create new RCPs. "
|
|
111
|
+
"Consider implementing RCPs for: restricting access to resources based on organization "
|
|
112
|
+
"membership, enforcing HTTPS connections, and cross-service confused deputy protection."
|
|
113
|
+
),
|
|
114
|
+
checked_value="Custom RCPs configured"
|
|
115
|
+
))
|
|
116
|
+
else:
|
|
117
|
+
# Custom RCPs exist
|
|
118
|
+
custom_rcp_names = [p.get("Name", "Unknown") for p in custom_rcps]
|
|
119
|
+
self.findings.append(self.create_finding(
|
|
120
|
+
status="PASS",
|
|
121
|
+
region=region,
|
|
122
|
+
resource_id=org_id,
|
|
123
|
+
actual_value=f"{custom_rcp_count} custom RCP(s) configured: {', '.join(custom_rcp_names)}",
|
|
124
|
+
remediation="No remediation needed",
|
|
125
|
+
checked_value="Custom RCPs configured"
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
return self.findings
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if audit account is in Security OU.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_08(OrganizationsCheck):
|
|
9
|
+
"""Check if audit account is in Security OU."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize audit account in Security OU check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::Account")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-08"
|
|
15
|
+
self.check_name = "Audit account is in Security OU"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the audit account (Security Tooling account) is located in the "
|
|
18
|
+
"Security organizational unit. According to AWS SRA best practices, the audit account should "
|
|
19
|
+
"be placed in the Security OU to ensure proper isolation and governance of security tooling."
|
|
20
|
+
)
|
|
21
|
+
self.severity = "HIGH"
|
|
22
|
+
self.check_logic = (
|
|
23
|
+
"Get the Security OU under the organization root, then list all accounts in the Security OU. "
|
|
24
|
+
"Check passes if the audit account (provided via --audit-account CLI parameter) is found in "
|
|
25
|
+
"the Security OU."
|
|
26
|
+
)
|
|
27
|
+
self._audit_accounts = [] # Will be populated from CLI --audit-account parameter
|
|
28
|
+
|
|
29
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
30
|
+
"""
|
|
31
|
+
Execute the check.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of findings
|
|
35
|
+
"""
|
|
36
|
+
# Organizations is a global service, use "global" as region
|
|
37
|
+
region = "global"
|
|
38
|
+
|
|
39
|
+
# Check if audit account is provided
|
|
40
|
+
if not self._audit_accounts:
|
|
41
|
+
self.findings.append(self.create_finding(
|
|
42
|
+
status="FAIL",
|
|
43
|
+
region=region,
|
|
44
|
+
resource_id=None,
|
|
45
|
+
actual_value="Audit account ID not provided",
|
|
46
|
+
remediation="Run check with --audit-account parameter to specify the audit account ID",
|
|
47
|
+
checked_value="Audit account in Security OU"
|
|
48
|
+
))
|
|
49
|
+
return self.findings
|
|
50
|
+
|
|
51
|
+
# Get organization roots
|
|
52
|
+
roots_response = self.get_roots()
|
|
53
|
+
if "Error" in roots_response:
|
|
54
|
+
error_message = roots_response["Error"].get("Message", "Unknown error")
|
|
55
|
+
self.findings.append(self.create_finding(
|
|
56
|
+
status="ERROR",
|
|
57
|
+
region=region,
|
|
58
|
+
resource_id=None,
|
|
59
|
+
actual_value=f"Error: {error_message}",
|
|
60
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
61
|
+
checked_value="Audit account in Security OU"
|
|
62
|
+
))
|
|
63
|
+
return self.findings
|
|
64
|
+
|
|
65
|
+
roots = roots_response.get("Roots", [])
|
|
66
|
+
if not roots:
|
|
67
|
+
self.findings.append(self.create_finding(
|
|
68
|
+
status="ERROR",
|
|
69
|
+
region=region,
|
|
70
|
+
resource_id=None,
|
|
71
|
+
actual_value="No organization root found",
|
|
72
|
+
remediation="Ensure AWS Organizations is enabled and properly configured",
|
|
73
|
+
checked_value="Audit account in Security OU"
|
|
74
|
+
))
|
|
75
|
+
return self.findings
|
|
76
|
+
|
|
77
|
+
root = roots[0]
|
|
78
|
+
root_id = root.get("Id", "")
|
|
79
|
+
|
|
80
|
+
# Get OUs under the root to find Security OU
|
|
81
|
+
ous_response = self.get_ous_for_parent(root_id)
|
|
82
|
+
if "Error" in ous_response:
|
|
83
|
+
error_message = ous_response["Error"].get("Message", "Unknown error")
|
|
84
|
+
self.findings.append(self.create_finding(
|
|
85
|
+
status="ERROR",
|
|
86
|
+
region=region,
|
|
87
|
+
resource_id=root_id,
|
|
88
|
+
actual_value=f"Error: {error_message}",
|
|
89
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
90
|
+
checked_value="Audit account in Security OU"
|
|
91
|
+
))
|
|
92
|
+
return self.findings
|
|
93
|
+
|
|
94
|
+
ous = ous_response.get("OrganizationalUnits", [])
|
|
95
|
+
|
|
96
|
+
# Find Security OU
|
|
97
|
+
security_ou = None
|
|
98
|
+
for ou in ous:
|
|
99
|
+
if ou.get("Name") == "Security":
|
|
100
|
+
security_ou = ou
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
if not security_ou:
|
|
104
|
+
self.findings.append(self.create_finding(
|
|
105
|
+
status="FAIL",
|
|
106
|
+
region=region,
|
|
107
|
+
resource_id=root_id,
|
|
108
|
+
actual_value="Security OU not found under organization root",
|
|
109
|
+
remediation=(
|
|
110
|
+
"Create a Security organizational unit under the organization root and move the "
|
|
111
|
+
"audit account into it. Navigate to AWS Organizations in the console, create the "
|
|
112
|
+
"Security OU, then move the audit account to the Security OU."
|
|
113
|
+
),
|
|
114
|
+
checked_value="Audit account in Security OU"
|
|
115
|
+
))
|
|
116
|
+
return self.findings
|
|
117
|
+
|
|
118
|
+
security_ou_id = security_ou.get("Id", "")
|
|
119
|
+
|
|
120
|
+
# Get accounts in Security OU
|
|
121
|
+
accounts_response = self.get_accounts_for_parent(security_ou_id)
|
|
122
|
+
if "Error" in accounts_response:
|
|
123
|
+
error_message = accounts_response["Error"].get("Message", "Unknown error")
|
|
124
|
+
self.findings.append(self.create_finding(
|
|
125
|
+
status="ERROR",
|
|
126
|
+
region=region,
|
|
127
|
+
resource_id=security_ou_id,
|
|
128
|
+
actual_value=f"Error: {error_message}",
|
|
129
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
130
|
+
checked_value="Audit account in Security OU"
|
|
131
|
+
))
|
|
132
|
+
return self.findings
|
|
133
|
+
|
|
134
|
+
accounts = accounts_response.get("Accounts", [])
|
|
135
|
+
account_ids_in_security_ou = [acc.get("Id") for acc in accounts]
|
|
136
|
+
|
|
137
|
+
# Check if audit account(s) are in Security OU
|
|
138
|
+
for audit_account_id in self._audit_accounts:
|
|
139
|
+
if audit_account_id in account_ids_in_security_ou:
|
|
140
|
+
# Find account name for better reporting
|
|
141
|
+
account_name = next(
|
|
142
|
+
(acc.get("Name", "Unknown") for acc in accounts if acc.get("Id") == audit_account_id),
|
|
143
|
+
"Unknown"
|
|
144
|
+
)
|
|
145
|
+
self.findings.append(self.create_finding(
|
|
146
|
+
status="PASS",
|
|
147
|
+
region=region,
|
|
148
|
+
resource_id=audit_account_id,
|
|
149
|
+
actual_value=f"Audit account {audit_account_id} ({account_name}) is in Security OU",
|
|
150
|
+
remediation="No remediation needed",
|
|
151
|
+
checked_value="Audit account in Security OU"
|
|
152
|
+
))
|
|
153
|
+
else:
|
|
154
|
+
self.findings.append(self.create_finding(
|
|
155
|
+
status="FAIL",
|
|
156
|
+
region=region,
|
|
157
|
+
resource_id=audit_account_id,
|
|
158
|
+
actual_value=f"Audit account {audit_account_id} is not in Security OU",
|
|
159
|
+
remediation=(
|
|
160
|
+
f"Move the audit account {audit_account_id} to the Security OU. "
|
|
161
|
+
"Navigate to AWS Organizations in the console, select the audit account, "
|
|
162
|
+
"and move it to the Security OU."
|
|
163
|
+
),
|
|
164
|
+
checked_value="Audit account in Security OU"
|
|
165
|
+
))
|
|
166
|
+
|
|
167
|
+
return self.findings
|