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
complio/core/runner.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test runner for executing compliance tests.
|
|
3
|
+
|
|
4
|
+
This module provides the TestRunner class that orchestrates the execution
|
|
5
|
+
of compliance tests, handles parallelization, and collects results.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
9
|
+
>>> from complio.core.runner import TestRunner
|
|
10
|
+
>>>
|
|
11
|
+
>>> connector = AWSConnector("production", "us-east-1", password="...")
|
|
12
|
+
>>> connector.connect()
|
|
13
|
+
>>>
|
|
14
|
+
>>> runner = TestRunner(connector)
|
|
15
|
+
>>> results = runner.run_all()
|
|
16
|
+
>>> print(f"Score: {results.overall_score}%")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import time
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Callable, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
from complio.connectors.aws.client import AWSConnector
|
|
25
|
+
from complio.core.registry import TestRegistry
|
|
26
|
+
from complio.tests_library.base import ComplianceTest, TestResult, TestStatus
|
|
27
|
+
from complio.utils.logger import get_logger
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ScanResults:
|
|
32
|
+
"""Results from a compliance scan.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
test_results: List of individual test results
|
|
36
|
+
total_tests: Total number of tests executed
|
|
37
|
+
passed_tests: Number of tests that passed
|
|
38
|
+
failed_tests: Number of tests that failed
|
|
39
|
+
error_tests: Number of tests that errored
|
|
40
|
+
overall_score: Average score across all tests
|
|
41
|
+
execution_time: Total execution time in seconds
|
|
42
|
+
region: AWS region scanned
|
|
43
|
+
timestamp: Unix timestamp of scan start
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
test_results: List[TestResult]
|
|
47
|
+
total_tests: int = 0
|
|
48
|
+
passed_tests: int = 0
|
|
49
|
+
failed_tests: int = 0
|
|
50
|
+
error_tests: int = 0
|
|
51
|
+
overall_score: float = 0.0
|
|
52
|
+
execution_time: float = 0.0
|
|
53
|
+
region: str = ""
|
|
54
|
+
timestamp: float = field(default_factory=time.time)
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
"""Calculate statistics from test results."""
|
|
58
|
+
self.total_tests = len(self.test_results)
|
|
59
|
+
|
|
60
|
+
if self.total_tests == 0:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Count test statuses
|
|
64
|
+
for result in self.test_results:
|
|
65
|
+
if result.status == TestStatus.PASSED:
|
|
66
|
+
self.passed_tests += 1
|
|
67
|
+
elif result.status == TestStatus.ERROR:
|
|
68
|
+
self.error_tests += 1
|
|
69
|
+
elif result.status == TestStatus.WARNING:
|
|
70
|
+
# WARNING counts as passed (score >= 70) but with findings
|
|
71
|
+
self.passed_tests += 1
|
|
72
|
+
else:
|
|
73
|
+
self.failed_tests += 1
|
|
74
|
+
|
|
75
|
+
# Calculate overall score (average of all test scores)
|
|
76
|
+
total_score = sum(r.score for r in self.test_results)
|
|
77
|
+
self.overall_score = round(total_score / self.total_tests, 2)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestRunner:
|
|
81
|
+
"""Execute compliance tests and collect results.
|
|
82
|
+
|
|
83
|
+
This class orchestrates the execution of compliance tests,
|
|
84
|
+
with support for parallel execution and progress reporting.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
connector: AWS connector for accessing cloud resources
|
|
88
|
+
registry: Test registry for discovering tests
|
|
89
|
+
max_workers: Maximum number of parallel test executions
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> connector = AWSConnector("prod", "us-east-1", password="...")
|
|
93
|
+
>>> connector.connect()
|
|
94
|
+
>>>
|
|
95
|
+
>>> runner = TestRunner(connector, max_workers=4)
|
|
96
|
+
>>> results = runner.run_all()
|
|
97
|
+
>>>
|
|
98
|
+
>>> print(f"Passed: {results.passed_tests}/{results.total_tests}")
|
|
99
|
+
>>> print(f"Score: {results.overall_score}%")
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
connector: AWSConnector,
|
|
105
|
+
max_workers: int = 4,
|
|
106
|
+
progress_callback: Optional[Callable[[str, int, int], None]] = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Initialize test runner.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
connector: AWS connector instance
|
|
112
|
+
max_workers: Maximum parallel workers (default: 4)
|
|
113
|
+
progress_callback: Optional callback for progress updates
|
|
114
|
+
Signature: (test_name, current, total) -> None
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> def progress(test_name, current, total):
|
|
118
|
+
... print(f"[{current}/{total}] Running {test_name}")
|
|
119
|
+
>>>
|
|
120
|
+
>>> runner = TestRunner(connector, progress_callback=progress)
|
|
121
|
+
"""
|
|
122
|
+
self.connector = connector
|
|
123
|
+
self.registry = TestRegistry()
|
|
124
|
+
self.max_workers = max_workers
|
|
125
|
+
self.progress_callback = progress_callback
|
|
126
|
+
self.logger = get_logger(__name__)
|
|
127
|
+
|
|
128
|
+
self.logger.info(
|
|
129
|
+
"test_runner_initialized",
|
|
130
|
+
max_workers=max_workers,
|
|
131
|
+
parallel=max_workers > 1
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def run_all(self, parallel: bool = False) -> ScanResults:
|
|
135
|
+
"""Run all registered compliance tests.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
parallel: Whether to run tests in parallel (default: False)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
ScanResults with all test results and statistics
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> results = runner.run_all(parallel=True)
|
|
145
|
+
>>> for result in results.test_results:
|
|
146
|
+
... print(f"{result.test_name}: {result.score}%")
|
|
147
|
+
"""
|
|
148
|
+
test_ids = self.registry.get_test_ids()
|
|
149
|
+
return self.run_tests(test_ids, parallel=parallel)
|
|
150
|
+
|
|
151
|
+
def run_tests(
|
|
152
|
+
self,
|
|
153
|
+
test_ids: List[str],
|
|
154
|
+
parallel: bool = False,
|
|
155
|
+
) -> ScanResults:
|
|
156
|
+
"""Run specific compliance tests.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
test_ids: List of test IDs to execute
|
|
160
|
+
parallel: Whether to run tests in parallel
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
ScanResults with test results and statistics
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
KeyError: If any test_id is not found in registry
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> results = runner.run_tests(["s3_encryption", "ec2_security_groups"])
|
|
170
|
+
>>> print(f"Overall score: {results.overall_score}%")
|
|
171
|
+
"""
|
|
172
|
+
self.logger.info(
|
|
173
|
+
"starting_compliance_scan",
|
|
174
|
+
test_count=len(test_ids),
|
|
175
|
+
parallel=parallel,
|
|
176
|
+
region=self.connector.region,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
start_time = time.time()
|
|
180
|
+
test_results: List[TestResult] = []
|
|
181
|
+
|
|
182
|
+
if parallel:
|
|
183
|
+
test_results = self._run_parallel(test_ids)
|
|
184
|
+
else:
|
|
185
|
+
test_results = self._run_sequential(test_ids)
|
|
186
|
+
|
|
187
|
+
execution_time = time.time() - start_time
|
|
188
|
+
|
|
189
|
+
results = ScanResults(
|
|
190
|
+
test_results=test_results,
|
|
191
|
+
execution_time=execution_time,
|
|
192
|
+
region=self.connector.region,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
self.logger.info(
|
|
196
|
+
"compliance_scan_complete",
|
|
197
|
+
total_tests=results.total_tests,
|
|
198
|
+
passed=results.passed_tests,
|
|
199
|
+
failed=results.failed_tests,
|
|
200
|
+
errors=results.error_tests,
|
|
201
|
+
score=results.overall_score,
|
|
202
|
+
duration=execution_time,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return results
|
|
206
|
+
|
|
207
|
+
def run_single_test(self, test_id: str) -> TestResult:
|
|
208
|
+
"""Run a single compliance test.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
test_id: Test identifier
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
TestResult for the executed test
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
KeyError: If test_id not found
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
>>> result = runner.run_single_test("s3_encryption")
|
|
221
|
+
>>> print(f"S3 Encryption: {result.score}% - {'PASS' if result.passed else 'FAIL'}")
|
|
222
|
+
"""
|
|
223
|
+
test_class = self.registry.get_test(test_id)
|
|
224
|
+
test_instance = test_class(self.connector)
|
|
225
|
+
|
|
226
|
+
self.logger.info("running_test", test_id=test_id, test_name=test_instance.test_name)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
result = test_instance.run()
|
|
230
|
+
self.logger.info(
|
|
231
|
+
"test_complete",
|
|
232
|
+
test_id=test_id,
|
|
233
|
+
passed=result.passed,
|
|
234
|
+
score=result.score,
|
|
235
|
+
findings_count=len(result.findings),
|
|
236
|
+
)
|
|
237
|
+
return result
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.error(
|
|
240
|
+
"test_execution_failed",
|
|
241
|
+
test_id=test_id,
|
|
242
|
+
error=str(e),
|
|
243
|
+
)
|
|
244
|
+
# Return error result
|
|
245
|
+
from complio.tests_library.base import Finding, Severity
|
|
246
|
+
|
|
247
|
+
return TestResult(
|
|
248
|
+
test_id=test_id,
|
|
249
|
+
test_name=test_instance.test_name,
|
|
250
|
+
status=TestStatus.ERROR,
|
|
251
|
+
passed=False,
|
|
252
|
+
score=0.0,
|
|
253
|
+
findings=[
|
|
254
|
+
Finding(
|
|
255
|
+
resource_id="N/A",
|
|
256
|
+
resource_type="test_execution",
|
|
257
|
+
severity=Severity.CRITICAL,
|
|
258
|
+
title=f"Test execution failed: {test_id}",
|
|
259
|
+
description=f"Error during test execution: {str(e)}",
|
|
260
|
+
remediation="Check test logs for details",
|
|
261
|
+
)
|
|
262
|
+
],
|
|
263
|
+
evidence=[],
|
|
264
|
+
metadata={"error": str(e), "test_id": test_id},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _run_sequential(self, test_ids: List[str]) -> List[TestResult]:
|
|
268
|
+
"""Run tests sequentially.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
test_ids: List of test IDs to execute
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of test results
|
|
275
|
+
"""
|
|
276
|
+
results: List[TestResult] = []
|
|
277
|
+
total = len(test_ids)
|
|
278
|
+
|
|
279
|
+
for index, test_id in enumerate(test_ids, start=1):
|
|
280
|
+
if self.progress_callback:
|
|
281
|
+
test_class = self.registry.get_test(test_id)
|
|
282
|
+
test_instance = test_class(self.connector)
|
|
283
|
+
test_name = test_instance.test_name
|
|
284
|
+
scope = getattr(test_instance, 'scope', 'regional')
|
|
285
|
+
self.progress_callback(test_name, index, total, scope)
|
|
286
|
+
|
|
287
|
+
result = self.run_single_test(test_id)
|
|
288
|
+
results.append(result)
|
|
289
|
+
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
def _run_parallel(self, test_ids: List[str]) -> List[TestResult]:
|
|
293
|
+
"""Run tests in parallel using thread pool.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
test_ids: List of test IDs to execute
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of test results (order may differ from input)
|
|
300
|
+
"""
|
|
301
|
+
results: List[TestResult] = []
|
|
302
|
+
total = len(test_ids)
|
|
303
|
+
completed = 0
|
|
304
|
+
|
|
305
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
306
|
+
# Submit all tests
|
|
307
|
+
future_to_test_id = {
|
|
308
|
+
executor.submit(self.run_single_test, test_id): test_id
|
|
309
|
+
for test_id in test_ids
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Collect results as they complete
|
|
313
|
+
for future in as_completed(future_to_test_id):
|
|
314
|
+
test_id = future_to_test_id[future]
|
|
315
|
+
completed += 1
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
result = future.result()
|
|
319
|
+
results.append(result)
|
|
320
|
+
|
|
321
|
+
if self.progress_callback:
|
|
322
|
+
self.progress_callback(result.test_name, completed, total)
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
self.logger.error(
|
|
326
|
+
"parallel_test_failed",
|
|
327
|
+
test_id=test_id,
|
|
328
|
+
error=str(e),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return results
|
|
332
|
+
|
|
333
|
+
def get_available_tests(self) -> Dict[str, str]:
|
|
334
|
+
"""Get dictionary of available tests.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Dictionary of test_id -> test_name mappings
|
|
338
|
+
|
|
339
|
+
Example:
|
|
340
|
+
>>> tests = runner.get_available_tests()
|
|
341
|
+
>>> for test_id, test_name in tests.items():
|
|
342
|
+
... print(f"{test_id}: {test_name}")
|
|
343
|
+
"""
|
|
344
|
+
test_dict = {}
|
|
345
|
+
for test_id in self.registry.get_test_ids():
|
|
346
|
+
test_class = self.registry.get_test(test_id)
|
|
347
|
+
# Create temporary instance to get test name
|
|
348
|
+
temp_instance = test_class(self.connector)
|
|
349
|
+
test_dict[test_id] = temp_instance.test_name
|
|
350
|
+
|
|
351
|
+
return test_dict
|
complio/py.typed
ADDED
|
File without changes
|