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,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CloudWatch Alarms compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that critical CloudWatch alarms are configured for monitoring.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.16 - Monitoring activities
|
|
7
|
+
Requirement: Critical resources should have monitoring alarms configured
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.logging.cloudwatch_alarms import CloudWatchAlarmsTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = CloudWatchAlarmsTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict, List
|
|
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 CloudWatchAlarmsTest(ComplianceTest):
|
|
35
|
+
"""Test for CloudWatch Alarms compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that critical CloudWatch alarms are configured:
|
|
38
|
+
- Alarms should be in OK or ALARM state (not INSUFFICIENT_DATA for long)
|
|
39
|
+
- Alarms should have actions configured (SNS notifications)
|
|
40
|
+
- Critical metrics should have alarms (EC2, RDS, Lambda, etc.)
|
|
41
|
+
|
|
42
|
+
Compliance Requirements:
|
|
43
|
+
- Alarms configured for critical resources
|
|
44
|
+
- Actions configured (SNS topics for notifications)
|
|
45
|
+
- Alarms in actionable states (not stuck in INSUFFICIENT_DATA)
|
|
46
|
+
|
|
47
|
+
Scoring:
|
|
48
|
+
- Based on alarm configuration and health
|
|
49
|
+
- Checks for presence of critical alarms
|
|
50
|
+
- Validates alarm actions are configured
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> test = CloudWatchAlarmsTest(connector)
|
|
54
|
+
>>> result = test.execute()
|
|
55
|
+
>>> for finding in result.findings:
|
|
56
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
60
|
+
"""Initialize CloudWatch Alarms test.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
connector: AWS connector instance
|
|
64
|
+
"""
|
|
65
|
+
super().__init__(
|
|
66
|
+
test_id="cloudwatch_alarms",
|
|
67
|
+
test_name="CloudWatch Alarms Check",
|
|
68
|
+
description="Verify critical CloudWatch alarms are configured for monitoring",
|
|
69
|
+
control_id="A.8.16",
|
|
70
|
+
connector=connector,
|
|
71
|
+
scope="regional",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def execute(self) -> TestResult:
|
|
75
|
+
"""Execute CloudWatch Alarms compliance test.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
TestResult with findings for missing or misconfigured alarms
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> test = CloudWatchAlarmsTest(connector)
|
|
82
|
+
>>> result = test.execute()
|
|
83
|
+
>>> print(result.score)
|
|
84
|
+
85.0
|
|
85
|
+
"""
|
|
86
|
+
result = TestResult(
|
|
87
|
+
test_id=self.test_id,
|
|
88
|
+
test_name=self.test_name,
|
|
89
|
+
status=TestStatus.PASSED,
|
|
90
|
+
passed=True,
|
|
91
|
+
score=100.0,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Get CloudWatch client
|
|
96
|
+
cloudwatch_client = self.connector.get_client("cloudwatch")
|
|
97
|
+
|
|
98
|
+
# List all metric alarms
|
|
99
|
+
self.logger.info("listing_cloudwatch_alarms")
|
|
100
|
+
alarms = []
|
|
101
|
+
|
|
102
|
+
paginator = cloudwatch_client.get_paginator("describe_alarms")
|
|
103
|
+
for page in paginator.paginate():
|
|
104
|
+
metric_alarms = page.get("MetricAlarms", [])
|
|
105
|
+
alarms.extend(metric_alarms)
|
|
106
|
+
|
|
107
|
+
if not alarms:
|
|
108
|
+
self.logger.info("no_cloudwatch_alarms_found")
|
|
109
|
+
result.metadata["message"] = "No CloudWatch alarms found (consider creating alarms for critical resources)"
|
|
110
|
+
result.metadata["recommendation"] = "Create alarms for EC2, RDS, Lambda, and other critical resources"
|
|
111
|
+
# Not a hard failure, but should be monitored
|
|
112
|
+
result.score = 50.0 # Partial score for having no alarms
|
|
113
|
+
result.passed = False
|
|
114
|
+
result.status = TestStatus.FAILED
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
self.logger.info("cloudwatch_alarms_found", count=len(alarms))
|
|
118
|
+
|
|
119
|
+
# Analyze alarm configurations
|
|
120
|
+
properly_configured_count = 0
|
|
121
|
+
alarms_without_actions = 0
|
|
122
|
+
alarms_insufficient_data = 0
|
|
123
|
+
|
|
124
|
+
for alarm in alarms:
|
|
125
|
+
alarm_name = alarm.get("AlarmName", "")
|
|
126
|
+
alarm_arn = alarm.get("AlarmArn", "")
|
|
127
|
+
state_value = alarm.get("StateValue", "")
|
|
128
|
+
actions_enabled = alarm.get("ActionsEnabled", False)
|
|
129
|
+
alarm_actions = alarm.get("AlarmActions", [])
|
|
130
|
+
metric_name = alarm.get("MetricName", "")
|
|
131
|
+
namespace = alarm.get("Namespace", "")
|
|
132
|
+
|
|
133
|
+
result.resources_scanned += 1
|
|
134
|
+
|
|
135
|
+
# Determine issues
|
|
136
|
+
issues = []
|
|
137
|
+
severity = Severity.MEDIUM
|
|
138
|
+
|
|
139
|
+
# Check if alarm has actions configured
|
|
140
|
+
if actions_enabled and len(alarm_actions) == 0:
|
|
141
|
+
issues.append("Alarm has actions enabled but no actions configured")
|
|
142
|
+
alarms_without_actions += 1
|
|
143
|
+
severity = Severity.MEDIUM
|
|
144
|
+
|
|
145
|
+
if not actions_enabled:
|
|
146
|
+
issues.append("Alarm actions are disabled")
|
|
147
|
+
alarms_without_actions += 1
|
|
148
|
+
severity = Severity.MEDIUM
|
|
149
|
+
|
|
150
|
+
# Check alarm state
|
|
151
|
+
if state_value == "INSUFFICIENT_DATA":
|
|
152
|
+
issues.append("Alarm in INSUFFICIENT_DATA state (may indicate misconfiguration)")
|
|
153
|
+
alarms_insufficient_data += 1
|
|
154
|
+
severity = Severity.LOW
|
|
155
|
+
|
|
156
|
+
# Create evidence
|
|
157
|
+
evidence = self.create_evidence(
|
|
158
|
+
resource_id=alarm_arn,
|
|
159
|
+
resource_type="cloudwatch_alarm",
|
|
160
|
+
data={
|
|
161
|
+
"alarm_name": alarm_name,
|
|
162
|
+
"alarm_arn": alarm_arn,
|
|
163
|
+
"state_value": state_value,
|
|
164
|
+
"actions_enabled": actions_enabled,
|
|
165
|
+
"alarm_actions_count": len(alarm_actions),
|
|
166
|
+
"metric_name": metric_name,
|
|
167
|
+
"namespace": namespace,
|
|
168
|
+
"has_issues": len(issues) > 0,
|
|
169
|
+
"issues": issues,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
result.add_evidence(evidence)
|
|
173
|
+
|
|
174
|
+
if len(issues) == 0:
|
|
175
|
+
properly_configured_count += 1
|
|
176
|
+
self.logger.debug(
|
|
177
|
+
"alarm_properly_configured",
|
|
178
|
+
alarm_name=alarm_name
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# Create finding for misconfigured alarm
|
|
182
|
+
finding = self.create_finding(
|
|
183
|
+
resource_id=alarm_arn,
|
|
184
|
+
resource_type="cloudwatch_alarm",
|
|
185
|
+
severity=severity,
|
|
186
|
+
title="CloudWatch alarm has configuration issues",
|
|
187
|
+
description=f"CloudWatch alarm '{alarm_name}' (monitoring {namespace}/{metric_name}) has "
|
|
188
|
+
f"configuration issues: {'; '.join(issues)}. Alarms should have actions "
|
|
189
|
+
"configured (SNS notifications) and be in actionable states to effectively "
|
|
190
|
+
"monitor resources. ISO 27001 A.8.16 requires monitoring of system activities.",
|
|
191
|
+
remediation=(
|
|
192
|
+
f"Improve CloudWatch alarm '{alarm_name}' configuration:\n\n"
|
|
193
|
+
"1. Enable alarm actions:\n"
|
|
194
|
+
f"aws cloudwatch enable-alarm-actions \\\n"
|
|
195
|
+
f" --alarm-names {alarm_name}\n\n"
|
|
196
|
+
"2. Configure SNS topic for notifications:\n"
|
|
197
|
+
"# Create SNS topic if needed\n"
|
|
198
|
+
"aws sns create-topic --name alarm-notifications\n\n"
|
|
199
|
+
"# Subscribe email to topic\n"
|
|
200
|
+
"aws sns subscribe \\\n"
|
|
201
|
+
" --topic-arn <SNS-TOPIC-ARN> \\\n"
|
|
202
|
+
" --protocol email \\\n"
|
|
203
|
+
" --notification-endpoint your-email@example.com\n\n"
|
|
204
|
+
"# Add SNS topic to alarm\n"
|
|
205
|
+
f"aws cloudwatch put-metric-alarm \\\n"
|
|
206
|
+
f" --alarm-name {alarm_name} \\\n"
|
|
207
|
+
f" --metric-name {metric_name} \\\n"
|
|
208
|
+
f" --namespace {namespace} \\\n"
|
|
209
|
+
" --statistic Average \\\n"
|
|
210
|
+
" --period 300 \\\n"
|
|
211
|
+
" --evaluation-periods 2 \\\n"
|
|
212
|
+
" --threshold 80 \\\n"
|
|
213
|
+
" --comparison-operator GreaterThanThreshold \\\n"
|
|
214
|
+
" --alarm-actions <SNS-TOPIC-ARN> \\\n"
|
|
215
|
+
" --ok-actions <SNS-TOPIC-ARN> \\\n"
|
|
216
|
+
" --insufficient-data-actions <SNS-TOPIC-ARN>\n\n"
|
|
217
|
+
"3. Fix INSUFFICIENT_DATA state:\n"
|
|
218
|
+
"# Check if metric is publishing data\n"
|
|
219
|
+
f"aws cloudwatch get-metric-statistics \\\n"
|
|
220
|
+
f" --namespace {namespace} \\\n"
|
|
221
|
+
f" --metric-name {metric_name} \\\n"
|
|
222
|
+
" --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \\\n"
|
|
223
|
+
" --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \\\n"
|
|
224
|
+
" --period 300 \\\n"
|
|
225
|
+
" --statistics Average\n\n"
|
|
226
|
+
"# Adjust alarm configuration if needed:\n"
|
|
227
|
+
"- Increase evaluation periods\n"
|
|
228
|
+
"- Adjust metric dimensions\n"
|
|
229
|
+
"- Verify resource is active and publishing metrics\n\n"
|
|
230
|
+
"Or use AWS Console:\n"
|
|
231
|
+
"1. Go to CloudWatch console → Alarms\n"
|
|
232
|
+
f"2. Select alarm '{alarm_name}'\n"
|
|
233
|
+
"3. Click 'Actions' → 'Edit'\n"
|
|
234
|
+
"4. Configure notification:\n"
|
|
235
|
+
" - Send notification to: Select or create SNS topic\n"
|
|
236
|
+
" - Add email/SMS endpoints to SNS topic\n"
|
|
237
|
+
"5. Verify 'Enable actions' is checked\n"
|
|
238
|
+
"6. Configure actions for:\n"
|
|
239
|
+
" - In alarm\n"
|
|
240
|
+
" - OK\n"
|
|
241
|
+
" - Insufficient data\n"
|
|
242
|
+
"7. Save changes\n\n"
|
|
243
|
+
"Critical alarms to configure:\n"
|
|
244
|
+
"EC2 Instances:\n"
|
|
245
|
+
"- CPUUtilization > 80%\n"
|
|
246
|
+
"- StatusCheckFailed (system + instance)\n"
|
|
247
|
+
"- DiskReadOps/WriteOps (high I/O)\n\n"
|
|
248
|
+
"RDS Databases:\n"
|
|
249
|
+
"- CPUUtilization > 80%\n"
|
|
250
|
+
"- FreeableMemory < 1GB\n"
|
|
251
|
+
"- DatabaseConnections > 80% of max\n"
|
|
252
|
+
"- ReadLatency/WriteLatency\n\n"
|
|
253
|
+
"Lambda Functions:\n"
|
|
254
|
+
"- Errors > 0\n"
|
|
255
|
+
"- Duration > threshold\n"
|
|
256
|
+
"- Throttles > 0\n"
|
|
257
|
+
"- ConcurrentExecutions near limit\n\n"
|
|
258
|
+
"ELB/ALB:\n"
|
|
259
|
+
"- UnHealthyHostCount > 0\n"
|
|
260
|
+
"- TargetResponseTime\n"
|
|
261
|
+
"- HTTPCode_Target_5XX_Count\n\n"
|
|
262
|
+
"S3 Buckets:\n"
|
|
263
|
+
"- 4xxErrors\n"
|
|
264
|
+
"- 5xxErrors\n\n"
|
|
265
|
+
"Best practices:\n"
|
|
266
|
+
"- Use composite alarms for complex conditions\n"
|
|
267
|
+
"- Configure multiple notification channels\n"
|
|
268
|
+
"- Set appropriate thresholds based on baselines\n"
|
|
269
|
+
"- Use alarm descriptions for runbook links\n"
|
|
270
|
+
"- Test alarm notifications regularly\n"
|
|
271
|
+
"- Use anomaly detection for dynamic thresholds\n"
|
|
272
|
+
"- Create alarms using Infrastructure as Code (Terraform, CloudFormation)\n"
|
|
273
|
+
"- Monitor alarm state changes in EventBridge\n"
|
|
274
|
+
"- Use alarm actions for auto-remediation (Lambda, Auto Scaling)\n"
|
|
275
|
+
"- Tag alarms for organization and cost allocation"
|
|
276
|
+
),
|
|
277
|
+
evidence=evidence
|
|
278
|
+
)
|
|
279
|
+
result.add_finding(finding)
|
|
280
|
+
|
|
281
|
+
self.logger.warning(
|
|
282
|
+
"alarm_misconfigured",
|
|
283
|
+
alarm_name=alarm_name,
|
|
284
|
+
issues=issues
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Calculate compliance score
|
|
288
|
+
result.score = (properly_configured_count / len(alarms)) * 100
|
|
289
|
+
|
|
290
|
+
# Determine pass/fail
|
|
291
|
+
result.passed = properly_configured_count == len(alarms)
|
|
292
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
293
|
+
|
|
294
|
+
# Add metadata
|
|
295
|
+
result.metadata = {
|
|
296
|
+
"total_alarms": len(alarms),
|
|
297
|
+
"properly_configured": properly_configured_count,
|
|
298
|
+
"alarms_without_actions": alarms_without_actions,
|
|
299
|
+
"alarms_insufficient_data": alarms_insufficient_data,
|
|
300
|
+
"misconfigured": len(alarms) - properly_configured_count,
|
|
301
|
+
"compliance_percentage": result.score,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
self.logger.info(
|
|
305
|
+
"cloudwatch_alarms_test_completed",
|
|
306
|
+
total_alarms=len(alarms),
|
|
307
|
+
properly_configured=properly_configured_count,
|
|
308
|
+
score=result.score,
|
|
309
|
+
passed=result.passed
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
except ClientError as e:
|
|
313
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
314
|
+
self.logger.error("cloudwatch_alarms_test_error", error_code=error_code, error=str(e))
|
|
315
|
+
result.status = TestStatus.ERROR
|
|
316
|
+
result.passed = False
|
|
317
|
+
result.score = 0.0
|
|
318
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
self.logger.error("cloudwatch_alarms_test_error", error=str(e))
|
|
322
|
+
result.status = TestStatus.ERROR
|
|
323
|
+
result.passed = False
|
|
324
|
+
result.score = 0.0
|
|
325
|
+
result.error_message = str(e)
|
|
326
|
+
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# ============================================================================
|
|
331
|
+
# CONVENIENCE FUNCTION
|
|
332
|
+
# ============================================================================
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def run_cloudwatch_alarms_test(connector: AWSConnector) -> TestResult:
|
|
336
|
+
"""Run CloudWatch Alarms compliance test.
|
|
337
|
+
|
|
338
|
+
Convenience function for running the test.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
connector: AWS connector
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
TestResult
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
348
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
349
|
+
>>> connector.connect()
|
|
350
|
+
>>> result = run_cloudwatch_alarms_test(connector)
|
|
351
|
+
>>> print(f"Score: {result.score}%")
|
|
352
|
+
"""
|
|
353
|
+
test = CloudWatchAlarmsTest(connector)
|
|
354
|
+
return test.execute()
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CloudWatch Logs encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all CloudWatch log groups use encryption at rest with KMS.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.24 - Use of cryptography
|
|
7
|
+
Requirement: Log groups must be encrypted with KMS keys
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.logging.cloudwatch_logs_encryption import CloudWatchLogsEncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = CloudWatchLogsEncryptionTest(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 CloudWatchLogsEncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for CloudWatch Logs encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all CloudWatch log groups use server-side encryption
|
|
38
|
+
with AWS KMS keys to protect log data at rest.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- All log groups must have kmsKeyId configured
|
|
42
|
+
- Encryption protects sensitive log data
|
|
43
|
+
- Customer-managed KMS keys recommended for audit trail
|
|
44
|
+
|
|
45
|
+
Scoring:
|
|
46
|
+
- 100% if all log groups are encrypted
|
|
47
|
+
- Proportional score based on compliant/total ratio
|
|
48
|
+
- 100% if no log groups exist
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> test = CloudWatchLogsEncryptionTest(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 Logs encryption test.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connector: AWS connector instance
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
test_id="cloudwatch_logs_encryption",
|
|
65
|
+
test_name="CloudWatch Logs Encryption Check",
|
|
66
|
+
description="Verify all CloudWatch log groups use encryption at rest with KMS",
|
|
67
|
+
control_id="A.8.24",
|
|
68
|
+
connector=connector,
|
|
69
|
+
scope="regional",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def execute(self) -> TestResult:
|
|
73
|
+
"""Execute CloudWatch Logs encryption compliance test.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TestResult with findings for unencrypted log groups
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> test = CloudWatchLogsEncryptionTest(connector)
|
|
80
|
+
>>> result = test.execute()
|
|
81
|
+
>>> print(result.score)
|
|
82
|
+
100.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 encryption for each log group
|
|
112
|
+
encrypted_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 KMS key is configured
|
|
119
|
+
kms_key_id = log_group.get("kmsKeyId")
|
|
120
|
+
is_encrypted = kms_key_id is not None and kms_key_id != ""
|
|
121
|
+
|
|
122
|
+
# Create evidence
|
|
123
|
+
evidence = self.create_evidence(
|
|
124
|
+
resource_id=log_group_name,
|
|
125
|
+
resource_type="cloudwatch_log_group",
|
|
126
|
+
data={
|
|
127
|
+
"log_group_name": log_group_name,
|
|
128
|
+
"kms_key_id": kms_key_id,
|
|
129
|
+
"is_encrypted": is_encrypted,
|
|
130
|
+
"creation_time": log_group.get("creationTime"),
|
|
131
|
+
"stored_bytes": log_group.get("storedBytes", 0),
|
|
132
|
+
"retention_in_days": log_group.get("retentionInDays"),
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
result.add_evidence(evidence)
|
|
136
|
+
|
|
137
|
+
if is_encrypted:
|
|
138
|
+
encrypted_count += 1
|
|
139
|
+
self.logger.debug(
|
|
140
|
+
"log_group_encrypted",
|
|
141
|
+
log_group=log_group_name,
|
|
142
|
+
kms_key_id=kms_key_id
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
# Create finding for unencrypted log group
|
|
146
|
+
finding = self.create_finding(
|
|
147
|
+
resource_id=log_group_name,
|
|
148
|
+
resource_type="cloudwatch_log_group",
|
|
149
|
+
severity=Severity.HIGH,
|
|
150
|
+
title="CloudWatch log group encryption not enabled",
|
|
151
|
+
description=f"CloudWatch log group '{log_group_name}' does not have encryption enabled. "
|
|
152
|
+
"Without encryption, log data is stored unencrypted at rest, potentially "
|
|
153
|
+
"exposing sensitive information such as application logs, system events, "
|
|
154
|
+
"security events, and operational data. CloudWatch Logs encryption with "
|
|
155
|
+
"AWS KMS provides an additional layer of security for sensitive log data. "
|
|
156
|
+
"ISO 27001 A.8.24 requires cryptographic controls for protecting "
|
|
157
|
+
"sensitive information.",
|
|
158
|
+
remediation=(
|
|
159
|
+
f"Enable encryption for CloudWatch log group:\n\n"
|
|
160
|
+
"Note: You cannot add encryption to existing log groups. "
|
|
161
|
+
"You must create a new encrypted log group and migrate.\n\n"
|
|
162
|
+
"Step 1: Create new encrypted log group:\n"
|
|
163
|
+
f"aws logs create-log-group \\\n"
|
|
164
|
+
f" --log-group-name {log_group_name}-encrypted \\\n"
|
|
165
|
+
" --kms-key-id arn:aws:kms:REGION:ACCOUNT:key/KEY-ID\n\n"
|
|
166
|
+
"Step 2: Update applications to use new log group\n\n"
|
|
167
|
+
"Step 3: Copy retention settings if needed:\n"
|
|
168
|
+
f"aws logs put-retention-policy \\\n"
|
|
169
|
+
f" --log-group-name {log_group_name}-encrypted \\\n"
|
|
170
|
+
" --retention-in-days <RETENTION-DAYS>\n\n"
|
|
171
|
+
"Step 4: Verify logs are flowing to new group\n\n"
|
|
172
|
+
"Step 5: Delete old unencrypted log group:\n"
|
|
173
|
+
f"aws logs delete-log-group \\\n"
|
|
174
|
+
f" --log-group-name {log_group_name}\n\n"
|
|
175
|
+
"Or use AWS Console:\n"
|
|
176
|
+
"1. Go to CloudWatch Logs console\n"
|
|
177
|
+
"2. Click 'Create log group'\n"
|
|
178
|
+
"3. Enter new log group name\n"
|
|
179
|
+
"4. Under 'KMS key', select a customer-managed key\n"
|
|
180
|
+
"5. Click 'Create'\n"
|
|
181
|
+
"6. Update applications to use new log group\n"
|
|
182
|
+
"7. Delete old log group after migration\n\n"
|
|
183
|
+
"KMS Key Policy Requirements:\n"
|
|
184
|
+
"Ensure the KMS key policy allows CloudWatch Logs:\n"
|
|
185
|
+
"{\n"
|
|
186
|
+
' "Sid": "Allow CloudWatch Logs",\n'
|
|
187
|
+
' "Effect": "Allow",\n'
|
|
188
|
+
' "Principal": {\n'
|
|
189
|
+
' "Service": "logs.REGION.amazonaws.com"\n'
|
|
190
|
+
' },\n'
|
|
191
|
+
' "Action": [\n'
|
|
192
|
+
' "kms:Encrypt",\n'
|
|
193
|
+
' "kms:Decrypt",\n'
|
|
194
|
+
' "kms:ReEncrypt*",\n'
|
|
195
|
+
' "kms:GenerateDataKey*",\n'
|
|
196
|
+
' "kms:CreateGrant",\n'
|
|
197
|
+
' "kms:DescribeKey"\n'
|
|
198
|
+
' ],\n'
|
|
199
|
+
' "Resource": "*",\n'
|
|
200
|
+
' "Condition": {\n'
|
|
201
|
+
' "ArnLike": {\n'
|
|
202
|
+
f' "kms:EncryptionContext:aws:logs:arn": "arn:aws:logs:REGION:ACCOUNT:log-group:{log_group_name}*"\n'
|
|
203
|
+
' }\n'
|
|
204
|
+
' }\n'
|
|
205
|
+
'}'
|
|
206
|
+
),
|
|
207
|
+
evidence=evidence
|
|
208
|
+
)
|
|
209
|
+
result.add_finding(finding)
|
|
210
|
+
|
|
211
|
+
self.logger.warning(
|
|
212
|
+
"log_group_not_encrypted",
|
|
213
|
+
log_group=log_group_name
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Calculate compliance score
|
|
217
|
+
result.score = (encrypted_count / len(log_groups)) * 100
|
|
218
|
+
|
|
219
|
+
# Determine pass/fail
|
|
220
|
+
result.passed = encrypted_count == len(log_groups)
|
|
221
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
222
|
+
|
|
223
|
+
# Add metadata
|
|
224
|
+
result.metadata = {
|
|
225
|
+
"total_log_groups": len(log_groups),
|
|
226
|
+
"encrypted_log_groups": encrypted_count,
|
|
227
|
+
"unencrypted_log_groups": len(log_groups) - encrypted_count,
|
|
228
|
+
"compliance_percentage": result.score,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
self.logger.info(
|
|
232
|
+
"cloudwatch_logs_encryption_test_completed",
|
|
233
|
+
total_log_groups=len(log_groups),
|
|
234
|
+
encrypted=encrypted_count,
|
|
235
|
+
score=result.score,
|
|
236
|
+
passed=result.passed
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
except ClientError as e:
|
|
240
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
241
|
+
self.logger.error("cloudwatch_logs_encryption_test_error", error_code=error_code, error=str(e))
|
|
242
|
+
result.status = TestStatus.ERROR
|
|
243
|
+
result.passed = False
|
|
244
|
+
result.score = 0.0
|
|
245
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
self.logger.error("cloudwatch_logs_encryption_test_error", error=str(e))
|
|
249
|
+
result.status = TestStatus.ERROR
|
|
250
|
+
result.passed = False
|
|
251
|
+
result.score = 0.0
|
|
252
|
+
result.error_message = str(e)
|
|
253
|
+
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ============================================================================
|
|
258
|
+
# CONVENIENCE FUNCTION
|
|
259
|
+
# ============================================================================
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def run_cloudwatch_logs_encryption_test(connector: AWSConnector) -> TestResult:
|
|
263
|
+
"""Run CloudWatch Logs encryption compliance test.
|
|
264
|
+
|
|
265
|
+
Convenience function for running the test.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
connector: AWS connector
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
TestResult
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
275
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
276
|
+
>>> connector.connect()
|
|
277
|
+
>>> result = run_cloudwatch_logs_encryption_test(connector)
|
|
278
|
+
>>> print(f"Score: {result.score}%")
|
|
279
|
+
"""
|
|
280
|
+
test = CloudWatchLogsEncryptionTest(connector)
|
|
281
|
+
return test.execute()
|