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,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