sraverify 0.1.0__py3-none-any.whl → 0.1.2__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 +1 -1
- 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.0.dist-info → sraverify-0.1.2.dist-info}/METADATA +1 -1
- {sraverify-0.1.0.dist-info → sraverify-0.1.2.dist-info}/RECORD +22 -9
- {sraverify-0.1.0.dist-info → sraverify-0.1.2.dist-info}/LICENSE +0 -0
- {sraverify-0.1.0.dist-info → sraverify-0.1.2.dist-info}/NOTICE +0 -0
- {sraverify-0.1.0.dist-info → sraverify-0.1.2.dist-info}/WHEEL +0 -0
- {sraverify-0.1.0.dist-info → sraverify-0.1.2.dist-info}/entry_points.txt +0 -0
- {sraverify-0.1.0.dist-info → sraverify-0.1.2.dist-info}/top_level.txt +0 -0
sraverify/core/logging.py
CHANGED
|
@@ -8,7 +8,7 @@ import sys
|
|
|
8
8
|
logger = logging.getLogger("sraverify")
|
|
9
9
|
|
|
10
10
|
# Create handlers
|
|
11
|
-
console_handler = logging.StreamHandler(sys.
|
|
11
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
12
12
|
|
|
13
13
|
# Create formatters
|
|
14
14
|
default_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
sraverify/main.py
CHANGED
|
@@ -32,6 +32,7 @@ from sraverify.services.auditmanager import CHECKS as auditmanager_checks
|
|
|
32
32
|
from sraverify.services.firewallmanager import CHECKS as firewallmanager_checks
|
|
33
33
|
from sraverify.services.securitylake import CHECKS as securitylake_checks
|
|
34
34
|
from sraverify.services.securityincidentresponse import CHECKS as securityincidentresponse_checks
|
|
35
|
+
from sraverify.services.organizations import CHECKS as organizations_checks
|
|
35
36
|
|
|
36
37
|
# Collect all checks from different services
|
|
37
38
|
ALL_CHECKS = {
|
|
@@ -50,10 +51,8 @@ ALL_CHECKS = {
|
|
|
50
51
|
**auditmanager_checks,
|
|
51
52
|
**firewallmanager_checks,
|
|
52
53
|
**securitylake_checks,
|
|
53
|
-
**securityincidentresponse_checks
|
|
54
|
-
|
|
55
|
-
# **config_checks,
|
|
56
|
-
# etc.
|
|
54
|
+
**securityincidentresponse_checks,
|
|
55
|
+
**organizations_checks
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
class SRAVerify:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AWS Organizations security checks.
|
|
3
|
+
"""
|
|
4
|
+
from sraverify.services.organizations.checks.sra_organizations_01 import SRA_ORGANIZATIONS_01
|
|
5
|
+
from sraverify.services.organizations.checks.sra_organizations_02 import SRA_ORGANIZATIONS_02
|
|
6
|
+
from sraverify.services.organizations.checks.sra_organizations_03 import SRA_ORGANIZATIONS_03
|
|
7
|
+
from sraverify.services.organizations.checks.sra_organizations_04 import SRA_ORGANIZATIONS_04
|
|
8
|
+
from sraverify.services.organizations.checks.sra_organizations_05 import SRA_ORGANIZATIONS_05
|
|
9
|
+
from sraverify.services.organizations.checks.sra_organizations_06 import SRA_ORGANIZATIONS_06
|
|
10
|
+
from sraverify.services.organizations.checks.sra_organizations_07 import SRA_ORGANIZATIONS_07
|
|
11
|
+
from sraverify.services.organizations.checks.sra_organizations_08 import SRA_ORGANIZATIONS_08
|
|
12
|
+
from sraverify.services.organizations.checks.sra_organizations_09 import SRA_ORGANIZATIONS_09
|
|
13
|
+
|
|
14
|
+
# Map check IDs to check classes for easy lookup
|
|
15
|
+
CHECKS = {
|
|
16
|
+
"SRA-ORGANIZATIONS-01": SRA_ORGANIZATIONS_01,
|
|
17
|
+
"SRA-ORGANIZATIONS-02": SRA_ORGANIZATIONS_02,
|
|
18
|
+
"SRA-ORGANIZATIONS-03": SRA_ORGANIZATIONS_03,
|
|
19
|
+
"SRA-ORGANIZATIONS-04": SRA_ORGANIZATIONS_04,
|
|
20
|
+
"SRA-ORGANIZATIONS-05": SRA_ORGANIZATIONS_05,
|
|
21
|
+
"SRA-ORGANIZATIONS-06": SRA_ORGANIZATIONS_06,
|
|
22
|
+
"SRA-ORGANIZATIONS-07": SRA_ORGANIZATIONS_07,
|
|
23
|
+
"SRA-ORGANIZATIONS-08": SRA_ORGANIZATIONS_08,
|
|
24
|
+
"SRA-ORGANIZATIONS-09": SRA_ORGANIZATIONS_09,
|
|
25
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for AWS Organizations security checks.
|
|
3
|
+
"""
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
|
+
from sraverify.core.check import SecurityCheck
|
|
6
|
+
from sraverify.services.organizations.client import OrganizationsClient
|
|
7
|
+
from sraverify.core.logging import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OrganizationsCheck(SecurityCheck):
|
|
11
|
+
"""Base class for all AWS Organizations security checks."""
|
|
12
|
+
|
|
13
|
+
# Class-level caches shared across all instances
|
|
14
|
+
_organization_cache = {}
|
|
15
|
+
_roots_cache = {}
|
|
16
|
+
_ous_cache = {}
|
|
17
|
+
_policies_cache = {}
|
|
18
|
+
_accounts_cache = {}
|
|
19
|
+
|
|
20
|
+
def __init__(self, resource_type: str = "AWS::Organizations::Organization"):
|
|
21
|
+
"""
|
|
22
|
+
Initialize Organizations base check.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
resource_type: AWS resource type for findings (default: Organization)
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(
|
|
28
|
+
account_type="management",
|
|
29
|
+
service="Organizations",
|
|
30
|
+
resource_type=resource_type
|
|
31
|
+
)
|
|
32
|
+
self._org_client = None
|
|
33
|
+
|
|
34
|
+
def _setup_clients(self):
|
|
35
|
+
"""Set up Organizations client (global service, no per-region clients needed)."""
|
|
36
|
+
# Organizations is a global service, we only need one client
|
|
37
|
+
self._org_client = OrganizationsClient(session=self.session)
|
|
38
|
+
# Clear existing regional clients dict since Organizations doesn't use them
|
|
39
|
+
self._clients.clear()
|
|
40
|
+
|
|
41
|
+
def get_org_client(self) -> OrganizationsClient:
|
|
42
|
+
"""
|
|
43
|
+
Get the Organizations client.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
OrganizationsClient instance
|
|
47
|
+
"""
|
|
48
|
+
return self._org_client
|
|
49
|
+
|
|
50
|
+
def get_organization(self) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Get organization details with caching.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary containing organization details or Error key if failed.
|
|
56
|
+
"""
|
|
57
|
+
# Check class-level cache
|
|
58
|
+
cache_key = f"{self.account_id}:organization"
|
|
59
|
+
if cache_key in OrganizationsCheck._organization_cache:
|
|
60
|
+
logger.debug("Organizations: Using cached organization details")
|
|
61
|
+
return OrganizationsCheck._organization_cache[cache_key]
|
|
62
|
+
|
|
63
|
+
# Get organization details
|
|
64
|
+
logger.debug("Organizations: Fetching organization details")
|
|
65
|
+
response = self._org_client.describe_organization()
|
|
66
|
+
|
|
67
|
+
# Cache the response
|
|
68
|
+
OrganizationsCheck._organization_cache[cache_key] = response
|
|
69
|
+
logger.debug("Organizations: Cached organization details")
|
|
70
|
+
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
def get_roots(self) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Get organization roots with caching.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary with Roots key containing list of roots,
|
|
79
|
+
or Error key if failed.
|
|
80
|
+
"""
|
|
81
|
+
# Check class-level cache
|
|
82
|
+
cache_key = f"{self.account_id}:roots"
|
|
83
|
+
if cache_key in OrganizationsCheck._roots_cache:
|
|
84
|
+
logger.debug("Organizations: Using cached roots")
|
|
85
|
+
return OrganizationsCheck._roots_cache[cache_key]
|
|
86
|
+
|
|
87
|
+
# Get roots
|
|
88
|
+
logger.debug("Organizations: Fetching organization roots")
|
|
89
|
+
response = self._org_client.list_roots()
|
|
90
|
+
|
|
91
|
+
# Cache the response
|
|
92
|
+
OrganizationsCheck._roots_cache[cache_key] = response
|
|
93
|
+
logger.debug("Organizations: Cached roots")
|
|
94
|
+
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
def get_ous_for_parent(self, parent_id: str) -> Dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
Get organizational units for a parent with caching.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
parent_id: The ID of the parent root or OU
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary with OrganizationalUnits key containing list of OUs,
|
|
106
|
+
or Error key if failed.
|
|
107
|
+
"""
|
|
108
|
+
# Check class-level cache
|
|
109
|
+
cache_key = f"{self.account_id}:{parent_id}:ous"
|
|
110
|
+
if cache_key in OrganizationsCheck._ous_cache:
|
|
111
|
+
logger.debug(f"Organizations: Using cached OUs for parent {parent_id}")
|
|
112
|
+
return OrganizationsCheck._ous_cache[cache_key]
|
|
113
|
+
|
|
114
|
+
# Get OUs
|
|
115
|
+
logger.debug(f"Organizations: Fetching OUs for parent {parent_id}")
|
|
116
|
+
response = self._org_client.list_organizational_units_for_parent(parent_id)
|
|
117
|
+
|
|
118
|
+
# Cache the response
|
|
119
|
+
OrganizationsCheck._ous_cache[cache_key] = response
|
|
120
|
+
logger.debug(f"Organizations: Cached OUs for parent {parent_id}")
|
|
121
|
+
|
|
122
|
+
return response
|
|
123
|
+
|
|
124
|
+
def list_policies(self, policy_type: str = "SERVICE_CONTROL_POLICY") -> Dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
List policies by type with caching.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
policy_type: Type of policy to list (default: SERVICE_CONTROL_POLICY)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dictionary with Policies key containing list of policies,
|
|
133
|
+
or Error key if failed.
|
|
134
|
+
"""
|
|
135
|
+
# Check class-level cache
|
|
136
|
+
cache_key = f"{self.account_id}:{policy_type}:policies"
|
|
137
|
+
if cache_key in OrganizationsCheck._policies_cache:
|
|
138
|
+
logger.debug(f"Organizations: Using cached policies of type {policy_type}")
|
|
139
|
+
return OrganizationsCheck._policies_cache[cache_key]
|
|
140
|
+
|
|
141
|
+
# Get policies
|
|
142
|
+
logger.debug(f"Organizations: Fetching policies of type {policy_type}")
|
|
143
|
+
response = self._org_client.list_policies(policy_type)
|
|
144
|
+
|
|
145
|
+
# Cache the response
|
|
146
|
+
OrganizationsCheck._policies_cache[cache_key] = response
|
|
147
|
+
logger.debug(f"Organizations: Cached policies of type {policy_type}")
|
|
148
|
+
|
|
149
|
+
return response
|
|
150
|
+
|
|
151
|
+
def get_accounts_for_parent(self, parent_id: str) -> Dict[str, Any]:
|
|
152
|
+
"""
|
|
153
|
+
Get accounts for a parent (root or OU) with caching.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
parent_id: The ID of the parent root or OU
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary with Accounts key containing list of accounts,
|
|
160
|
+
or Error key if failed.
|
|
161
|
+
"""
|
|
162
|
+
# Check class-level cache
|
|
163
|
+
cache_key = f"{self.account_id}:{parent_id}:accounts"
|
|
164
|
+
if cache_key in OrganizationsCheck._accounts_cache:
|
|
165
|
+
logger.debug(f"Organizations: Using cached accounts for parent {parent_id}")
|
|
166
|
+
return OrganizationsCheck._accounts_cache[cache_key]
|
|
167
|
+
|
|
168
|
+
# Get accounts
|
|
169
|
+
logger.debug(f"Organizations: Fetching accounts for parent {parent_id}")
|
|
170
|
+
response = self._org_client.list_accounts_for_parent(parent_id)
|
|
171
|
+
|
|
172
|
+
# Cache the response
|
|
173
|
+
OrganizationsCheck._accounts_cache[cache_key] = response
|
|
174
|
+
logger.debug(f"Organizations: Cached accounts for parent {parent_id}")
|
|
175
|
+
|
|
176
|
+
return response
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if AWS Organizations is enabled.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_01(OrganizationsCheck):
|
|
9
|
+
"""Check if AWS Organizations is enabled."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize Organizations enabled check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::Organization")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-01"
|
|
15
|
+
self.check_name = "AWS Organizations is enabled"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that AWS Organizations is enabled for the account. "
|
|
18
|
+
"AWS Organizations enables central management and governance of multiple AWS accounts, "
|
|
19
|
+
"providing consolidated billing, account management, and policy-based controls."
|
|
20
|
+
)
|
|
21
|
+
self.severity = "HIGH"
|
|
22
|
+
self.check_logic = (
|
|
23
|
+
"Call DescribeOrganization API to confirm an organization exists. "
|
|
24
|
+
"Check passes if an organization is found, fails if no organization exists."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def execute(self) -> List[Dict[str, Any]]:
|
|
28
|
+
"""
|
|
29
|
+
Execute the check.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of findings
|
|
33
|
+
"""
|
|
34
|
+
# Organizations is a global service, use "global" as region
|
|
35
|
+
region = "global"
|
|
36
|
+
|
|
37
|
+
# Get organization details
|
|
38
|
+
response = self.get_organization()
|
|
39
|
+
|
|
40
|
+
# Check for errors
|
|
41
|
+
if "Error" in response:
|
|
42
|
+
error_code = response["Error"].get("Code", "")
|
|
43
|
+
error_message = response["Error"].get("Message", "Unknown error")
|
|
44
|
+
|
|
45
|
+
# AWSOrganizationsNotInUseException means no organization exists
|
|
46
|
+
if error_code == "AWSOrganizationsNotInUseException":
|
|
47
|
+
self.findings.append(self.create_finding(
|
|
48
|
+
status="FAIL",
|
|
49
|
+
region=region,
|
|
50
|
+
resource_id=None,
|
|
51
|
+
actual_value="No organization exists",
|
|
52
|
+
remediation=(
|
|
53
|
+
"Create an AWS Organization by navigating to AWS Organizations in the console "
|
|
54
|
+
"and clicking 'Create organization', or use the AWS CLI command: "
|
|
55
|
+
"aws organizations create-organization"
|
|
56
|
+
),
|
|
57
|
+
checked_value="AWS Organizations enabled"
|
|
58
|
+
))
|
|
59
|
+
else:
|
|
60
|
+
# Other errors (permissions, service errors)
|
|
61
|
+
self.findings.append(self.create_finding(
|
|
62
|
+
status="ERROR",
|
|
63
|
+
region=region,
|
|
64
|
+
resource_id=None,
|
|
65
|
+
actual_value=f"Error: {error_message}",
|
|
66
|
+
remediation="Check IAM permissions for Organizations API access",
|
|
67
|
+
checked_value="AWS Organizations enabled"
|
|
68
|
+
))
|
|
69
|
+
return self.findings
|
|
70
|
+
|
|
71
|
+
# Organization exists - extract details
|
|
72
|
+
organization = response.get("Organization", {})
|
|
73
|
+
org_id = organization.get("Id", "Unknown")
|
|
74
|
+
|
|
75
|
+
self.findings.append(self.create_finding(
|
|
76
|
+
status="PASS",
|
|
77
|
+
region=region,
|
|
78
|
+
resource_id=org_id,
|
|
79
|
+
actual_value=f"Organization exists: {org_id}",
|
|
80
|
+
remediation="No remediation needed",
|
|
81
|
+
checked_value="AWS Organizations enabled"
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
return self.findings
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if organization has foundational OU - Security.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_02(OrganizationsCheck):
|
|
9
|
+
"""Check if organization has foundational OU - Security."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize Security OU check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::OrganizationalUnit")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-02"
|
|
15
|
+
self.check_name = "Organization has foundational OU - Security"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the organization has a Security organizational unit (OU) "
|
|
18
|
+
"directly under the root. The Security OU is a foundational OU recommended by AWS SRA "
|
|
19
|
+
"for isolating security-related accounts such as the Security Tooling and Log Archive accounts."
|
|
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 'Security' "
|
|
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="Security 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="Security 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="Security OU exists under root"
|
|
83
|
+
))
|
|
84
|
+
return self.findings
|
|
85
|
+
|
|
86
|
+
ous = ous_response.get("OrganizationalUnits", [])
|
|
87
|
+
|
|
88
|
+
# Look for Security OU
|
|
89
|
+
security_ou = None
|
|
90
|
+
for ou in ous:
|
|
91
|
+
if ou.get("Name") == "Security":
|
|
92
|
+
security_ou = ou
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if security_ou:
|
|
96
|
+
ou_id = security_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"Security OU exists: {ou_id}",
|
|
102
|
+
remediation="No remediation needed",
|
|
103
|
+
checked_value="Security 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"Security OU not found. Existing OUs under root: {existing_ous_str}",
|
|
114
|
+
remediation=(
|
|
115
|
+
"Create a Security organizational unit under the organization root. "
|
|
116
|
+
"Navigate to AWS Organizations in the console, select the root, and create "
|
|
117
|
+
"a new OU named 'Security'. Alternatively, use the AWS CLI: "
|
|
118
|
+
f"aws organizations create-organizational-unit --parent-id {root_id} --name Security"
|
|
119
|
+
),
|
|
120
|
+
checked_value="Security OU exists under root"
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
return self.findings
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check if organization has foundational OU - Infrastructure.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from sraverify.services.organizations.base import OrganizationsCheck
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SRA_ORGANIZATIONS_03(OrganizationsCheck):
|
|
9
|
+
"""Check if organization has foundational OU - Infrastructure."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize Infrastructure OU check."""
|
|
13
|
+
super().__init__(resource_type="AWS::Organizations::OrganizationalUnit")
|
|
14
|
+
self.check_id = "SRA-ORGANIZATIONS-03"
|
|
15
|
+
self.check_name = "Organization has foundational OU - Infrastructure"
|
|
16
|
+
self.description = (
|
|
17
|
+
"This check verifies that the organization has an Infrastructure organizational unit (OU) "
|
|
18
|
+
"directly under the root. The Infrastructure OU is a foundational OU recommended by AWS SRA "
|
|
19
|
+
"for organizing infrastructure-related accounts such as shared services and networking accounts."
|
|
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 'Infrastructure' "
|
|
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="Infrastructure 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="Infrastructure 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="Infrastructure OU exists under root"
|
|
83
|
+
))
|
|
84
|
+
return self.findings
|
|
85
|
+
|
|
86
|
+
ous = ous_response.get("OrganizationalUnits", [])
|
|
87
|
+
|
|
88
|
+
# Look for Infrastructure OU
|
|
89
|
+
infrastructure_ou = None
|
|
90
|
+
for ou in ous:
|
|
91
|
+
if ou.get("Name") == "Infrastructure":
|
|
92
|
+
infrastructure_ou = ou
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if infrastructure_ou:
|
|
96
|
+
ou_id = infrastructure_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"Infrastructure OU exists: {ou_id}",
|
|
102
|
+
remediation="No remediation needed",
|
|
103
|
+
checked_value="Infrastructure 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"Infrastructure OU not found. Existing OUs under root: {existing_ous_str}",
|
|
114
|
+
remediation=(
|
|
115
|
+
"Create an Infrastructure organizational unit under the organization root. "
|
|
116
|
+
"Navigate to AWS Organizations in the console, select the root, and create "
|
|
117
|
+
"a new OU named 'Infrastructure'. Alternatively, use the AWS CLI: "
|
|
118
|
+
f"aws organizations create-organizational-unit --parent-id {root_id} --name Infrastructure"
|
|
119
|
+
),
|
|
120
|
+
checked_value="Infrastructure OU exists under root"
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
return self.findings
|