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,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
|