runbooks 0.2.3__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.
Files changed (221) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env.template +2 -0
  3. jupyter-agent/.gitattributes +35 -0
  4. jupyter-agent/README.md +16 -0
  5. jupyter-agent/app.py +256 -0
  6. jupyter-agent/cloudops-agent.png +0 -0
  7. jupyter-agent/ds-system-prompt.txt +154 -0
  8. jupyter-agent/jupyter-agent.png +0 -0
  9. jupyter-agent/llama3_template.jinja +123 -0
  10. jupyter-agent/requirements.txt +9 -0
  11. jupyter-agent/utils.py +409 -0
  12. runbooks/__init__.py +71 -3
  13. runbooks/__main__.py +13 -0
  14. runbooks/aws/ec2_describe_instances.py +1 -1
  15. runbooks/aws/ec2_run_instances.py +8 -2
  16. runbooks/aws/ec2_start_stop_instances.py +17 -4
  17. runbooks/aws/ec2_unused_volumes.py +5 -1
  18. runbooks/aws/s3_create_bucket.py +4 -2
  19. runbooks/aws/s3_list_objects.py +6 -1
  20. runbooks/aws/tagging_lambda_handler.py +13 -2
  21. runbooks/aws/tags.json +12 -0
  22. runbooks/base.py +353 -0
  23. runbooks/cfat/README.md +49 -0
  24. runbooks/cfat/__init__.py +74 -0
  25. runbooks/cfat/app.ts +644 -0
  26. runbooks/cfat/assessment/__init__.py +40 -0
  27. runbooks/cfat/assessment/asana-import.csv +39 -0
  28. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  29. runbooks/cfat/assessment/cfat.txt +520 -0
  30. runbooks/cfat/assessment/collectors.py +200 -0
  31. runbooks/cfat/assessment/jira-import.csv +39 -0
  32. runbooks/cfat/assessment/runner.py +387 -0
  33. runbooks/cfat/assessment/validators.py +290 -0
  34. runbooks/cfat/cli.py +103 -0
  35. runbooks/cfat/docs/asana-import.csv +24 -0
  36. runbooks/cfat/docs/cfat-checks.csv +31 -0
  37. runbooks/cfat/docs/cfat.txt +335 -0
  38. runbooks/cfat/docs/checks-output.png +0 -0
  39. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  40. runbooks/cfat/docs/cloudshell-download.png +0 -0
  41. runbooks/cfat/docs/cloudshell-output.png +0 -0
  42. runbooks/cfat/docs/downloadfile.png +0 -0
  43. runbooks/cfat/docs/jira-import.csv +24 -0
  44. runbooks/cfat/docs/open-cloudshell.png +0 -0
  45. runbooks/cfat/docs/report-header.png +0 -0
  46. runbooks/cfat/models.py +1026 -0
  47. runbooks/cfat/package-lock.json +5116 -0
  48. runbooks/cfat/package.json +38 -0
  49. runbooks/cfat/report.py +496 -0
  50. runbooks/cfat/reporting/__init__.py +46 -0
  51. runbooks/cfat/reporting/exporters.py +337 -0
  52. runbooks/cfat/reporting/formatters.py +496 -0
  53. runbooks/cfat/reporting/templates.py +135 -0
  54. runbooks/cfat/run-assessment.sh +23 -0
  55. runbooks/cfat/runner.py +69 -0
  56. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  57. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  58. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  59. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  60. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  61. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  62. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  63. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  64. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  65. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  66. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  67. runbooks/cfat/src/actions/create-report.ts +616 -0
  68. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  69. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  70. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  71. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  72. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  73. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  74. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  75. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  76. runbooks/cfat/src/actions/get-regions.ts +22 -0
  77. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  78. runbooks/cfat/src/types/index.d.ts +147 -0
  79. runbooks/cfat/tests/__init__.py +141 -0
  80. runbooks/cfat/tests/test_cli.py +340 -0
  81. runbooks/cfat/tests/test_integration.py +290 -0
  82. runbooks/cfat/tests/test_models.py +505 -0
  83. runbooks/cfat/tests/test_reporting.py +354 -0
  84. runbooks/cfat/tsconfig.json +16 -0
  85. runbooks/cfat/webpack.config.cjs +27 -0
  86. runbooks/config.py +260 -0
  87. runbooks/finops/__init__.py +88 -0
  88. runbooks/finops/aws_client.py +245 -0
  89. runbooks/finops/cli.py +151 -0
  90. runbooks/finops/cost_processor.py +410 -0
  91. runbooks/finops/dashboard_runner.py +448 -0
  92. runbooks/finops/helpers.py +355 -0
  93. runbooks/finops/main.py +14 -0
  94. runbooks/finops/profile_processor.py +174 -0
  95. runbooks/finops/types.py +66 -0
  96. runbooks/finops/visualisations.py +80 -0
  97. runbooks/inventory/.gitignore +354 -0
  98. runbooks/inventory/ArgumentsClass.py +261 -0
  99. runbooks/inventory/Inventory_Modules.py +6130 -0
  100. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  101. runbooks/inventory/README.md +1320 -0
  102. runbooks/inventory/__init__.py +62 -0
  103. runbooks/inventory/account_class.py +532 -0
  104. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  105. runbooks/inventory/aws_decorators.py +201 -0
  106. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  107. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  108. runbooks/inventory/check_controltower_readiness.py +1107 -0
  109. runbooks/inventory/check_landingzone_readiness.py +711 -0
  110. runbooks/inventory/cloudtrail.md +727 -0
  111. runbooks/inventory/collectors/__init__.py +20 -0
  112. runbooks/inventory/collectors/aws_compute.py +518 -0
  113. runbooks/inventory/collectors/aws_networking.py +275 -0
  114. runbooks/inventory/collectors/base.py +222 -0
  115. runbooks/inventory/core/__init__.py +19 -0
  116. runbooks/inventory/core/collector.py +303 -0
  117. runbooks/inventory/core/formatter.py +296 -0
  118. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  119. runbooks/inventory/discovery.md +81 -0
  120. runbooks/inventory/draw_org_structure.py +748 -0
  121. runbooks/inventory/ec2_vpc_utils.py +341 -0
  122. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  123. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  124. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  125. runbooks/inventory/find_ec2_security_groups.py +669 -0
  126. runbooks/inventory/find_landingzone_versions.py +201 -0
  127. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  128. runbooks/inventory/inventory.sh +659 -0
  129. runbooks/inventory/list_cfn_stacks.py +558 -0
  130. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  131. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  132. runbooks/inventory/list_cfn_stacksets.py +453 -0
  133. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  134. runbooks/inventory/list_ds_directories.py +354 -0
  135. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  136. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  137. runbooks/inventory/list_ec2_instances.py +425 -0
  138. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  139. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  140. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  141. runbooks/inventory/list_guardduty_detectors.py +568 -0
  142. runbooks/inventory/list_iam_policies.py +404 -0
  143. runbooks/inventory/list_iam_roles.py +518 -0
  144. runbooks/inventory/list_iam_saml_providers.py +359 -0
  145. runbooks/inventory/list_lambda_functions.py +882 -0
  146. runbooks/inventory/list_org_accounts.py +446 -0
  147. runbooks/inventory/list_org_accounts_users.py +354 -0
  148. runbooks/inventory/list_rds_db_instances.py +406 -0
  149. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  150. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  151. runbooks/inventory/list_sns_topics.py +360 -0
  152. runbooks/inventory/list_ssm_parameters.py +402 -0
  153. runbooks/inventory/list_vpc_subnets.py +433 -0
  154. runbooks/inventory/list_vpcs.py +422 -0
  155. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  156. runbooks/inventory/models/__init__.py +24 -0
  157. runbooks/inventory/models/account.py +192 -0
  158. runbooks/inventory/models/inventory.py +309 -0
  159. runbooks/inventory/models/resource.py +247 -0
  160. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  161. runbooks/inventory/requirements.txt +12 -0
  162. runbooks/inventory/run_on_multi_accounts.py +211 -0
  163. runbooks/inventory/tests/common_test_data.py +3661 -0
  164. runbooks/inventory/tests/common_test_functions.py +204 -0
  165. runbooks/inventory/tests/script_test_data.py +0 -0
  166. runbooks/inventory/tests/setup.py +24 -0
  167. runbooks/inventory/tests/src.py +18 -0
  168. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  169. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  170. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  171. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  172. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  173. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  174. runbooks/inventory/update_aws_actions.py +173 -0
  175. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  176. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  177. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  178. runbooks/inventory/update_s3_public_access_block.py +539 -0
  179. runbooks/inventory/utils/__init__.py +23 -0
  180. runbooks/inventory/utils/aws_helpers.py +510 -0
  181. runbooks/inventory/utils/threading_utils.py +493 -0
  182. runbooks/inventory/utils/validation.py +682 -0
  183. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  184. runbooks/main.py +785 -0
  185. runbooks/organizations/__init__.py +12 -0
  186. runbooks/organizations/manager.py +374 -0
  187. runbooks/security_baseline/README.md +324 -0
  188. runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
  189. runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
  190. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
  191. runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
  192. runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
  193. runbooks/security_baseline/checklist/root_access_key.py +6 -1
  194. runbooks/security_baseline/config-origin.json +1 -1
  195. runbooks/security_baseline/config.json +1 -1
  196. runbooks/security_baseline/permission.json +1 -1
  197. runbooks/security_baseline/report_generator.py +10 -2
  198. runbooks/security_baseline/report_template_en.html +8 -8
  199. runbooks/security_baseline/report_template_jp.html +8 -8
  200. runbooks/security_baseline/report_template_kr.html +13 -13
  201. runbooks/security_baseline/report_template_vn.html +8 -8
  202. runbooks/security_baseline/requirements.txt +7 -0
  203. runbooks/security_baseline/run_script.py +8 -2
  204. runbooks/security_baseline/security_baseline_tester.py +10 -2
  205. runbooks/security_baseline/utils/common.py +5 -1
  206. runbooks/utils/__init__.py +204 -0
  207. runbooks-0.6.1.dist-info/METADATA +373 -0
  208. runbooks-0.6.1.dist-info/RECORD +237 -0
  209. {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
  210. runbooks-0.6.1.dist-info/entry_points.txt +7 -0
  211. runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
  212. runbooks-0.6.1.dist-info/top_level.txt +3 -0
  213. runbooks/python101/calculator.py +0 -34
  214. runbooks/python101/config.py +0 -1
  215. runbooks/python101/exceptions.py +0 -16
  216. runbooks/python101/file_manager.py +0 -218
  217. runbooks/python101/toolkit.py +0 -153
  218. runbooks-0.2.3.dist-info/METADATA +0 -435
  219. runbooks-0.2.3.dist-info/RECORD +0 -61
  220. runbooks-0.2.3.dist-info/entry_points.txt +0 -3
  221. runbooks-0.2.3.dist-info/top_level.txt +0 -1
@@ -0,0 +1,682 @@
1
+ """
2
+ Validation utilities for inventory operations.
3
+
4
+ This module provides input validation, sanitization, and constraint
5
+ checking for AWS resource types, account IDs, regions, and other parameters.
6
+ """
7
+
8
+ import re
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional, Set, Union
12
+
13
+ from loguru import logger
14
+
15
+
16
+ class ValidationSeverity(str, Enum):
17
+ """Validation result severity levels."""
18
+
19
+ INFO = "info"
20
+ WARNING = "warning"
21
+ ERROR = "error"
22
+ CRITICAL = "critical"
23
+
24
+
25
+ @dataclass
26
+ class ValidationResult:
27
+ """Result of a validation operation."""
28
+
29
+ is_valid: bool
30
+ severity: ValidationSeverity
31
+ message: str
32
+ field_name: Optional[str] = None
33
+ suggested_value: Optional[Any] = None
34
+
35
+ def __str__(self) -> str:
36
+ """String representation of validation result."""
37
+ field_info = f" ({self.field_name})" if self.field_name else ""
38
+ return f"{self.severity.upper()}{field_info}: {self.message}"
39
+
40
+
41
+ class ValidationError(Exception):
42
+ """Exception raised for validation failures."""
43
+
44
+ def __init__(self, message: str, results: List[ValidationResult]):
45
+ super().__init__(message)
46
+ self.results = results
47
+
48
+
49
+ # AWS-specific validation patterns
50
+ AWS_ACCOUNT_ID_PATTERN = re.compile(r"^\d{12}$")
51
+ AWS_REGION_PATTERN = re.compile(r"^[a-z]{2,3}-[a-z]+-\d+$")
52
+ AWS_ARN_PATTERN = re.compile(r"^arn:aws[a-z\-]*:[a-z0-9\-]*:[a-z0-9\-]*:\d{12}:[a-zA-Z0-9\-_/\.\:]+$")
53
+ AWS_RESOURCE_ID_PATTERN = re.compile(r"^[a-zA-Z0-9\-_\.]+$")
54
+
55
+ # Known AWS services and resource types
56
+ KNOWN_AWS_SERVICES = {
57
+ "ec2",
58
+ "rds",
59
+ "s3",
60
+ "lambda",
61
+ "iam",
62
+ "vpc",
63
+ "elb",
64
+ "elbv2",
65
+ "cloudformation",
66
+ "cloudtrail",
67
+ "config",
68
+ "guardduty",
69
+ "securityhub",
70
+ "organizations",
71
+ "sts",
72
+ "ssm",
73
+ "cloudwatch",
74
+ "logs",
75
+ "sns",
76
+ "sqs",
77
+ "dynamodb",
78
+ "elasticache",
79
+ "redshift",
80
+ "efs",
81
+ "fsx",
82
+ "route53",
83
+ "cloudfront",
84
+ "apigateway",
85
+ "apigatewayv2",
86
+ "waf",
87
+ "wafv2",
88
+ "ecs",
89
+ "eks",
90
+ "batch",
91
+ "fargate",
92
+ "autoscaling",
93
+ }
94
+
95
+ KNOWN_RESOURCE_TYPES = {
96
+ # Compute
97
+ "ec2:instance",
98
+ "ec2:image",
99
+ "ec2:snapshot",
100
+ "ec2:volume",
101
+ "lambda:function",
102
+ "lambda:layer",
103
+ "ecs:cluster",
104
+ "ecs:service",
105
+ "ecs:task",
106
+ # Storage
107
+ "s3:bucket",
108
+ "s3:object",
109
+ "ebs:volume",
110
+ "ebs:snapshot",
111
+ "efs:filesystem",
112
+ "efs:access-point",
113
+ # Database
114
+ "rds:instance",
115
+ "rds:cluster",
116
+ "rds:snapshot",
117
+ "dynamodb:table",
118
+ "dynamodb:backup",
119
+ "elasticache:cluster",
120
+ "elasticache:replication-group",
121
+ # Network
122
+ "vpc:vpc",
123
+ "vpc:subnet",
124
+ "vpc:route-table",
125
+ "vpc:security-group",
126
+ "vpc:nacl",
127
+ "vpc:internet-gateway",
128
+ "vpc:nat-gateway",
129
+ "elb:load-balancer",
130
+ "elbv2:load-balancer",
131
+ "elbv2:target-group",
132
+ "ec2:network-interface",
133
+ "ec2:elastic-ip",
134
+ # Security
135
+ "iam:user",
136
+ "iam:role",
137
+ "iam:policy",
138
+ "iam:group",
139
+ "guardduty:detector",
140
+ "guardduty:finding",
141
+ "config:recorder",
142
+ "config:rule",
143
+ # Management
144
+ "cloudformation:stack",
145
+ "cloudformation:stackset",
146
+ "cloudtrail:trail",
147
+ "logs:log-group",
148
+ "ssm:parameter",
149
+ "ssm:document",
150
+ }
151
+
152
+ # Common AWS regions
153
+ KNOWN_AWS_REGIONS = {
154
+ "us-east-1",
155
+ "us-east-2",
156
+ "us-west-1",
157
+ "us-west-2",
158
+ "eu-west-1",
159
+ "eu-west-2",
160
+ "eu-west-3",
161
+ "eu-central-1",
162
+ "eu-north-1",
163
+ "ap-northeast-1",
164
+ "ap-northeast-2",
165
+ "ap-northeast-3",
166
+ "ap-southeast-1",
167
+ "ap-southeast-2",
168
+ "ap-south-1",
169
+ "ca-central-1",
170
+ "sa-east-1",
171
+ "us-gov-east-1",
172
+ "us-gov-west-1",
173
+ "cn-north-1",
174
+ "cn-northwest-1",
175
+ }
176
+
177
+
178
+ def validate_aws_account_id(account_id: str) -> ValidationResult:
179
+ """
180
+ Validate AWS account ID format.
181
+
182
+ Args:
183
+ account_id: Account ID to validate
184
+
185
+ Returns:
186
+ ValidationResult with validation outcome
187
+ """
188
+ if not account_id:
189
+ return ValidationResult(
190
+ is_valid=False,
191
+ severity=ValidationSeverity.ERROR,
192
+ message="Account ID cannot be empty",
193
+ field_name="account_id",
194
+ )
195
+
196
+ if not isinstance(account_id, str):
197
+ return ValidationResult(
198
+ is_valid=False,
199
+ severity=ValidationSeverity.ERROR,
200
+ message=f"Account ID must be a string, got {type(account_id)}",
201
+ field_name="account_id",
202
+ )
203
+
204
+ # Remove any whitespace
205
+ account_id = account_id.strip()
206
+
207
+ if not AWS_ACCOUNT_ID_PATTERN.match(account_id):
208
+ return ValidationResult(
209
+ is_valid=False,
210
+ severity=ValidationSeverity.ERROR,
211
+ message="Account ID must be exactly 12 digits",
212
+ field_name="account_id",
213
+ suggested_value="123456789012" if len(account_id) != 12 else None,
214
+ )
215
+
216
+ return ValidationResult(
217
+ is_valid=True, severity=ValidationSeverity.INFO, message="Valid AWS account ID", field_name="account_id"
218
+ )
219
+
220
+
221
+ def validate_aws_region(region: str) -> ValidationResult:
222
+ """
223
+ Validate AWS region format and existence.
224
+
225
+ Args:
226
+ region: Region name to validate
227
+
228
+ Returns:
229
+ ValidationResult with validation outcome
230
+ """
231
+ if not region:
232
+ return ValidationResult(
233
+ is_valid=False, severity=ValidationSeverity.ERROR, message="Region cannot be empty", field_name="region"
234
+ )
235
+
236
+ if not isinstance(region, str):
237
+ return ValidationResult(
238
+ is_valid=False,
239
+ severity=ValidationSeverity.ERROR,
240
+ message=f"Region must be a string, got {type(region)}",
241
+ field_name="region",
242
+ )
243
+
244
+ # Remove whitespace and convert to lowercase
245
+ region = region.strip().lower()
246
+
247
+ # Check format
248
+ if not AWS_REGION_PATTERN.match(region):
249
+ return ValidationResult(
250
+ is_valid=False,
251
+ severity=ValidationSeverity.ERROR,
252
+ message="Invalid AWS region format (expected: us-east-1, eu-west-1, etc.)",
253
+ field_name="region",
254
+ suggested_value="us-east-1",
255
+ )
256
+
257
+ # Check if region is known
258
+ if region not in KNOWN_AWS_REGIONS:
259
+ return ValidationResult(
260
+ is_valid=True, # Still valid format, but unknown
261
+ severity=ValidationSeverity.WARNING,
262
+ message=f"Unknown AWS region: {region}",
263
+ field_name="region",
264
+ )
265
+
266
+ return ValidationResult(
267
+ is_valid=True, severity=ValidationSeverity.INFO, message="Valid AWS region", field_name="region"
268
+ )
269
+
270
+
271
+ def validate_resource_types(resource_types: Union[str, List[str]]) -> ValidationResult:
272
+ """
273
+ Validate AWS resource types.
274
+
275
+ Args:
276
+ resource_types: Resource type(s) to validate
277
+
278
+ Returns:
279
+ ValidationResult with validation outcome
280
+ """
281
+ if not resource_types:
282
+ return ValidationResult(
283
+ is_valid=False,
284
+ severity=ValidationSeverity.ERROR,
285
+ message="Resource types cannot be empty",
286
+ field_name="resource_types",
287
+ )
288
+
289
+ # Convert to list if string
290
+ if isinstance(resource_types, str):
291
+ resource_types = [resource_types]
292
+
293
+ if not isinstance(resource_types, list):
294
+ return ValidationResult(
295
+ is_valid=False,
296
+ severity=ValidationSeverity.ERROR,
297
+ message=f"Resource types must be string or list, got {type(resource_types)}",
298
+ field_name="resource_types",
299
+ )
300
+
301
+ invalid_types = []
302
+ unknown_types = []
303
+ valid_types = []
304
+
305
+ for resource_type in resource_types:
306
+ if not isinstance(resource_type, str):
307
+ invalid_types.append(f"{resource_type} (not a string)")
308
+ continue
309
+
310
+ resource_type = resource_type.strip().lower()
311
+
312
+ # Check if resource type follows expected format (service:type)
313
+ if ":" not in resource_type:
314
+ # Try to infer service name
315
+ if resource_type in KNOWN_AWS_SERVICES:
316
+ # This is likely a service name, suggest common resource types
317
+ return ValidationResult(
318
+ is_valid=False,
319
+ severity=ValidationSeverity.WARNING,
320
+ message=f"Resource type should include service prefix (e.g., {resource_type}:instance)",
321
+ field_name="resource_types",
322
+ suggested_value=f"{resource_type}:instance",
323
+ )
324
+ else:
325
+ invalid_types.append(resource_type)
326
+ continue
327
+
328
+ service, resource = resource_type.split(":", 1)
329
+
330
+ # Validate service name
331
+ if service not in KNOWN_AWS_SERVICES:
332
+ unknown_types.append(resource_type)
333
+
334
+ # Check if full resource type is known
335
+ if resource_type not in KNOWN_RESOURCE_TYPES:
336
+ unknown_types.append(resource_type)
337
+ else:
338
+ valid_types.append(resource_type)
339
+
340
+ # Generate result based on validation outcomes
341
+ if invalid_types:
342
+ return ValidationResult(
343
+ is_valid=False,
344
+ severity=ValidationSeverity.ERROR,
345
+ message=f"Invalid resource types: {', '.join(invalid_types)}",
346
+ field_name="resource_types",
347
+ )
348
+
349
+ if unknown_types and not valid_types:
350
+ return ValidationResult(
351
+ is_valid=False,
352
+ severity=ValidationSeverity.WARNING,
353
+ message=f"Unknown resource types: {', '.join(unknown_types)}",
354
+ field_name="resource_types",
355
+ )
356
+
357
+ if unknown_types:
358
+ return ValidationResult(
359
+ is_valid=True,
360
+ severity=ValidationSeverity.WARNING,
361
+ message=f"Some unknown resource types: {', '.join(unknown_types)}",
362
+ field_name="resource_types",
363
+ )
364
+
365
+ return ValidationResult(
366
+ is_valid=True,
367
+ severity=ValidationSeverity.INFO,
368
+ message=f"Valid resource types: {', '.join(valid_types)}",
369
+ field_name="resource_types",
370
+ )
371
+
372
+
373
+ def validate_account_ids(account_ids: Union[str, List[str]]) -> ValidationResult:
374
+ """
375
+ Validate multiple AWS account IDs.
376
+
377
+ Args:
378
+ account_ids: Account ID(s) to validate
379
+
380
+ Returns:
381
+ ValidationResult with validation outcome
382
+ """
383
+ if not account_ids:
384
+ return ValidationResult(
385
+ is_valid=False,
386
+ severity=ValidationSeverity.ERROR,
387
+ message="Account IDs cannot be empty",
388
+ field_name="account_ids",
389
+ )
390
+
391
+ # Convert to list if string
392
+ if isinstance(account_ids, str):
393
+ account_ids = [account_ids]
394
+
395
+ if not isinstance(account_ids, list):
396
+ return ValidationResult(
397
+ is_valid=False,
398
+ severity=ValidationSeverity.ERROR,
399
+ message=f"Account IDs must be string or list, got {type(account_ids)}",
400
+ field_name="account_ids",
401
+ )
402
+
403
+ invalid_accounts = []
404
+ valid_accounts = []
405
+
406
+ for account_id in account_ids:
407
+ result = validate_aws_account_id(str(account_id))
408
+ if result.is_valid:
409
+ valid_accounts.append(account_id)
410
+ else:
411
+ invalid_accounts.append(account_id)
412
+
413
+ if invalid_accounts:
414
+ return ValidationResult(
415
+ is_valid=False,
416
+ severity=ValidationSeverity.ERROR,
417
+ message=f"Invalid account IDs: {', '.join(invalid_accounts)}",
418
+ field_name="account_ids",
419
+ )
420
+
421
+ return ValidationResult(
422
+ is_valid=True,
423
+ severity=ValidationSeverity.INFO,
424
+ message=f"All {len(valid_accounts)} account IDs are valid",
425
+ field_name="account_ids",
426
+ )
427
+
428
+
429
+ def validate_aws_arn(arn: str) -> ValidationResult:
430
+ """
431
+ Validate AWS ARN format.
432
+
433
+ Args:
434
+ arn: ARN to validate
435
+
436
+ Returns:
437
+ ValidationResult with validation outcome
438
+ """
439
+ if not arn:
440
+ return ValidationResult(
441
+ is_valid=False, severity=ValidationSeverity.ERROR, message="ARN cannot be empty", field_name="arn"
442
+ )
443
+
444
+ if not isinstance(arn, str):
445
+ return ValidationResult(
446
+ is_valid=False,
447
+ severity=ValidationSeverity.ERROR,
448
+ message=f"ARN must be a string, got {type(arn)}",
449
+ field_name="arn",
450
+ )
451
+
452
+ # Remove whitespace
453
+ arn = arn.strip()
454
+
455
+ if not AWS_ARN_PATTERN.match(arn):
456
+ return ValidationResult(
457
+ is_valid=False,
458
+ severity=ValidationSeverity.ERROR,
459
+ message="Invalid ARN format (expected: arn:aws:service:region:account:resource)",
460
+ field_name="arn",
461
+ suggested_value="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
462
+ )
463
+
464
+ # Parse ARN components
465
+ try:
466
+ parts = arn.split(":")
467
+ if len(parts) < 6:
468
+ return ValidationResult(
469
+ is_valid=False,
470
+ severity=ValidationSeverity.ERROR,
471
+ message="ARN must have at least 6 components separated by colons",
472
+ field_name="arn",
473
+ )
474
+
475
+ arn_prefix, partition, service, region, account, resource = parts[:6]
476
+
477
+ # Validate components
478
+ if arn_prefix != "arn":
479
+ return ValidationResult(
480
+ is_valid=False,
481
+ severity=ValidationSeverity.ERROR,
482
+ message="ARN must start with 'arn:'",
483
+ field_name="arn",
484
+ )
485
+
486
+ if partition and not partition.startswith("aws"):
487
+ return ValidationResult(
488
+ is_valid=False,
489
+ severity=ValidationSeverity.WARNING,
490
+ message=f"Unknown AWS partition: {partition}",
491
+ field_name="arn",
492
+ )
493
+
494
+ if service and service not in KNOWN_AWS_SERVICES:
495
+ return ValidationResult(
496
+ is_valid=True,
497
+ severity=ValidationSeverity.WARNING,
498
+ message=f"Unknown AWS service in ARN: {service}",
499
+ field_name="arn",
500
+ )
501
+
502
+ if account:
503
+ account_result = validate_aws_account_id(account)
504
+ if not account_result.is_valid:
505
+ return ValidationResult(
506
+ is_valid=False,
507
+ severity=ValidationSeverity.ERROR,
508
+ message=f"Invalid account ID in ARN: {account}",
509
+ field_name="arn",
510
+ )
511
+
512
+ if region:
513
+ region_result = validate_aws_region(region)
514
+ if not region_result.is_valid and region_result.severity == ValidationSeverity.ERROR:
515
+ return ValidationResult(
516
+ is_valid=False,
517
+ severity=ValidationSeverity.ERROR,
518
+ message=f"Invalid region in ARN: {region}",
519
+ field_name="arn",
520
+ )
521
+
522
+ except Exception as e:
523
+ return ValidationResult(
524
+ is_valid=False, severity=ValidationSeverity.ERROR, message=f"Error parsing ARN: {e}", field_name="arn"
525
+ )
526
+
527
+ return ValidationResult(is_valid=True, severity=ValidationSeverity.INFO, message="Valid AWS ARN", field_name="arn")
528
+
529
+
530
+ def validate_inventory_parameters(parameters: Dict[str, Any]) -> List[ValidationResult]:
531
+ """
532
+ Validate a complete set of inventory parameters.
533
+
534
+ Args:
535
+ parameters: Dictionary of parameters to validate
536
+
537
+ Returns:
538
+ List of ValidationResult objects
539
+ """
540
+ results = []
541
+
542
+ # Validate account IDs
543
+ if "account_ids" in parameters:
544
+ result = validate_account_ids(parameters["account_ids"])
545
+ results.append(result)
546
+
547
+ # Validate regions
548
+ if "regions" in parameters:
549
+ regions = parameters["regions"]
550
+ if isinstance(regions, str):
551
+ regions = [regions]
552
+
553
+ for region in regions:
554
+ result = validate_aws_region(region)
555
+ results.append(result)
556
+
557
+ # Validate resource types
558
+ if "resource_types" in parameters:
559
+ result = validate_resource_types(parameters["resource_types"])
560
+ results.append(result)
561
+
562
+ # Validate numeric parameters
563
+ numeric_params = {"max_workers": (1, 100), "timeout": (1, 3600), "batch_size": (1, 1000)}
564
+
565
+ for param_name, (min_val, max_val) in numeric_params.items():
566
+ if param_name in parameters:
567
+ value = parameters[param_name]
568
+
569
+ if not isinstance(value, (int, float)):
570
+ results.append(
571
+ ValidationResult(
572
+ is_valid=False,
573
+ severity=ValidationSeverity.ERROR,
574
+ message=f"{param_name} must be a number, got {type(value)}",
575
+ field_name=param_name,
576
+ )
577
+ )
578
+ elif value < min_val or value > max_val:
579
+ results.append(
580
+ ValidationResult(
581
+ is_valid=False,
582
+ severity=ValidationSeverity.ERROR,
583
+ message=f"{param_name} must be between {min_val} and {max_val}",
584
+ field_name=param_name,
585
+ suggested_value=max(min_val, min(max_val, value)),
586
+ )
587
+ )
588
+ else:
589
+ results.append(
590
+ ValidationResult(
591
+ is_valid=True,
592
+ severity=ValidationSeverity.INFO,
593
+ message=f"Valid {param_name}: {value}",
594
+ field_name=param_name,
595
+ )
596
+ )
597
+
598
+ return results
599
+
600
+
601
+ def sanitize_resource_type(resource_type: str) -> str:
602
+ """
603
+ Sanitize and normalize a resource type string.
604
+
605
+ Args:
606
+ resource_type: Resource type to sanitize
607
+
608
+ Returns:
609
+ Sanitized resource type
610
+ """
611
+ if not isinstance(resource_type, str):
612
+ raise ValueError(f"Resource type must be a string, got {type(resource_type)}")
613
+
614
+ # Remove whitespace and convert to lowercase
615
+ resource_type = resource_type.strip().lower()
616
+
617
+ # Handle common variations
618
+ type_mappings = {
619
+ "instances": "ec2:instance",
620
+ "buckets": "s3:bucket",
621
+ "functions": "lambda:function",
622
+ "volumes": "ebs:volume",
623
+ "vpcs": "vpc:vpc",
624
+ "loadbalancers": "elb:load-balancer",
625
+ }
626
+
627
+ if resource_type in type_mappings:
628
+ return type_mappings[resource_type]
629
+
630
+ # If no colon, try to add service prefix
631
+ if ":" not in resource_type:
632
+ service_guesses = {
633
+ "instance": "ec2",
634
+ "bucket": "s3",
635
+ "function": "lambda",
636
+ "volume": "ebs",
637
+ "vpc": "vpc",
638
+ "subnet": "vpc",
639
+ "security-group": "vpc",
640
+ }
641
+
642
+ if resource_type in service_guesses:
643
+ service = service_guesses[resource_type]
644
+ return f"{service}:{resource_type}"
645
+
646
+ return resource_type
647
+
648
+
649
+ def check_validation_results(results: List[ValidationResult], raise_on_error: bool = True) -> bool:
650
+ """
651
+ Check validation results and optionally raise exception for errors.
652
+
653
+ Args:
654
+ results: List of validation results to check
655
+ raise_on_error: Whether to raise exception for errors
656
+
657
+ Returns:
658
+ True if all validations passed, False otherwise
659
+
660
+ Raises:
661
+ ValidationError: If raise_on_error is True and there are errors
662
+ """
663
+ errors = [r for r in results if r.severity == ValidationSeverity.ERROR]
664
+ warnings = [r for r in results if r.severity == ValidationSeverity.WARNING]
665
+
666
+ # Log warnings
667
+ for warning in warnings:
668
+ logger.warning(str(warning))
669
+
670
+ # Handle errors
671
+ if errors:
672
+ error_message = f"Validation failed with {len(errors)} error(s)"
673
+
674
+ for error in errors:
675
+ logger.error(str(error))
676
+
677
+ if raise_on_error:
678
+ raise ValidationError(error_message, errors)
679
+
680
+ return False
681
+
682
+ return True