iam-policy-validator 1.7.2__py3-none-any.whl → 1.8.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.
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -6
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/RECORD +38 -35
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +61 -23
- iam_validator/checks/action_resource_matching.py +6 -2
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +1 -1
- iam_validator/checks/condition_type_mismatch.py +6 -6
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +65 -133
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +7 -3
- iam_validator/checks/service_wildcard.py +2 -2
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +3 -1
- iam_validator/checks/wildcard_resource.py +3 -1
- iam_validator/commands/validate.py +6 -12
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +45 -43
- iam_validator/core/check_registry.py +83 -79
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/defaults.py +58 -52
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +31 -472
- iam_validator/core/policy_loader.py +27 -4
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,15 +5,13 @@ configurations, supporting exact matches, regex patterns, and any_of/all_of logi
|
|
|
5
5
|
|
|
6
6
|
Performance optimizations:
|
|
7
7
|
- Uses frozenset for O(1) lookups
|
|
8
|
-
- LRU cache for compiled regex patterns
|
|
8
|
+
- Centralized LRU cache for compiled regex patterns (from ignore_patterns module)
|
|
9
9
|
- Lazy loading of default actions from modular data structure
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import re
|
|
13
|
-
from functools import lru_cache
|
|
14
|
-
|
|
15
12
|
from iam_validator.core.check_registry import CheckConfig
|
|
16
13
|
from iam_validator.core.config.sensitive_actions import get_sensitive_actions
|
|
14
|
+
from iam_validator.core.ignore_patterns import compile_pattern
|
|
17
15
|
|
|
18
16
|
# Lazy-loaded default set of sensitive actions
|
|
19
17
|
# This will be loaded only when first accessed
|
|
@@ -37,7 +35,9 @@ def _get_default_sensitive_actions() -> frozenset[str]:
|
|
|
37
35
|
return _DEFAULT_SENSITIVE_ACTIONS_CACHE
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
def get_sensitive_actions_by_categories(
|
|
38
|
+
def get_sensitive_actions_by_categories(
|
|
39
|
+
categories: list[str] | None = None,
|
|
40
|
+
) -> frozenset[str]:
|
|
41
41
|
"""
|
|
42
42
|
Get sensitive actions filtered by categories.
|
|
43
43
|
|
|
@@ -66,25 +66,10 @@ def get_sensitive_actions_by_categories(categories: list[str] | None = None) ->
|
|
|
66
66
|
DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
# Global regex pattern cache for performance
|
|
70
|
-
@lru_cache(maxsize=256)
|
|
71
|
-
def compile_pattern(pattern: str) -> re.Pattern[str] | None:
|
|
72
|
-
"""Compile and cache regex patterns.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
pattern: Regex pattern string
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
Compiled pattern or None if invalid
|
|
79
|
-
"""
|
|
80
|
-
try:
|
|
81
|
-
return re.compile(pattern)
|
|
82
|
-
except re.error:
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
|
|
86
69
|
def check_sensitive_actions(
|
|
87
|
-
actions: list[str],
|
|
70
|
+
actions: list[str],
|
|
71
|
+
config: CheckConfig,
|
|
72
|
+
default_actions: frozenset[str] | None = None,
|
|
88
73
|
) -> tuple[bool, list[str]]:
|
|
89
74
|
"""
|
|
90
75
|
Check if actions match sensitive action criteria with any_of/all_of support.
|
|
@@ -115,6 +100,10 @@ def check_sensitive_actions(
|
|
|
115
100
|
# Use all categories if no specific categories configured
|
|
116
101
|
default_actions = _get_default_sensitive_actions()
|
|
117
102
|
|
|
103
|
+
# Apply ignore_patterns to filter out default actions
|
|
104
|
+
# This allows users to exclude specific actions from the default 490 actions
|
|
105
|
+
default_actions = config.filter_actions(default_actions)
|
|
106
|
+
|
|
118
107
|
# Filter out wildcards
|
|
119
108
|
filtered_actions = [a for a in actions if a != "*"]
|
|
120
109
|
if not filtered_actions:
|
|
@@ -141,6 +130,17 @@ def check_sensitive_actions(
|
|
|
141
130
|
matched_set = set(actions_matched) | set(patterns_matched)
|
|
142
131
|
matched_actions = list(matched_set)
|
|
143
132
|
|
|
133
|
+
# Apply ignore_patterns to filter the final matched actions
|
|
134
|
+
# This ensures ignore_patterns work for:
|
|
135
|
+
# 1. Default actions (490 actions from Python modules)
|
|
136
|
+
# 2. Custom sensitive_actions configuration
|
|
137
|
+
# 3. Custom sensitive_action_patterns configuration
|
|
138
|
+
if matched_actions and config.ignore_patterns:
|
|
139
|
+
filtered_matched = config.filter_actions(frozenset(matched_actions))
|
|
140
|
+
matched_actions = list(filtered_matched)
|
|
141
|
+
# Update is_sensitive based on filtered results
|
|
142
|
+
is_sensitive = len(matched_actions) > 0
|
|
143
|
+
|
|
144
144
|
return is_sensitive, matched_actions
|
|
145
145
|
|
|
146
146
|
|
|
@@ -243,7 +243,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
|
|
|
243
243
|
# Each item can be a string pattern, or a dict with any_of/all_of
|
|
244
244
|
if isinstance(item, str):
|
|
245
245
|
# Simple string pattern - check if any action matches
|
|
246
|
-
# Use cached compiled pattern
|
|
246
|
+
# Use cached compiled pattern from centralized ignore_patterns module
|
|
247
247
|
compiled = compile_pattern(item)
|
|
248
248
|
if compiled:
|
|
249
249
|
for action in actions:
|
|
@@ -262,7 +262,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
|
|
|
262
262
|
# any_of: at least one action must match at least one pattern
|
|
263
263
|
if "any_of" in config:
|
|
264
264
|
matched = set()
|
|
265
|
-
# Pre-compile all patterns
|
|
265
|
+
# Pre-compile all patterns using centralized cache
|
|
266
266
|
compiled_patterns = [compile_pattern(p) for p in config["any_of"]]
|
|
267
267
|
|
|
268
268
|
for action in actions:
|
|
@@ -274,7 +274,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
|
|
|
274
274
|
|
|
275
275
|
# all_of: at least one action must match ALL patterns
|
|
276
276
|
if "all_of" in config:
|
|
277
|
-
# Pre-compile all patterns
|
|
277
|
+
# Pre-compile all patterns using centralized cache
|
|
278
278
|
compiled_patterns = [compile_pattern(p) for p in config["all_of"]]
|
|
279
279
|
# Filter out invalid patterns
|
|
280
280
|
compiled_patterns = [p for p in compiled_patterns if p]
|
|
@@ -72,7 +72,7 @@ async def expand_wildcard_actions(actions: list[str], fetcher: AWSServiceFetcher
|
|
|
72
72
|
available_actions = list(service_detail.actions.keys())
|
|
73
73
|
|
|
74
74
|
# Match wildcard pattern against available actions
|
|
75
|
-
_, matched_actions = fetcher.
|
|
75
|
+
_, matched_actions = fetcher.match_wildcard_action(action_name, available_actions)
|
|
76
76
|
|
|
77
77
|
# Add expanded actions with service prefix
|
|
78
78
|
for matched_action in matched_actions:
|
|
@@ -38,7 +38,9 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
38
38
|
|
|
39
39
|
# Check for wildcard action (Action: "*")
|
|
40
40
|
if "*" in actions:
|
|
41
|
-
message = config.config.get(
|
|
41
|
+
message = config.config.get(
|
|
42
|
+
"message", 'Statement allows all actions `"*"` (wildcard action).'
|
|
43
|
+
)
|
|
42
44
|
suggestion = config.config.get(
|
|
43
45
|
"suggestion",
|
|
44
46
|
"Replace wildcard with specific actions needed for your use case",
|
|
@@ -62,7 +62,9 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
62
62
|
return issues
|
|
63
63
|
|
|
64
64
|
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
65
|
-
message = config.config.get(
|
|
65
|
+
message = config.config.get(
|
|
66
|
+
"message", 'Statement applies to all resources `"*"` (wildcard resource).'
|
|
67
|
+
)
|
|
66
68
|
suggestion = config.config.get(
|
|
67
69
|
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
68
70
|
)
|
|
@@ -114,13 +114,17 @@ Examples:
|
|
|
114
114
|
choices=[
|
|
115
115
|
"IDENTITY_POLICY",
|
|
116
116
|
"RESOURCE_POLICY",
|
|
117
|
+
"TRUST_POLICY",
|
|
117
118
|
"SERVICE_CONTROL_POLICY",
|
|
118
119
|
"RESOURCE_CONTROL_POLICY",
|
|
119
120
|
],
|
|
120
121
|
default="IDENTITY_POLICY",
|
|
121
122
|
help="Type of IAM policy being validated (default: IDENTITY_POLICY). "
|
|
122
|
-
"
|
|
123
|
-
"
|
|
123
|
+
"IDENTITY_POLICY: Attached to users/groups/roles | "
|
|
124
|
+
"RESOURCE_POLICY: S3/SNS/SQS policies | "
|
|
125
|
+
"TRUST_POLICY: Role assumption policies | "
|
|
126
|
+
"SERVICE_CONTROL_POLICY: AWS Orgs SCPs | "
|
|
127
|
+
"RESOURCE_CONTROL_POLICY: AWS Orgs RCPs",
|
|
124
128
|
)
|
|
125
129
|
|
|
126
130
|
parser.add_argument(
|
|
@@ -159,12 +163,6 @@ Examples:
|
|
|
159
163
|
help="Path to directory containing custom checks for auto-discovery",
|
|
160
164
|
)
|
|
161
165
|
|
|
162
|
-
parser.add_argument(
|
|
163
|
-
"--no-registry",
|
|
164
|
-
action="store_true",
|
|
165
|
-
help="Use legacy validation (disable check registry system)",
|
|
166
|
-
)
|
|
167
|
-
|
|
168
166
|
parser.add_argument(
|
|
169
167
|
"--stream",
|
|
170
168
|
action="store_true",
|
|
@@ -242,14 +240,12 @@ Examples:
|
|
|
242
240
|
logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
|
|
243
241
|
|
|
244
242
|
# Validate policies
|
|
245
|
-
use_registry = not getattr(args, "no_registry", False)
|
|
246
243
|
config_path = getattr(args, "config", None)
|
|
247
244
|
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
248
245
|
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
249
246
|
results = await validate_policies(
|
|
250
247
|
policies,
|
|
251
248
|
config_path=config_path,
|
|
252
|
-
use_registry=use_registry,
|
|
253
249
|
custom_checks_dir=custom_checks_dir,
|
|
254
250
|
policy_type=policy_type,
|
|
255
251
|
)
|
|
@@ -329,7 +325,6 @@ Examples:
|
|
|
329
325
|
"""
|
|
330
326
|
loader = PolicyLoader()
|
|
331
327
|
generator = ReportGenerator()
|
|
332
|
-
use_registry = not getattr(args, "no_registry", False)
|
|
333
328
|
config_path = getattr(args, "config", None)
|
|
334
329
|
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
335
330
|
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
@@ -354,7 +349,6 @@ Examples:
|
|
|
354
349
|
results = await validate_policies(
|
|
355
350
|
[(file_path, policy)],
|
|
356
351
|
config_path=config_path,
|
|
357
|
-
use_registry=use_registry,
|
|
358
352
|
custom_checks_dir=custom_checks_dir,
|
|
359
353
|
policy_type=policy_type,
|
|
360
354
|
)
|
iam_validator/core/__init__.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""Core validation modules."""
|
|
2
2
|
|
|
3
3
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
4
|
-
from iam_validator.core.policy_checks import
|
|
4
|
+
from iam_validator.core.policy_checks import validate_policies
|
|
5
5
|
from iam_validator.core.policy_loader import PolicyLoader
|
|
6
6
|
from iam_validator.core.report import ReportGenerator
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
9
|
"AWSServiceFetcher",
|
|
10
|
-
"PolicyValidator",
|
|
11
10
|
"validate_policies",
|
|
12
11
|
"PolicyLoader",
|
|
13
12
|
"ReportGenerator",
|
|
@@ -577,7 +577,7 @@ class AccessAnalyzerValidator:
|
|
|
577
577
|
)
|
|
578
578
|
results.append(result)
|
|
579
579
|
|
|
580
|
-
except Exception as e:
|
|
580
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
581
581
|
self.logger.error(f"Failed to validate {policy_file}: {e}")
|
|
582
582
|
result = AccessAnalyzerResult(
|
|
583
583
|
policy_file=policy_file,
|
|
@@ -259,7 +259,7 @@ class AccessAnalyzerReportFormatter:
|
|
|
259
259
|
file_path: Path to save JSON report
|
|
260
260
|
"""
|
|
261
261
|
json_content = self.generate_json_report(report)
|
|
262
|
-
with open(file_path, "w") as f:
|
|
262
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
263
263
|
f.write(json_content)
|
|
264
264
|
|
|
265
265
|
def generate_markdown_report(
|
|
@@ -636,5 +636,5 @@ class AccessAnalyzerReportFormatter:
|
|
|
636
636
|
file_path: Path to save Markdown report
|
|
637
637
|
"""
|
|
638
638
|
markdown_content = self.generate_markdown_report(report)
|
|
639
|
-
with open(file_path, "w") as f:
|
|
639
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
640
640
|
f.write(markdown_content)
|
|
@@ -160,7 +160,7 @@ class AWSServiceFetcher:
|
|
|
160
160
|
|
|
161
161
|
Public API - Parsing:
|
|
162
162
|
- parse_action: Split action into service and name
|
|
163
|
-
-
|
|
163
|
+
- match_wildcard_action: Match wildcard patterns
|
|
164
164
|
|
|
165
165
|
Utilities:
|
|
166
166
|
- get_stats: Get cache statistics
|
|
@@ -352,7 +352,7 @@ class AWSServiceFetcher:
|
|
|
352
352
|
try:
|
|
353
353
|
await self.fetch_service_by_name(name)
|
|
354
354
|
self._prefetched_services.add(name)
|
|
355
|
-
except Exception as e:
|
|
355
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
356
356
|
logger.warning(f"Failed to prefetch service {name}: {e}")
|
|
357
357
|
|
|
358
358
|
# Fetch in batches to avoid overwhelming the API
|
|
@@ -400,7 +400,7 @@ class AWSServiceFetcher:
|
|
|
400
400
|
logger.debug(f"Disk cache hit for {url}")
|
|
401
401
|
return data
|
|
402
402
|
|
|
403
|
-
except Exception as e:
|
|
403
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
404
404
|
logger.warning(f"Failed to read cache for {url}: {e}")
|
|
405
405
|
return None
|
|
406
406
|
|
|
@@ -415,7 +415,7 @@ class AWSServiceFetcher:
|
|
|
415
415
|
with open(cache_path, "w", encoding="utf-8") as f:
|
|
416
416
|
json.dump(data, f, indent=2)
|
|
417
417
|
logger.debug(f"Written to disk cache: {url}")
|
|
418
|
-
except Exception as e:
|
|
418
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
419
419
|
logger.warning(f"Failed to write cache for {url}: {e}")
|
|
420
420
|
|
|
421
421
|
async def _make_request_with_batching(self, url: str) -> Any:
|
|
@@ -459,7 +459,7 @@ class AWSServiceFetcher:
|
|
|
459
459
|
if not future.done():
|
|
460
460
|
future.set_result(result)
|
|
461
461
|
return result
|
|
462
|
-
except Exception as e:
|
|
462
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
463
463
|
if not future.done():
|
|
464
464
|
future.set_exception(e)
|
|
465
465
|
raise
|
|
@@ -504,21 +504,23 @@ class AWSServiceFetcher:
|
|
|
504
504
|
|
|
505
505
|
return data
|
|
506
506
|
|
|
507
|
-
except Exception as json_error:
|
|
507
|
+
except Exception as json_error: # pylint: disable=broad-exception-caught
|
|
508
508
|
logger.error(f"Failed to parse response as JSON: {json_error}")
|
|
509
|
-
raise ValueError(
|
|
509
|
+
raise ValueError(
|
|
510
|
+
f"Invalid JSON response from {url}: {json_error}"
|
|
511
|
+
) from json_error
|
|
510
512
|
|
|
511
513
|
except httpx.HTTPStatusError as e:
|
|
512
514
|
logger.error(f"HTTP error {e.response.status_code} for {url}")
|
|
513
515
|
if e.response.status_code == 404:
|
|
514
|
-
raise ValueError(f"Service not found: {url}")
|
|
516
|
+
raise ValueError(f"Service not found: {url}") from e
|
|
515
517
|
last_exception = e
|
|
516
518
|
|
|
517
519
|
except httpx.RequestError as e:
|
|
518
520
|
logger.error(f"Request error for {url}: {e}")
|
|
519
521
|
last_exception = e
|
|
520
522
|
|
|
521
|
-
except Exception as e:
|
|
523
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
522
524
|
logger.error(f"Unexpected error for {url}: {e}")
|
|
523
525
|
last_exception = e
|
|
524
526
|
|
|
@@ -547,7 +549,7 @@ class AWSServiceFetcher:
|
|
|
547
549
|
raise FileNotFoundError(f"_services.json not found in {self.aws_services_dir}")
|
|
548
550
|
|
|
549
551
|
try:
|
|
550
|
-
with open(services_file) as f:
|
|
552
|
+
with open(services_file, encoding="utf-8") as f:
|
|
551
553
|
data = json.load(f)
|
|
552
554
|
|
|
553
555
|
if not isinstance(data, list):
|
|
@@ -565,7 +567,7 @@ class AWSServiceFetcher:
|
|
|
565
567
|
return services
|
|
566
568
|
|
|
567
569
|
except json.JSONDecodeError as e:
|
|
568
|
-
raise ValueError(f"Invalid JSON in services.json: {e}")
|
|
570
|
+
raise ValueError(f"Invalid JSON in services.json: {e}") from e
|
|
569
571
|
|
|
570
572
|
def _load_service_from_file(self, service_name: str) -> ServiceDetail:
|
|
571
573
|
"""Load service detail from local JSON file.
|
|
@@ -591,7 +593,7 @@ class AWSServiceFetcher:
|
|
|
591
593
|
raise FileNotFoundError(f"Service file not found: {service_file}")
|
|
592
594
|
|
|
593
595
|
try:
|
|
594
|
-
with open(service_file) as f:
|
|
596
|
+
with open(service_file, encoding="utf-8") as f:
|
|
595
597
|
data = json.load(f)
|
|
596
598
|
|
|
597
599
|
service_detail = ServiceDetail.model_validate(data)
|
|
@@ -599,7 +601,7 @@ class AWSServiceFetcher:
|
|
|
599
601
|
return service_detail
|
|
600
602
|
|
|
601
603
|
except json.JSONDecodeError as e:
|
|
602
|
-
raise ValueError(f"Invalid JSON in {service_file}: {e}")
|
|
604
|
+
raise ValueError(f"Invalid JSON in {service_file}: {e}") from e
|
|
603
605
|
|
|
604
606
|
async def fetch_services(self) -> list[ServiceInfo]:
|
|
605
607
|
"""Fetch list of AWS services with caching.
|
|
@@ -677,7 +679,9 @@ class AWSServiceFetcher:
|
|
|
677
679
|
return service_detail
|
|
678
680
|
except FileNotFoundError:
|
|
679
681
|
pass
|
|
680
|
-
raise ValueError(
|
|
682
|
+
raise ValueError(
|
|
683
|
+
f"Service `{service_name}` not found in {self.aws_services_dir}"
|
|
684
|
+
) from FileNotFoundError
|
|
681
685
|
|
|
682
686
|
# Fetch service list and find URL from API
|
|
683
687
|
services = await self.fetch_services()
|
|
@@ -704,7 +708,7 @@ class AWSServiceFetcher:
|
|
|
704
708
|
try:
|
|
705
709
|
detail = await self.fetch_service_by_name(name)
|
|
706
710
|
return name, detail
|
|
707
|
-
except Exception as e:
|
|
711
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
708
712
|
logger.error(f"Failed to fetch service {name}: {e}")
|
|
709
713
|
raise
|
|
710
714
|
|
|
@@ -717,7 +721,7 @@ class AWSServiceFetcher:
|
|
|
717
721
|
if isinstance(result, Exception):
|
|
718
722
|
logger.error(f"Failed to fetch service {service_names[i]}: {result}")
|
|
719
723
|
raise result
|
|
720
|
-
|
|
724
|
+
if isinstance(result, tuple):
|
|
721
725
|
name, detail = result
|
|
722
726
|
services[name] = detail
|
|
723
727
|
|
|
@@ -731,7 +735,7 @@ class AWSServiceFetcher:
|
|
|
731
735
|
|
|
732
736
|
return match.group("service").lower(), match.group("action")
|
|
733
737
|
|
|
734
|
-
def
|
|
738
|
+
def match_wildcard_action(self, pattern: str, actions: list[str]) -> tuple[bool, list[str]]:
|
|
735
739
|
"""Match wildcard pattern against list of actions.
|
|
736
740
|
|
|
737
741
|
Args:
|
|
@@ -774,8 +778,7 @@ class AWSServiceFetcher:
|
|
|
774
778
|
# Just verify service exists
|
|
775
779
|
await self.fetch_service_by_name(service_prefix)
|
|
776
780
|
return True, None, True
|
|
777
|
-
|
|
778
|
-
return False, "Wildcard actions are not allowed", True
|
|
781
|
+
return False, "Wildcard actions are not allowed", True
|
|
779
782
|
|
|
780
783
|
# Fetch service details (will use cache)
|
|
781
784
|
service_detail = await self.fetch_service_by_name(service_prefix)
|
|
@@ -786,7 +789,7 @@ class AWSServiceFetcher:
|
|
|
786
789
|
if not allow_wildcards:
|
|
787
790
|
return False, "Wildcard actions are not allowed", True
|
|
788
791
|
|
|
789
|
-
has_matches, matched_actions = self.
|
|
792
|
+
has_matches, matched_actions = self.match_wildcard_action(
|
|
790
793
|
action_name, available_actions
|
|
791
794
|
)
|
|
792
795
|
|
|
@@ -799,33 +802,32 @@ class AWSServiceFetcher:
|
|
|
799
802
|
examples += f", ... ({match_count - 5} more)"
|
|
800
803
|
|
|
801
804
|
return True, None, True
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
)
|
|
805
|
+
# Wildcard doesn't match any actions
|
|
806
|
+
return (
|
|
807
|
+
False,
|
|
808
|
+
f"Action pattern `{action_name}` does not match any actions in service `{service_prefix}`",
|
|
809
|
+
True,
|
|
810
|
+
)
|
|
809
811
|
|
|
810
812
|
# Check if exact action exists (case-insensitive)
|
|
811
813
|
action_exists = any(a.lower() == action_name.lower() for a in available_actions)
|
|
812
814
|
|
|
813
815
|
if action_exists:
|
|
814
816
|
return True, None, False
|
|
815
|
-
else:
|
|
816
|
-
# Suggest similar actions
|
|
817
|
-
similar = [a for a in available_actions if action_name.lower() in a.lower()][:3]
|
|
818
817
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
818
|
+
# Suggest similar actions
|
|
819
|
+
similar = [f"`{a}`" for a in available_actions if action_name.lower() in a.lower()][:3]
|
|
820
|
+
|
|
821
|
+
suggestion = f" Did you mean: {', '.join(similar)}?" if similar else ""
|
|
822
|
+
return (
|
|
823
|
+
False,
|
|
824
|
+
f"Action `{action_name}` not found in service `{service_prefix}`.{suggestion}",
|
|
825
|
+
False,
|
|
826
|
+
)
|
|
825
827
|
|
|
826
828
|
except ValueError as e:
|
|
827
829
|
return False, str(e), False
|
|
828
|
-
except Exception as e:
|
|
830
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
829
831
|
logger.error(f"Error validating action {action}: {e}")
|
|
830
832
|
return False, f"Failed to validate action: {str(e)}", False
|
|
831
833
|
|
|
@@ -859,7 +861,7 @@ class AWSServiceFetcher:
|
|
|
859
861
|
- suggestion: Detailed suggestion with valid keys (shown in collapsible section)
|
|
860
862
|
"""
|
|
861
863
|
try:
|
|
862
|
-
from iam_validator.core.config.aws_global_conditions import (
|
|
864
|
+
from iam_validator.core.config.aws_global_conditions import ( # pylint: disable=import-outside-toplevel
|
|
863
865
|
get_global_conditions,
|
|
864
866
|
)
|
|
865
867
|
|
|
@@ -911,11 +913,11 @@ class AWSServiceFetcher:
|
|
|
911
913
|
# AWS allows it but the key may not be available in every request context
|
|
912
914
|
if is_global_key and action_detail.action_condition_keys is not None:
|
|
913
915
|
warning_msg = (
|
|
914
|
-
f"Global condition key
|
|
916
|
+
f"Global condition key `{condition_key}` is used with action `{action}`. "
|
|
915
917
|
f"While global condition keys can be used across all AWS services, "
|
|
916
918
|
f"the key may not be available in every request context. "
|
|
917
|
-
f"Verify that
|
|
918
|
-
f"Consider using
|
|
919
|
+
f"Verify that `{condition_key}` is available for this specific action's request context. "
|
|
920
|
+
f"Consider using `*IfExists` operators (e.g., `StringEqualsIfExists`) if the key might be missing."
|
|
919
921
|
)
|
|
920
922
|
return ConditionKeyValidationResult(is_valid=True, warning_message=warning_msg)
|
|
921
923
|
|
|
@@ -1000,7 +1002,7 @@ class AWSServiceFetcher:
|
|
|
1000
1002
|
suggestion=suggestion,
|
|
1001
1003
|
)
|
|
1002
1004
|
|
|
1003
|
-
except Exception as e:
|
|
1005
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1004
1006
|
logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
|
|
1005
1007
|
return ConditionKeyValidationResult(
|
|
1006
1008
|
is_valid=False,
|
|
@@ -1017,7 +1019,7 @@ class AWSServiceFetcher:
|
|
|
1017
1019
|
for cache_file in self._cache_dir.glob("*.json"):
|
|
1018
1020
|
try:
|
|
1019
1021
|
cache_file.unlink()
|
|
1020
|
-
except Exception as e:
|
|
1022
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1021
1023
|
logger.warning(f"Failed to delete cache file {cache_file}: {e}")
|
|
1022
1024
|
|
|
1023
1025
|
logger.info("Cleared all caches")
|