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,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for Complio.
|
|
3
|
+
|
|
4
|
+
This module provides centralized configuration using Pydantic Settings.
|
|
5
|
+
Supports environment variables, defaults, and validation.
|
|
6
|
+
|
|
7
|
+
Configuration Sources (priority order):
|
|
8
|
+
1. Environment variables (COMPLIO_*)
|
|
9
|
+
2. Configuration file (~/.complio/config.yaml)
|
|
10
|
+
3. Default values
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from complio.config.settings import get_settings
|
|
14
|
+
>>> settings = get_settings()
|
|
15
|
+
>>> print(settings.default_region)
|
|
16
|
+
'us-east-1'
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
from pydantic import Field, field_validator
|
|
23
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# CONSTANTS
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
# Application version
|
|
30
|
+
VERSION = "0.1.0"
|
|
31
|
+
|
|
32
|
+
# Default configuration directory
|
|
33
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".complio"
|
|
34
|
+
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
|
35
|
+
DEFAULT_CREDENTIALS_FILE = DEFAULT_CONFIG_DIR / "credentials.enc"
|
|
36
|
+
DEFAULT_LOG_DIR = DEFAULT_CONFIG_DIR / "logs"
|
|
37
|
+
DEFAULT_REPORT_DIR = DEFAULT_CONFIG_DIR / "reports"
|
|
38
|
+
|
|
39
|
+
# Valid AWS regions (as of 2024)
|
|
40
|
+
VALID_AWS_REGIONS: List[str] = [
|
|
41
|
+
"us-east-1",
|
|
42
|
+
"us-east-2",
|
|
43
|
+
"us-west-1",
|
|
44
|
+
"us-west-2",
|
|
45
|
+
"af-south-1",
|
|
46
|
+
"ap-east-1",
|
|
47
|
+
"ap-south-1",
|
|
48
|
+
"ap-southeast-1",
|
|
49
|
+
"ap-southeast-2",
|
|
50
|
+
"ap-southeast-3",
|
|
51
|
+
"ap-northeast-1",
|
|
52
|
+
"ap-northeast-2",
|
|
53
|
+
"ap-northeast-3",
|
|
54
|
+
"ca-central-1",
|
|
55
|
+
"eu-central-1",
|
|
56
|
+
"eu-west-1",
|
|
57
|
+
"eu-west-2",
|
|
58
|
+
"eu-west-3",
|
|
59
|
+
"eu-south-1",
|
|
60
|
+
"eu-north-1",
|
|
61
|
+
"me-south-1",
|
|
62
|
+
"sa-east-1",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Valid output formats
|
|
66
|
+
VALID_OUTPUT_FORMATS: List[str] = ["json", "markdown", "pdf", "html"]
|
|
67
|
+
|
|
68
|
+
# Valid log levels
|
|
69
|
+
VALID_LOG_LEVELS: List[str] = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ============================================================================
|
|
73
|
+
# SETTINGS MODEL
|
|
74
|
+
# ============================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ComplioSettings(BaseSettings):
|
|
78
|
+
"""Application settings with validation and defaults.
|
|
79
|
+
|
|
80
|
+
Settings can be configured via:
|
|
81
|
+
- Environment variables (COMPLIO_DEFAULT_REGION, etc.)
|
|
82
|
+
- Configuration file (~/.complio/config.yaml)
|
|
83
|
+
- Default values (defined here)
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
default_region: Default AWS region for scans
|
|
87
|
+
default_profile: Default credential profile name
|
|
88
|
+
default_output_format: Default report format
|
|
89
|
+
log_level: Logging level
|
|
90
|
+
log_to_file: Enable file logging
|
|
91
|
+
log_dir: Directory for log files
|
|
92
|
+
log_max_bytes: Max log file size before rotation
|
|
93
|
+
log_backup_count: Number of backup log files to keep
|
|
94
|
+
config_dir: Configuration directory path
|
|
95
|
+
credentials_file: Path to encrypted credentials
|
|
96
|
+
report_dir: Directory for generated reports
|
|
97
|
+
aws_timeout: Timeout for AWS API calls (seconds)
|
|
98
|
+
max_concurrent_tests: Maximum concurrent compliance tests
|
|
99
|
+
enable_colors: Enable colored terminal output
|
|
100
|
+
verbose: Enable verbose output
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> settings = ComplioSettings()
|
|
104
|
+
>>> print(settings.default_region)
|
|
105
|
+
'us-east-1'
|
|
106
|
+
|
|
107
|
+
>>> # Override with environment variable
|
|
108
|
+
>>> import os
|
|
109
|
+
>>> os.environ['COMPLIO_DEFAULT_REGION'] = 'eu-west-1'
|
|
110
|
+
>>> settings = ComplioSettings()
|
|
111
|
+
>>> print(settings.default_region)
|
|
112
|
+
'eu-west-1'
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
model_config = SettingsConfigDict(
|
|
116
|
+
env_prefix="COMPLIO_",
|
|
117
|
+
env_file=".env",
|
|
118
|
+
env_file_encoding="utf-8",
|
|
119
|
+
case_sensitive=False,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Application Version
|
|
123
|
+
VERSION: str = Field(
|
|
124
|
+
default=VERSION,
|
|
125
|
+
description="Application version",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# AWS Configuration
|
|
129
|
+
default_region: str = Field(
|
|
130
|
+
default="us-east-1",
|
|
131
|
+
description="Default AWS region for compliance scans",
|
|
132
|
+
)
|
|
133
|
+
default_profile: str = Field(
|
|
134
|
+
default="default",
|
|
135
|
+
description="Default credential profile name",
|
|
136
|
+
)
|
|
137
|
+
aws_timeout: int = Field(
|
|
138
|
+
default=30,
|
|
139
|
+
ge=1,
|
|
140
|
+
le=300,
|
|
141
|
+
description="Timeout for AWS API calls in seconds",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Output Configuration
|
|
145
|
+
default_output_format: str = Field(
|
|
146
|
+
default="json",
|
|
147
|
+
description="Default format for compliance reports",
|
|
148
|
+
)
|
|
149
|
+
enable_colors: bool = Field(
|
|
150
|
+
default=True,
|
|
151
|
+
description="Enable colored terminal output",
|
|
152
|
+
)
|
|
153
|
+
verbose: bool = Field(
|
|
154
|
+
default=False,
|
|
155
|
+
description="Enable verbose output",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Logging Configuration
|
|
159
|
+
log_level: str = Field(
|
|
160
|
+
default="INFO",
|
|
161
|
+
description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
162
|
+
)
|
|
163
|
+
log_to_file: bool = Field(
|
|
164
|
+
default=True,
|
|
165
|
+
description="Enable logging to file",
|
|
166
|
+
)
|
|
167
|
+
log_dir: Path = Field(
|
|
168
|
+
default=DEFAULT_LOG_DIR,
|
|
169
|
+
description="Directory for log files",
|
|
170
|
+
)
|
|
171
|
+
log_max_bytes: int = Field(
|
|
172
|
+
default=10_000_000, # 10 MB
|
|
173
|
+
ge=1_000_000, # Min 1 MB
|
|
174
|
+
le=100_000_000, # Max 100 MB
|
|
175
|
+
description="Maximum log file size before rotation",
|
|
176
|
+
)
|
|
177
|
+
log_backup_count: int = Field(
|
|
178
|
+
default=5,
|
|
179
|
+
ge=1,
|
|
180
|
+
le=20,
|
|
181
|
+
description="Number of backup log files to keep",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Directory Configuration
|
|
185
|
+
config_dir: Path = Field(
|
|
186
|
+
default=DEFAULT_CONFIG_DIR,
|
|
187
|
+
description="Configuration directory path",
|
|
188
|
+
)
|
|
189
|
+
credentials_file: Path = Field(
|
|
190
|
+
default=DEFAULT_CREDENTIALS_FILE,
|
|
191
|
+
description="Path to encrypted credentials file",
|
|
192
|
+
)
|
|
193
|
+
report_dir: Path = Field(
|
|
194
|
+
default=DEFAULT_REPORT_DIR,
|
|
195
|
+
description="Directory for generated reports",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Test Execution Configuration
|
|
199
|
+
max_concurrent_tests: int = Field(
|
|
200
|
+
default=10,
|
|
201
|
+
ge=1,
|
|
202
|
+
le=50,
|
|
203
|
+
description="Maximum number of concurrent compliance tests",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Validation
|
|
207
|
+
@field_validator("default_region")
|
|
208
|
+
@classmethod
|
|
209
|
+
def validate_region(cls, v: str) -> str:
|
|
210
|
+
"""Validate AWS region.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
v: Region string to validate
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Validated region string
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If region is invalid
|
|
220
|
+
"""
|
|
221
|
+
if v not in VALID_AWS_REGIONS:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"Invalid AWS region: {v}. Must be one of: {', '.join(VALID_AWS_REGIONS)}"
|
|
224
|
+
)
|
|
225
|
+
return v
|
|
226
|
+
|
|
227
|
+
@field_validator("default_output_format")
|
|
228
|
+
@classmethod
|
|
229
|
+
def validate_output_format(cls, v: str) -> str:
|
|
230
|
+
"""Validate output format.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
v: Format string to validate
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Validated format string (lowercase)
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If format is invalid
|
|
240
|
+
"""
|
|
241
|
+
v_lower = v.lower()
|
|
242
|
+
if v_lower not in VALID_OUTPUT_FORMATS:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Invalid output format: {v}. Must be one of: {', '.join(VALID_OUTPUT_FORMATS)}"
|
|
245
|
+
)
|
|
246
|
+
return v_lower
|
|
247
|
+
|
|
248
|
+
@field_validator("log_level")
|
|
249
|
+
@classmethod
|
|
250
|
+
def validate_log_level(cls, v: str) -> str:
|
|
251
|
+
"""Validate log level.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
v: Log level string to validate
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Validated log level string (uppercase)
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: If log level is invalid
|
|
261
|
+
"""
|
|
262
|
+
v_upper = v.upper()
|
|
263
|
+
if v_upper not in VALID_LOG_LEVELS:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"Invalid log level: {v}. Must be one of: {', '.join(VALID_LOG_LEVELS)}"
|
|
266
|
+
)
|
|
267
|
+
return v_upper
|
|
268
|
+
|
|
269
|
+
def ensure_directories(self) -> None:
|
|
270
|
+
"""Create required directories if they don't exist.
|
|
271
|
+
|
|
272
|
+
Creates:
|
|
273
|
+
- Configuration directory
|
|
274
|
+
- Log directory
|
|
275
|
+
- Report directory
|
|
276
|
+
|
|
277
|
+
Sets appropriate permissions (700 for directories).
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
>>> settings = ComplioSettings()
|
|
281
|
+
>>> settings.ensure_directories()
|
|
282
|
+
>>> assert settings.log_dir.exists()
|
|
283
|
+
"""
|
|
284
|
+
for directory in [self.config_dir, self.log_dir, self.report_dir]:
|
|
285
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
# Set directory permissions to 700 (rwx------)
|
|
287
|
+
directory.chmod(0o700)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ============================================================================
|
|
291
|
+
# SINGLETON SETTINGS INSTANCE
|
|
292
|
+
# ============================================================================
|
|
293
|
+
|
|
294
|
+
_settings: Optional[ComplioSettings] = None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_settings() -> ComplioSettings:
|
|
298
|
+
"""Get application settings singleton.
|
|
299
|
+
|
|
300
|
+
Returns cached settings instance if available, otherwise creates new one.
|
|
301
|
+
This ensures consistent settings across the application.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
ComplioSettings instance
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> settings = get_settings()
|
|
308
|
+
>>> print(settings.default_region)
|
|
309
|
+
'us-east-1'
|
|
310
|
+
|
|
311
|
+
>>> # Subsequent calls return same instance
|
|
312
|
+
>>> settings2 = get_settings()
|
|
313
|
+
>>> assert settings is settings2
|
|
314
|
+
"""
|
|
315
|
+
global _settings
|
|
316
|
+
if _settings is None:
|
|
317
|
+
_settings = ComplioSettings()
|
|
318
|
+
# Ensure required directories exist
|
|
319
|
+
_settings.ensure_directories()
|
|
320
|
+
return _settings
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def reset_settings() -> None:
|
|
324
|
+
"""Reset settings singleton.
|
|
325
|
+
|
|
326
|
+
Useful for testing to force re-initialization of settings.
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
>>> reset_settings()
|
|
330
|
+
>>> settings = get_settings() # Creates new instance
|
|
331
|
+
"""
|
|
332
|
+
global _settings
|
|
333
|
+
_settings = None
|
|
File without changes
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AWS connector using boto3.
|
|
3
|
+
|
|
4
|
+
This module provides AWS connectivity using standard AWS credentials (from
|
|
5
|
+
~/.aws/credentials, environment variables, or IAM roles) via boto3 SDK.
|
|
6
|
+
|
|
7
|
+
Security Features:
|
|
8
|
+
- Uses boto3's standard credential chain
|
|
9
|
+
- Reads from ~/.aws/credentials (same as AWS CLI)
|
|
10
|
+
- Supports environment variables and IAM roles
|
|
11
|
+
- Optional encrypted credential storage (legacy mode)
|
|
12
|
+
- STS validation before use
|
|
13
|
+
- Configurable timeouts
|
|
14
|
+
- Automatic retry with exponential backoff
|
|
15
|
+
- No credential logging
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
19
|
+
>>> # Uses credentials from ~/.aws/credentials (no password needed)
|
|
20
|
+
>>> connector = AWSConnector(profile_name="default", region="us-east-1")
|
|
21
|
+
>>> connector.connect()
|
|
22
|
+
>>> # Validate credentials
|
|
23
|
+
>>> result = connector.validate_credentials()
|
|
24
|
+
>>> print(result["account_id"])
|
|
25
|
+
'123456789012'
|
|
26
|
+
>>> # Get AWS clients
|
|
27
|
+
>>> s3 = connector.get_client("s3")
|
|
28
|
+
>>> ec2 = connector.get_client("ec2")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from typing import Any, Dict, Optional
|
|
32
|
+
|
|
33
|
+
import boto3
|
|
34
|
+
from boto3.session import Session
|
|
35
|
+
from botocore.config import Config
|
|
36
|
+
from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
|
|
37
|
+
|
|
38
|
+
from complio.config.settings import get_settings
|
|
39
|
+
from complio.connectors.base import CloudConnector
|
|
40
|
+
from complio.utils.exceptions import AWSConnectionError, AWSCredentialsError
|
|
41
|
+
from complio.utils.logger import get_logger, log_aws_api_call
|
|
42
|
+
|
|
43
|
+
logger = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AWSConnector(CloudConnector):
|
|
47
|
+
"""AWS cloud connector using boto3.
|
|
48
|
+
|
|
49
|
+
Manages AWS connections using standard AWS credentials (from ~/.aws/credentials,
|
|
50
|
+
environment variables, or IAM roles). Optionally supports encrypted credential
|
|
51
|
+
storage for legacy compatibility.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
profile_name: Credential profile name (from ~/.aws/credentials)
|
|
55
|
+
region: AWS region
|
|
56
|
+
password: Optional password for encrypted credentials (legacy mode)
|
|
57
|
+
session: Boto3 session (created on connect)
|
|
58
|
+
connected: Connection status
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
>>> # Standard usage (reads from ~/.aws/credentials)
|
|
62
|
+
>>> connector = AWSConnector("default", "us-east-1")
|
|
63
|
+
>>> connector.connect()
|
|
64
|
+
>>> s3_client = connector.get_client("s3")
|
|
65
|
+
>>> buckets = s3_client.list_buckets()
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
profile_name: str,
|
|
71
|
+
region: Optional[str] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Initialize AWS connector.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
profile_name: Name of credential profile (from ~/.aws/credentials)
|
|
77
|
+
region: AWS region (defaults to settings.default_region)
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> connector = AWSConnector("default", "us-east-1")
|
|
81
|
+
>>> connector.connect()
|
|
82
|
+
"""
|
|
83
|
+
settings = get_settings()
|
|
84
|
+
region = region or settings.default_region
|
|
85
|
+
|
|
86
|
+
super().__init__(profile_name=profile_name, region=region)
|
|
87
|
+
|
|
88
|
+
self.session: Optional[Session] = None
|
|
89
|
+
self._clients: Dict[str, Any] = {}
|
|
90
|
+
self._account_id: Optional[str] = None
|
|
91
|
+
self._user_arn: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
def connect(self) -> bool:
|
|
94
|
+
"""Connect to AWS using standard AWS credentials.
|
|
95
|
+
|
|
96
|
+
Uses boto3's default credential chain (reads from ~/.aws/credentials, environment
|
|
97
|
+
variables, EC2 instance metadata, etc). Falls back to encrypted credentials if
|
|
98
|
+
password is explicitly provided.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if connection successful
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
AWSCredentialsError: If credentials cannot be loaded or are invalid
|
|
105
|
+
AWSConnectionError: If connection fails
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
>>> connector.connect()
|
|
109
|
+
True
|
|
110
|
+
"""
|
|
111
|
+
logger.info("connecting_to_aws", profile=self.profile_name, region=self.region)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Use standard AWS credential chain
|
|
115
|
+
# This reads from:
|
|
116
|
+
# 1. ~/.aws/credentials (AWS config file)
|
|
117
|
+
# 2. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
118
|
+
# 3. IAM role (for EC2 instances)
|
|
119
|
+
# 4. Other boto3 credential providers
|
|
120
|
+
logger.debug("using_standard_aws_credentials", profile=self.profile_name)
|
|
121
|
+
|
|
122
|
+
# Create boto3 session using profile from ~/.aws/credentials
|
|
123
|
+
self.session = boto3.Session(
|
|
124
|
+
profile_name=self.profile_name if self.profile_name != "default" else None,
|
|
125
|
+
region_name=self.region
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.connected = True
|
|
129
|
+
logger.info("aws_connection_established", profile=self.profile_name, region=self.region)
|
|
130
|
+
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
except AWSCredentialsError:
|
|
134
|
+
raise
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error("aws_connection_failed", error=str(e), profile=self.profile_name)
|
|
137
|
+
raise AWSConnectionError(
|
|
138
|
+
f"Failed to connect to AWS: {str(e)}",
|
|
139
|
+
details={"profile": self.profile_name, "region": self.region}
|
|
140
|
+
) from e
|
|
141
|
+
|
|
142
|
+
def disconnect(self) -> None:
|
|
143
|
+
"""Disconnect from AWS.
|
|
144
|
+
|
|
145
|
+
Clears session and cached clients.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
>>> connector.disconnect()
|
|
149
|
+
"""
|
|
150
|
+
self.session = None
|
|
151
|
+
self._clients = {}
|
|
152
|
+
self.connected = False
|
|
153
|
+
logger.info("aws_disconnected", profile=self.profile_name)
|
|
154
|
+
|
|
155
|
+
def get_client(self, service_name: str, region: Optional[str] = None) -> Any:
|
|
156
|
+
"""Get boto3 client for AWS service.
|
|
157
|
+
|
|
158
|
+
Creates and caches boto3 clients with proper configuration.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
service_name: AWS service name (e.g., 's3', 'ec2', 'iam')
|
|
162
|
+
region: AWS region (uses connector region if not specified)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Boto3 client for the specified service
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
AWSConnectionError: If not connected
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> s3 = connector.get_client("s3")
|
|
172
|
+
>>> buckets = s3.list_buckets()
|
|
173
|
+
"""
|
|
174
|
+
if not self.connected or not self.session:
|
|
175
|
+
raise AWSConnectionError(
|
|
176
|
+
"Not connected to AWS. Call connect() first.",
|
|
177
|
+
details={"profile": self.profile_name}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
region = region or self.region
|
|
181
|
+
cache_key = f"{service_name}:{region}"
|
|
182
|
+
|
|
183
|
+
# Return cached client if available
|
|
184
|
+
if cache_key in self._clients:
|
|
185
|
+
return self._clients[cache_key]
|
|
186
|
+
|
|
187
|
+
# Create new client with configuration
|
|
188
|
+
settings = get_settings()
|
|
189
|
+
|
|
190
|
+
client_config = Config(
|
|
191
|
+
region_name=region,
|
|
192
|
+
retries={
|
|
193
|
+
"max_attempts": 3,
|
|
194
|
+
"mode": "adaptive"
|
|
195
|
+
},
|
|
196
|
+
connect_timeout=settings.aws_timeout,
|
|
197
|
+
read_timeout=settings.aws_timeout,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
client = self.session.client(service_name, config=client_config)
|
|
202
|
+
self._clients[cache_key] = client
|
|
203
|
+
|
|
204
|
+
logger.debug("aws_client_created", service=service_name, region=region)
|
|
205
|
+
|
|
206
|
+
return client
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error("aws_client_creation_failed", service=service_name, error=str(e))
|
|
210
|
+
raise AWSConnectionError(
|
|
211
|
+
f"Failed to create {service_name} client: {str(e)}",
|
|
212
|
+
details={"service": service_name, "region": region}
|
|
213
|
+
) from e
|
|
214
|
+
|
|
215
|
+
def validate_credentials(self) -> Dict[str, Any]:
|
|
216
|
+
"""Validate AWS credentials using STS GetCallerIdentity.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dictionary with validation result:
|
|
220
|
+
{
|
|
221
|
+
"valid": True,
|
|
222
|
+
"account_id": "123456789012",
|
|
223
|
+
"user_arn": "arn:aws:iam::123456789012:user/admin",
|
|
224
|
+
"user_id": "AIDAI..."
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
AWSCredentialsError: If credentials are invalid
|
|
229
|
+
AWSConnectionError: If not connected
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
>>> result = connector.validate_credentials()
|
|
233
|
+
>>> print(f"Connected to account: {result['account_id']}")
|
|
234
|
+
"""
|
|
235
|
+
if not self.connected:
|
|
236
|
+
raise AWSConnectionError(
|
|
237
|
+
"Not connected to AWS. Call connect() first.",
|
|
238
|
+
details={"profile": self.profile_name}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
logger.info("validating_aws_credentials", profile=self.profile_name)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
sts = self.get_client("sts")
|
|
245
|
+
log_aws_api_call(logger, "sts", "get_caller_identity", self.region)
|
|
246
|
+
|
|
247
|
+
response = sts.get_caller_identity()
|
|
248
|
+
|
|
249
|
+
result = {
|
|
250
|
+
"valid": True,
|
|
251
|
+
"account_id": response["Account"],
|
|
252
|
+
"user_arn": response["Arn"],
|
|
253
|
+
"user_id": response["UserId"],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Cache for future use
|
|
257
|
+
self._account_id = result["account_id"]
|
|
258
|
+
self._user_arn = result["user_arn"]
|
|
259
|
+
|
|
260
|
+
logger.info(
|
|
261
|
+
"aws_credentials_valid",
|
|
262
|
+
account_id=result["account_id"],
|
|
263
|
+
user_id=result["user_id"]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
except (ClientError, NoCredentialsError, PartialCredentialsError) as e:
|
|
269
|
+
logger.error("aws_credential_validation_failed", error=str(e))
|
|
270
|
+
raise AWSCredentialsError(
|
|
271
|
+
f"AWS credential validation failed: {str(e)}",
|
|
272
|
+
details={"profile": self.profile_name}
|
|
273
|
+
) from e
|
|
274
|
+
|
|
275
|
+
def test_connection(self) -> bool:
|
|
276
|
+
"""Test if AWS connection is working.
|
|
277
|
+
|
|
278
|
+
Performs a simple STS call to verify connectivity.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if connection is healthy, False otherwise
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
>>> if connector.test_connection():
|
|
285
|
+
... print("AWS connection is healthy")
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
self.validate_credentials()
|
|
289
|
+
return True
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning("aws_connection_test_failed", error=str(e))
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def get_account_id(self) -> str:
|
|
295
|
+
"""Get AWS account ID.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
AWS account ID
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
AWSConnectionError: If credentials haven't been validated
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> account_id = connector.get_account_id()
|
|
305
|
+
>>> print(account_id)
|
|
306
|
+
'123456789012'
|
|
307
|
+
"""
|
|
308
|
+
if not self._account_id:
|
|
309
|
+
# Validate to get account ID
|
|
310
|
+
self.validate_credentials()
|
|
311
|
+
|
|
312
|
+
if not self._account_id:
|
|
313
|
+
raise AWSConnectionError(
|
|
314
|
+
"Account ID not available. Validate credentials first.",
|
|
315
|
+
details={"profile": self.profile_name}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return self._account_id
|
|
319
|
+
|
|
320
|
+
def list_regions(self, service: str = "ec2") -> list[str]:
|
|
321
|
+
"""List available AWS regions for a service.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
service: AWS service name (default: ec2)
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
List of region names
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
>>> regions = connector.list_regions()
|
|
331
|
+
>>> print(regions)
|
|
332
|
+
['us-east-1', 'us-west-2', 'eu-west-1', ...]
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
client = self.get_client(service)
|
|
336
|
+
regions = client.describe_regions()["Regions"]
|
|
337
|
+
return [region["RegionName"] for region in regions]
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.warning("failed_to_list_regions", error=str(e))
|
|
340
|
+
# Return default list if API call fails
|
|
341
|
+
from complio.config.settings import VALID_AWS_REGIONS
|
|
342
|
+
return VALID_AWS_REGIONS
|