aws-inventory-manager 0.2.0__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.

Potentially problematic release.


This version of aws-inventory-manager might be problematic. Click here for more details.

Files changed (65) hide show
  1. aws_inventory_manager-0.2.0.dist-info/METADATA +508 -0
  2. aws_inventory_manager-0.2.0.dist-info/RECORD +65 -0
  3. aws_inventory_manager-0.2.0.dist-info/WHEEL +5 -0
  4. aws_inventory_manager-0.2.0.dist-info/entry_points.txt +2 -0
  5. aws_inventory_manager-0.2.0.dist-info/licenses/LICENSE +21 -0
  6. aws_inventory_manager-0.2.0.dist-info/top_level.txt +1 -0
  7. src/__init__.py +3 -0
  8. src/aws/__init__.py +11 -0
  9. src/aws/client.py +128 -0
  10. src/aws/credentials.py +191 -0
  11. src/aws/rate_limiter.py +177 -0
  12. src/cli/__init__.py +5 -0
  13. src/cli/config.py +130 -0
  14. src/cli/main.py +1450 -0
  15. src/cost/__init__.py +5 -0
  16. src/cost/analyzer.py +226 -0
  17. src/cost/explorer.py +209 -0
  18. src/cost/reporter.py +237 -0
  19. src/delta/__init__.py +5 -0
  20. src/delta/calculator.py +180 -0
  21. src/delta/reporter.py +225 -0
  22. src/models/__init__.py +17 -0
  23. src/models/cost_report.py +87 -0
  24. src/models/delta_report.py +111 -0
  25. src/models/inventory.py +124 -0
  26. src/models/resource.py +99 -0
  27. src/models/snapshot.py +108 -0
  28. src/snapshot/__init__.py +6 -0
  29. src/snapshot/capturer.py +347 -0
  30. src/snapshot/filter.py +245 -0
  31. src/snapshot/inventory_storage.py +264 -0
  32. src/snapshot/resource_collectors/__init__.py +5 -0
  33. src/snapshot/resource_collectors/apigateway.py +140 -0
  34. src/snapshot/resource_collectors/backup.py +136 -0
  35. src/snapshot/resource_collectors/base.py +81 -0
  36. src/snapshot/resource_collectors/cloudformation.py +55 -0
  37. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  38. src/snapshot/resource_collectors/codebuild.py +69 -0
  39. src/snapshot/resource_collectors/codepipeline.py +82 -0
  40. src/snapshot/resource_collectors/dynamodb.py +65 -0
  41. src/snapshot/resource_collectors/ec2.py +240 -0
  42. src/snapshot/resource_collectors/ecs.py +215 -0
  43. src/snapshot/resource_collectors/eks.py +200 -0
  44. src/snapshot/resource_collectors/elb.py +126 -0
  45. src/snapshot/resource_collectors/eventbridge.py +156 -0
  46. src/snapshot/resource_collectors/iam.py +188 -0
  47. src/snapshot/resource_collectors/kms.py +111 -0
  48. src/snapshot/resource_collectors/lambda_func.py +112 -0
  49. src/snapshot/resource_collectors/rds.py +109 -0
  50. src/snapshot/resource_collectors/route53.py +86 -0
  51. src/snapshot/resource_collectors/s3.py +105 -0
  52. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  53. src/snapshot/resource_collectors/sns.py +68 -0
  54. src/snapshot/resource_collectors/sqs.py +72 -0
  55. src/snapshot/resource_collectors/ssm.py +160 -0
  56. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  57. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  58. src/snapshot/resource_collectors/waf.py +159 -0
  59. src/snapshot/storage.py +259 -0
  60. src/utils/__init__.py +12 -0
  61. src/utils/export.py +87 -0
  62. src/utils/hash.py +60 -0
  63. src/utils/logging.py +63 -0
  64. src/utils/paths.py +51 -0
  65. src/utils/progress.py +41 -0
