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,470 @@
1
+ """
2
+ Root account protection compliance test.
3
+
4
+ Checks multiple security controls for the AWS root account.
5
+
6
+ ISO 27001 Control: A.8.2 - Privileged access rights
7
+ Requirement: Root account must be properly secured
8
+
9
+ Example:
10
+ >>> from complio.connectors.aws.client import AWSConnector
11
+ >>> from complio.tests_library.identity.root_account_protection import RootAccountProtectionTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = RootAccountProtectionTest(connector)
17
+ >>> result = test.run()
18
+ >>> print(f"Passed: {result.passed}, Score: {result.score}")
19
+ """
20
+
21
+ import csv
22
+ import io
23
+ import time
24
+ from datetime import datetime, timezone
25
+ from typing import Any, Dict
26
+
27
+ from botocore.exceptions import ClientError
28
+
29
+ from complio.connectors.aws.client import AWSConnector
30
+ from complio.tests_library.base import (
31
+ ComplianceTest,
32
+ Severity,
33
+ TestResult,
34
+ TestStatus,
35
+ )
36
+
37
+
38
+ class RootAccountProtectionTest(ComplianceTest):
39
+ """Test for root account protection compliance.
40
+
41
+ Performs multiple checks on the AWS root account:
42
+ 1. Root account has MFA enabled (CRITICAL)
43
+ 2. Root account has no access keys (HIGH)
44
+ 3. Root account not used recently (MEDIUM if used in last 90 days)
45
+
46
+ Compliance Requirements:
47
+ - Root MFA must be enabled
48
+ - Root access keys must not exist
49
+ - Root account should not be used for day-to-day operations
50
+
51
+ Scoring:
52
+ - MFA enabled: 50 points
53
+ - No access keys: 30 points
54
+ - Not used recently: 20 points
55
+ - Total: 100 points
56
+
57
+ Example:
58
+ >>> test = RootAccountProtectionTest(connector)
59
+ >>> result = test.execute()
60
+ >>> for finding in result.findings:
61
+ ... print(f"{finding.resource_id}: {finding.title}")
62
+ """
63
+
64
+ def __init__(self, connector: AWSConnector) -> None:
65
+ """Initialize root account protection test.
66
+
67
+ Args:
68
+ connector: AWS connector instance
69
+ """
70
+ super().__init__(
71
+ test_id="root_account_protection",
72
+ test_name="Root Account Protection Check",
73
+ description="Verify root account has MFA, no access keys, and is not used regularly",
74
+ control_id="A.8.2",
75
+ connector=connector,
76
+ scope="global",
77
+ )
78
+
79
+ def execute(self) -> TestResult:
80
+ """Execute root account protection compliance test.
81
+
82
+ Returns:
83
+ TestResult with findings for root account security issues
84
+
85
+ Example:
86
+ >>> test = RootAccountProtectionTest(connector)
87
+ >>> result = test.execute()
88
+ >>> print(result.score)
89
+ 70.0
90
+ """
91
+ result = TestResult(
92
+ test_id=self.test_id,
93
+ test_name=self.test_name,
94
+ status=TestStatus.PASSED,
95
+ passed=True,
96
+ score=100.0,
97
+ )
98
+
99
+ try:
100
+ # Get IAM client
101
+ iam_client = self.connector.get_client("iam")
102
+
103
+ # Initialize score components
104
+ mfa_score = 0 # 50 points
105
+ access_keys_score = 0 # 30 points
106
+ usage_score = 0 # 20 points
107
+
108
+ # Check 1: Root account MFA status
109
+ self.logger.info("checking_root_mfa_status")
110
+ root_mfa_enabled = self._check_root_mfa(iam_client, result)
111
+
112
+ if root_mfa_enabled:
113
+ mfa_score = 50
114
+ self.logger.info("root_mfa_enabled")
115
+ else:
116
+ self.logger.error("root_mfa_disabled")
117
+ # Finding already created in _check_root_mfa
118
+
119
+ # Check 2: Root access keys
120
+ self.logger.info("checking_root_access_keys")
121
+ has_access_keys, access_key_details = self._check_root_access_keys(iam_client, result)
122
+
123
+ if not has_access_keys:
124
+ access_keys_score = 30
125
+ self.logger.info("root_no_access_keys")
126
+ else:
127
+ self.logger.warning("root_has_access_keys", details=access_key_details)
128
+ # Finding already created in _check_root_access_keys
129
+
130
+ # Check 3: Root account usage
131
+ self.logger.info("checking_root_usage")
132
+ recently_used, last_used = self._check_root_usage(iam_client, result)
133
+
134
+ if not recently_used:
135
+ usage_score = 20
136
+ self.logger.info("root_not_recently_used", last_used=last_used)
137
+ else:
138
+ self.logger.warning("root_recently_used", last_used=last_used)
139
+ # Finding already created in _check_root_usage
140
+
141
+ # Calculate total score
142
+ result.score = mfa_score + access_keys_score + usage_score
143
+ result.resources_scanned = 1 # Root account
144
+
145
+ # Determine pass/fail (require all checks to pass)
146
+ result.passed = result.score == 100
147
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
148
+
149
+ # Add metadata
150
+ result.metadata = {
151
+ "root_mfa_enabled": root_mfa_enabled,
152
+ "root_has_access_keys": has_access_keys,
153
+ "root_recently_used": recently_used,
154
+ "root_last_used": last_used,
155
+ "mfa_score": mfa_score,
156
+ "access_keys_score": access_keys_score,
157
+ "usage_score": usage_score,
158
+ "total_score": result.score,
159
+ }
160
+
161
+ self.logger.info(
162
+ "root_account_protection_test_completed",
163
+ score=result.score,
164
+ passed=result.passed,
165
+ mfa=root_mfa_enabled,
166
+ access_keys=has_access_keys,
167
+ recently_used=recently_used
168
+ )
169
+
170
+ except ClientError as e:
171
+ error_code = e.response.get("Error", {}).get("Code")
172
+ self.logger.error("root_account_protection_test_error", error_code=error_code, error=str(e))
173
+ result.status = TestStatus.ERROR
174
+ result.passed = False
175
+ result.score = 0.0
176
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
177
+
178
+ except Exception as e:
179
+ self.logger.error("root_account_protection_test_error", error=str(e))
180
+ result.status = TestStatus.ERROR
181
+ result.passed = False
182
+ result.score = 0.0
183
+ result.error_message = str(e)
184
+
185
+ return result
186
+
187
+ def _check_root_mfa(self, iam_client: Any, result: TestResult) -> bool:
188
+ """Check if root account has MFA enabled.
189
+
190
+ Args:
191
+ iam_client: Boto3 IAM client
192
+ result: TestResult to add findings to
193
+
194
+ Returns:
195
+ True if MFA is enabled, False otherwise
196
+ """
197
+ try:
198
+ summary = iam_client.get_account_summary()
199
+ account_mfa_enabled = summary.get("SummaryMap", {}).get("AccountMFAEnabled", 0)
200
+
201
+ mfa_enabled = account_mfa_enabled == 1
202
+
203
+ if not mfa_enabled:
204
+ finding = self.create_finding(
205
+ resource_id="root_account",
206
+ resource_type="aws_account",
207
+ severity=Severity.CRITICAL,
208
+ title="Root account MFA not enabled",
209
+ description="The AWS root account does not have Multi-Factor Authentication (MFA) enabled. "
210
+ "The root account has unrestricted access to all resources and services. "
211
+ "Without MFA, the account is vulnerable to credential compromise. "
212
+ "If root credentials are stolen, attackers gain complete control of the AWS account. "
213
+ "ISO 27001 A.8.2 requires strong protection for privileged accounts.",
214
+ remediation=(
215
+ "Enable MFA for the root account immediately:\n\n"
216
+ "1. Sign in to AWS Console as root user\n"
217
+ "2. Click on account name → Security credentials\n"
218
+ "3. Under 'Multi-factor authentication (MFA)', click 'Assign MFA device'\n"
219
+ "4. Choose virtual MFA device (recommended) or hardware MFA device\n"
220
+ "5. For virtual MFA:\n"
221
+ " - Install an authenticator app (Google Authenticator, Authy, etc.)\n"
222
+ " - Scan the QR code\n"
223
+ " - Enter two consecutive MFA codes\n"
224
+ "6. Securely store MFA recovery codes\n\n"
225
+ "Best practices:\n"
226
+ "- Use hardware MFA device for maximum security\n"
227
+ "- Store recovery codes in secure offline location\n"
228
+ "- Never share MFA device or recovery codes\n"
229
+ "- Avoid using root account for day-to-day operations"
230
+ ),
231
+ evidence=self.create_evidence(
232
+ resource_id="root_account",
233
+ resource_type="aws_account",
234
+ data={"mfa_enabled": mfa_enabled, "check": "root_mfa"}
235
+ )
236
+ )
237
+ result.add_finding(finding)
238
+
239
+ return mfa_enabled
240
+
241
+ except Exception as e:
242
+ self.logger.error("root_mfa_check_error", error=str(e))
243
+ return False
244
+
245
+ def _check_root_access_keys(self, iam_client: Any, result: TestResult) -> tuple:
246
+ """Check if root account has access keys.
247
+
248
+ Args:
249
+ iam_client: Boto3 IAM client
250
+ result: TestResult to add findings to
251
+
252
+ Returns:
253
+ Tuple of (has_keys: bool, key_details: dict)
254
+ """
255
+ try:
256
+ # Generate credential report to check root access keys
257
+ self._generate_credential_report(iam_client)
258
+
259
+ report_response = iam_client.get_credential_report()
260
+ report_content = report_response["Content"]
261
+ report_csv = report_content.decode("utf-8")
262
+ csv_reader = csv.DictReader(io.StringIO(report_csv))
263
+
264
+ for row in csv_reader:
265
+ if row.get("user") == "<root_account>":
266
+ key1_active = row.get("access_key_1_active", "false") == "true"
267
+ key2_active = row.get("access_key_2_active", "false") == "true"
268
+
269
+ has_keys = key1_active or key2_active
270
+
271
+ if has_keys:
272
+ active_keys = []
273
+ if key1_active:
274
+ active_keys.append("access_key_1")
275
+ if key2_active:
276
+ active_keys.append("access_key_2")
277
+
278
+ finding = self.create_finding(
279
+ resource_id="root_account",
280
+ resource_type="aws_account",
281
+ severity=Severity.HIGH,
282
+ title="Root account has active access keys",
283
+ description=f"The AWS root account has {len(active_keys)} active access key(s): "
284
+ f"{', '.join(active_keys)}. Root account access keys provide "
285
+ "unrestricted access to all AWS resources and should never exist. "
286
+ "If these keys are leaked or compromised, attackers gain full control. "
287
+ "ISO 27001 A.8.2 requires minimizing use of highly privileged credentials.",
288
+ remediation=(
289
+ "Delete root account access keys immediately:\n\n"
290
+ "1. Sign in to AWS Console as root user\n"
291
+ "2. Click on account name → Security credentials\n"
292
+ "3. Under 'Access keys', locate the active keys\n"
293
+ "4. Click 'Delete' for each access key\n"
294
+ "5. Confirm deletion\n\n"
295
+ "If you need programmatic access:\n"
296
+ "1. Create IAM users with specific permissions\n"
297
+ "2. Use IAM roles for EC2 instances and Lambda functions\n"
298
+ "3. Enable AWS Organizations for multi-account management\n"
299
+ "4. Never use root access keys for any purpose\n\n"
300
+ "CRITICAL: If root keys were exposed:\n"
301
+ "1. Delete keys immediately\n"
302
+ "2. Check CloudTrail for unauthorized activity\n"
303
+ "3. Review all resources for changes\n"
304
+ "4. Contact AWS Support if compromise is suspected"
305
+ ),
306
+ evidence=self.create_evidence(
307
+ resource_id="root_account",
308
+ resource_type="aws_account",
309
+ data={
310
+ "access_key_1_active": key1_active,
311
+ "access_key_2_active": key2_active,
312
+ "check": "root_access_keys"
313
+ }
314
+ )
315
+ )
316
+ result.add_finding(finding)
317
+
318
+ return has_keys, {"key1": key1_active, "key2": key2_active}
319
+
320
+ return False, {}
321
+
322
+ except Exception as e:
323
+ self.logger.error("root_access_keys_check_error", error=str(e))
324
+ return False, {}
325
+
326
+ def _check_root_usage(self, iam_client: Any, result: TestResult) -> tuple:
327
+ """Check when root account was last used.
328
+
329
+ Args:
330
+ iam_client: Boto3 IAM client
331
+ result: TestResult to add findings to
332
+
333
+ Returns:
334
+ Tuple of (recently_used: bool, last_used: str)
335
+ """
336
+ try:
337
+ # Get credential report for root usage info
338
+ self._generate_credential_report(iam_client)
339
+
340
+ report_response = iam_client.get_credential_report()
341
+ report_content = report_response["Content"]
342
+ report_csv = report_content.decode("utf-8")
343
+ csv_reader = csv.DictReader(io.StringIO(report_csv))
344
+
345
+ for row in csv_reader:
346
+ if row.get("user") == "<root_account>":
347
+ password_last_used = row.get("password_last_used", "no_information")
348
+
349
+ if password_last_used == "no_information" or password_last_used == "N/A":
350
+ # Root has never been used (good)
351
+ return False, "never"
352
+
353
+ # Parse last used date
354
+ try:
355
+ last_used_date = datetime.fromisoformat(password_last_used.replace("Z", "+00:00"))
356
+ now = datetime.now(timezone.utc)
357
+ days_since_use = (now - last_used_date).days
358
+
359
+ recently_used = days_since_use < 90
360
+
361
+ if recently_used:
362
+ finding = self.create_finding(
363
+ resource_id="root_account",
364
+ resource_type="aws_account",
365
+ severity=Severity.MEDIUM,
366
+ title=f"Root account used recently ({days_since_use} days ago)",
367
+ description=f"The AWS root account was last used {days_since_use} days ago "
368
+ f"(on {password_last_used}). Root account should only be used for "
369
+ "account and service management tasks that cannot be performed by IAM users. "
370
+ "Regular use of root account increases risk of credential compromise. "
371
+ "ISO 27001 A.8.2 requires limiting use of privileged accounts.",
372
+ remediation=(
373
+ "Minimize root account usage:\n\n"
374
+ "1. Create IAM users with appropriate permissions for day-to-day tasks\n"
375
+ "2. Use IAM roles for service-to-service access\n"
376
+ "3. Enable AWS Organizations for centralized management\n"
377
+ "4. Use IAM users for administrative tasks\n\n"
378
+ "Root account should only be used for:\n"
379
+ "- Changing account settings\n"
380
+ "- Closing the account\n"
381
+ "- Restoring IAM user permissions\n"
382
+ "- Managing AWS support plan\n"
383
+ "- Tasks specifically requiring root access\n\n"
384
+ "Best practices:\n"
385
+ "- Lock away root credentials securely\n"
386
+ "- Enable MFA on root account\n"
387
+ "- Monitor root account usage via CloudTrail\n"
388
+ "- Set up SNS alerts for root account activity"
389
+ ),
390
+ evidence=self.create_evidence(
391
+ resource_id="root_account",
392
+ resource_type="aws_account",
393
+ data={
394
+ "password_last_used": password_last_used,
395
+ "days_since_use": days_since_use,
396
+ "check": "root_usage"
397
+ }
398
+ )
399
+ )
400
+ result.add_finding(finding)
401
+
402
+ return recently_used, password_last_used
403
+
404
+ except Exception as e:
405
+ self.logger.error("parse_last_used_date_error", error=str(e))
406
+ return False, password_last_used
407
+
408
+ return False, "unknown"
409
+
410
+ except Exception as e:
411
+ self.logger.error("root_usage_check_error", error=str(e))
412
+ return False, "error"
413
+
414
+ def _generate_credential_report(self, iam_client: Any, max_retries: int = 3) -> bool:
415
+ """Generate IAM credential report with retry logic.
416
+
417
+ Args:
418
+ iam_client: Boto3 IAM client
419
+ max_retries: Maximum number of retries
420
+
421
+ Returns:
422
+ True if successful, False otherwise
423
+ """
424
+ for attempt in range(max_retries):
425
+ try:
426
+ response = iam_client.generate_credential_report()
427
+ state = response.get("State")
428
+
429
+ if state == "COMPLETE":
430
+ return True
431
+ elif state == "INPROGRESS":
432
+ time.sleep(5)
433
+ else:
434
+ time.sleep(5)
435
+
436
+ except ClientError as e:
437
+ error_code = e.response.get("Error", {}).get("Code")
438
+ if error_code == "LimitExceededException":
439
+ time.sleep(5)
440
+ else:
441
+ raise
442
+
443
+ return False
444
+
445
+
446
+ # ============================================================================
447
+ # CONVENIENCE FUNCTION
448
+ # ============================================================================
449
+
450
+
451
+ def run_root_account_protection_test(connector: AWSConnector) -> TestResult:
452
+ """Run root account protection compliance test.
453
+
454
+ Convenience function for running the test.
455
+
456
+ Args:
457
+ connector: AWS connector
458
+
459
+ Returns:
460
+ TestResult
461
+
462
+ Example:
463
+ >>> from complio.connectors.aws.client import AWSConnector
464
+ >>> connector = AWSConnector("production", "us-east-1")
465
+ >>> connector.connect()
466
+ >>> result = run_root_account_protection_test(connector)
467
+ >>> print(f"Score: {result.score}%")
468
+ """
469
+ test = RootAccountProtectionTest(connector)
470
+ return test.execute()
File without changes