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.
Files changed (79) hide show
  1. CHANGELOG.md +208 -0
  2. README.md +343 -0
  3. complio/__init__.py +48 -0
  4. complio/cli/__init__.py +0 -0
  5. complio/cli/banner.py +87 -0
  6. complio/cli/commands/__init__.py +0 -0
  7. complio/cli/commands/history.py +439 -0
  8. complio/cli/commands/scan.py +700 -0
  9. complio/cli/main.py +115 -0
  10. complio/cli/output.py +338 -0
  11. complio/config/__init__.py +17 -0
  12. complio/config/settings.py +333 -0
  13. complio/connectors/__init__.py +9 -0
  14. complio/connectors/aws/__init__.py +0 -0
  15. complio/connectors/aws/client.py +342 -0
  16. complio/connectors/base.py +135 -0
  17. complio/core/__init__.py +10 -0
  18. complio/core/registry.py +228 -0
  19. complio/core/runner.py +351 -0
  20. complio/py.typed +0 -0
  21. complio/reporters/__init__.py +7 -0
  22. complio/reporters/generator.py +417 -0
  23. complio/tests_library/__init__.py +0 -0
  24. complio/tests_library/base.py +492 -0
  25. complio/tests_library/identity/__init__.py +0 -0
  26. complio/tests_library/identity/access_key_rotation.py +302 -0
  27. complio/tests_library/identity/mfa_enforcement.py +327 -0
  28. complio/tests_library/identity/root_account_protection.py +470 -0
  29. complio/tests_library/infrastructure/__init__.py +0 -0
  30. complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
  31. complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
  32. complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
  33. complio/tests_library/infrastructure/ebs_encryption.py +244 -0
  34. complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
  35. complio/tests_library/infrastructure/iam_password_policy.py +460 -0
  36. complio/tests_library/infrastructure/nacl_security.py +356 -0
  37. complio/tests_library/infrastructure/rds_encryption.py +252 -0
  38. complio/tests_library/infrastructure/s3_encryption.py +301 -0
  39. complio/tests_library/infrastructure/s3_public_access.py +369 -0
  40. complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
  41. complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
  42. complio/tests_library/logging/__init__.py +0 -0
  43. complio/tests_library/logging/cloudwatch_alarms.py +354 -0
  44. complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
  45. complio/tests_library/logging/cloudwatch_retention.py +252 -0
  46. complio/tests_library/logging/config_enabled.py +393 -0
  47. complio/tests_library/logging/eventbridge_rules.py +460 -0
  48. complio/tests_library/logging/guardduty_enabled.py +436 -0
  49. complio/tests_library/logging/security_hub_enabled.py +416 -0
  50. complio/tests_library/logging/sns_encryption.py +273 -0
  51. complio/tests_library/network/__init__.py +0 -0
  52. complio/tests_library/network/alb_nlb_security.py +421 -0
  53. complio/tests_library/network/api_gateway_security.py +452 -0
  54. complio/tests_library/network/cloudfront_https.py +332 -0
  55. complio/tests_library/network/direct_connect_security.py +343 -0
  56. complio/tests_library/network/nacl_configuration.py +367 -0
  57. complio/tests_library/network/network_firewall.py +355 -0
  58. complio/tests_library/network/transit_gateway_security.py +318 -0
  59. complio/tests_library/network/vpc_endpoints_security.py +339 -0
  60. complio/tests_library/network/vpn_security.py +333 -0
  61. complio/tests_library/network/waf_configuration.py +428 -0
  62. complio/tests_library/security/__init__.py +0 -0
  63. complio/tests_library/security/kms_key_rotation.py +314 -0
  64. complio/tests_library/storage/__init__.py +0 -0
  65. complio/tests_library/storage/backup_encryption.py +288 -0
  66. complio/tests_library/storage/dynamodb_encryption.py +280 -0
  67. complio/tests_library/storage/efs_encryption.py +257 -0
  68. complio/tests_library/storage/elasticache_encryption.py +370 -0
  69. complio/tests_library/storage/redshift_encryption.py +252 -0
  70. complio/tests_library/storage/s3_versioning.py +264 -0
  71. complio/utils/__init__.py +26 -0
  72. complio/utils/errors.py +179 -0
  73. complio/utils/exceptions.py +151 -0
  74. complio/utils/history.py +243 -0
  75. complio/utils/logger.py +391 -0
  76. complio-0.1.1.dist-info/METADATA +385 -0
  77. complio-0.1.1.dist-info/RECORD +79 -0
  78. complio-0.1.1.dist-info/WHEEL +4 -0
  79. 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()