src/aws/client.py ADDED
@@ -0,0 +1,128 @@
1
+ """Boto3 client wrapper with retry configuration and error handling."""
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ import boto3
7
+ from botocore.config import Config
8
+ from botocore.exceptions import ClientError, NoCredentialsError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ # Aggressive retry configuration for batch operations
14
+ DEFAULT_RETRY_CONFIG = Config(
15
+ retries={"max_attempts": 10, "mode": "adaptive"}, # Adaptive retry mode (boto3 1.16+)
16
+ max_pool_connections=50,
17
+ connect_timeout=60,
18
+ read_timeout=60,
19
+ )
20
+
21
+
22
+ def create_boto_client(
23
+ service_name: str,
24
+ region_name: str = "us-east-1",
25
+ profile_name: Optional[str] = None,
26
+ retry_config: Optional[Config] = None,
27
+ ) -> Any:
28
+ """Create boto3 client with retry configuration and error handling.
29
+
30
+ Args:
31
+ service_name: AWS service name (e.g., 'ec2', 'iam', 'lambda')
32
+ region_name: AWS region (default: 'us-east-1')
33
+ profile_name: AWS profile name from ~/.aws/config (optional)
34
+ retry_config: Custom botocore Config object (optional)
35
+
36
+ Returns:
37
+ Configured boto3 client
38
+
39
+ Raises:
40
+ NoCredentialsError: If AWS credentials are not found
41
+ ClientError: If client creation fails
42
+ """
43
+ if retry_config is None:
44
+ retry_config = DEFAULT_RETRY_CONFIG
45
+
46
+ try:
47
+ # Create session (with or without profile)
48
+ if profile_name:
49
+ session = boto3.Session(profile_name=profile_name)
50
+ client = session.client(service_name, region_name=region_name, config=retry_config)
51
+ else:
52
+ client = boto3.client(service_name, region_name=region_name, config=retry_config)
53
+
54
+ logger.debug(f"Created {service_name} client for region {region_name}")
55
+ return client
56
+
57
+ except NoCredentialsError:
58
+ logger.error("AWS credentials not found")
59
+ raise
60
+ except ClientError as e:
61
+ logger.error(f"Failed to create {service_name} client: {e}")
62
+ raise
63
+ except Exception as e:
64
+ logger.error(f"Unexpected error creating {service_name} client: {e}")
65
+ raise
66
+
67
+
68
+ def get_enabled_regions(profile_name: Optional[str] = None) -> list[str]: # type: ignore
69
+ """Get list of enabled AWS regions for the account.
70
+
71
+ Args:
72
+ profile_name: AWS profile name (optional)
73
+
74
+ Returns:
75
+ List of enabled region names
76
+ """
77
+ try:
78
+ ec2_client = create_boto_client("ec2", region_name="us-east-1", profile_name=profile_name)
79
+ response = ec2_client.describe_regions(AllRegions=False) # Only enabled regions
80
+ regions = [region["RegionName"] for region in response["Regions"]]
81
+ logger.info(f"Found {len(regions)} enabled regions")
82
+ return regions
83
+ except Exception as e:
84
+ logger.warning(f"Could not retrieve enabled regions: {e}")
85
+ # Return default set of common regions
86
+ return [
87
+ "us-east-1",
88
+ "us-east-2",
89
+ "us-west-1",
90
+ "us-west-2",
91
+ "eu-west-1",
92
+ "eu-central-1",
93
+ "ap-southeast-1",
94
+ "ap-northeast-1",
95
+ ]
96
+
97
+
98
+ def test_client_connection(client: Any) -> bool:
99
+ """Test if a boto3 client can connect to AWS.
100
+
101
+ Args:
102
+ client: Boto3 client instance
103
+
104
+ Returns:
105
+ True if connection successful, False otherwise
106
+ """
107
+ try:
108
+ # Different services have different test methods
109
+ service_name = client._service_model.service_name
110
+
111
+ if service_name == "sts":
112
+ client.get_caller_identity()
113
+ elif service_name == "ec2":
114
+ client.describe_regions(MaxResults=1)
115
+ elif service_name == "iam":
116
+ client.list_users(MaxItems=1)
117
+ elif service_name == "lambda":
118
+ client.list_functions(MaxItems=1)
119
+ elif service_name == "s3":
120
+ client.list_buckets()
121
+ else:
122
+ # Generic test - just try to get service metadata
123
+ client._make_api_call("ListObjects", {})
124
+
125
+ return True
126
+ except Exception as e:
127
+ logger.debug(f"Client connection test failed: {e}")
128
+ return False
src/aws/credentials.py ADDED
@@ -0,0 +1,191 @@
1
+ """AWS credential validation and permission checking."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import boto3
7
+ from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class CredentialValidationError(Exception):
13
+ """Raised when AWS credentials are invalid or missing."""
14
+
15
+ pass
16
+
17
+
18
+ def validate_credentials(profile_name: Optional[str] = None) -> Dict[str, Any]:
19
+ """Validate AWS credentials and return caller identity information.
20
+
21
+ Args:
22
+ profile_name: AWS profile name from ~/.aws/config (optional)
23
+
24
+ Returns:
25
+ Dictionary with account ID, user ID, and ARN
26
+
27
+ Raises:
28
+ CredentialValidationError: If credentials are invalid or missing
29
+ """
30
+ try:
31
+ if profile_name:
32
+ session = boto3.Session(profile_name=profile_name)
33
+ sts_client = session.client("sts")
34
+ else:
35
+ sts_client = boto3.client("sts")
36
+
37
+ # Get caller identity to validate credentials
38
+ identity = sts_client.get_caller_identity()
39
+
40
+ logger.debug(f"Validated credentials for account {identity['Account']}")
41
+
42
+ return {
43
+ "account_id": identity["Account"],
44
+ "user_id": identity["UserId"],
45
+ "arn": identity["Arn"],
46
+ }
47
+
48
+ except NoCredentialsError:
49
+ error_msg = (
50
+ "AWS credentials not found. Please configure credentials using one of these methods:\n"
51
+ " 1. Run: aws configure\n"
52
+ " 2. Set environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY\n"
53
+ " 3. Use --profile option with a configured profile\n\n"
54
+ "For more info: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
55
+ )
56
+ logger.error("No AWS credentials found")
57
+ raise CredentialValidationError(error_msg)
58
+
59
+ except PartialCredentialsError as e:
60
+ error_msg = f"Incomplete AWS credentials: {e}"
61
+ logger.error(error_msg)
62
+ raise CredentialValidationError(error_msg)
63
+
64
+ except ClientError as e:
65
+ error_code = e.response.get("Error", {}).get("Code", "Unknown")
66
+ if error_code == "InvalidClientTokenId":
67
+ error_msg = "AWS credentials are invalid. Please check your access key ID."
68
+ elif error_code == "SignatureDoesNotMatch":
69
+ error_msg = "AWS credentials signature mismatch. Please check your secret access key."
70
+ elif error_code == "ExpiredToken":
71
+ error_msg = "AWS credentials have expired. Please refresh your temporary credentials."
72
+ else:
73
+ error_msg = f"AWS credential validation failed: {e}"
74
+
75
+ logger.error(error_msg)
76
+ raise CredentialValidationError(error_msg)
77
+
78
+ except Exception as e:
79
+ error_msg = f"Unexpected error validating credentials: {e}"
80
+ logger.error(error_msg)
81
+ raise CredentialValidationError(error_msg)
82
+
83
+
84
+ def check_required_permissions(
85
+ profile_name: Optional[str] = None, required_actions: Optional[List[str]] = None
86
+ ) -> Dict[str, bool]:
87
+ """Check if credentials have required IAM permissions.
88
+
89
+ Note: This is a best-effort check using IAM policy simulation.
90
+ Some permissions may not be accurately detected.
91
+
92
+ Args:
93
+ profile_name: AWS profile name (optional)
94
+ required_actions: List of IAM actions to check (e.g., ['ec2:DescribeInstances'])
95
+
96
+ Returns:
97
+ Dictionary mapping action names to permission status (True/False)
98
+ """
99
+ if required_actions is None:
100
+ # Default minimum required permissions for snapshot operations
101
+ required_actions = [
102
+ "ec2:DescribeInstances",
103
+ "ec2:DescribeRegions",
104
+ "iam:ListRoles",
105
+ "lambda:ListFunctions",
106
+ "s3:ListAllMyBuckets",
107
+ ]
108
+
109
+ try:
110
+ # Get caller identity first
111
+ identity = validate_credentials(profile_name)
112
+
113
+ if profile_name:
114
+ session = boto3.Session(profile_name=profile_name)
115
+ iam_client = session.client("iam")
116
+ else:
117
+ iam_client = boto3.client("iam")
118
+
119
+ results = {}
120
+
121
+ # Try to simulate policy for each action
122
+ for action in required_actions:
123
+ try:
124
+ response = iam_client.simulate_principal_policy(
125
+ PolicySourceArn=identity["arn"],
126
+ ActionNames=[action],
127
+ )
128
+
129
+ # Check if action is allowed
130
+ eval_results = response.get("EvaluationResults", [])
131
+ if eval_results:
132
+ decision = eval_results[0].get("EvalDecision", "deny")
133
+ results[action] = decision.lower() == "allowed"
134
+ else:
135
+ results[action] = False
136
+
137
+ except ClientError as e:
138
+ # If simulation fails (e.g., lack of iam:SimulatePrincipalPolicy permission),
139
+ # we can't determine the permission status
140
+ logger.debug(f"Could not check permission for {action}: {e}")
141
+ results[action] = None # Unknown
142
+
143
+ return results
144
+
145
+ except Exception as e:
146
+ logger.warning(f"Permission check failed: {e}")
147
+ return {action: None for action in required_actions} # type: ignore
148
+
149
+
150
+ def get_account_id(profile_name: Optional[str] = None) -> str:
151
+ """Get AWS account ID for the current credentials.
152
+
153
+ Args:
154
+ profile_name: AWS profile name (optional)
155
+
156
+ Returns:
157
+ 12-digit AWS account ID
158
+
159
+ Raises:
160
+ CredentialValidationError: If credentials are invalid
161
+ """
162
+ identity = validate_credentials(profile_name)
163
+ return identity["account_id"]
164
+
165
+
166
+ def get_credential_summary(profile_name: Optional[str] = None) -> str:
167
+ """Get a human-readable summary of current AWS credentials.
168
+
169
+ Args:
170
+ profile_name: AWS profile name (optional)
171
+
172
+ Returns:
173
+ Formatted string with credential information
174
+ """
175
+ try:
176
+ identity = validate_credentials(profile_name)
177
+
178
+ summary = f"""
179
+ AWS Credentials Valid
180
+ Account ID: {identity['account_id']}
181
+ User/Role: {identity['arn'].split('/')[-1]}
182
+ ARN: {identity['arn']}
183
+ """
184
+
185
+ if profile_name:
186
+ summary += f"Profile: {profile_name}\n"
187
+
188
+ return summary.strip()
189
+
190
+ except CredentialValidationError as e:
191
+ return f"Credential Validation Failed:\n{e}"
@@ -0,0 +1,177 @@
1
+ """Rate limiter utility using token bucket algorithm."""
2
+
3
+ import logging
4
+ import time
5
+ from threading import Lock
6
+ from typing import Dict, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ # Service-specific rate limits (calls per second)
12
+ # Based on AWS API throttling limits
13
+ SERVICE_RATE_LIMITS: Dict[str, float] = {
14
+ "iam": 5.0, # IAM has strict rate limits (global service)
15
+ "cloudformation": 2.0, # CloudFormation is particularly slow
16
+ "sts": 10.0, # STS is also rate-limited
17
+ "default": 10.0, # Conservative default for other services
18
+ }
19
+
20
+
21
+ class RateLimiter:
22
+ """Token bucket rate limiter for controlling API call frequency.
23
+
24
+ This prevents hitting AWS API rate limits by throttling client-side
25
+ before the request is even made.
26
+ """
27
+
28
+ def __init__(self, rate: float):
29
+ """Initialize rate limiter.
30
+
31
+ Args:
32
+ rate: Maximum number of calls per second
33
+ """
34
+ self.rate = rate
35
+ self.tokens = rate
36
+ self.last_update = time.time()
37
+ self.lock = Lock()
38
+
39
+ logger.debug(f"Initialized rate limiter with rate {rate} calls/sec")
40
+
41
+ def acquire(self, blocking: bool = True) -> bool:
42
+ """Acquire permission to make an API call.
43
+
44
+ This method will block until a token is available (if blocking=True)
45
+ or return immediately (if blocking=False).
46
+
47
+ Args:
48
+ blocking: If True, wait until token available. If False, return immediately.
49
+
50
+ Returns:
51
+ True if token acquired, False if blocking=False and no token available
52
+ """
53
+ with self.lock:
54
+ now = time.time()
55
+ elapsed = now - self.last_update
56
+
57
+ # Refill tokens based on elapsed time
58
+ self.tokens = min(self.rate, self.tokens + elapsed * self.rate)
59
+ self.last_update = now
60
+
61
+ if self.tokens >= 1:
62
+ # Token available
63
+ self.tokens -= 1
64
+ return True
65
+ else:
66
+ # No token available
67
+ if not blocking:
68
+ return False
69
+
70
+ # Calculate how long to sleep
71
+ sleep_time = (1 - self.tokens) / self.rate
72
+ logger.debug(f"Rate limiter sleeping for {sleep_time:.3f}s")
73
+
74
+ # Sleep outside the lock to allow other threads
75
+ time.sleep(sleep_time)
76
+
77
+ # Acquire token after sleeping
78
+ with self.lock:
79
+ self.tokens = 0
80
+ self.last_update = time.time()
81
+ return True
82
+
83
+ def try_acquire(self) -> bool:
84
+ """Try to acquire a token without blocking.
85
+
86
+ Returns:
87
+ True if token acquired, False otherwise
88
+ """
89
+ return self.acquire(blocking=False)
90
+
91
+
92
+ class ServiceRateLimiter:
93
+ """Manages rate limiters for different AWS services."""
94
+
95
+ def __init__(self, rate_limits: Optional[Dict[str, float]] = None):
96
+ """Initialize service rate limiter.
97
+
98
+ Args:
99
+ rate_limits: Dictionary mapping service names to rates (optional)
100
+ """
101
+ self.rate_limits = rate_limits or SERVICE_RATE_LIMITS
102
+ self._limiters: Dict[str, RateLimiter] = {}
103
+ self._lock = Lock()
104
+
105
+ def get_limiter(self, service_name: str) -> RateLimiter:
106
+ """Get or create a rate limiter for a service.
107
+
108
+ Args:
109
+ service_name: AWS service name (e.g., 'iam', 'ec2')
110
+
111
+ Returns:
112
+ RateLimiter instance for the service
113
+ """
114
+ with self._lock:
115
+ if service_name not in self._limiters:
116
+ rate = self.rate_limits.get(service_name, self.rate_limits["default"])
117
+ self._limiters[service_name] = RateLimiter(rate)
118
+ logger.debug(f"Created rate limiter for {service_name} ({rate} calls/sec)")
119
+
120
+ return self._limiters[service_name]
121
+
122
+ def acquire(self, service_name: str, blocking: bool = True) -> bool:
123
+ """Acquire permission to call an AWS service API.
124
+
125
+ Args:
126
+ service_name: AWS service name
127
+ blocking: Whether to block until token available
128
+
129
+ Returns:
130
+ True if token acquired
131
+ """
132
+ limiter = self.get_limiter(service_name)
133
+ return limiter.acquire(blocking=blocking)
134
+
135
+ def try_acquire(self, service_name: str) -> bool:
136
+ """Try to acquire permission without blocking.
137
+
138
+ Args:
139
+ service_name: AWS service name
140
+
141
+ Returns:
142
+ True if token acquired, False otherwise
143
+ """
144
+ return self.acquire(service_name, blocking=False)
145
+
146
+
147
+ # Global service rate limiter instance
148
+ _global_limiter: Optional[ServiceRateLimiter] = None
149
+
150
+
151
+ def get_global_rate_limiter() -> ServiceRateLimiter:
152
+ """Get the global service rate limiter instance.
153
+
154
+ Returns:
155
+ Global ServiceRateLimiter instance
156
+ """
157
+ global _global_limiter
158
+ if _global_limiter is None:
159
+ _global_limiter = ServiceRateLimiter()
160
+ return _global_limiter
161
+
162
+
163
+ def rate_limited_call(service_name: str, func, *args, **kwargs): # type: ignore[no-untyped-def]
164
+ """Execute a function with rate limiting applied.
165
+
166
+ Args:
167
+ service_name: AWS service name for rate limiting
168
+ func: Function to call
169
+ *args: Positional arguments to pass to func
170
+ **kwargs: Keyword arguments to pass to func
171
+
172
+ Returns:
173
+ Result of func(*args, **kwargs)
174
+ """
175
+ limiter = get_global_rate_limiter()
176
+ limiter.acquire(service_name)
177
+ return func(*args, **kwargs)
src/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CLI module for AWS Baseline Snapshot tool."""
2
+
3
+ from .main import app, cli_main
4
+
5
+ __all__ = ["app", "cli_main"]
src/cli/config.py ADDED
@@ -0,0 +1,130 @@
1
+ """Configuration loader for CLI."""
2
+
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+
8
+ import yaml
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Config:
14
+ """Application configuration manager."""
15
+
16
+ def __init__(self) -> None:
17
+ """Initialize configuration with defaults."""
18
+ self.snapshot_dir: str = ".snapshots"
19
+ self.storage_path: Optional[str] = None # Runtime override for snapshot storage path
20
+ self.regions: list[str] = [] # Empty means all enabled regions
21
+ self.resource_types: list[str] = [] # Empty means all supported types
22
+ self.aws_profile: Optional[str] = None
23
+ self.parallel_workers: int = 10
24
+ self.auto_compress_mb: int = 10
25
+ self.log_level: str = "INFO"
26
+
27
+ @classmethod
28
+ def load(cls, config_file: Optional[str] = None) -> "Config":
29
+ """Load configuration from file and environment variables.
30
+
31
+ Priority (highest to lowest):
32
+ 1. Environment variables
33
+ 2. Config file
34
+ 3. Defaults
35
+
36
+ Args:
37
+ config_file: Path to config file (optional)
38
+
39
+ Returns:
40
+ Config instance
41
+ """
42
+ config = cls()
43
+
44
+ # Try to load from config file
45
+ config_path = None
46
+ if config_file:
47
+ config_path = Path(config_file)
48
+ else:
49
+ # Try current directory first
50
+ config_path = Path(".aws-baseline.yaml")
51
+ if not config_path.exists():
52
+ # Try home directory
53
+ config_path = Path.home() / ".aws-baseline.yaml"
54
+
55
+ if config_path and config_path.exists():
56
+ config._load_from_file(config_path)
57
+
58
+ # Override with environment variables
59
+ config._load_from_env()
60
+
61
+ return config
62
+
63
+ def _load_from_file(self, config_path: Path) -> None:
64
+ """Load configuration from YAML file.
65
+
66
+ Args:
67
+ config_path: Path to config file
68
+ """
69
+ try:
70
+ with open(config_path, "r") as f:
71
+ data = yaml.safe_load(f)
72
+
73
+ if not data:
74
+ return
75
+
76
+ self.snapshot_dir = data.get("snapshot_dir", self.snapshot_dir)
77
+ self.regions = data.get("regions", self.regions)
78
+ self.aws_profile = data.get("aws_profile", self.aws_profile)
79
+ self.parallel_workers = data.get("parallel_workers", self.parallel_workers)
80
+ self.auto_compress_mb = data.get("auto_compress_mb", self.auto_compress_mb)
81
+
82
+ # Handle resource_types (include/exclude)
83
+ resource_types_config = data.get("resource_types", {})
84
+ if isinstance(resource_types_config, dict):
85
+ self.resource_types = resource_types_config.get("include", [])
86
+ elif isinstance(resource_types_config, list):
87
+ self.resource_types = resource_types_config
88
+
89
+ logger.info(f"Loaded configuration from {config_path}")
90
+
91
+ except Exception as e:
92
+ logger.warning(f"Could not load config file {config_path}: {e}")
93
+
94
+ def _load_from_env(self) -> None:
95
+ """Load configuration from environment variables."""
96
+ # AWS_BASELINE_SNAPSHOT_DIR
97
+ snapshot_dir = os.getenv("AWS_BASELINE_SNAPSHOT_DIR")
98
+ if snapshot_dir:
99
+ self.snapshot_dir = snapshot_dir
100
+
101
+ # AWS_BASELINE_LOG_LEVEL
102
+ log_level = os.getenv("AWS_BASELINE_LOG_LEVEL")
103
+ if log_level:
104
+ self.log_level = log_level
105
+
106
+ # AWS_PROFILE
107
+ aws_profile = os.getenv("AWS_PROFILE")
108
+ if aws_profile:
109
+ self.aws_profile = aws_profile
110
+
111
+ # AWS_REGION (single region from env)
112
+ aws_region = os.getenv("AWS_REGION")
113
+ if aws_region:
114
+ self.regions = [aws_region]
115
+
116
+ def to_dict(self) -> Dict[str, Any]:
117
+ """Convert configuration to dictionary.
118
+
119
+ Returns:
120
+ Configuration as dictionary
121
+ """
122
+ return {
123
+ "snapshot_dir": self.snapshot_dir,
124
+ "regions": self.regions,
125
+ "resource_types": self.resource_types,
126
+ "aws_profile": self.aws_profile,
127
+ "parallel_workers": self.parallel_workers,
128
+ "auto_compress_mb": self.auto_compress_mb,
129
+ "log_level": self.log_level,
130
+ }