aws-inventory-manager 0.17.12__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.
- aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
- aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
- aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/aws/__init__.py +11 -0
- src/aws/client.py +128 -0
- src/aws/credentials.py +191 -0
- src/aws/rate_limiter.py +177 -0
- src/cli/__init__.py +12 -0
- src/cli/config.py +130 -0
- src/cli/main.py +4046 -0
- src/cloudtrail/__init__.py +5 -0
- src/cloudtrail/query.py +642 -0
- src/config_service/__init__.py +21 -0
- src/config_service/collector.py +346 -0
- src/config_service/detector.py +256 -0
- src/config_service/resource_type_mapping.py +328 -0
- src/cost/__init__.py +5 -0
- src/cost/analyzer.py +226 -0
- src/cost/explorer.py +209 -0
- src/cost/reporter.py +237 -0
- src/delta/__init__.py +5 -0
- src/delta/calculator.py +206 -0
- src/delta/differ.py +185 -0
- src/delta/formatters.py +272 -0
- src/delta/models.py +154 -0
- src/delta/reporter.py +234 -0
- src/matching/__init__.py +6 -0
- src/matching/config.py +52 -0
- src/matching/normalizer.py +450 -0
- src/matching/prompts.py +33 -0
- src/models/__init__.py +21 -0
- src/models/config_diff.py +135 -0
- src/models/cost_report.py +87 -0
- src/models/deletion_operation.py +104 -0
- src/models/deletion_record.py +97 -0
- src/models/delta_report.py +122 -0
- src/models/efs_resource.py +80 -0
- src/models/elasticache_resource.py +90 -0
- src/models/group.py +318 -0
- src/models/inventory.py +133 -0
- src/models/protection_rule.py +123 -0
- src/models/report.py +288 -0
- src/models/resource.py +111 -0
- src/models/security_finding.py +102 -0
- src/models/snapshot.py +122 -0
- src/restore/__init__.py +20 -0
- src/restore/audit.py +175 -0
- src/restore/cleaner.py +461 -0
- src/restore/config.py +209 -0
- src/restore/deleter.py +976 -0
- src/restore/dependency.py +254 -0
- src/restore/safety.py +115 -0
- src/security/__init__.py +0 -0
- src/security/checks/__init__.py +0 -0
- src/security/checks/base.py +56 -0
- src/security/checks/ec2_checks.py +88 -0
- src/security/checks/elasticache_checks.py +149 -0
- src/security/checks/iam_checks.py +102 -0
- src/security/checks/rds_checks.py +140 -0
- src/security/checks/s3_checks.py +95 -0
- src/security/checks/secrets_checks.py +96 -0
- src/security/checks/sg_checks.py +142 -0
- src/security/cis_mapper.py +97 -0
- src/security/models.py +53 -0
- src/security/reporter.py +174 -0
- src/security/scanner.py +87 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +453 -0
- src/snapshot/filter.py +259 -0
- src/snapshot/inventory_storage.py +236 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -0
- src/snapshot/resource_collectors/__init__.py +5 -0
- src/snapshot/resource_collectors/apigateway.py +140 -0
- src/snapshot/resource_collectors/backup.py +136 -0
- src/snapshot/resource_collectors/base.py +81 -0
- src/snapshot/resource_collectors/cloudformation.py +55 -0
- src/snapshot/resource_collectors/cloudwatch.py +109 -0
- src/snapshot/resource_collectors/codebuild.py +69 -0
- src/snapshot/resource_collectors/codepipeline.py +82 -0
- src/snapshot/resource_collectors/dynamodb.py +65 -0
- src/snapshot/resource_collectors/ec2.py +240 -0
- src/snapshot/resource_collectors/ecs.py +215 -0
- src/snapshot/resource_collectors/efs_collector.py +102 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elasticache_collector.py +79 -0
- src/snapshot/resource_collectors/elb.py +126 -0
- src/snapshot/resource_collectors/eventbridge.py +156 -0
- src/snapshot/resource_collectors/glue.py +199 -0
- src/snapshot/resource_collectors/iam.py +188 -0
- src/snapshot/resource_collectors/kms.py +111 -0
- src/snapshot/resource_collectors/lambda_func.py +139 -0
- src/snapshot/resource_collectors/rds.py +109 -0
- src/snapshot/resource_collectors/route53.py +86 -0
- src/snapshot/resource_collectors/s3.py +105 -0
- src/snapshot/resource_collectors/secretsmanager.py +70 -0
- src/snapshot/resource_collectors/sns.py +68 -0
- src/snapshot/resource_collectors/sqs.py +82 -0
- src/snapshot/resource_collectors/ssm.py +160 -0
- src/snapshot/resource_collectors/stepfunctions.py +74 -0
- src/snapshot/resource_collectors/vpcendpoints.py +79 -0
- src/snapshot/resource_collectors/waf.py +159 -0
- src/snapshot/storage.py +351 -0
- src/storage/__init__.py +21 -0
- src/storage/audit_store.py +419 -0
- src/storage/database.py +294 -0
- src/storage/group_store.py +763 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +416 -0
- src/storage/schema.py +339 -0
- src/storage/snapshot_store.py +363 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +305 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/pagination.py +41 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
- src/utils/unsupported_resources.py +306 -0
- src/web/__init__.py +5 -0
- src/web/app.py +97 -0
- src/web/dependencies.py +69 -0
- src/web/routes/__init__.py +1 -0
- src/web/routes/api/__init__.py +18 -0
- src/web/routes/api/charts.py +156 -0
- src/web/routes/api/cleanup.py +186 -0
- src/web/routes/api/filters.py +253 -0
- src/web/routes/api/groups.py +305 -0
- src/web/routes/api/inventories.py +80 -0
- src/web/routes/api/queries.py +202 -0
- src/web/routes/api/resources.py +393 -0
- src/web/routes/api/snapshots.py +314 -0
- src/web/routes/api/views.py +260 -0
- src/web/routes/pages.py +198 -0
- src/web/services/__init__.py +1 -0
- src/web/templates/base.html +955 -0
- src/web/templates/components/navbar.html +31 -0
- src/web/templates/components/sidebar.html +104 -0
- src/web/templates/pages/audit_logs.html +86 -0
- src/web/templates/pages/cleanup.html +279 -0
- src/web/templates/pages/dashboard.html +227 -0
- src/web/templates/pages/diff.html +175 -0
- src/web/templates/pages/error.html +30 -0
- src/web/templates/pages/groups.html +721 -0
- src/web/templates/pages/queries.html +246 -0
- src/web/templates/pages/resources.html +2429 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- src/web/templates/pages/snapshots.html +429 -0
src/aws/rate_limiter.py
ADDED
|
@@ -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,12 @@
|
|
|
1
|
+
"""CLI module for AWS Baseline Snapshot tool."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["app", "cli_main"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name: str):
|
|
7
|
+
"""Lazy import to avoid RuntimeWarning when running with python -m."""
|
|
8
|
+
if name in ("app", "cli_main"):
|
|
9
|
+
from .main import app, cli_main
|
|
10
|
+
|
|
11
|
+
return app if name == "app" else cli_main
|
|
12
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
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, List, 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
|
+
}
|