sraverify 0.1.1__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.
@@ -0,0 +1,167 @@
1
+ """
2
+ Check if log archive 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_09(OrganizationsCheck):
9
+ """Check if log archive account is in Security OU."""
10
+
11
+ def __init__(self):
12
+ """Initialize log archive account in Security OU check."""
13
+ super().__init__(resource_type="AWS::Organizations::Account")
14
+ self.check_id = "SRA-ORGANIZATIONS-09"
15
+ self.check_name = "Log Archive account is in Security OU"
16
+ self.description = (
17
+ "This check verifies that the log archive account is located in the Security organizational unit. "
18
+ "According to AWS SRA best practices, the log archive account should be placed in the Security OU "
19
+ "to ensure proper isolation and governance of centralized logging infrastructure."
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 log archive account (provided via --log-archive-account CLI parameter) "
25
+ "is found in the Security OU."
26
+ )
27
+ self._log_archive_accounts = [] # Will be populated from CLI --log-archive-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 log archive account is provided
40
+ if not self._log_archive_accounts:
41
+ self.findings.append(self.create_finding(
42
+ status="FAIL",
43
+ region=region,
44
+ resource_id=None,
45
+ actual_value="Log archive account ID not provided",
46
+ remediation="Run check with --log-archive-account parameter to specify the log archive account ID",
47
+ checked_value="Log Archive 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="Log Archive 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="Log Archive 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="Log Archive 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
+ "log archive account into it. Navigate to AWS Organizations in the console, create the "
112
+ "Security OU, then move the log archive account to the Security OU."
113
+ ),
114
+ checked_value="Log Archive 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="Log Archive 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 log archive account(s) are in Security OU
138
+ for log_archive_account_id in self._log_archive_accounts:
139
+ if log_archive_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") == log_archive_account_id),
143
+ "Unknown"
144
+ )
145
+ self.findings.append(self.create_finding(
146
+ status="PASS",
147
+ region=region,
148
+ resource_id=log_archive_account_id,
149
+ actual_value=f"Log archive account {log_archive_account_id} ({account_name}) is in Security OU",
150
+ remediation="No remediation needed",
151
+ checked_value="Log Archive account in Security OU"
152
+ ))
153
+ else:
154
+ self.findings.append(self.create_finding(
155
+ status="FAIL",
156
+ region=region,
157
+ resource_id=log_archive_account_id,
158
+ actual_value=f"Log archive account {log_archive_account_id} is not in Security OU",
159
+ remediation=(
160
+ f"Move the log archive account {log_archive_account_id} to the Security OU. "
161
+ "Navigate to AWS Organizations in the console, select the log archive account, "
162
+ "and move it to the Security OU."
163
+ ),
164
+ checked_value="Log Archive account in Security OU"
165
+ ))
166
+
167
+ return self.findings
@@ -0,0 +1,153 @@
1
+ """
2
+ Organizations client for interacting with AWS Organizations service.
3
+ """
4
+ from typing import Dict, List, Optional, Any
5
+ import boto3
6
+ from botocore.exceptions import ClientError
7
+ from sraverify.core.logging import logger
8
+
9
+
10
+ class OrganizationsClient:
11
+ """Client for interacting with AWS Organizations service."""
12
+
13
+ def __init__(self, session: Optional[boto3.Session] = None):
14
+ """
15
+ Initialize Organizations client.
16
+
17
+ Args:
18
+ session: AWS session to use (if None, a new session will be created)
19
+ """
20
+ self.session = session or boto3.Session()
21
+ # Organizations is a global service, always use us-east-1
22
+ self.client = self.session.client('organizations', region_name='us-east-1')
23
+
24
+ def describe_organization(self) -> Dict[str, Any]:
25
+ """
26
+ Get organization details.
27
+
28
+ Returns:
29
+ Dictionary with Organization key containing organization details,
30
+ or Error key if an error occurred.
31
+ """
32
+ try:
33
+ response = self.client.describe_organization()
34
+ return response
35
+ except ClientError as e:
36
+ error_code = e.response.get('Error', {}).get('Code', '')
37
+ error_message = e.response.get('Error', {}).get('Message', str(e))
38
+ logger.error(f"Error describing organization: {error_message}")
39
+ return {
40
+ "Error": {
41
+ "Code": error_code,
42
+ "Message": error_message
43
+ }
44
+ }
45
+
46
+ def list_roots(self) -> Dict[str, Any]:
47
+ """
48
+ List organization roots with pagination support.
49
+
50
+ Returns:
51
+ Dictionary with Roots key containing list of roots,
52
+ or Error key if an error occurred.
53
+ """
54
+ try:
55
+ roots = []
56
+ paginator = self.client.get_paginator('list_roots')
57
+ for page in paginator.paginate():
58
+ roots.extend(page.get('Roots', []))
59
+ return {"Roots": roots}
60
+ except ClientError as e:
61
+ error_code = e.response.get('Error', {}).get('Code', '')
62
+ error_message = e.response.get('Error', {}).get('Message', str(e))
63
+ logger.error(f"Error listing roots: {error_message}")
64
+ return {
65
+ "Error": {
66
+ "Code": error_code,
67
+ "Message": error_message
68
+ }
69
+ }
70
+
71
+ def list_organizational_units_for_parent(self, parent_id: str) -> Dict[str, Any]:
72
+ """
73
+ List organizational units under a parent with pagination support.
74
+
75
+ Args:
76
+ parent_id: The ID of the parent root or OU
77
+
78
+ Returns:
79
+ Dictionary with OrganizationalUnits key containing list of OUs,
80
+ or Error key if an error occurred.
81
+ """
82
+ try:
83
+ ous = []
84
+ paginator = self.client.get_paginator('list_organizational_units_for_parent')
85
+ for page in paginator.paginate(ParentId=parent_id):
86
+ ous.extend(page.get('OrganizationalUnits', []))
87
+ return {"OrganizationalUnits": ous}
88
+ except ClientError as e:
89
+ error_code = e.response.get('Error', {}).get('Code', '')
90
+ error_message = e.response.get('Error', {}).get('Message', str(e))
91
+ logger.error(f"Error listing OUs for parent {parent_id}: {error_message}")
92
+ return {
93
+ "Error": {
94
+ "Code": error_code,
95
+ "Message": error_message
96
+ }
97
+ }
98
+
99
+ def list_policies(self, policy_type: str = "SERVICE_CONTROL_POLICY") -> Dict[str, Any]:
100
+ """
101
+ List policies by type with pagination support.
102
+
103
+ Args:
104
+ policy_type: Type of policy to list (default: SERVICE_CONTROL_POLICY)
105
+
106
+ Returns:
107
+ Dictionary with Policies key containing list of policies,
108
+ or Error key if an error occurred.
109
+ """
110
+ try:
111
+ policies = []
112
+ paginator = self.client.get_paginator('list_policies')
113
+ for page in paginator.paginate(Filter=policy_type):
114
+ policies.extend(page.get('Policies', []))
115
+ return {"Policies": policies}
116
+ except ClientError as e:
117
+ error_code = e.response.get('Error', {}).get('Code', '')
118
+ error_message = e.response.get('Error', {}).get('Message', str(e))
119
+ logger.error(f"Error listing policies of type {policy_type}: {error_message}")
120
+ return {
121
+ "Error": {
122
+ "Code": error_code,
123
+ "Message": error_message
124
+ }
125
+ }
126
+
127
+ def list_accounts_for_parent(self, parent_id: str) -> Dict[str, Any]:
128
+ """
129
+ List accounts under a parent (root or OU) with pagination support.
130
+
131
+ Args:
132
+ parent_id: The ID of the parent root or OU
133
+
134
+ Returns:
135
+ Dictionary with Accounts key containing list of accounts,
136
+ or Error key if an error occurred.
137
+ """
138
+ try:
139
+ accounts = []
140
+ paginator = self.client.get_paginator('list_accounts_for_parent')
141
+ for page in paginator.paginate(ParentId=parent_id):
142
+ accounts.extend(page.get('Accounts', []))
143
+ return {"Accounts": accounts}
144
+ except ClientError as e:
145
+ error_code = e.response.get('Error', {}).get('Code', '')
146
+ error_message = e.response.get('Error', {}).get('Message', str(e))
147
+ logger.error(f"Error listing accounts for parent {parent_id}: {error_message}")
148
+ return {
149
+ "Error": {
150
+ "Code": error_code,
151
+ "Message": error_message
152
+ }
153
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sraverify
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: AWS Security Reference Architecture Verification Tool
5
5
  Home-page: https://github.com/awslabs/sra-verify
6
6
  Author: SRA Verify team
@@ -1,5 +1,5 @@
1
1
  sraverify/__init__.py,sha256=6gfuKM0QW6xcFPeN782mbPiCK8PEolaXL05Mbdli32Y,1201
2
- sraverify/main.py,sha256=nq6Z5oPcgZeOYUKZEKSr9tDEEoODiqHgVNAwrgsFJpk,14449
2
+ sraverify/main.py,sha256=Ohfe-jymToqLpdsMnDm_q73MwQ-5DnkEU4cd98aczHI,14461
3
3
  sraverify/checks/__init__.py,sha256=I12r97y4IJH00Y2yUgxK_VJn1eANva19v0FMdydnGvA,2033
4
4
  sraverify/checks/accessanalyzer/SRA_IAA_1.py,sha256=hsVfgo-ia7uvNs4PNdk2ikTAxVUOmViQ8KeTJXJ7Ljg,8205
5
5
  sraverify/checks/accessanalyzer/SRA_IAA_2.py,sha256=mcDEhQdqu4w9e_Egdtcc0iTKHSnpm4VMf_R559Wi5pQ,8251
@@ -24,7 +24,7 @@ sraverify/checks/config/SRA-CONFIG-1.py,sha256=DT2Nce99vFHBuFjVDyLYQc2VCcBmziXlj
24
24
  sraverify/checks/config/__init__.py,sha256=ZcR-cj0dQNGFi_TKwOREXGr3Onju9VtFsds8rtbBg6s,55
25
25
  sraverify/core/__init__.py,sha256=zoDY1Q-oGF9IVGD5f07yJ4d1KuWKQq3IqEDmetOFdok,54
26
26
  sraverify/core/check.py,sha256=MDjjHV3mFYprEeHCEadzme3A0UHUpLEbLJ4wGpi0rSU,8559
27
- sraverify/core/logging.py,sha256=yBxy9Js3ktMfzv1N_HKVRViCvW7d5gVK2-AzIvwQYFQ,870
27
+ sraverify/core/logging.py,sha256=25k76RLkxDaGQW9I__88r45bqWMP7A0vEJaP87hv_Hs,870
28
28
  sraverify/core/session.py,sha256=vbLyGEhFJ8thU9BHgN80MexZLoJMKlZDFIcGwKECblo,1507
29
29
  sraverify/lib/__init__.py,sha256=ENiW27PYy5d_2BeoIxdxckFmW9KcrVX1dp97smmVx0c,55
30
30
  sraverify/lib/audit_info.py,sha256=c6O4I9mJFjxzHFLROQ_1PcR0lQmT_tA0JQMtCNUlS2s,1553
@@ -164,6 +164,19 @@ sraverify/services/macie/checks/sra_macie_07.py,sha256=QMNS6lds_9yzM3IAwVUBM0utG
164
164
  sraverify/services/macie/checks/sra_macie_08.py,sha256=2CX2r8JZ58OgkpO2VpCtA32mtsItxbRRC-8k2NQuiN8,3631
165
165
  sraverify/services/macie/checks/sra_macie_09.py,sha256=ivKtS2ZX9AfpJ4iQyie1f5bM_Y0FolnWM2zppilLNw0,4701
166
166
  sraverify/services/macie/checks/sra_macie_10.py,sha256=CpsIR43yxXtcEOo0NdlTUdSZSa_2g-jSSQ_Y6on5biM,3406
167
+ sraverify/services/organizations/__init__.py,sha256=CSnyU3J9e985U7W4HSAHwQA2Q5UXzxjEhCALzKpxcRE,1402
168
+ sraverify/services/organizations/base.py,sha256=BWR656oIIhvSCnSMRb6GUWfvo7BAaXpLGwxIG30VWAM,6578
169
+ sraverify/services/organizations/client.py,sha256=xyIm34ei5jRtSMxLPInGLSjzutryiu_xpKCm8wy_KB4,5860
170
+ sraverify/services/organizations/checks/__init__.py,sha256=y6QCfZhmcKUPml_LUuIiDOiAFjwKr4Bibog9JxPxazQ,58
171
+ sraverify/services/organizations/checks/sra_organizations_01.py,sha256=lOvLU7f0FL54R6DEnf5trjVEfq8s1ZYcpqYWtarmdgA,3410
172
+ sraverify/services/organizations/checks/sra_organizations_02.py,sha256=ntDmc2MXmCyLgC8S-AxTh-g5t5-q-Yd5PMRZHoc6HOA,5090
173
+ sraverify/services/organizations/checks/sra_organizations_03.py,sha256=QpRwu1_hidgDnhVJvOcrozSGsEZe8XB0gGP8Fe1vFcQ,5143
174
+ sraverify/services/organizations/checks/sra_organizations_04.py,sha256=GNmORLBislFj85JQveiLwpKPSP8NzeuPdJ5hq18mei8,5029
175
+ sraverify/services/organizations/checks/sra_organizations_05.py,sha256=hxXky9D4aclvf3jb3SfchYPwvT6TiV1YUr0uqjx00nk,3814
176
+ sraverify/services/organizations/checks/sra_organizations_06.py,sha256=tZ5kaH5eBsJKaSo7BUdvjndlR08eX9BZf3L8PDOtERM,5622
177
+ sraverify/services/organizations/checks/sra_organizations_07.py,sha256=YG1KwupIGFp2Vw1amt4QIk992OZOsg4GF6ALUnjpKC0,5965
178
+ sraverify/services/organizations/checks/sra_organizations_08.py,sha256=-atpxnq6lSoH8qE50Drpl7Qh-W2BV0rEJBTWU5k5QL8,7120
179
+ sraverify/services/organizations/checks/sra_organizations_09.py,sha256=rAM3m8bNpkK7Bz0R4W6SWbXkAKGM0f5gmX7AGvRMvKk,7345
167
180
  sraverify/services/s3/__init__.py,sha256=x0vB4X1I_b66RlKfcJDn7ZvIoTywySHK0vWBvoLDcyY,417
168
181
  sraverify/services/s3/base.py,sha256=-3czNyp2vZ_HKQ3xh6gtAG9AHnPpxtDTaIA1OF1I1BQ,2542
169
182
  sraverify/services/s3/client.py,sha256=acd3YlcK-FyIvdAg5obcqHG2HdGbJk8WO0BwYHrHvCc,2146
@@ -252,10 +265,10 @@ sraverify/utils/__init__.py,sha256=IjhsKkC2WOXrINnNksrNX69R5XwTCTO6gVlLkv_guxQ,5
252
265
  sraverify/utils/banner.py,sha256=zMLBKuw7G8mielJeMXXPaAMnwKy6CpapX-1GVfKyux8,2756
253
266
  sraverify/utils/outputs.py,sha256=HlEuy21RgrcTuRjWkFDfrY6GfO1DNVzGMBLOxvemjek,1721
254
267
  sraverify/utils/progress.py,sha256=B_Dhaep3inav3SC1ZpKPcH74tgJqf6wmY2ILzAGbOeU,3190
255
- sraverify-0.1.1.dist-info/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
256
- sraverify-0.1.1.dist-info/METADATA,sha256=VnxpjniXZflUNTEnpIsPIgtxXAP9YXA78nexza0Q_qs,17007
257
- sraverify-0.1.1.dist-info/NOTICE,sha256=1CkO1kwu3Q_OHYTj-d-yiBJA_lNN73a4zSntavaD4oc,67
258
- sraverify-0.1.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
259
- sraverify-0.1.1.dist-info/entry_points.txt,sha256=lewX6FKnqbko4m2xq4N_UiJbzSrghnjoZyD1YdBmoO8,50
260
- sraverify-0.1.1.dist-info/top_level.txt,sha256=dqkttmF4ZzAyRMF2tDxRuIvDZGjyVIoV9eDqK5sMzYM,10
261
- sraverify-0.1.1.dist-info/RECORD,,
268
+ sraverify-0.1.2.dist-info/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
269
+ sraverify-0.1.2.dist-info/METADATA,sha256=arIlCtuSkVPoi-MDbjx5kg3lBalq5zVh0ghAn0ZMxkI,17007
270
+ sraverify-0.1.2.dist-info/NOTICE,sha256=1CkO1kwu3Q_OHYTj-d-yiBJA_lNN73a4zSntavaD4oc,67
271
+ sraverify-0.1.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
272
+ sraverify-0.1.2.dist-info/entry_points.txt,sha256=lewX6FKnqbko4m2xq4N_UiJbzSrghnjoZyD1YdBmoO8,50
273
+ sraverify-0.1.2.dist-info/top_level.txt,sha256=dqkttmF4ZzAyRMF2tDxRuIvDZGjyVIoV9eDqK5sMzYM,10
274
+ sraverify-0.1.2.dist-info/RECORD,,