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,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S3 bucket versioning compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all S3 buckets have versioning enabled for data recovery.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.13 - Information backup
|
|
7
|
+
Requirement: S3 buckets must have versioning enabled
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.storage.s3_versioning import S3VersioningTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = S3VersioningTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
from botocore.exceptions import ClientError
|
|
24
|
+
|
|
25
|
+
from complio.connectors.aws.client import AWSConnector
|
|
26
|
+
from complio.tests_library.base import (
|
|
27
|
+
ComplianceTest,
|
|
28
|
+
Severity,
|
|
29
|
+
TestResult,
|
|
30
|
+
TestStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class S3VersioningTest(ComplianceTest):
|
|
35
|
+
"""Test for S3 bucket versioning compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all S3 buckets have versioning enabled to protect
|
|
38
|
+
against accidental deletion and provide data recovery capabilities.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- All S3 buckets must have versioning enabled (Status='Enabled')
|
|
42
|
+
- MFA Delete is recommended for additional protection (bonus check)
|
|
43
|
+
- Versioning protects against accidental deletion and modification
|
|
44
|
+
|
|
45
|
+
Scoring:
|
|
46
|
+
- 100% if all buckets have versioning enabled
|
|
47
|
+
- Proportional score based on compliant/total ratio
|
|
48
|
+
- 0% if no buckets have versioning enabled
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> test = S3VersioningTest(connector)
|
|
52
|
+
>>> result = test.execute()
|
|
53
|
+
>>> for finding in result.findings:
|
|
54
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
58
|
+
"""Initialize S3 versioning test.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connector: AWS connector instance
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
test_id="s3_versioning",
|
|
65
|
+
test_name="S3 Bucket Versioning Check",
|
|
66
|
+
description="Verify all S3 buckets have versioning enabled for data recovery",
|
|
67
|
+
control_id="A.8.13",
|
|
68
|
+
connector=connector,
|
|
69
|
+
scope="global",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def execute(self) -> TestResult:
|
|
73
|
+
"""Execute S3 bucket versioning compliance test.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TestResult with findings for buckets without versioning
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> test = S3VersioningTest(connector)
|
|
80
|
+
>>> result = test.execute()
|
|
81
|
+
>>> print(result.score)
|
|
82
|
+
85.0
|
|
83
|
+
"""
|
|
84
|
+
result = TestResult(
|
|
85
|
+
test_id=self.test_id,
|
|
86
|
+
test_name=self.test_name,
|
|
87
|
+
status=TestStatus.PASSED,
|
|
88
|
+
passed=True,
|
|
89
|
+
score=100.0,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Get S3 client
|
|
94
|
+
s3_client = self.connector.get_client("s3")
|
|
95
|
+
|
|
96
|
+
# List all buckets
|
|
97
|
+
self.logger.info("listing_s3_buckets")
|
|
98
|
+
buckets_response = s3_client.list_buckets()
|
|
99
|
+
buckets = buckets_response.get("Buckets", [])
|
|
100
|
+
|
|
101
|
+
if not buckets:
|
|
102
|
+
self.logger.info("no_s3_buckets_found")
|
|
103
|
+
result.metadata["message"] = "No S3 buckets found in account"
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
self.logger.info("s3_buckets_found", count=len(buckets))
|
|
107
|
+
|
|
108
|
+
# Check versioning for each bucket
|
|
109
|
+
versioning_enabled_count = 0
|
|
110
|
+
|
|
111
|
+
for bucket in buckets:
|
|
112
|
+
bucket_name = bucket["Name"]
|
|
113
|
+
result.resources_scanned += 1
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Get bucket versioning configuration
|
|
117
|
+
versioning_response = s3_client.get_bucket_versioning(Bucket=bucket_name)
|
|
118
|
+
|
|
119
|
+
# Check versioning status
|
|
120
|
+
versioning_status = versioning_response.get("Status", "Disabled")
|
|
121
|
+
mfa_delete = versioning_response.get("MFADelete", "Disabled")
|
|
122
|
+
|
|
123
|
+
versioning_enabled = versioning_status == "Enabled"
|
|
124
|
+
|
|
125
|
+
# Create evidence
|
|
126
|
+
evidence = self.create_evidence(
|
|
127
|
+
resource_id=bucket_name,
|
|
128
|
+
resource_type="s3_bucket",
|
|
129
|
+
data={
|
|
130
|
+
"bucket_name": bucket_name,
|
|
131
|
+
"versioning_status": versioning_status,
|
|
132
|
+
"mfa_delete": mfa_delete,
|
|
133
|
+
"creation_date": bucket.get("CreationDate").isoformat() if bucket.get("CreationDate") else None,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
result.add_evidence(evidence)
|
|
137
|
+
|
|
138
|
+
if versioning_enabled:
|
|
139
|
+
versioning_enabled_count += 1
|
|
140
|
+
self.logger.debug(
|
|
141
|
+
"bucket_versioning_enabled",
|
|
142
|
+
bucket=bucket_name,
|
|
143
|
+
mfa_delete=mfa_delete
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
# Create finding for bucket without versioning
|
|
147
|
+
finding = self.create_finding(
|
|
148
|
+
resource_id=bucket_name,
|
|
149
|
+
resource_type="s3_bucket",
|
|
150
|
+
severity=Severity.MEDIUM,
|
|
151
|
+
title="S3 bucket versioning not enabled",
|
|
152
|
+
description=f"S3 bucket '{bucket_name}' does not have versioning enabled "
|
|
153
|
+
f"(current status: {versioning_status}). Without versioning, objects "
|
|
154
|
+
"can be permanently deleted or overwritten accidentally. Versioning "
|
|
155
|
+
"protects against unintended user actions, application failures, and "
|
|
156
|
+
"provides the ability to recover previous versions. "
|
|
157
|
+
"ISO 27001 A.8.13 requires proper backup and recovery capabilities.",
|
|
158
|
+
remediation=(
|
|
159
|
+
f"Enable versioning for S3 bucket '{bucket_name}':\n\n"
|
|
160
|
+
"Using AWS CLI:\n"
|
|
161
|
+
f"aws s3api put-bucket-versioning --bucket {bucket_name} \\\n"
|
|
162
|
+
" --versioning-configuration Status=Enabled\n\n"
|
|
163
|
+
"Or use AWS Console:\n"
|
|
164
|
+
"1. Go to AWS S3 console\n"
|
|
165
|
+
f"2. Select bucket '{bucket_name}'\n"
|
|
166
|
+
"3. Go to 'Properties' tab\n"
|
|
167
|
+
"4. Under 'Bucket Versioning', click 'Edit'\n"
|
|
168
|
+
"5. Select 'Enable'\n"
|
|
169
|
+
"6. Click 'Save changes'\n\n"
|
|
170
|
+
"Optional: Enable MFA Delete for additional protection:\n"
|
|
171
|
+
f"aws s3api put-bucket-versioning --bucket {bucket_name} \\\n"
|
|
172
|
+
" --versioning-configuration Status=Enabled,MFADelete=Enabled \\\n"
|
|
173
|
+
" --mfa \"arn:aws:iam::ACCOUNT-ID:mfa/root-account-mfa-device XXXXXX\"\n\n"
|
|
174
|
+
"Note: MFA Delete requires root account credentials.\n"
|
|
175
|
+
"Consider lifecycle policies to manage versioned objects and costs."
|
|
176
|
+
),
|
|
177
|
+
evidence=evidence
|
|
178
|
+
)
|
|
179
|
+
result.add_finding(finding)
|
|
180
|
+
|
|
181
|
+
self.logger.warning(
|
|
182
|
+
"bucket_versioning_disabled",
|
|
183
|
+
bucket=bucket_name,
|
|
184
|
+
status=versioning_status
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
except ClientError as e:
|
|
188
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
189
|
+
if error_code in ["NoSuchBucket", "AccessDenied"]:
|
|
190
|
+
self.logger.warning(
|
|
191
|
+
"bucket_versioning_check_error",
|
|
192
|
+
bucket=bucket_name,
|
|
193
|
+
error_code=error_code
|
|
194
|
+
)
|
|
195
|
+
continue
|
|
196
|
+
else:
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
# Calculate compliance score
|
|
200
|
+
result.score = (versioning_enabled_count / len(buckets)) * 100
|
|
201
|
+
|
|
202
|
+
# Determine pass/fail
|
|
203
|
+
result.passed = versioning_enabled_count == len(buckets)
|
|
204
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
205
|
+
|
|
206
|
+
# Add metadata
|
|
207
|
+
result.metadata = {
|
|
208
|
+
"total_buckets": len(buckets),
|
|
209
|
+
"versioning_enabled": versioning_enabled_count,
|
|
210
|
+
"versioning_disabled": len(buckets) - versioning_enabled_count,
|
|
211
|
+
"compliance_percentage": result.score,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
self.logger.info(
|
|
215
|
+
"s3_versioning_test_completed",
|
|
216
|
+
total_buckets=len(buckets),
|
|
217
|
+
versioning_enabled=versioning_enabled_count,
|
|
218
|
+
score=result.score,
|
|
219
|
+
passed=result.passed
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
except ClientError as e:
|
|
223
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
224
|
+
self.logger.error("s3_versioning_test_error", error_code=error_code, error=str(e))
|
|
225
|
+
result.status = TestStatus.ERROR
|
|
226
|
+
result.passed = False
|
|
227
|
+
result.score = 0.0
|
|
228
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
self.logger.error("s3_versioning_test_error", error=str(e))
|
|
232
|
+
result.status = TestStatus.ERROR
|
|
233
|
+
result.passed = False
|
|
234
|
+
result.score = 0.0
|
|
235
|
+
result.error_message = str(e)
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ============================================================================
|
|
241
|
+
# CONVENIENCE FUNCTION
|
|
242
|
+
# ============================================================================
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_s3_versioning_test(connector: AWSConnector) -> TestResult:
|
|
246
|
+
"""Run S3 bucket versioning compliance test.
|
|
247
|
+
|
|
248
|
+
Convenience function for running the test.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
connector: AWS connector
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
TestResult
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
258
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
259
|
+
>>> connector.connect()
|
|
260
|
+
>>> result = run_s3_versioning_test(connector)
|
|
261
|
+
>>> print(f"Score: {result.score}%")
|
|
262
|
+
"""
|
|
263
|
+
test = S3VersioningTest(connector)
|
|
264
|
+
return test.execute()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Utility modules for Complio."""
|
|
2
|
+
|
|
3
|
+
from complio.utils.exceptions import (
|
|
4
|
+
AWSConnectionError,
|
|
5
|
+
AWSCredentialsError,
|
|
6
|
+
AWSError,
|
|
7
|
+
ComplioError,
|
|
8
|
+
InvalidRegionError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
from complio.utils.logger import get_logger, setup_logging
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# Logging
|
|
15
|
+
"get_logger",
|
|
16
|
+
"setup_logging",
|
|
17
|
+
# Exceptions - Base
|
|
18
|
+
"ComplioError",
|
|
19
|
+
"AWSError",
|
|
20
|
+
# Exceptions - AWS
|
|
21
|
+
"AWSConnectionError",
|
|
22
|
+
"AWSCredentialsError",
|
|
23
|
+
"InvalidRegionError",
|
|
24
|
+
# Exceptions - Validation
|
|
25
|
+
"ValidationError",
|
|
26
|
+
]
|
complio/utils/errors.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User-friendly error message translation for Complio.
|
|
3
|
+
|
|
4
|
+
This module translates technical AWS and system errors into actionable,
|
|
5
|
+
user-friendly messages with clear next steps.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from complio.utils.errors import handle_aws_error
|
|
9
|
+
>>> try:
|
|
10
|
+
>>> connector.connect()
|
|
11
|
+
>>> except Exception as e:
|
|
12
|
+
>>> handle_aws_error(e)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import configparser
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from typing import NoReturn
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
from botocore.exceptions import (
|
|
22
|
+
ClientError,
|
|
23
|
+
EndpointConnectionError,
|
|
24
|
+
NoCredentialsError,
|
|
25
|
+
PartialCredentialsError,
|
|
26
|
+
ProfileNotFound,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_aws_error(error: Exception) -> NoReturn:
|
|
31
|
+
"""Translate technical AWS errors to user-friendly messages.
|
|
32
|
+
|
|
33
|
+
This function catches common AWS errors and displays helpful messages
|
|
34
|
+
with actionable next steps instead of technical stack traces.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
error: The exception that was raised
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
SystemExit: Always exits with code 1 after displaying the error
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> try:
|
|
44
|
+
>>> session = boto3.Session(profile_name="nonexistent")
|
|
45
|
+
>>> except Exception as e:
|
|
46
|
+
>>> handle_aws_error(e)
|
|
47
|
+
>>> # Displays: "❌ AWS profile 'nonexistent' not found"
|
|
48
|
+
>>> # Then lists available profiles
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if isinstance(error, NoCredentialsError):
|
|
52
|
+
click.echo("❌ AWS credentials not found", err=True)
|
|
53
|
+
click.echo("\nTo configure AWS credentials:", err=True)
|
|
54
|
+
click.echo(" 1. Run: aws configure", err=True)
|
|
55
|
+
click.echo(" 2. Enter your Access Key ID and Secret Access Key", err=True)
|
|
56
|
+
click.echo("\nFor help: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html", err=True)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
elif isinstance(error, PartialCredentialsError):
|
|
60
|
+
click.echo("❌ Incomplete AWS credentials", err=True)
|
|
61
|
+
click.echo("\nYour AWS credentials are missing required information.", err=True)
|
|
62
|
+
click.echo("Run: aws configure", err=True)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
elif isinstance(error, ProfileNotFound):
|
|
66
|
+
# Extract profile name from error message
|
|
67
|
+
profile = str(error).split("'")[1] if "'" in str(error) else "unknown"
|
|
68
|
+
click.echo(f"❌ AWS profile '{profile}' not found", err=True)
|
|
69
|
+
click.echo(f"\nAvailable profiles in ~/.aws/credentials:", err=True)
|
|
70
|
+
try:
|
|
71
|
+
config = configparser.ConfigParser()
|
|
72
|
+
config.read(os.path.expanduser('~/.aws/credentials'))
|
|
73
|
+
if config.sections():
|
|
74
|
+
for prof in config.sections():
|
|
75
|
+
click.echo(f" • {prof}", err=True)
|
|
76
|
+
else:
|
|
77
|
+
click.echo(" (No profiles configured)", err=True)
|
|
78
|
+
click.echo("\nRun 'aws configure' to create a profile", err=True)
|
|
79
|
+
except Exception:
|
|
80
|
+
click.echo(" (Unable to read credentials file)", err=True)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
elif isinstance(error, EndpointConnectionError):
|
|
84
|
+
click.echo("❌ Cannot connect to AWS", err=True)
|
|
85
|
+
click.echo("\nPossible causes:", err=True)
|
|
86
|
+
click.echo(" • No internet connection", err=True)
|
|
87
|
+
click.echo(" • Invalid region specified", err=True)
|
|
88
|
+
click.echo(" • Corporate firewall blocking AWS", err=True)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
elif isinstance(error, ClientError):
|
|
92
|
+
error_code = error.response.get('Error', {}).get('Code', 'Unknown')
|
|
93
|
+
error_message = error.response.get('Error', {}).get('Message', '')
|
|
94
|
+
|
|
95
|
+
if error_code == 'UnauthorizedOperation':
|
|
96
|
+
click.echo("❌ Insufficient AWS permissions", err=True)
|
|
97
|
+
click.echo("\nYour AWS user needs the 'SecurityAudit' policy.", err=True)
|
|
98
|
+
click.echo("Contact your AWS administrator to grant permissions.", err=True)
|
|
99
|
+
|
|
100
|
+
elif error_code == 'InvalidClientTokenId':
|
|
101
|
+
click.echo("❌ Invalid AWS credentials", err=True)
|
|
102
|
+
click.echo("\nYour Access Key ID is not recognized by AWS.", err=True)
|
|
103
|
+
click.echo("Run 'aws configure' to update your credentials.", err=True)
|
|
104
|
+
|
|
105
|
+
elif error_code == 'SignatureDoesNotMatch':
|
|
106
|
+
click.echo("❌ Invalid AWS credentials", err=True)
|
|
107
|
+
click.echo("\nYour Secret Access Key is incorrect.", err=True)
|
|
108
|
+
click.echo("Run 'aws configure' to update your credentials.", err=True)
|
|
109
|
+
|
|
110
|
+
elif error_code == 'AccessDenied':
|
|
111
|
+
click.echo("❌ Access denied", err=True)
|
|
112
|
+
click.echo(f"\n{error_message}", err=True)
|
|
113
|
+
click.echo("\nYour AWS user needs additional permissions.", err=True)
|
|
114
|
+
click.echo("Contact your AWS administrator.", err=True)
|
|
115
|
+
|
|
116
|
+
elif error_code == 'InvalidRegion':
|
|
117
|
+
click.echo("❌ Invalid AWS region", err=True)
|
|
118
|
+
click.echo("\nValid regions: us-east-1, eu-west-1, eu-west-3, etc.", err=True)
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
click.echo(f"❌ AWS Error: {error_code}", err=True)
|
|
122
|
+
if error_message:
|
|
123
|
+
click.echo(f"\n{error_message}", err=True)
|
|
124
|
+
click.echo(f"\nFor help: https://docs.complio.tech/errors", err=True)
|
|
125
|
+
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
# Unknown error - show technical message but add help
|
|
130
|
+
click.echo(f"❌ Unexpected error: {str(error)}", err=True)
|
|
131
|
+
click.echo(f"\nFor help: https://docs.complio.tech/troubleshooting", err=True)
|
|
132
|
+
click.echo(f"Support: support@complio.tech", err=True)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_region_format(region: str) -> bool:
|
|
137
|
+
"""Validate AWS region format.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
region: Region string to validate
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if valid format, False otherwise
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> validate_region_format("us-east-1")
|
|
147
|
+
True
|
|
148
|
+
>>> validate_region_format("invalid")
|
|
149
|
+
False
|
|
150
|
+
"""
|
|
151
|
+
import re
|
|
152
|
+
# AWS region format: 2 letters, dash, direction/location, dash, number
|
|
153
|
+
# Examples: us-east-1, eu-west-3, ap-southeast-2
|
|
154
|
+
pattern = r'^[a-z]{2}-[a-z]+-\d+$'
|
|
155
|
+
return bool(re.match(pattern, region))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_profile_exists(profile: str) -> bool:
|
|
159
|
+
"""Check if AWS profile exists in credentials file.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
profile: Profile name to check
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if profile exists, False otherwise
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> validate_profile_exists("default")
|
|
169
|
+
True
|
|
170
|
+
"""
|
|
171
|
+
if profile == "default":
|
|
172
|
+
return True # Default profile is implicit
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
config = configparser.ConfigParser()
|
|
176
|
+
config.read(os.path.expanduser('~/.aws/credentials'))
|
|
177
|
+
return profile in config.sections()
|
|
178
|
+
except Exception:
|
|
179
|
+
return True # If can't read file, let boto3 handle it
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for Complio.
|
|
3
|
+
|
|
4
|
+
This module defines all custom exceptions used throughout the application.
|
|
5
|
+
All exceptions inherit from ComplioError for easy catching of all application errors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComplioError(Exception):
|
|
12
|
+
"""Base exception for all Complio errors.
|
|
13
|
+
|
|
14
|
+
All custom exceptions should inherit from this class to allow
|
|
15
|
+
catching all application-specific errors.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str, details: Optional[dict[str, str]] = None) -> None:
|
|
19
|
+
"""Initialize ComplioError.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
message: Human-readable error message
|
|
23
|
+
details: Optional dictionary with additional error context
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.message = message
|
|
27
|
+
self.details = details or {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CryptoError(ComplioError):
|
|
31
|
+
"""Base exception for cryptography-related errors."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EncryptionError(CryptoError):
|
|
36
|
+
"""Raised when encryption operations fail.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
- Encryption algorithm failure
|
|
40
|
+
- Invalid encryption key
|
|
41
|
+
- Corrupted data during encryption
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DecryptionError(CryptoError):
|
|
47
|
+
"""Raised when decryption operations fail.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
- Invalid decryption key (wrong password)
|
|
51
|
+
- Corrupted encrypted data
|
|
52
|
+
- Tampered encrypted file
|
|
53
|
+
- Invalid token format
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CredentialError(ComplioError):
|
|
59
|
+
"""Base exception for credential-related errors."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class InvalidCredentialsError(CredentialError):
|
|
64
|
+
"""Raised when AWS credentials are invalid or malformed.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
- Empty access key or secret key
|
|
68
|
+
- Invalid access key format (not AKIA...)
|
|
69
|
+
- Credentials that don't match AWS format requirements
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CredentialStorageError(CredentialError):
|
|
75
|
+
"""Raised when credential storage operations fail.
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
- Unable to create credentials directory
|
|
79
|
+
- Insufficient permissions to write credentials file
|
|
80
|
+
- Disk full when saving credentials
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CredentialNotFoundError(CredentialError):
|
|
86
|
+
"""Raised when attempting to load non-existent credentials.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
- Credentials file doesn't exist
|
|
90
|
+
- Profile name not found in credentials file
|
|
91
|
+
"""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class InvalidPasswordError(CredentialError):
|
|
96
|
+
"""Raised when password validation fails.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
- Password too short (< 8 characters)
|
|
100
|
+
- Password doesn't meet complexity requirements
|
|
101
|
+
- Wrong password during decryption
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ValidationError(ComplioError):
|
|
107
|
+
"""Raised when input validation fails.
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
- Invalid AWS region
|
|
111
|
+
- Malformed account ID
|
|
112
|
+
- Invalid file path
|
|
113
|
+
"""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AWSError(ComplioError):
|
|
118
|
+
"""Base exception for AWS-related errors."""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AWSCredentialsError(AWSError):
|
|
123
|
+
"""Raised when AWS API rejects credentials.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
- Invalid access key or secret key
|
|
127
|
+
- Expired temporary credentials
|
|
128
|
+
- Insufficient IAM permissions
|
|
129
|
+
"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AWSConnectionError(AWSError):
|
|
134
|
+
"""Raised when unable to connect to AWS API.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
- Network timeout
|
|
138
|
+
- DNS resolution failure
|
|
139
|
+
- AWS service unavailable
|
|
140
|
+
"""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class InvalidRegionError(AWSError):
|
|
145
|
+
"""Raised when AWS region is invalid.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
- Non-existent region name
|
|
149
|
+
- Region not enabled for account
|
|
150
|
+
"""
|
|
151
|
+
pass
|