runbooks 0.2.5__py3-none-any.whl → 0.6.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.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/__init__.py +88 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/script_test_data.py +0 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +7 -7
- runbooks/security_baseline/report_template_jp.html +7 -7
- runbooks/security_baseline/report_template_kr.html +12 -12
- runbooks/security_baseline/report_template_vn.html +7 -7
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.5.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
@@ -0,0 +1,510 @@
|
|
1
|
+
"""
|
2
|
+
AWS utility functions and helpers for inventory operations.
|
3
|
+
|
4
|
+
This module provides AWS-specific utility functions including session
|
5
|
+
management, region discovery, credential validation, and common operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from datetime import datetime, timedelta
|
9
|
+
from functools import wraps
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
11
|
+
|
12
|
+
import boto3
|
13
|
+
from botocore.config import Config
|
14
|
+
from botocore.exceptions import ClientError, NoCredentialsError, ProfileNotFound
|
15
|
+
from loguru import logger
|
16
|
+
|
17
|
+
# AWS Service Endpoints and Regions
|
18
|
+
AWS_PARTITIONS = {
|
19
|
+
"aws": {
|
20
|
+
"regions": [
|
21
|
+
"us-east-1",
|
22
|
+
"us-east-2",
|
23
|
+
"us-west-1",
|
24
|
+
"us-west-2",
|
25
|
+
"eu-west-1",
|
26
|
+
"eu-west-2",
|
27
|
+
"eu-west-3",
|
28
|
+
"eu-central-1",
|
29
|
+
"eu-north-1",
|
30
|
+
"ap-northeast-1",
|
31
|
+
"ap-northeast-2",
|
32
|
+
"ap-northeast-3",
|
33
|
+
"ap-southeast-1",
|
34
|
+
"ap-southeast-2",
|
35
|
+
"ap-south-1",
|
36
|
+
"ca-central-1",
|
37
|
+
"sa-east-1",
|
38
|
+
]
|
39
|
+
},
|
40
|
+
"aws-us-gov": {"regions": ["us-gov-east-1", "us-gov-west-1"]},
|
41
|
+
"aws-cn": {"regions": ["cn-north-1", "cn-northwest-1"]},
|
42
|
+
}
|
43
|
+
|
44
|
+
# Global AWS services (not region-specific)
|
45
|
+
GLOBAL_SERVICES = {"iam", "route53", "cloudfront", "waf", "wafv2", "organizations", "support", "trustedadvisor"}
|
46
|
+
|
47
|
+
# Services that require special handling
|
48
|
+
SPECIAL_HANDLING_SERVICES = {
|
49
|
+
"s3": "bucket-region-specific",
|
50
|
+
"cloudtrail": "global-with-region-config",
|
51
|
+
"config": "region-specific-with-global-view",
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
def get_boto3_session(
|
56
|
+
profile_name: Optional[str] = None,
|
57
|
+
region_name: Optional[str] = None,
|
58
|
+
access_key_id: Optional[str] = None,
|
59
|
+
secret_access_key: Optional[str] = None,
|
60
|
+
session_token: Optional[str] = None,
|
61
|
+
) -> boto3.Session:
|
62
|
+
"""
|
63
|
+
Create a configured boto3 session with retry and timeout settings.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
profile_name: AWS profile name from ~/.aws/credentials
|
67
|
+
region_name: Default AWS region
|
68
|
+
access_key_id: AWS access key ID
|
69
|
+
secret_access_key: AWS secret access key
|
70
|
+
session_token: AWS session token (for temporary credentials)
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
Configured boto3 session
|
74
|
+
|
75
|
+
Raises:
|
76
|
+
ProfileNotFound: If specified profile doesn't exist
|
77
|
+
NoCredentialsError: If no valid credentials are found
|
78
|
+
"""
|
79
|
+
try:
|
80
|
+
# Create session with provided credentials
|
81
|
+
session = boto3.Session(
|
82
|
+
profile_name=profile_name,
|
83
|
+
region_name=region_name or "us-east-1",
|
84
|
+
aws_access_key_id=access_key_id,
|
85
|
+
aws_secret_access_key=secret_access_key,
|
86
|
+
aws_session_token=session_token,
|
87
|
+
)
|
88
|
+
|
89
|
+
# Test the session by getting caller identity
|
90
|
+
sts_client = session.client("sts")
|
91
|
+
identity = sts_client.get_caller_identity()
|
92
|
+
|
93
|
+
logger.debug(f"Created session for account: {identity.get('Account')}, user: {identity.get('Arn')}")
|
94
|
+
|
95
|
+
return session
|
96
|
+
|
97
|
+
except ProfileNotFound as e:
|
98
|
+
logger.error(f"AWS profile '{profile_name}' not found: {e}")
|
99
|
+
raise
|
100
|
+
except NoCredentialsError as e:
|
101
|
+
logger.error(f"No valid AWS credentials found: {e}")
|
102
|
+
raise
|
103
|
+
except ClientError as e:
|
104
|
+
error_code = e.response["Error"]["Code"]
|
105
|
+
if error_code in ["InvalidUserID.NotFound", "AccessDenied"]:
|
106
|
+
logger.error(f"Invalid AWS credentials or insufficient permissions: {e}")
|
107
|
+
else:
|
108
|
+
logger.error(f"AWS API error during session validation: {e}")
|
109
|
+
raise
|
110
|
+
|
111
|
+
|
112
|
+
def get_boto3_config(
|
113
|
+
max_retries: int = 5, read_timeout: int = 60, connect_timeout: int = 10, max_pool_connections: int = 50
|
114
|
+
) -> Config:
|
115
|
+
"""
|
116
|
+
Create standardized boto3 configuration for inventory operations.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
max_retries: Maximum number of retries for failed requests
|
120
|
+
read_timeout: Read timeout in seconds
|
121
|
+
connect_timeout: Connection timeout in seconds
|
122
|
+
max_pool_connections: Maximum number of connections in pool
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
Boto3 Config object with optimized settings
|
126
|
+
"""
|
127
|
+
return Config(
|
128
|
+
retries={"max_attempts": max_retries, "mode": "adaptive"},
|
129
|
+
read_timeout=read_timeout,
|
130
|
+
connect_timeout=connect_timeout,
|
131
|
+
max_pool_connections=max_pool_connections,
|
132
|
+
# Use signature v4 for all requests
|
133
|
+
signature_version="v4",
|
134
|
+
)
|
135
|
+
|
136
|
+
|
137
|
+
def get_aws_regions(
|
138
|
+
session: boto3.Session, service: Optional[str] = None, include_gov_cloud: bool = False, include_china: bool = False
|
139
|
+
) -> List[str]:
|
140
|
+
"""
|
141
|
+
Get list of available AWS regions for a service.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
session: Configured boto3 session
|
145
|
+
service: AWS service to check regions for (None for all regions)
|
146
|
+
include_gov_cloud: Include AWS GovCloud regions
|
147
|
+
include_china: Include AWS China regions
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
List of available region names
|
151
|
+
|
152
|
+
Raises:
|
153
|
+
ClientError: If unable to retrieve regions
|
154
|
+
"""
|
155
|
+
try:
|
156
|
+
# Use EC2 to get region information
|
157
|
+
ec2_client = session.client("ec2", region_name="us-east-1")
|
158
|
+
|
159
|
+
response = ec2_client.describe_regions(
|
160
|
+
Filters=[{"Name": "opt-in-status", "Values": ["opt-in-not-required", "opted-in"]}]
|
161
|
+
)
|
162
|
+
|
163
|
+
regions = [r["RegionName"] for r in response["Regions"]]
|
164
|
+
|
165
|
+
# Filter based on partition preferences
|
166
|
+
if not include_gov_cloud:
|
167
|
+
regions = [r for r in regions if not r.startswith("us-gov")]
|
168
|
+
|
169
|
+
if not include_china:
|
170
|
+
regions = [r for r in regions if not r.startswith("cn-")]
|
171
|
+
|
172
|
+
# If service specified, filter to regions where service is available
|
173
|
+
if service and service not in GLOBAL_SERVICES:
|
174
|
+
available_regions = []
|
175
|
+
for region in regions:
|
176
|
+
try:
|
177
|
+
# Test if service is available in region
|
178
|
+
client = session.client(service, region_name=region)
|
179
|
+
# Make a simple call to verify service availability
|
180
|
+
if hasattr(client, "describe_regions"):
|
181
|
+
client.describe_regions()
|
182
|
+
available_regions.append(region)
|
183
|
+
except ClientError as e:
|
184
|
+
if e.response["Error"]["Code"] not in ["UnauthorizedOperation", "AccessDenied"]:
|
185
|
+
logger.debug(f"Service {service} not available in {region}: {e}")
|
186
|
+
continue
|
187
|
+
available_regions.append(region)
|
188
|
+
except Exception as e:
|
189
|
+
logger.debug(f"Error checking {service} in {region}: {e}")
|
190
|
+
continue
|
191
|
+
|
192
|
+
regions = available_regions
|
193
|
+
|
194
|
+
logger.debug(f"Found {len(regions)} available regions for service: {service}")
|
195
|
+
return sorted(regions)
|
196
|
+
|
197
|
+
except ClientError as e:
|
198
|
+
logger.error(f"Failed to get AWS regions: {e}")
|
199
|
+
# Return fallback list of common regions
|
200
|
+
fallback_regions = [
|
201
|
+
"us-east-1",
|
202
|
+
"us-east-2",
|
203
|
+
"us-west-1",
|
204
|
+
"us-west-2",
|
205
|
+
"eu-west-1",
|
206
|
+
"eu-central-1",
|
207
|
+
"ap-southeast-1",
|
208
|
+
"ap-northeast-1",
|
209
|
+
]
|
210
|
+
logger.warning(f"Using fallback region list: {fallback_regions}")
|
211
|
+
return fallback_regions
|
212
|
+
|
213
|
+
|
214
|
+
def validate_aws_credentials(session: boto3.Session) -> Dict[str, Any]:
|
215
|
+
"""
|
216
|
+
Validate AWS credentials and return account information.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
session: Boto3 session to validate
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Dictionary with account information and permissions
|
223
|
+
|
224
|
+
Raises:
|
225
|
+
NoCredentialsError: If credentials are invalid
|
226
|
+
ClientError: If validation fails
|
227
|
+
"""
|
228
|
+
try:
|
229
|
+
sts_client = session.client("sts")
|
230
|
+
identity = sts_client.get_caller_identity()
|
231
|
+
|
232
|
+
# Get additional account information
|
233
|
+
account_info = {
|
234
|
+
"account_id": identity["Account"],
|
235
|
+
"user_arn": identity["Arn"],
|
236
|
+
"user_id": identity["UserId"],
|
237
|
+
"session_valid": True,
|
238
|
+
"permissions": {},
|
239
|
+
}
|
240
|
+
|
241
|
+
# Test basic permissions
|
242
|
+
try:
|
243
|
+
# Test IAM read permissions
|
244
|
+
iam_client = session.client("iam")
|
245
|
+
iam_client.list_users(MaxItems=1)
|
246
|
+
account_info["permissions"]["iam_read"] = True
|
247
|
+
except ClientError:
|
248
|
+
account_info["permissions"]["iam_read"] = False
|
249
|
+
|
250
|
+
try:
|
251
|
+
# Test EC2 read permissions
|
252
|
+
ec2_client = session.client("ec2")
|
253
|
+
ec2_client.describe_instances(MaxResults=5)
|
254
|
+
account_info["permissions"]["ec2_read"] = True
|
255
|
+
except ClientError:
|
256
|
+
account_info["permissions"]["ec2_read"] = False
|
257
|
+
|
258
|
+
try:
|
259
|
+
# Test Organizations permissions (if applicable)
|
260
|
+
orgs_client = session.client("organizations")
|
261
|
+
orgs_client.describe_organization()
|
262
|
+
account_info["permissions"]["organizations_read"] = True
|
263
|
+
account_info["is_organization_account"] = True
|
264
|
+
except ClientError:
|
265
|
+
account_info["permissions"]["organizations_read"] = False
|
266
|
+
account_info["is_organization_account"] = False
|
267
|
+
|
268
|
+
logger.info(f"Validated credentials for account: {account_info['account_id']}")
|
269
|
+
return account_info
|
270
|
+
|
271
|
+
except NoCredentialsError as e:
|
272
|
+
logger.error(f"No valid AWS credentials: {e}")
|
273
|
+
raise
|
274
|
+
except ClientError as e:
|
275
|
+
error_code = e.response["Error"]["Code"]
|
276
|
+
logger.error(f"Credential validation failed: {error_code} - {e}")
|
277
|
+
raise
|
278
|
+
|
279
|
+
|
280
|
+
def aws_api_retry(
|
281
|
+
max_retries: int = 3, backoff_factor: float = 2.0, retryable_errors: Optional[Set[str]] = None
|
282
|
+
) -> Callable:
|
283
|
+
"""
|
284
|
+
Decorator for retrying AWS API calls with exponential backoff.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
max_retries: Maximum number of retry attempts
|
288
|
+
backoff_factor: Exponential backoff multiplier
|
289
|
+
retryable_errors: Set of error codes that should trigger retries
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
Decorator function
|
293
|
+
"""
|
294
|
+
if retryable_errors is None:
|
295
|
+
retryable_errors = {
|
296
|
+
"Throttling",
|
297
|
+
"ThrottlingException",
|
298
|
+
"RequestLimitExceeded",
|
299
|
+
"ServiceUnavailable",
|
300
|
+
"InternalError",
|
301
|
+
"InternalServerError",
|
302
|
+
"RequestTimeout",
|
303
|
+
}
|
304
|
+
|
305
|
+
def decorator(func: Callable) -> Callable:
|
306
|
+
@wraps(func)
|
307
|
+
def wrapper(*args, **kwargs) -> Any:
|
308
|
+
last_exception = None
|
309
|
+
|
310
|
+
for attempt in range(max_retries + 1):
|
311
|
+
try:
|
312
|
+
return func(*args, **kwargs)
|
313
|
+
except ClientError as e:
|
314
|
+
error_code = e.response["Error"]["Code"]
|
315
|
+
last_exception = e
|
316
|
+
|
317
|
+
if error_code not in retryable_errors:
|
318
|
+
# Non-retryable error, raise immediately
|
319
|
+
raise
|
320
|
+
|
321
|
+
if attempt == max_retries:
|
322
|
+
# Final attempt failed
|
323
|
+
logger.error(f"Function {func.__name__} failed after {max_retries} retries: {e}")
|
324
|
+
raise
|
325
|
+
|
326
|
+
# Calculate delay for exponential backoff
|
327
|
+
delay = backoff_factor**attempt
|
328
|
+
logger.warning(
|
329
|
+
f"Attempt {attempt + 1} failed for {func.__name__}: {error_code}. "
|
330
|
+
f"Retrying in {delay:.1f} seconds..."
|
331
|
+
)
|
332
|
+
|
333
|
+
import time
|
334
|
+
|
335
|
+
time.sleep(delay)
|
336
|
+
except Exception as e:
|
337
|
+
# Non-AWS exception, don't retry
|
338
|
+
logger.error(f"Non-retryable error in {func.__name__}: {e}")
|
339
|
+
raise
|
340
|
+
|
341
|
+
# Should never reach here, but safety net
|
342
|
+
raise last_exception
|
343
|
+
|
344
|
+
return wrapper
|
345
|
+
|
346
|
+
return decorator
|
347
|
+
|
348
|
+
|
349
|
+
def get_account_aliases(session: boto3.Session) -> List[str]:
|
350
|
+
"""
|
351
|
+
Get account aliases for the current AWS account.
|
352
|
+
|
353
|
+
Args:
|
354
|
+
session: Boto3 session
|
355
|
+
|
356
|
+
Returns:
|
357
|
+
List of account aliases
|
358
|
+
"""
|
359
|
+
try:
|
360
|
+
iam_client = session.client("iam")
|
361
|
+
response = iam_client.list_account_aliases()
|
362
|
+
return response.get("AccountAliases", [])
|
363
|
+
except ClientError as e:
|
364
|
+
logger.warning(f"Could not retrieve account aliases: {e}")
|
365
|
+
return []
|
366
|
+
|
367
|
+
|
368
|
+
def get_organization_info(session: boto3.Session) -> Optional[Dict[str, Any]]:
|
369
|
+
"""
|
370
|
+
Get organization information if account is part of AWS Organizations.
|
371
|
+
|
372
|
+
Args:
|
373
|
+
session: Boto3 session
|
374
|
+
|
375
|
+
Returns:
|
376
|
+
Organization information or None if not in organization
|
377
|
+
"""
|
378
|
+
try:
|
379
|
+
orgs_client = session.client("organizations")
|
380
|
+
org_response = orgs_client.describe_organization()
|
381
|
+
org_info = org_response["Organization"]
|
382
|
+
|
383
|
+
# Get additional organization details
|
384
|
+
result = {
|
385
|
+
"organization_id": org_info["Id"],
|
386
|
+
"organization_arn": org_info["Arn"],
|
387
|
+
"feature_set": org_info["FeatureSet"],
|
388
|
+
"master_account_id": org_info["MasterAccountId"],
|
389
|
+
"master_account_email": org_info["MasterAccountEmail"],
|
390
|
+
"available_policy_types": [pt["Type"] for pt in org_info.get("AvailablePolicyTypes", [])],
|
391
|
+
}
|
392
|
+
|
393
|
+
# Check if current account is master account
|
394
|
+
sts_client = session.client("sts")
|
395
|
+
identity = sts_client.get_caller_identity()
|
396
|
+
result["is_master_account"] = identity["Account"] == org_info["MasterAccountId"]
|
397
|
+
|
398
|
+
return result
|
399
|
+
|
400
|
+
except ClientError as e:
|
401
|
+
error_code = e.response["Error"]["Code"]
|
402
|
+
if error_code in ["AWSOrganizationsNotInUseException", "AccessDenied"]:
|
403
|
+
logger.debug("Account is not part of AWS Organizations or lacks permissions")
|
404
|
+
return None
|
405
|
+
else:
|
406
|
+
logger.warning(f"Error getting organization info: {e}")
|
407
|
+
return None
|
408
|
+
|
409
|
+
|
410
|
+
def assume_role_session(
|
411
|
+
session: boto3.Session,
|
412
|
+
role_arn: str,
|
413
|
+
session_name: str,
|
414
|
+
external_id: Optional[str] = None,
|
415
|
+
duration_seconds: int = 3600,
|
416
|
+
) -> boto3.Session:
|
417
|
+
"""
|
418
|
+
Create a new session by assuming an IAM role.
|
419
|
+
|
420
|
+
Args:
|
421
|
+
session: Base boto3 session
|
422
|
+
role_arn: ARN of the role to assume
|
423
|
+
session_name: Session name for the assumed role
|
424
|
+
external_id: External ID for cross-account role assumption
|
425
|
+
duration_seconds: Session duration in seconds
|
426
|
+
|
427
|
+
Returns:
|
428
|
+
New boto3 session with assumed role credentials
|
429
|
+
|
430
|
+
Raises:
|
431
|
+
ClientError: If role assumption fails
|
432
|
+
"""
|
433
|
+
try:
|
434
|
+
sts_client = session.client("sts")
|
435
|
+
|
436
|
+
assume_role_args = {"RoleArn": role_arn, "RoleSessionName": session_name, "DurationSeconds": duration_seconds}
|
437
|
+
|
438
|
+
if external_id:
|
439
|
+
assume_role_args["ExternalId"] = external_id
|
440
|
+
|
441
|
+
response = sts_client.assume_role(**assume_role_args)
|
442
|
+
credentials = response["Credentials"]
|
443
|
+
|
444
|
+
# Create new session with temporary credentials
|
445
|
+
new_session = boto3.Session(
|
446
|
+
aws_access_key_id=credentials["AccessKeyId"],
|
447
|
+
aws_secret_access_key=credentials["SecretAccessKey"],
|
448
|
+
aws_session_token=credentials["SessionToken"],
|
449
|
+
region_name=session.region_name,
|
450
|
+
)
|
451
|
+
|
452
|
+
logger.info(f"Successfully assumed role: {role_arn}")
|
453
|
+
return new_session
|
454
|
+
|
455
|
+
except ClientError as e:
|
456
|
+
logger.error(f"Failed to assume role {role_arn}: {e}")
|
457
|
+
raise
|
458
|
+
|
459
|
+
|
460
|
+
def get_service_endpoints(
|
461
|
+
session: boto3.Session, service_name: str, region_name: Optional[str] = None
|
462
|
+
) -> Dict[str, str]:
|
463
|
+
"""
|
464
|
+
Get service endpoints for different regions.
|
465
|
+
|
466
|
+
Args:
|
467
|
+
session: Boto3 session
|
468
|
+
service_name: AWS service name
|
469
|
+
region_name: Specific region or None for all regions
|
470
|
+
|
471
|
+
Returns:
|
472
|
+
Dictionary mapping region names to endpoint URLs
|
473
|
+
"""
|
474
|
+
endpoints = {}
|
475
|
+
|
476
|
+
try:
|
477
|
+
regions = [region_name] if region_name else get_aws_regions(session, service_name)
|
478
|
+
|
479
|
+
for region in regions:
|
480
|
+
try:
|
481
|
+
client = session.client(service_name, region_name=region)
|
482
|
+
endpoint_url = client._endpoint.endpoint.host
|
483
|
+
endpoints[region] = endpoint_url
|
484
|
+
except Exception as e:
|
485
|
+
logger.debug(f"Could not get endpoint for {service_name} in {region}: {e}")
|
486
|
+
continue
|
487
|
+
|
488
|
+
return endpoints
|
489
|
+
|
490
|
+
except Exception as e:
|
491
|
+
logger.error(f"Failed to get service endpoints for {service_name}: {e}")
|
492
|
+
return {}
|
493
|
+
|
494
|
+
|
495
|
+
# Convenience functions for common operations
|
496
|
+
def is_global_service(service_name: str) -> bool:
|
497
|
+
"""Check if a service is global (not region-specific)."""
|
498
|
+
return service_name.lower() in GLOBAL_SERVICES
|
499
|
+
|
500
|
+
|
501
|
+
def requires_special_handling(service_name: str) -> bool:
|
502
|
+
"""Check if a service requires special handling for inventory."""
|
503
|
+
return service_name.lower() in SPECIAL_HANDLING_SERVICES
|
504
|
+
|
505
|
+
|
506
|
+
def get_default_region_for_service(service_name: str) -> str:
|
507
|
+
"""Get the default region for a service."""
|
508
|
+
if is_global_service(service_name):
|
509
|
+
return "us-east-1" # Global services typically use us-east-1
|
510
|
+
return "us-east-1" # Default fallback
|