runbooks 0.2.5__py3-none-any.whl → 0.7.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.
Files changed (249) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env +2 -0
  3. jupyter-agent/.env.template +2 -0
  4. jupyter-agent/.gitattributes +35 -0
  5. jupyter-agent/.gradio/certificate.pem +31 -0
  6. jupyter-agent/README.md +16 -0
  7. jupyter-agent/__main__.log +8 -0
  8. jupyter-agent/app.py +256 -0
  9. jupyter-agent/cloudops-agent.png +0 -0
  10. jupyter-agent/ds-system-prompt.txt +154 -0
  11. jupyter-agent/jupyter-agent.png +0 -0
  12. jupyter-agent/llama3_template.jinja +123 -0
  13. jupyter-agent/requirements.txt +9 -0
  14. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
  15. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
  16. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
  17. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
  18. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
  19. jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
  20. jupyter-agent/utils.py +409 -0
  21. runbooks/__init__.py +71 -3
  22. runbooks/__main__.py +13 -0
  23. runbooks/aws/ec2_describe_instances.py +1 -1
  24. runbooks/aws/ec2_run_instances.py +8 -2
  25. runbooks/aws/ec2_start_stop_instances.py +17 -4
  26. runbooks/aws/ec2_unused_volumes.py +5 -1
  27. runbooks/aws/s3_create_bucket.py +4 -2
  28. runbooks/aws/s3_list_objects.py +6 -1
  29. runbooks/aws/tagging_lambda_handler.py +13 -2
  30. runbooks/aws/tags.json +12 -0
  31. runbooks/base.py +353 -0
  32. runbooks/cfat/README.md +49 -0
  33. runbooks/cfat/__init__.py +74 -0
  34. runbooks/cfat/app.ts +644 -0
  35. runbooks/cfat/assessment/__init__.py +40 -0
  36. runbooks/cfat/assessment/asana-import.csv +39 -0
  37. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  38. runbooks/cfat/assessment/cfat.txt +520 -0
  39. runbooks/cfat/assessment/collectors.py +200 -0
  40. runbooks/cfat/assessment/jira-import.csv +39 -0
  41. runbooks/cfat/assessment/runner.py +387 -0
  42. runbooks/cfat/assessment/validators.py +290 -0
  43. runbooks/cfat/cli.py +103 -0
  44. runbooks/cfat/docs/asana-import.csv +24 -0
  45. runbooks/cfat/docs/cfat-checks.csv +31 -0
  46. runbooks/cfat/docs/cfat.txt +335 -0
  47. runbooks/cfat/docs/checks-output.png +0 -0
  48. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  49. runbooks/cfat/docs/cloudshell-download.png +0 -0
  50. runbooks/cfat/docs/cloudshell-output.png +0 -0
  51. runbooks/cfat/docs/downloadfile.png +0 -0
  52. runbooks/cfat/docs/jira-import.csv +24 -0
  53. runbooks/cfat/docs/open-cloudshell.png +0 -0
  54. runbooks/cfat/docs/report-header.png +0 -0
  55. runbooks/cfat/models.py +1026 -0
  56. runbooks/cfat/package-lock.json +5116 -0
  57. runbooks/cfat/package.json +38 -0
  58. runbooks/cfat/report.py +496 -0
  59. runbooks/cfat/reporting/__init__.py +46 -0
  60. runbooks/cfat/reporting/exporters.py +337 -0
  61. runbooks/cfat/reporting/formatters.py +496 -0
  62. runbooks/cfat/reporting/templates.py +135 -0
  63. runbooks/cfat/run-assessment.sh +23 -0
  64. runbooks/cfat/runner.py +69 -0
  65. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  66. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  67. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  68. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  69. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  70. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  71. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  72. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  73. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  74. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  75. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  76. runbooks/cfat/src/actions/create-report.ts +616 -0
  77. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  78. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  79. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  80. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  81. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  82. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  83. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  84. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  85. runbooks/cfat/src/actions/get-regions.ts +22 -0
  86. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  87. runbooks/cfat/src/types/index.d.ts +147 -0
  88. runbooks/cfat/tests/__init__.py +141 -0
  89. runbooks/cfat/tests/test_cli.py +340 -0
  90. runbooks/cfat/tests/test_integration.py +290 -0
  91. runbooks/cfat/tests/test_models.py +505 -0
  92. runbooks/cfat/tests/test_reporting.py +354 -0
  93. runbooks/cfat/tsconfig.json +16 -0
  94. runbooks/cfat/webpack.config.cjs +27 -0
  95. runbooks/config.py +260 -0
  96. runbooks/finops/README.md +337 -0
  97. runbooks/finops/__init__.py +86 -0
  98. runbooks/finops/aws_client.py +245 -0
  99. runbooks/finops/cli.py +151 -0
  100. runbooks/finops/cost_processor.py +410 -0
  101. runbooks/finops/dashboard_runner.py +448 -0
  102. runbooks/finops/helpers.py +355 -0
  103. runbooks/finops/main.py +14 -0
  104. runbooks/finops/profile_processor.py +174 -0
  105. runbooks/finops/types.py +66 -0
  106. runbooks/finops/visualisations.py +80 -0
  107. runbooks/inventory/.gitignore +354 -0
  108. runbooks/inventory/ArgumentsClass.py +261 -0
  109. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
  110. runbooks/inventory/Inventory_Modules.py +6130 -0
  111. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  112. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -0
  113. runbooks/inventory/README.md +1320 -0
  114. runbooks/inventory/__init__.py +62 -0
  115. runbooks/inventory/account_class.py +532 -0
  116. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  117. runbooks/inventory/aws_decorators.py +201 -0
  118. runbooks/inventory/aws_organization.png +0 -0
  119. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  120. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  121. runbooks/inventory/check_controltower_readiness.py +1107 -0
  122. runbooks/inventory/check_landingzone_readiness.py +711 -0
  123. runbooks/inventory/cloudtrail.md +727 -0
  124. runbooks/inventory/collectors/__init__.py +20 -0
  125. runbooks/inventory/collectors/aws_compute.py +518 -0
  126. runbooks/inventory/collectors/aws_networking.py +275 -0
  127. runbooks/inventory/collectors/base.py +222 -0
  128. runbooks/inventory/core/__init__.py +19 -0
  129. runbooks/inventory/core/collector.py +303 -0
  130. runbooks/inventory/core/formatter.py +296 -0
  131. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  132. runbooks/inventory/discovery.md +81 -0
  133. runbooks/inventory/draw_org_structure.py +748 -0
  134. runbooks/inventory/ec2_vpc_utils.py +341 -0
  135. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  136. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  137. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  138. runbooks/inventory/find_ec2_security_groups.py +669 -0
  139. runbooks/inventory/find_landingzone_versions.py +201 -0
  140. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  141. runbooks/inventory/inventory.sh +659 -0
  142. runbooks/inventory/list_cfn_stacks.py +558 -0
  143. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  144. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  145. runbooks/inventory/list_cfn_stacksets.py +453 -0
  146. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  147. runbooks/inventory/list_ds_directories.py +354 -0
  148. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  149. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  150. runbooks/inventory/list_ec2_instances.py +425 -0
  151. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  152. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  153. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  154. runbooks/inventory/list_guardduty_detectors.py +568 -0
  155. runbooks/inventory/list_iam_policies.py +404 -0
  156. runbooks/inventory/list_iam_roles.py +518 -0
  157. runbooks/inventory/list_iam_saml_providers.py +359 -0
  158. runbooks/inventory/list_lambda_functions.py +882 -0
  159. runbooks/inventory/list_org_accounts.py +446 -0
  160. runbooks/inventory/list_org_accounts_users.py +354 -0
  161. runbooks/inventory/list_rds_db_instances.py +406 -0
  162. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  163. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  164. runbooks/inventory/list_sns_topics.py +360 -0
  165. runbooks/inventory/list_ssm_parameters.py +402 -0
  166. runbooks/inventory/list_vpc_subnets.py +433 -0
  167. runbooks/inventory/list_vpcs.py +422 -0
  168. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  169. runbooks/inventory/models/__init__.py +24 -0
  170. runbooks/inventory/models/account.py +192 -0
  171. runbooks/inventory/models/inventory.py +309 -0
  172. runbooks/inventory/models/resource.py +247 -0
  173. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  174. runbooks/inventory/requirements.txt +12 -0
  175. runbooks/inventory/run_on_multi_accounts.py +211 -0
  176. runbooks/inventory/tests/common_test_data.py +3661 -0
  177. runbooks/inventory/tests/common_test_functions.py +204 -0
  178. runbooks/inventory/tests/setup.py +24 -0
  179. runbooks/inventory/tests/src.py +18 -0
  180. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  181. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  182. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  183. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  184. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  185. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  186. runbooks/inventory/update_aws_actions.py +173 -0
  187. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  188. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  189. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  190. runbooks/inventory/update_s3_public_access_block.py +539 -0
  191. runbooks/inventory/utils/__init__.py +23 -0
  192. runbooks/inventory/utils/aws_helpers.py +510 -0
  193. runbooks/inventory/utils/threading_utils.py +493 -0
  194. runbooks/inventory/utils/validation.py +682 -0
  195. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  196. runbooks/main.py +1004 -0
  197. runbooks/organizations/__init__.py +12 -0
  198. runbooks/organizations/manager.py +374 -0
  199. runbooks/security/README.md +447 -0
  200. runbooks/security/__init__.py +71 -0
  201. runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
  202. runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
  203. runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
  204. runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
  205. runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
  206. runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
  207. runbooks/{security_baseline → security}/config-origin.json +1 -1
  208. runbooks/{security_baseline → security}/config.json +1 -1
  209. runbooks/{security_baseline → security}/permission.json +1 -1
  210. runbooks/{security_baseline → security}/report_generator.py +10 -2
  211. runbooks/{security_baseline → security}/report_template_en.html +7 -7
  212. runbooks/{security_baseline → security}/report_template_jp.html +7 -7
  213. runbooks/{security_baseline → security}/report_template_kr.html +12 -12
  214. runbooks/{security_baseline → security}/report_template_vn.html +7 -7
  215. runbooks/{security_baseline → security}/run_script.py +8 -2
  216. runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
  217. runbooks/{security_baseline → security}/utils/common.py +5 -1
  218. runbooks/utils/__init__.py +204 -0
  219. runbooks-0.7.0.dist-info/METADATA +375 -0
  220. runbooks-0.7.0.dist-info/RECORD +249 -0
  221. {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
  222. runbooks-0.7.0.dist-info/entry_points.txt +7 -0
  223. runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
  224. runbooks-0.7.0.dist-info/top_level.txt +3 -0
  225. runbooks/python101/calculator.py +0 -34
  226. runbooks/python101/config.py +0 -1
  227. runbooks/python101/exceptions.py +0 -16
  228. runbooks/python101/file_manager.py +0 -218
  229. runbooks/python101/toolkit.py +0 -153
  230. runbooks-0.2.5.dist-info/METADATA +0 -439
  231. runbooks-0.2.5.dist-info/RECORD +0 -61
  232. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  233. runbooks-0.2.5.dist-info/top_level.txt +0 -1
  234. /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
  235. /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
  236. /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
  237. /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
  238. /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
  239. /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
  240. /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
  241. /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
  242. /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
  243. /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
  244. /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
  245. /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
  246. /runbooks/{security_baseline → security}/utils/enums.py +0 -0
  247. /runbooks/{security_baseline → security}/utils/language.py +0 -0
  248. /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
  249. /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
@@ -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