complio 0.1.1__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.
- CHANGELOG.md +208 -0
- README.md +343 -0
- complio/__init__.py +48 -0
- complio/cli/__init__.py +0 -0
- complio/cli/banner.py +87 -0
- complio/cli/commands/__init__.py +0 -0
- complio/cli/commands/history.py +439 -0
- complio/cli/commands/scan.py +700 -0
- complio/cli/main.py +115 -0
- complio/cli/output.py +338 -0
- complio/config/__init__.py +17 -0
- complio/config/settings.py +333 -0
- complio/connectors/__init__.py +9 -0
- complio/connectors/aws/__init__.py +0 -0
- complio/connectors/aws/client.py +342 -0
- complio/connectors/base.py +135 -0
- complio/core/__init__.py +10 -0
- complio/core/registry.py +228 -0
- complio/core/runner.py +351 -0
- complio/py.typed +0 -0
- complio/reporters/__init__.py +7 -0
- complio/reporters/generator.py +417 -0
- complio/tests_library/__init__.py +0 -0
- complio/tests_library/base.py +492 -0
- complio/tests_library/identity/__init__.py +0 -0
- complio/tests_library/identity/access_key_rotation.py +302 -0
- complio/tests_library/identity/mfa_enforcement.py +327 -0
- complio/tests_library/identity/root_account_protection.py +470 -0
- complio/tests_library/infrastructure/__init__.py +0 -0
- complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
- complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
- complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
- complio/tests_library/infrastructure/ebs_encryption.py +244 -0
- complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
- complio/tests_library/infrastructure/iam_password_policy.py +460 -0
- complio/tests_library/infrastructure/nacl_security.py +356 -0
- complio/tests_library/infrastructure/rds_encryption.py +252 -0
- complio/tests_library/infrastructure/s3_encryption.py +301 -0
- complio/tests_library/infrastructure/s3_public_access.py +369 -0
- complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
- complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
- complio/tests_library/logging/__init__.py +0 -0
- complio/tests_library/logging/cloudwatch_alarms.py +354 -0
- complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
- complio/tests_library/logging/cloudwatch_retention.py +252 -0
- complio/tests_library/logging/config_enabled.py +393 -0
- complio/tests_library/logging/eventbridge_rules.py +460 -0
- complio/tests_library/logging/guardduty_enabled.py +436 -0
- complio/tests_library/logging/security_hub_enabled.py +416 -0
- complio/tests_library/logging/sns_encryption.py +273 -0
- complio/tests_library/network/__init__.py +0 -0
- complio/tests_library/network/alb_nlb_security.py +421 -0
- complio/tests_library/network/api_gateway_security.py +452 -0
- complio/tests_library/network/cloudfront_https.py +332 -0
- complio/tests_library/network/direct_connect_security.py +343 -0
- complio/tests_library/network/nacl_configuration.py +367 -0
- complio/tests_library/network/network_firewall.py +355 -0
- complio/tests_library/network/transit_gateway_security.py +318 -0
- complio/tests_library/network/vpc_endpoints_security.py +339 -0
- complio/tests_library/network/vpn_security.py +333 -0
- complio/tests_library/network/waf_configuration.py +428 -0
- complio/tests_library/security/__init__.py +0 -0
- complio/tests_library/security/kms_key_rotation.py +314 -0
- complio/tests_library/storage/__init__.py +0 -0
- complio/tests_library/storage/backup_encryption.py +288 -0
- complio/tests_library/storage/dynamodb_encryption.py +280 -0
- complio/tests_library/storage/efs_encryption.py +257 -0
- complio/tests_library/storage/elasticache_encryption.py +370 -0
- complio/tests_library/storage/redshift_encryption.py +252 -0
- complio/tests_library/storage/s3_versioning.py +264 -0
- complio/utils/__init__.py +26 -0
- complio/utils/errors.py +179 -0
- complio/utils/exceptions.py +151 -0
- complio/utils/history.py +243 -0
- complio/utils/logger.py +391 -0
- complio-0.1.1.dist-info/METADATA +385 -0
- complio-0.1.1.dist-info/RECORD +79 -0
- complio-0.1.1.dist-info/WHEEL +4 -0
- complio-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CloudWatch Logs retention compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all CloudWatch log groups have retention policies configured.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.15 - Logging
|
|
7
|
+
Requirement: Log groups must have retention policies to manage log lifecycle
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.logging.cloudwatch_retention import CloudWatchRetentionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = CloudWatchRetentionTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
from botocore.exceptions import ClientError
|
|
24
|
+
|
|
25
|
+
from complio.connectors.aws.client import AWSConnector
|
|
26
|
+
from complio.tests_library.base import (
|
|
27
|
+
ComplianceTest,
|
|
28
|
+
Severity,
|
|
29
|
+
TestResult,
|
|
30
|
+
TestStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CloudWatchRetentionTest(ComplianceTest):
|
|
35
|
+
"""Test for CloudWatch Logs retention compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all CloudWatch log groups have retention policies configured
|
|
38
|
+
to prevent indefinite log storage and manage costs.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- All log groups must have retention policy set (not "Never expire")
|
|
42
|
+
- Retention period should align with compliance requirements
|
|
43
|
+
- Typical retention: 30, 90, 180, 365 days or longer
|
|
44
|
+
|
|
45
|
+
Scoring:
|
|
46
|
+
- 100% if all log groups have retention configured
|
|
47
|
+
- Proportional score based on compliant/total ratio
|
|
48
|
+
- 100% if no log groups exist
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> test = CloudWatchRetentionTest(connector)
|
|
52
|
+
>>> result = test.execute()
|
|
53
|
+
>>> for finding in result.findings:
|
|
54
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
58
|
+
"""Initialize CloudWatch retention test.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connector: AWS connector instance
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
test_id="cloudwatch_retention",
|
|
65
|
+
test_name="CloudWatch Logs Retention Check",
|
|
66
|
+
description="Verify all CloudWatch log groups have retention policies configured",
|
|
67
|
+
control_id="A.8.15",
|
|
68
|
+
connector=connector,
|
|
69
|
+
scope="regional",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def execute(self) -> TestResult:
|
|
73
|
+
"""Execute CloudWatch Logs retention compliance test.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TestResult with findings for log groups without retention
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> test = CloudWatchRetentionTest(connector)
|
|
80
|
+
>>> result = test.execute()
|
|
81
|
+
>>> print(result.score)
|
|
82
|
+
95.0
|
|
83
|
+
"""
|
|
84
|
+
result = TestResult(
|
|
85
|
+
test_id=self.test_id,
|
|
86
|
+
test_name=self.test_name,
|
|
87
|
+
status=TestStatus.PASSED,
|
|
88
|
+
passed=True,
|
|
89
|
+
score=100.0,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Get CloudWatch Logs client
|
|
94
|
+
logs_client = self.connector.get_client("logs")
|
|
95
|
+
|
|
96
|
+
# List all log groups
|
|
97
|
+
self.logger.info("listing_log_groups")
|
|
98
|
+
log_groups = []
|
|
99
|
+
|
|
100
|
+
paginator = logs_client.get_paginator("describe_log_groups")
|
|
101
|
+
for page in paginator.paginate():
|
|
102
|
+
log_groups.extend(page.get("logGroups", []))
|
|
103
|
+
|
|
104
|
+
if not log_groups:
|
|
105
|
+
self.logger.info("no_log_groups_found")
|
|
106
|
+
result.metadata["message"] = "No CloudWatch log groups found in region"
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
self.logger.info("log_groups_found", count=len(log_groups))
|
|
110
|
+
|
|
111
|
+
# Check retention for each log group
|
|
112
|
+
retention_configured_count = 0
|
|
113
|
+
|
|
114
|
+
for log_group in log_groups:
|
|
115
|
+
log_group_name = log_group["logGroupName"]
|
|
116
|
+
result.resources_scanned += 1
|
|
117
|
+
|
|
118
|
+
# Check if retention is configured
|
|
119
|
+
# retentionInDays is only present if retention is set
|
|
120
|
+
retention_in_days = log_group.get("retentionInDays")
|
|
121
|
+
has_retention = retention_in_days is not None
|
|
122
|
+
|
|
123
|
+
# Create evidence
|
|
124
|
+
evidence = self.create_evidence(
|
|
125
|
+
resource_id=log_group_name,
|
|
126
|
+
resource_type="cloudwatch_log_group",
|
|
127
|
+
data={
|
|
128
|
+
"log_group_name": log_group_name,
|
|
129
|
+
"retention_in_days": retention_in_days,
|
|
130
|
+
"has_retention": has_retention,
|
|
131
|
+
"creation_time": log_group.get("creationTime"),
|
|
132
|
+
"stored_bytes": log_group.get("storedBytes", 0),
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
result.add_evidence(evidence)
|
|
136
|
+
|
|
137
|
+
if has_retention:
|
|
138
|
+
retention_configured_count += 1
|
|
139
|
+
self.logger.debug(
|
|
140
|
+
"log_group_has_retention",
|
|
141
|
+
log_group=log_group_name,
|
|
142
|
+
retention_days=retention_in_days
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
# Create finding for log group without retention
|
|
146
|
+
finding = self.create_finding(
|
|
147
|
+
resource_id=log_group_name,
|
|
148
|
+
resource_type="cloudwatch_log_group",
|
|
149
|
+
severity=Severity.MEDIUM,
|
|
150
|
+
title="CloudWatch log group has no retention policy",
|
|
151
|
+
description=f"CloudWatch log group '{log_group_name}' does not have a retention "
|
|
152
|
+
"policy configured (set to 'Never expire'). Without retention policies, "
|
|
153
|
+
"logs are stored indefinitely, leading to unnecessary storage costs and "
|
|
154
|
+
"making it difficult to manage log lifecycle according to compliance "
|
|
155
|
+
"requirements. ISO 27001 A.8.15 requires proper logging practices "
|
|
156
|
+
"including log retention management.",
|
|
157
|
+
remediation=(
|
|
158
|
+
f"Set retention policy for log group '{log_group_name}':\n\n"
|
|
159
|
+
"Using AWS CLI (example: 90 days retention):\n"
|
|
160
|
+
f"aws logs put-retention-policy \\\n"
|
|
161
|
+
f" --log-group-name {log_group_name} \\\n"
|
|
162
|
+
" --retention-in-days 90\n\n"
|
|
163
|
+
"Common retention periods:\n"
|
|
164
|
+
"- 1, 3, 5, 7, 14 days (development/testing)\n"
|
|
165
|
+
"- 30, 60, 90 days (operational logs)\n"
|
|
166
|
+
"- 120, 150, 180 days (compliance logs)\n"
|
|
167
|
+
"- 365, 400, 545, 731 days (audit logs)\n"
|
|
168
|
+
"- 1827, 2192, 2557, 2922, 3288, 3653 days (long-term compliance)\n\n"
|
|
169
|
+
"Or use AWS Console:\n"
|
|
170
|
+
"1. Go to CloudWatch Logs console\n"
|
|
171
|
+
f"2. Select log group '{log_group_name}'\n"
|
|
172
|
+
"3. Click 'Actions' → 'Edit retention setting'\n"
|
|
173
|
+
"4. Select appropriate retention period\n"
|
|
174
|
+
"5. Click 'Save'\n\n"
|
|
175
|
+
"Note: Consider your compliance requirements (GDPR, HIPAA, PCI-DSS, etc.) "
|
|
176
|
+
"when choosing retention periods. Archive to S3 for longer retention if needed."
|
|
177
|
+
),
|
|
178
|
+
evidence=evidence
|
|
179
|
+
)
|
|
180
|
+
result.add_finding(finding)
|
|
181
|
+
|
|
182
|
+
self.logger.warning(
|
|
183
|
+
"log_group_no_retention",
|
|
184
|
+
log_group=log_group_name
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Calculate compliance score
|
|
188
|
+
result.score = (retention_configured_count / len(log_groups)) * 100
|
|
189
|
+
|
|
190
|
+
# Determine pass/fail
|
|
191
|
+
result.passed = retention_configured_count == len(log_groups)
|
|
192
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
193
|
+
|
|
194
|
+
# Add metadata
|
|
195
|
+
result.metadata = {
|
|
196
|
+
"total_log_groups": len(log_groups),
|
|
197
|
+
"retention_configured": retention_configured_count,
|
|
198
|
+
"no_retention": len(log_groups) - retention_configured_count,
|
|
199
|
+
"compliance_percentage": result.score,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
self.logger.info(
|
|
203
|
+
"cloudwatch_retention_test_completed",
|
|
204
|
+
total_log_groups=len(log_groups),
|
|
205
|
+
retention_configured=retention_configured_count,
|
|
206
|
+
score=result.score,
|
|
207
|
+
passed=result.passed
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
except ClientError as e:
|
|
211
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
212
|
+
self.logger.error("cloudwatch_retention_test_error", error_code=error_code, error=str(e))
|
|
213
|
+
result.status = TestStatus.ERROR
|
|
214
|
+
result.passed = False
|
|
215
|
+
result.score = 0.0
|
|
216
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
self.logger.error("cloudwatch_retention_test_error", error=str(e))
|
|
220
|
+
result.status = TestStatus.ERROR
|
|
221
|
+
result.passed = False
|
|
222
|
+
result.score = 0.0
|
|
223
|
+
result.error_message = str(e)
|
|
224
|
+
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ============================================================================
|
|
229
|
+
# CONVENIENCE FUNCTION
|
|
230
|
+
# ============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def run_cloudwatch_retention_test(connector: AWSConnector) -> TestResult:
|
|
234
|
+
"""Run CloudWatch Logs retention compliance test.
|
|
235
|
+
|
|
236
|
+
Convenience function for running the test.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
connector: AWS connector
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
TestResult
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
246
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
247
|
+
>>> connector.connect()
|
|
248
|
+
>>> result = run_cloudwatch_retention_test(connector)
|
|
249
|
+
>>> print(f"Score: {result.score}%")
|
|
250
|
+
"""
|
|
251
|
+
test = CloudWatchRetentionTest(connector)
|
|
252
|
+
return test.execute()
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AWS Config enabled compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that AWS Config is enabled for configuration tracking and compliance.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.16 - Monitoring activities
|
|
7
|
+
Requirement: AWS Config should be enabled for resource configuration tracking
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.logging.config_enabled import ConfigEnabledTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = ConfigEnabledTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
from botocore.exceptions import ClientError
|
|
24
|
+
|
|
25
|
+
from complio.connectors.aws.client import AWSConnector
|
|
26
|
+
from complio.tests_library.base import (
|
|
27
|
+
ComplianceTest,
|
|
28
|
+
Severity,
|
|
29
|
+
TestResult,
|
|
30
|
+
TestStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConfigEnabledTest(ComplianceTest):
|
|
35
|
+
"""Test for AWS Config enabled compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that AWS Config is properly enabled and configured:
|
|
38
|
+
- Configuration recorder should be created
|
|
39
|
+
- Configuration recorder should be recording (not stopped)
|
|
40
|
+
- Delivery channel should be configured (S3 bucket)
|
|
41
|
+
- Should be recording all resource types (global and regional)
|
|
42
|
+
|
|
43
|
+
Compliance Requirements:
|
|
44
|
+
- AWS Config enabled in region
|
|
45
|
+
- Recording all supported resource types
|
|
46
|
+
- Delivery channel configured with S3 bucket
|
|
47
|
+
- Configuration recorder in RECORDING state
|
|
48
|
+
|
|
49
|
+
Scoring:
|
|
50
|
+
- 100% if AWS Config is properly enabled and recording
|
|
51
|
+
- 0% if AWS Config is not enabled or not recording
|
|
52
|
+
- Partial score for partial configuration
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> test = ConfigEnabledTest(connector)
|
|
56
|
+
>>> result = test.execute()
|
|
57
|
+
>>> for finding in result.findings:
|
|
58
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
62
|
+
"""Initialize AWS Config enabled test.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
connector: AWS connector instance
|
|
66
|
+
"""
|
|
67
|
+
super().__init__(
|
|
68
|
+
test_id="config_enabled",
|
|
69
|
+
test_name="AWS Config Enabled Check",
|
|
70
|
+
description="Verify AWS Config is enabled for configuration tracking and compliance",
|
|
71
|
+
control_id="A.8.16",
|
|
72
|
+
connector=connector,
|
|
73
|
+
scope="regional",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def execute(self) -> TestResult:
|
|
77
|
+
"""Execute AWS Config enabled compliance test.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
TestResult with findings if Config is not properly enabled
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> test = ConfigEnabledTest(connector)
|
|
84
|
+
>>> result = test.execute()
|
|
85
|
+
>>> print(result.score)
|
|
86
|
+
100.0
|
|
87
|
+
"""
|
|
88
|
+
result = TestResult(
|
|
89
|
+
test_id=self.test_id,
|
|
90
|
+
test_name=self.test_name,
|
|
91
|
+
status=TestStatus.PASSED,
|
|
92
|
+
passed=True,
|
|
93
|
+
score=100.0,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Get Config client
|
|
98
|
+
config_client = self.connector.get_client("config")
|
|
99
|
+
|
|
100
|
+
# Check configuration recorders
|
|
101
|
+
self.logger.info("checking_config_recorders")
|
|
102
|
+
recorders_response = config_client.describe_configuration_recorders()
|
|
103
|
+
recorders = recorders_response.get("ConfigurationRecorders", [])
|
|
104
|
+
|
|
105
|
+
if not recorders:
|
|
106
|
+
self.logger.warning("no_config_recorders_found")
|
|
107
|
+
|
|
108
|
+
# Create finding for missing Config
|
|
109
|
+
finding = self.create_finding(
|
|
110
|
+
resource_id="aws-config",
|
|
111
|
+
resource_type="config_service",
|
|
112
|
+
severity=Severity.HIGH,
|
|
113
|
+
title="AWS Config not enabled",
|
|
114
|
+
description="AWS Config is not enabled in this region. AWS Config provides configuration "
|
|
115
|
+
"history, change tracking, and compliance monitoring for AWS resources. "
|
|
116
|
+
"Without Config, you cannot track configuration changes, assess compliance "
|
|
117
|
+
"against rules, or perform resource inventory. ISO 27001 A.8.16 requires "
|
|
118
|
+
"monitoring of system activities and configuration changes.",
|
|
119
|
+
remediation=(
|
|
120
|
+
"Enable AWS Config in this region:\n\n"
|
|
121
|
+
"1. Create S3 bucket for Config data:\n"
|
|
122
|
+
"aws s3 mb s3://my-config-bucket-<account-id>-<region>\n\n"
|
|
123
|
+
"# Enable versioning on the bucket\n"
|
|
124
|
+
"aws s3api put-bucket-versioning \\\n"
|
|
125
|
+
" --bucket my-config-bucket-<account-id>-<region> \\\n"
|
|
126
|
+
" --versioning-configuration Status=Enabled\n\n"
|
|
127
|
+
"2. Create IAM role for Config:\n"
|
|
128
|
+
"aws iam create-role \\\n"
|
|
129
|
+
" --role-name AWSConfigRole \\\n"
|
|
130
|
+
" --assume-role-policy-document file://config-trust-policy.json\n\n"
|
|
131
|
+
"# Attach AWS managed policy\n"
|
|
132
|
+
"aws iam attach-role-policy \\\n"
|
|
133
|
+
" --role-name AWSConfigRole \\\n"
|
|
134
|
+
" --policy-arn arn:aws:iam::aws:policy/service-role/ConfigRole\n\n"
|
|
135
|
+
"3. Create configuration recorder:\n"
|
|
136
|
+
"aws configservice put-configuration-recorder \\\n"
|
|
137
|
+
" --configuration-recorder name=default,roleARN=arn:aws:iam::<account-id>:role/AWSConfigRole \\\n"
|
|
138
|
+
" --recording-group allSupported=true,includeGlobalResourceTypes=true\n\n"
|
|
139
|
+
"4. Create delivery channel:\n"
|
|
140
|
+
"aws configservice put-delivery-channel \\\n"
|
|
141
|
+
" --delivery-channel name=default,s3BucketName=my-config-bucket-<account-id>-<region>\n\n"
|
|
142
|
+
"5. Start configuration recorder:\n"
|
|
143
|
+
"aws configservice start-configuration-recorder \\\n"
|
|
144
|
+
" --configuration-recorder-name default\n\n"
|
|
145
|
+
"Or use AWS Console:\n"
|
|
146
|
+
"1. Go to AWS Config console\n"
|
|
147
|
+
"2. Click 'Get started' (if first time)\n"
|
|
148
|
+
"3. Configure settings:\n"
|
|
149
|
+
" - Resource types to record: All resources\n"
|
|
150
|
+
" - Include global resources: Yes (in one region)\n"
|
|
151
|
+
" - S3 bucket: Create new or use existing\n"
|
|
152
|
+
" - SNS topic: Optional (for notifications)\n"
|
|
153
|
+
" - IAM role: Create new or use existing\n"
|
|
154
|
+
"4. Click 'Confirm'\n\n"
|
|
155
|
+
"Best practices:\n"
|
|
156
|
+
"- Enable Config in all regions\n"
|
|
157
|
+
"- Record all supported resource types\n"
|
|
158
|
+
"- Include global resources in one region (usually us-east-1)\n"
|
|
159
|
+
"- Enable S3 bucket encryption\n"
|
|
160
|
+
"- Set up SNS notifications for changes\n"
|
|
161
|
+
"- Configure Config Rules for compliance checks\n"
|
|
162
|
+
"- Use Config Aggregator for multi-account view\n"
|
|
163
|
+
"- Set up lifecycle policies for S3 bucket\n"
|
|
164
|
+
"- Enable CloudTrail for Config API activity\n"
|
|
165
|
+
"- Use AWS Organizations for centralized Config\n\n"
|
|
166
|
+
"Config Rules to consider:\n"
|
|
167
|
+
"- required-tags (ensure proper tagging)\n"
|
|
168
|
+
"- encrypted-volumes (EBS encryption)\n"
|
|
169
|
+
"- rds-storage-encrypted (RDS encryption)\n"
|
|
170
|
+
"- s3-bucket-public-read-prohibited\n"
|
|
171
|
+
"- s3-bucket-public-write-prohibited\n"
|
|
172
|
+
"- cloudtrail-enabled\n"
|
|
173
|
+
"- iam-password-policy\n"
|
|
174
|
+
"- root-account-mfa-enabled\n"
|
|
175
|
+
"- vpc-flow-logs-enabled"
|
|
176
|
+
),
|
|
177
|
+
evidence=None
|
|
178
|
+
)
|
|
179
|
+
result.add_finding(finding)
|
|
180
|
+
result.score = 0.0
|
|
181
|
+
result.passed = False
|
|
182
|
+
result.status = TestStatus.FAILED
|
|
183
|
+
result.metadata = {
|
|
184
|
+
"config_enabled": False,
|
|
185
|
+
"message": "AWS Config not enabled in region"
|
|
186
|
+
}
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
# Check first recorder (typically there's only one)
|
|
190
|
+
recorder = recorders[0]
|
|
191
|
+
recorder_name = recorder.get("name", "")
|
|
192
|
+
role_arn = recorder.get("roleARN", "")
|
|
193
|
+
recording_group = recorder.get("recordingGroup", {})
|
|
194
|
+
|
|
195
|
+
all_supported = recording_group.get("allSupported", False)
|
|
196
|
+
include_global = recording_group.get("includeGlobalResourceTypes", False)
|
|
197
|
+
|
|
198
|
+
result.resources_scanned += 1
|
|
199
|
+
|
|
200
|
+
# Check recorder status
|
|
201
|
+
status_response = config_client.describe_configuration_recorder_status(
|
|
202
|
+
ConfigurationRecorderNames=[recorder_name]
|
|
203
|
+
)
|
|
204
|
+
recorder_statuses = status_response.get("ConfigurationRecordersStatus", [])
|
|
205
|
+
|
|
206
|
+
is_recording = False
|
|
207
|
+
last_status = None
|
|
208
|
+
if recorder_statuses:
|
|
209
|
+
recorder_status = recorder_statuses[0]
|
|
210
|
+
is_recording = recorder_status.get("recording", False)
|
|
211
|
+
last_status = recorder_status.get("lastStatus", "")
|
|
212
|
+
|
|
213
|
+
# Check delivery channel
|
|
214
|
+
channels_response = config_client.describe_delivery_channels()
|
|
215
|
+
channels = channels_response.get("DeliveryChannels", [])
|
|
216
|
+
has_delivery_channel = len(channels) > 0
|
|
217
|
+
|
|
218
|
+
# Determine issues
|
|
219
|
+
issues = []
|
|
220
|
+
severity = Severity.HIGH
|
|
221
|
+
|
|
222
|
+
if not is_recording:
|
|
223
|
+
issues.append("Configuration recorder not recording (stopped)")
|
|
224
|
+
severity = Severity.HIGH
|
|
225
|
+
|
|
226
|
+
if not all_supported:
|
|
227
|
+
issues.append("Not recording all supported resource types")
|
|
228
|
+
severity = Severity.MEDIUM
|
|
229
|
+
|
|
230
|
+
if not has_delivery_channel:
|
|
231
|
+
issues.append("No delivery channel configured (no S3 bucket)")
|
|
232
|
+
severity = Severity.HIGH
|
|
233
|
+
|
|
234
|
+
if last_status and last_status != "SUCCESS":
|
|
235
|
+
issues.append(f"Last recording status: {last_status}")
|
|
236
|
+
severity = Severity.MEDIUM
|
|
237
|
+
|
|
238
|
+
# Create evidence
|
|
239
|
+
evidence = self.create_evidence(
|
|
240
|
+
resource_id=recorder_name,
|
|
241
|
+
resource_type="config_recorder",
|
|
242
|
+
data={
|
|
243
|
+
"recorder_name": recorder_name,
|
|
244
|
+
"role_arn": role_arn,
|
|
245
|
+
"is_recording": is_recording,
|
|
246
|
+
"all_supported": all_supported,
|
|
247
|
+
"include_global": include_global,
|
|
248
|
+
"has_delivery_channel": has_delivery_channel,
|
|
249
|
+
"last_status": last_status,
|
|
250
|
+
"has_issues": len(issues) > 0,
|
|
251
|
+
"issues": issues,
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
result.add_evidence(evidence)
|
|
255
|
+
|
|
256
|
+
if len(issues) > 0:
|
|
257
|
+
# Create finding for Config issues
|
|
258
|
+
finding = self.create_finding(
|
|
259
|
+
resource_id=recorder_name,
|
|
260
|
+
resource_type="config_recorder",
|
|
261
|
+
severity=severity,
|
|
262
|
+
title="AWS Config has configuration issues",
|
|
263
|
+
description=f"AWS Config recorder '{recorder_name}' has configuration issues: "
|
|
264
|
+
f"{'; '.join(issues)}. AWS Config should be actively recording all "
|
|
265
|
+
"supported resource types and delivering data to S3. ISO 27001 A.8.16 "
|
|
266
|
+
"requires proper monitoring and configuration tracking.",
|
|
267
|
+
remediation=(
|
|
268
|
+
f"Fix AWS Config recorder '{recorder_name}' issues:\n\n"
|
|
269
|
+
"1. Start configuration recorder if stopped:\n"
|
|
270
|
+
f"aws configservice start-configuration-recorder \\\n"
|
|
271
|
+
f" --configuration-recorder-name {recorder_name}\n\n"
|
|
272
|
+
"2. Update to record all resource types:\n"
|
|
273
|
+
f"aws configservice put-configuration-recorder \\\n"
|
|
274
|
+
f" --configuration-recorder name={recorder_name},roleARN={role_arn} \\\n"
|
|
275
|
+
" --recording-group allSupported=true,includeGlobalResourceTypes=true\n\n"
|
|
276
|
+
"3. Configure delivery channel if missing:\n"
|
|
277
|
+
"aws configservice put-delivery-channel \\\n"
|
|
278
|
+
" --delivery-channel name=default,s3BucketName=my-config-bucket\n\n"
|
|
279
|
+
"4. Verify recorder is working:\n"
|
|
280
|
+
f"aws configservice describe-configuration-recorder-status \\\n"
|
|
281
|
+
f" --configuration-recorder-names {recorder_name}\n\n"
|
|
282
|
+
"Or use AWS Console:\n"
|
|
283
|
+
"1. Go to AWS Config console\n"
|
|
284
|
+
"2. Click 'Settings'\n"
|
|
285
|
+
"3. Verify configuration:\n"
|
|
286
|
+
" - Recording is ON\n"
|
|
287
|
+
" - All resources selected\n"
|
|
288
|
+
" - Include global resources (if primary region)\n"
|
|
289
|
+
" - S3 bucket configured\n"
|
|
290
|
+
"4. Click 'Save' if changes made\n\n"
|
|
291
|
+
"Troubleshooting:\n"
|
|
292
|
+
"- Check IAM role has correct permissions\n"
|
|
293
|
+
"- Verify S3 bucket exists and Config has access\n"
|
|
294
|
+
"- Check S3 bucket policy allows Config writes\n"
|
|
295
|
+
"- Verify region is correct\n"
|
|
296
|
+
"- Check service limits not exceeded\n"
|
|
297
|
+
"- Review CloudTrail for Config API errors"
|
|
298
|
+
),
|
|
299
|
+
evidence=evidence
|
|
300
|
+
)
|
|
301
|
+
result.add_finding(finding)
|
|
302
|
+
|
|
303
|
+
# Calculate partial score
|
|
304
|
+
score_components = []
|
|
305
|
+
if is_recording:
|
|
306
|
+
score_components.append(40) # 40% for recording
|
|
307
|
+
if has_delivery_channel:
|
|
308
|
+
score_components.append(40) # 40% for delivery channel
|
|
309
|
+
if all_supported:
|
|
310
|
+
score_components.append(20) # 20% for recording all resources
|
|
311
|
+
|
|
312
|
+
result.score = sum(score_components)
|
|
313
|
+
result.passed = False
|
|
314
|
+
result.status = TestStatus.FAILED
|
|
315
|
+
|
|
316
|
+
self.logger.warning(
|
|
317
|
+
"config_has_issues",
|
|
318
|
+
recorder_name=recorder_name,
|
|
319
|
+
issues=issues
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
# Config is properly configured
|
|
323
|
+
result.score = 100.0
|
|
324
|
+
result.passed = True
|
|
325
|
+
result.status = TestStatus.PASSED
|
|
326
|
+
|
|
327
|
+
self.logger.info(
|
|
328
|
+
"config_properly_configured",
|
|
329
|
+
recorder_name=recorder_name
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Add metadata
|
|
333
|
+
result.metadata = {
|
|
334
|
+
"config_enabled": True,
|
|
335
|
+
"recorder_name": recorder_name,
|
|
336
|
+
"is_recording": is_recording,
|
|
337
|
+
"all_supported": all_supported,
|
|
338
|
+
"include_global": include_global,
|
|
339
|
+
"has_delivery_channel": has_delivery_channel,
|
|
340
|
+
"compliance_percentage": result.score,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
self.logger.info(
|
|
344
|
+
"config_enabled_test_completed",
|
|
345
|
+
recorder_name=recorder_name,
|
|
346
|
+
is_recording=is_recording,
|
|
347
|
+
score=result.score,
|
|
348
|
+
passed=result.passed
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
except ClientError as e:
|
|
352
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
353
|
+
self.logger.error("config_enabled_test_error", error_code=error_code, error=str(e))
|
|
354
|
+
result.status = TestStatus.ERROR
|
|
355
|
+
result.passed = False
|
|
356
|
+
result.score = 0.0
|
|
357
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
self.logger.error("config_enabled_test_error", error=str(e))
|
|
361
|
+
result.status = TestStatus.ERROR
|
|
362
|
+
result.passed = False
|
|
363
|
+
result.score = 0.0
|
|
364
|
+
result.error_message = str(e)
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ============================================================================
|
|
370
|
+
# CONVENIENCE FUNCTION
|
|
371
|
+
# ============================================================================
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def run_config_enabled_test(connector: AWSConnector) -> TestResult:
|
|
375
|
+
"""Run AWS Config enabled compliance test.
|
|
376
|
+
|
|
377
|
+
Convenience function for running the test.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
connector: AWS connector
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
TestResult
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
387
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
388
|
+
>>> connector.connect()
|
|
389
|
+
>>> result = run_config_enabled_test(connector)
|
|
390
|
+
>>> print(f"Score: {result.score}%")
|
|
391
|
+
"""
|
|
392
|
+
test = ConfigEnabledTest(connector)
|
|
393
|
+
return test.execute()
|