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,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EC2 security group compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that EC2 security groups don't have overly permissive ingress rules.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.13.1.1 - Network Controls
|
|
7
|
+
Requirement: Networks must be controlled and protected
|
|
8
|
+
|
|
9
|
+
Dangerous configurations:
|
|
10
|
+
- 0.0.0.0/0 (all IPs) access on sensitive ports (22, 3389, 3306, 5432, etc.)
|
|
11
|
+
- Unrestricted access to databases
|
|
12
|
+
- Public SSH/RDP access
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
16
|
+
>>> from complio.tests_library.infrastructure.ec2_security_groups import EC2SecurityGroupTest
|
|
17
|
+
>>>
|
|
18
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
19
|
+
>>> connector.connect()
|
|
20
|
+
>>>
|
|
21
|
+
>>> test = EC2SecurityGroupTest(connector)
|
|
22
|
+
>>> result = test.run()
|
|
23
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from typing import Any, Dict, List
|
|
27
|
+
|
|
28
|
+
from botocore.exceptions import ClientError
|
|
29
|
+
|
|
30
|
+
from complio.connectors.aws.client import AWSConnector
|
|
31
|
+
from complio.tests_library.base import (
|
|
32
|
+
ComplianceTest,
|
|
33
|
+
Evidence,
|
|
34
|
+
Finding,
|
|
35
|
+
Severity,
|
|
36
|
+
TestResult,
|
|
37
|
+
TestStatus,
|
|
38
|
+
)
|
|
39
|
+
from complio.utils.logger import get_logger
|
|
40
|
+
|
|
41
|
+
# Sensitive ports that should never be open to 0.0.0.0/0
|
|
42
|
+
SENSITIVE_PORTS = {
|
|
43
|
+
22: "SSH",
|
|
44
|
+
3389: "RDP (Remote Desktop)",
|
|
45
|
+
3306: "MySQL",
|
|
46
|
+
5432: "PostgreSQL",
|
|
47
|
+
1433: "SQL Server",
|
|
48
|
+
27017: "MongoDB",
|
|
49
|
+
6379: "Redis",
|
|
50
|
+
5984: "CouchDB",
|
|
51
|
+
9200: "Elasticsearch",
|
|
52
|
+
11211: "Memcached",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EC2SecurityGroupTest(ComplianceTest):
|
|
57
|
+
"""Test for EC2 security group compliance.
|
|
58
|
+
|
|
59
|
+
Verifies that security groups don't have overly permissive ingress rules.
|
|
60
|
+
|
|
61
|
+
Compliance Requirements:
|
|
62
|
+
- No 0.0.0.0/0 access on sensitive ports (SSH, RDP, databases)
|
|
63
|
+
- Security groups should follow principle of least privilege
|
|
64
|
+
- Database ports should only be accessible from app servers
|
|
65
|
+
|
|
66
|
+
Scoring:
|
|
67
|
+
- 100% if no security groups have dangerous rules
|
|
68
|
+
- Deduction for each dangerous rule found
|
|
69
|
+
- 0% if critical ports (SSH/RDP) are publicly accessible
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> test = EC2SecurityGroupTest(connector)
|
|
73
|
+
>>> result = test.run()
|
|
74
|
+
>>> if not result.passed:
|
|
75
|
+
... for finding in result.findings:
|
|
76
|
+
... print(f"{finding.severity}: {finding.title}")
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
80
|
+
"""Initialize EC2 security group test.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
connector: AWS connector instance
|
|
84
|
+
"""
|
|
85
|
+
super().__init__(
|
|
86
|
+
test_id="ec2_security_groups",
|
|
87
|
+
test_name="EC2 Security Group Network Controls",
|
|
88
|
+
description="Ensures no security groups allow unrestricted access from 0.0.0.0/0 to critical ports (scans specified region only)",
|
|
89
|
+
control_id="A.13.1.1",
|
|
90
|
+
connector=connector,
|
|
91
|
+
scope="regional",
|
|
92
|
+
)
|
|
93
|
+
self.logger = get_logger(__name__)
|
|
94
|
+
|
|
95
|
+
def execute(self) -> TestResult:
|
|
96
|
+
"""Execute the EC2 security group compliance test.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
TestResult with findings and evidence
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
AWSConnectionError: If unable to connect to AWS
|
|
103
|
+
AWSCredentialsError: If credentials are invalid
|
|
104
|
+
"""
|
|
105
|
+
self.logger.info(
|
|
106
|
+
"starting_ec2_security_group_test",
|
|
107
|
+
region=self.connector.region,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
findings: List[Finding] = []
|
|
111
|
+
evidence_list: List[Evidence] = []
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Get EC2 client
|
|
115
|
+
ec2_client = self.connector.get_client("ec2")
|
|
116
|
+
|
|
117
|
+
# Describe all security groups
|
|
118
|
+
response = ec2_client.describe_security_groups()
|
|
119
|
+
security_groups = response.get("SecurityGroups", [])
|
|
120
|
+
|
|
121
|
+
self.logger.info(
|
|
122
|
+
"security_groups_found",
|
|
123
|
+
count=len(security_groups),
|
|
124
|
+
region=self.connector.region,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not security_groups:
|
|
128
|
+
# No security groups is unusual but not a failure
|
|
129
|
+
return TestResult(
|
|
130
|
+
test_id=self.test_id,
|
|
131
|
+
test_name=self.test_name,
|
|
132
|
+
status=TestStatus.PASSED,
|
|
133
|
+
passed=True,
|
|
134
|
+
score=100.0,
|
|
135
|
+
findings=[],
|
|
136
|
+
evidence=[],
|
|
137
|
+
metadata={
|
|
138
|
+
"region": self.connector.region,
|
|
139
|
+
"total_security_groups": 0,
|
|
140
|
+
"dangerous_rules": 0,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Check each security group for dangerous rules
|
|
145
|
+
total_security_groups = len(security_groups)
|
|
146
|
+
groups_with_issues = 0
|
|
147
|
+
total_dangerous_rules = 0
|
|
148
|
+
|
|
149
|
+
for sg in security_groups:
|
|
150
|
+
sg_id = sg.get("GroupId", "unknown")
|
|
151
|
+
sg_name = sg.get("GroupName", "unknown")
|
|
152
|
+
ingress_rules = sg.get("IpPermissions", [])
|
|
153
|
+
|
|
154
|
+
# Create evidence for this security group
|
|
155
|
+
evidence = Evidence(
|
|
156
|
+
resource_id=sg_id,
|
|
157
|
+
resource_type="ec2_security_group",
|
|
158
|
+
region=self.connector.region,
|
|
159
|
+
data={
|
|
160
|
+
"group_name": sg_name,
|
|
161
|
+
"group_id": sg_id,
|
|
162
|
+
"ingress_rules_count": len(ingress_rules),
|
|
163
|
+
"vpc_id": sg.get("VpcId", "N/A"),
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
evidence_list.append(evidence)
|
|
167
|
+
|
|
168
|
+
# Check each ingress rule
|
|
169
|
+
dangerous_rules = []
|
|
170
|
+
for rule in ingress_rules:
|
|
171
|
+
from_port = rule.get("FromPort", 0)
|
|
172
|
+
to_port = rule.get("ToPort", 65535)
|
|
173
|
+
ip_ranges = rule.get("IpRanges", [])
|
|
174
|
+
|
|
175
|
+
# Check if rule allows 0.0.0.0/0
|
|
176
|
+
for ip_range in ip_ranges:
|
|
177
|
+
cidr = ip_range.get("CidrIp", "")
|
|
178
|
+
if cidr == "0.0.0.0/0":
|
|
179
|
+
# Check if it's on a sensitive port
|
|
180
|
+
for port in range(from_port, to_port + 1):
|
|
181
|
+
if port in SENSITIVE_PORTS:
|
|
182
|
+
dangerous_rules.append({
|
|
183
|
+
"port": port,
|
|
184
|
+
"service": SENSITIVE_PORTS[port],
|
|
185
|
+
"cidr": cidr,
|
|
186
|
+
"from_port": from_port,
|
|
187
|
+
"to_port": to_port,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if dangerous_rules:
|
|
191
|
+
groups_with_issues += 1
|
|
192
|
+
total_dangerous_rules += len(dangerous_rules)
|
|
193
|
+
|
|
194
|
+
# Create finding for each dangerous rule
|
|
195
|
+
for rule in dangerous_rules:
|
|
196
|
+
severity = self._determine_severity(rule["port"])
|
|
197
|
+
|
|
198
|
+
finding = Finding(
|
|
199
|
+
resource_id=sg_id,
|
|
200
|
+
resource_type="ec2_security_group",
|
|
201
|
+
severity=severity,
|
|
202
|
+
title=f"Security group allows public access on {rule['service']} port",
|
|
203
|
+
description=(
|
|
204
|
+
f"Security group '{sg_name}' ({sg_id}) allows unrestricted "
|
|
205
|
+
f"access (0.0.0.0/0) on port {rule['port']} ({rule['service']}). "
|
|
206
|
+
f"This violates the principle of least privilege and exposes "
|
|
207
|
+
f"the service to potential attacks."
|
|
208
|
+
),
|
|
209
|
+
remediation=(
|
|
210
|
+
f"Restrict access to port {rule['port']} to specific IP ranges:\n"
|
|
211
|
+
f"1. Identify which IPs need access to {rule['service']}\n"
|
|
212
|
+
f"2. Update security group rule to use specific CIDR blocks\n"
|
|
213
|
+
f"3. Remove 0.0.0.0/0 rule on port {rule['port']}\n"
|
|
214
|
+
f"4. Consider using VPN or bastion host for sensitive services"
|
|
215
|
+
),
|
|
216
|
+
iso27001_control="A.13.1.1",
|
|
217
|
+
metadata={
|
|
218
|
+
"group_name": sg_name,
|
|
219
|
+
"port": rule["port"],
|
|
220
|
+
"service": rule["service"],
|
|
221
|
+
"cidr": rule["cidr"],
|
|
222
|
+
"port_range": f"{rule['from_port']}-{rule['to_port']}",
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
findings.append(finding)
|
|
226
|
+
|
|
227
|
+
# Calculate score
|
|
228
|
+
if total_dangerous_rules == 0:
|
|
229
|
+
score = 100.0
|
|
230
|
+
status = TestStatus.PASSED
|
|
231
|
+
passed = True
|
|
232
|
+
else:
|
|
233
|
+
# Deduct points for each dangerous rule
|
|
234
|
+
# Critical ports (SSH/RDP) cause immediate failure
|
|
235
|
+
has_critical_exposure = any(
|
|
236
|
+
f.metadata.get("port") in [22, 3389]
|
|
237
|
+
for f in findings
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if has_critical_exposure:
|
|
241
|
+
score = 0.0
|
|
242
|
+
status = TestStatus.FAILED
|
|
243
|
+
passed = False
|
|
244
|
+
else:
|
|
245
|
+
# Deduct 10 points per dangerous rule, minimum 0
|
|
246
|
+
score = max(0.0, 100.0 - (total_dangerous_rules * 10))
|
|
247
|
+
status = TestStatus.FAILED if score < 70 else TestStatus.WARNING
|
|
248
|
+
passed = score >= 70
|
|
249
|
+
|
|
250
|
+
self.logger.info(
|
|
251
|
+
"ec2_security_group_test_complete",
|
|
252
|
+
total_security_groups=total_security_groups,
|
|
253
|
+
groups_with_issues=groups_with_issues,
|
|
254
|
+
dangerous_rules=total_dangerous_rules,
|
|
255
|
+
score=score,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return TestResult(
|
|
259
|
+
test_id=self.test_id,
|
|
260
|
+
test_name=self.test_name,
|
|
261
|
+
status=status,
|
|
262
|
+
passed=passed,
|
|
263
|
+
score=score,
|
|
264
|
+
findings=findings,
|
|
265
|
+
evidence=evidence_list,
|
|
266
|
+
metadata={
|
|
267
|
+
"region": self.connector.region,
|
|
268
|
+
"total_security_groups": total_security_groups,
|
|
269
|
+
"groups_with_issues": groups_with_issues,
|
|
270
|
+
"dangerous_rules": total_dangerous_rules,
|
|
271
|
+
"iso27001_control": "A.13.1.1",
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
except ClientError as e:
|
|
276
|
+
self.logger.error(
|
|
277
|
+
"ec2_security_group_test_failed",
|
|
278
|
+
error=str(e),
|
|
279
|
+
error_code=e.response.get("Error", {}).get("Code"),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return TestResult(
|
|
283
|
+
test_id=self.test_id,
|
|
284
|
+
test_name=self.test_name,
|
|
285
|
+
status=TestStatus.ERROR,
|
|
286
|
+
passed=False,
|
|
287
|
+
score=0.0,
|
|
288
|
+
findings=[
|
|
289
|
+
Finding(
|
|
290
|
+
resource_id="N/A",
|
|
291
|
+
resource_type="ec2_security_group",
|
|
292
|
+
severity=Severity.HIGH,
|
|
293
|
+
title="Failed to check EC2 security groups",
|
|
294
|
+
description=f"Error accessing EC2: {str(e)}",
|
|
295
|
+
remediation="Check AWS credentials and permissions. Ensure IAM policy allows ec2:DescribeSecurityGroups",
|
|
296
|
+
iso27001_control="A.13.1.1",
|
|
297
|
+
)
|
|
298
|
+
],
|
|
299
|
+
evidence=[],
|
|
300
|
+
metadata={"error": str(e)},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _determine_severity(self, port: int) -> Severity:
|
|
304
|
+
"""Determine severity level based on exposed port.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
port: Port number being exposed
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Severity level (CRITICAL for SSH/RDP, HIGH for databases)
|
|
311
|
+
"""
|
|
312
|
+
# SSH and RDP are critical - direct system access
|
|
313
|
+
if port in [22, 3389]:
|
|
314
|
+
return Severity.CRITICAL
|
|
315
|
+
|
|
316
|
+
# Database ports are high severity - data exposure
|
|
317
|
+
if port in [3306, 5432, 1433, 27017, 6379, 5984, 9200, 11211]:
|
|
318
|
+
return Severity.HIGH
|
|
319
|
+
|
|
320
|
+
# Other sensitive ports
|
|
321
|
+
return Severity.MEDIUM
|