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,492 @@
1
+ """
2
+ Base classes for compliance tests.
3
+
4
+ This module defines the abstract base class for all compliance tests,
5
+ along with models for test results, evidence, and findings.
6
+
7
+ Example:
8
+ >>> from complio.tests_library.base import ComplianceTest, TestResult
9
+ >>> class MyTest(ComplianceTest):
10
+ ... def execute(self) -> TestResult:
11
+ ... # Implementation
12
+ ... pass
13
+ """
14
+
15
+ import hashlib
16
+ import json
17
+ from abc import ABC, abstractmethod
18
+ from datetime import datetime, timezone
19
+ from enum import Enum
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from pydantic import BaseModel, ConfigDict, Field
23
+
24
+ from complio.connectors.aws.client import AWSConnector
25
+ from complio.utils.logger import get_logger
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ # ============================================================================
31
+ # ENUMS
32
+ # ============================================================================
33
+
34
+
35
+ class TestStatus(str, Enum):
36
+ """Test execution status."""
37
+
38
+ PASSED = "passed"
39
+ WARNING = "warning"
40
+ FAILED = "failed"
41
+ SKIPPED = "skipped"
42
+ ERROR = "error"
43
+
44
+
45
+ class Severity(str, Enum):
46
+ """Finding severity levels."""
47
+
48
+ CRITICAL = "critical"
49
+ HIGH = "high"
50
+ MEDIUM = "medium"
51
+ LOW = "low"
52
+ INFO = "info"
53
+
54
+
55
+ # ============================================================================
56
+ # EVIDENCE MODELS
57
+ # ============================================================================
58
+
59
+
60
+ class Evidence(BaseModel):
61
+ """Evidence collected during compliance test.
62
+
63
+ Attributes:
64
+ resource_id: Unique identifier for the AWS resource
65
+ resource_type: Type of resource (e.g., 's3_bucket', 'ec2_instance')
66
+ region: AWS region
67
+ data: Raw data collected from AWS
68
+ timestamp: When evidence was collected
69
+ signature: SHA-256 hash of evidence for integrity
70
+
71
+ Example:
72
+ >>> evidence = Evidence(
73
+ ... resource_id="my-bucket",
74
+ ... resource_type="s3_bucket",
75
+ ... region="us-east-1",
76
+ ... data={"encryption": {"status": "enabled"}}
77
+ ... )
78
+ """
79
+
80
+ resource_id: str = Field(..., description="Unique resource identifier")
81
+ resource_type: str = Field(..., description="Type of AWS resource")
82
+ region: str = Field(..., description="AWS region")
83
+ data: Dict[str, Any] = Field(default_factory=dict, description="Raw evidence data")
84
+ timestamp: datetime = Field(
85
+ default_factory=lambda: datetime.now(timezone.utc),
86
+ description="Evidence collection timestamp"
87
+ )
88
+ signature: Optional[str] = Field(None, description="SHA-256 signature for integrity")
89
+
90
+ def model_post_init(self, __context: Any) -> None:
91
+ """Calculate signature after initialization."""
92
+ if not self.signature:
93
+ self.signature = self.calculate_signature()
94
+
95
+ def calculate_signature(self) -> str:
96
+ """Calculate SHA-256 signature of evidence.
97
+
98
+ Creates a tamper-proof signature of the evidence data.
99
+
100
+ Returns:
101
+ Hexadecimal SHA-256 hash string
102
+
103
+ Example:
104
+ >>> evidence = Evidence(resource_id="test", resource_type="s3", region="us-east-1", data={})
105
+ >>> sig = evidence.calculate_signature()
106
+ >>> len(sig)
107
+ 64
108
+ """
109
+ # Create canonical representation
110
+ canonical_data = {
111
+ "resource_id": self.resource_id,
112
+ "resource_type": self.resource_type,
113
+ "region": self.region,
114
+ "data": self.data,
115
+ "timestamp": self.timestamp.isoformat(),
116
+ }
117
+
118
+ # Sort keys for deterministic hashing
119
+ canonical_json = json.dumps(canonical_data, sort_keys=True)
120
+
121
+ # Calculate SHA-256
122
+ return hashlib.sha256(canonical_json.encode()).hexdigest()
123
+
124
+ def verify_signature(self) -> bool:
125
+ """Verify evidence signature.
126
+
127
+ Returns:
128
+ True if signature is valid, False otherwise
129
+
130
+ Example:
131
+ >>> evidence = Evidence(resource_id="test", resource_type="s3", region="us-east-1", data={})
132
+ >>> evidence.verify_signature()
133
+ True
134
+ """
135
+ expected_signature = self.calculate_signature()
136
+ return self.signature == expected_signature
137
+
138
+
139
+ class Finding(BaseModel):
140
+ """Compliance finding from a test.
141
+
142
+ Attributes:
143
+ resource_id: Resource with the finding
144
+ resource_type: Type of resource
145
+ severity: Finding severity level
146
+ title: Short finding title
147
+ description: Detailed description
148
+ remediation: How to fix the finding
149
+ evidence: Associated evidence
150
+
151
+ Example:
152
+ >>> finding = Finding(
153
+ ... resource_id="my-bucket",
154
+ ... resource_type="s3_bucket",
155
+ ... severity=Severity.HIGH,
156
+ ... title="S3 bucket not encrypted",
157
+ ... description="Bucket does not have default encryption enabled",
158
+ ... remediation="Enable default encryption in bucket settings"
159
+ ... )
160
+ """
161
+
162
+ resource_id: str = Field(..., description="Resource identifier")
163
+ resource_type: str = Field(..., description="Resource type")
164
+ severity: Severity = Field(..., description="Finding severity")
165
+ title: str = Field(..., description="Short finding title")
166
+ description: str = Field(..., description="Detailed description")
167
+ remediation: str = Field(..., description="Remediation steps")
168
+ evidence: Optional[Evidence] = Field(None, description="Supporting evidence")
169
+
170
+ model_config = ConfigDict(use_enum_values=True)
171
+
172
+
173
+ # ============================================================================
174
+ # TEST RESULT MODELS
175
+ # ============================================================================
176
+
177
+
178
+ class TestResult(BaseModel):
179
+ """Result of a compliance test execution.
180
+
181
+ Attributes:
182
+ test_id: Unique test identifier
183
+ test_name: Human-readable test name
184
+ status: Test execution status
185
+ passed: Whether test passed
186
+ score: Compliance score (0-100)
187
+ findings: List of findings
188
+ evidence: List of evidence collected
189
+ resources_scanned: Number of resources scanned
190
+ start_time: Test start timestamp
191
+ end_time: Test end timestamp
192
+ duration_seconds: Test duration
193
+ error_message: Error message if test failed
194
+ metadata: Additional test metadata
195
+
196
+ Example:
197
+ >>> result = TestResult(
198
+ ... test_id="s3_encryption",
199
+ ... test_name="S3 Bucket Encryption Check",
200
+ ... status=TestStatus.PASSED,
201
+ ... passed=True,
202
+ ... score=100.0,
203
+ ... resources_scanned=5
204
+ ... )
205
+ """
206
+
207
+ test_id: str = Field(..., description="Unique test identifier")
208
+ test_name: str = Field(..., description="Human-readable test name")
209
+ status: TestStatus = Field(..., description="Test status")
210
+ passed: bool = Field(..., description="Whether test passed")
211
+ score: float = Field(..., ge=0.0, le=100.0, description="Compliance score (0-100)")
212
+ findings: List[Finding] = Field(default_factory=list, description="Test findings")
213
+ evidence: List[Evidence] = Field(default_factory=list, description="Collected evidence")
214
+ resources_scanned: int = Field(default=0, ge=0, description="Resources scanned")
215
+ start_time: datetime = Field(
216
+ default_factory=lambda: datetime.now(timezone.utc),
217
+ description="Test start time"
218
+ )
219
+ end_time: Optional[datetime] = Field(None, description="Test end time")
220
+ duration_seconds: Optional[float] = Field(None, description="Test duration")
221
+ error_message: Optional[str] = Field(None, description="Error message if failed")
222
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
223
+
224
+ model_config = ConfigDict(use_enum_values=True)
225
+
226
+ def complete(self) -> None:
227
+ """Mark test as complete and calculate duration.
228
+
229
+ Example:
230
+ >>> result = TestResult(test_id="test", test_name="Test", status=TestStatus.PASSED, passed=True, score=100)
231
+ >>> result.complete()
232
+ >>> assert result.duration_seconds is not None
233
+ """
234
+ self.end_time = datetime.now(timezone.utc)
235
+ if self.start_time and self.end_time:
236
+ delta = self.end_time - self.start_time
237
+ self.duration_seconds = delta.total_seconds()
238
+
239
+ def add_finding(self, finding: Finding) -> None:
240
+ """Add a finding to the test result.
241
+
242
+ Args:
243
+ finding: Finding to add
244
+
245
+ Example:
246
+ >>> result = TestResult(test_id="test", test_name="Test", status=TestStatus.FAILED, passed=False, score=50)
247
+ >>> finding = Finding(
248
+ ... resource_id="bucket1",
249
+ ... resource_type="s3_bucket",
250
+ ... severity=Severity.HIGH,
251
+ ... title="Not encrypted",
252
+ ... description="Bucket not encrypted",
253
+ ... remediation="Enable encryption"
254
+ ... )
255
+ >>> result.add_finding(finding)
256
+ >>> len(result.findings)
257
+ 1
258
+ """
259
+ self.findings.append(finding)
260
+
261
+ def add_evidence(self, evidence: Evidence) -> None:
262
+ """Add evidence to the test result.
263
+
264
+ Args:
265
+ evidence: Evidence to add
266
+
267
+ Example:
268
+ >>> result = TestResult(test_id="test", test_name="Test", status=TestStatus.PASSED, passed=True, score=100)
269
+ >>> evidence = Evidence(resource_id="bucket1", resource_type="s3_bucket", region="us-east-1", data={})
270
+ >>> result.add_evidence(evidence)
271
+ >>> len(result.evidence)
272
+ 1
273
+ """
274
+ self.evidence.append(evidence)
275
+
276
+
277
+ # ============================================================================
278
+ # BASE COMPLIANCE TEST
279
+ # ============================================================================
280
+
281
+
282
+ class ComplianceTest(ABC):
283
+ """Abstract base class for compliance tests.
284
+
285
+ All compliance tests must inherit from this class and implement
286
+ the execute() method.
287
+
288
+ Attributes:
289
+ test_id: Unique test identifier
290
+ test_name: Human-readable test name
291
+ description: Test description
292
+ control_id: ISO 27001 control ID (e.g., 'A.9.1.2')
293
+ scope: Test scope ('global' or 'regional')
294
+ connector: AWS connector
295
+ region: AWS region
296
+
297
+ Example:
298
+ >>> class MyComplianceTest(ComplianceTest):
299
+ ... def execute(self) -> TestResult:
300
+ ... result = TestResult(
301
+ ... test_id=self.test_id,
302
+ ... test_name=self.test_name,
303
+ ... status=TestStatus.PASSED,
304
+ ... passed=True,
305
+ ... score=100.0
306
+ ... )
307
+ ... return result
308
+ """
309
+
310
+ def __init__(
311
+ self,
312
+ test_id: str,
313
+ test_name: str,
314
+ description: str,
315
+ control_id: str,
316
+ connector: AWSConnector,
317
+ region: Optional[str] = None,
318
+ scope: str = "regional",
319
+ ) -> None:
320
+ """Initialize compliance test.
321
+
322
+ Args:
323
+ test_id: Unique test identifier
324
+ test_name: Human-readable test name
325
+ description: Test description
326
+ control_id: ISO 27001 control ID
327
+ connector: AWS connector
328
+ region: AWS region (uses connector region if not specified)
329
+ scope: Test scope - 'global' (account-wide) or 'regional' (region-specific)
330
+ """
331
+ self.test_id = test_id
332
+ self.test_name = test_name
333
+ self.description = description
334
+ self.control_id = control_id
335
+ self.scope = scope
336
+ self.connector = connector
337
+ self.region = region or connector.region
338
+
339
+ self.logger = get_logger(f"{__name__}.{self.test_id}")
340
+
341
+ @abstractmethod
342
+ def execute(self) -> TestResult:
343
+ """Execute the compliance test.
344
+
345
+ Returns:
346
+ TestResult with findings and evidence
347
+
348
+ Raises:
349
+ Exception: If test execution fails
350
+
351
+ Example:
352
+ >>> test = MyComplianceTest(...)
353
+ >>> result = test.execute()
354
+ >>> print(result.passed)
355
+ True
356
+ """
357
+ pass
358
+
359
+ def run(self) -> TestResult:
360
+ """Run the test with error handling.
361
+
362
+ Wrapper around execute() that handles errors and logging.
363
+
364
+ Returns:
365
+ TestResult
366
+
367
+ Example:
368
+ >>> test = MyComplianceTest(...)
369
+ >>> result = test.run()
370
+ """
371
+ self.logger.info(
372
+ "test_started",
373
+ test_id=self.test_id,
374
+ test_name=self.test_name,
375
+ region=self.region
376
+ )
377
+
378
+ try:
379
+ result = self.execute()
380
+ result.complete()
381
+
382
+ # Add scope to metadata
383
+ result.metadata["scope"] = self.scope
384
+ result.metadata["iso27001_control"] = self.control_id
385
+
386
+ self.logger.info(
387
+ "test_completed",
388
+ test_id=self.test_id,
389
+ status=result.status,
390
+ passed=result.passed,
391
+ score=result.score,
392
+ findings_count=len(result.findings),
393
+ duration=result.duration_seconds
394
+ )
395
+
396
+ return result
397
+
398
+ except Exception as e:
399
+ self.logger.error(
400
+ "test_failed",
401
+ test_id=self.test_id,
402
+ error=str(e)
403
+ )
404
+
405
+ # Create error result
406
+ result = TestResult(
407
+ test_id=self.test_id,
408
+ test_name=self.test_name,
409
+ status=TestStatus.ERROR,
410
+ passed=False,
411
+ score=0.0,
412
+ error_message=str(e)
413
+ )
414
+ result.complete()
415
+
416
+ return result
417
+
418
+ def create_evidence(
419
+ self,
420
+ resource_id: str,
421
+ resource_type: str,
422
+ data: Dict[str, Any],
423
+ ) -> Evidence:
424
+ """Create evidence for a resource.
425
+
426
+ Args:
427
+ resource_id: Resource identifier
428
+ resource_type: Resource type
429
+ data: Evidence data
430
+
431
+ Returns:
432
+ Evidence object with signature
433
+
434
+ Example:
435
+ >>> test = MyComplianceTest(...)
436
+ >>> evidence = test.create_evidence(
437
+ ... resource_id="my-bucket",
438
+ ... resource_type="s3_bucket",
439
+ ... data={"encryption": "enabled"}
440
+ ... )
441
+ """
442
+ return Evidence(
443
+ resource_id=resource_id,
444
+ resource_type=resource_type,
445
+ region=self.region,
446
+ data=data
447
+ )
448
+
449
+ def create_finding(
450
+ self,
451
+ resource_id: str,
452
+ resource_type: str,
453
+ severity: Severity,
454
+ title: str,
455
+ description: str,
456
+ remediation: str,
457
+ evidence: Optional[Evidence] = None,
458
+ ) -> Finding:
459
+ """Create a compliance finding.
460
+
461
+ Args:
462
+ resource_id: Resource identifier
463
+ resource_type: Resource type
464
+ severity: Finding severity
465
+ title: Finding title
466
+ description: Finding description
467
+ remediation: Remediation steps
468
+ evidence: Supporting evidence
469
+
470
+ Returns:
471
+ Finding object
472
+
473
+ Example:
474
+ >>> test = MyComplianceTest(...)
475
+ >>> finding = test.create_finding(
476
+ ... resource_id="my-bucket",
477
+ ... resource_type="s3_bucket",
478
+ ... severity=Severity.HIGH,
479
+ ... title="Bucket not encrypted",
480
+ ... description="Default encryption not enabled",
481
+ ... remediation="Enable default encryption"
482
+ ... )
483
+ """
484
+ return Finding(
485
+ resource_id=resource_id,
486
+ resource_type=resource_type,
487
+ severity=severity,
488
+ title=title,
489
+ description=description,
490
+ remediation=remediation,
491
+ evidence=evidence
492
+ )
File without changes