iam-policy-validator 1.4.0__py3-none-any.whl → 1.6.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 (57) hide show
  1. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +106 -78
  2. iam_policy_validator-1.6.0.dist-info/RECORD +82 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +20 -4
  5. iam_validator/checks/action_condition_enforcement.py +165 -8
  6. iam_validator/checks/action_resource_matching.py +424 -0
  7. iam_validator/checks/condition_key_validation.py +24 -2
  8. iam_validator/checks/condition_type_mismatch.py +259 -0
  9. iam_validator/checks/full_wildcard.py +67 -0
  10. iam_validator/checks/mfa_condition_check.py +112 -0
  11. iam_validator/checks/principal_validation.py +497 -3
  12. iam_validator/checks/sensitive_action.py +250 -0
  13. iam_validator/checks/service_wildcard.py +105 -0
  14. iam_validator/checks/set_operator_validation.py +157 -0
  15. iam_validator/checks/utils/sensitive_action_matcher.py +74 -32
  16. iam_validator/checks/wildcard_action.py +62 -0
  17. iam_validator/checks/wildcard_resource.py +131 -0
  18. iam_validator/commands/cache.py +1 -1
  19. iam_validator/commands/download_services.py +3 -8
  20. iam_validator/commands/validate.py +72 -13
  21. iam_validator/core/aws_fetcher.py +114 -64
  22. iam_validator/core/check_registry.py +167 -29
  23. iam_validator/core/condition_validators.py +626 -0
  24. iam_validator/core/config/__init__.py +81 -0
  25. iam_validator/core/config/aws_api.py +35 -0
  26. iam_validator/core/config/aws_global_conditions.py +160 -0
  27. iam_validator/core/config/category_suggestions.py +104 -0
  28. iam_validator/core/config/condition_requirements.py +155 -0
  29. iam_validator/core/{config_loader.py → config/config_loader.py} +32 -9
  30. iam_validator/core/config/defaults.py +523 -0
  31. iam_validator/core/config/principal_requirements.py +421 -0
  32. iam_validator/core/config/sensitive_actions.py +672 -0
  33. iam_validator/core/config/service_principals.py +95 -0
  34. iam_validator/core/config/wildcards.py +124 -0
  35. iam_validator/core/formatters/enhanced.py +11 -5
  36. iam_validator/core/formatters/sarif.py +78 -14
  37. iam_validator/core/models.py +14 -1
  38. iam_validator/core/policy_checks.py +4 -4
  39. iam_validator/core/pr_commenter.py +1 -1
  40. iam_validator/sdk/__init__.py +187 -0
  41. iam_validator/sdk/arn_matching.py +274 -0
  42. iam_validator/sdk/context.py +222 -0
  43. iam_validator/sdk/exceptions.py +48 -0
  44. iam_validator/sdk/helpers.py +177 -0
  45. iam_validator/sdk/policy_utils.py +425 -0
  46. iam_validator/sdk/shortcuts.py +283 -0
  47. iam_validator/utils/__init__.py +31 -0
  48. iam_validator/utils/cache.py +105 -0
  49. iam_validator/utils/regex.py +206 -0
  50. iam_policy_validator-1.4.0.dist-info/RECORD +0 -56
  51. iam_validator/checks/action_resource_constraint.py +0 -151
  52. iam_validator/checks/security_best_practices.py +0 -536
  53. iam_validator/core/aws_global_conditions.py +0 -137
  54. iam_validator/core/defaults.py +0 -393
  55. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
  56. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
  57. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,62 @@
1
+ """Wildcard action check - detects Action: '*' in IAM policies."""
2
+
3
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
4
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
5
+ from iam_validator.core.models import Statement, ValidationIssue
6
+
7
+
8
+ class WildcardActionCheck(PolicyCheck):
9
+ """Checks for wildcard actions (Action: '*') which grant all permissions."""
10
+
11
+ @property
12
+ def check_id(self) -> str:
13
+ return "wildcard_action"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "Checks for wildcard actions (*)"
18
+
19
+ @property
20
+ def default_severity(self) -> str:
21
+ return "medium"
22
+
23
+ async def execute(
24
+ self,
25
+ statement: Statement,
26
+ statement_idx: int,
27
+ fetcher: AWSServiceFetcher,
28
+ config: CheckConfig,
29
+ ) -> list[ValidationIssue]:
30
+ """Execute wildcard action check on a statement."""
31
+ issues = []
32
+
33
+ # Only check Allow statements
34
+ if statement.effect != "Allow":
35
+ return issues
36
+
37
+ actions = statement.get_actions()
38
+
39
+ # Check for wildcard action (Action: "*")
40
+ if "*" in actions:
41
+ message = config.config.get("message", "Statement allows all actions (*)")
42
+ suggestion_text = config.config.get(
43
+ "suggestion", "Replace wildcard with specific actions needed for your use case"
44
+ )
45
+ example = config.config.get("example", "")
46
+
47
+ # Combine suggestion + example
48
+ suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
49
+
50
+ issues.append(
51
+ ValidationIssue(
52
+ severity=self.get_severity(config),
53
+ statement_sid=statement.sid,
54
+ statement_index=statement_idx,
55
+ issue_type="overly_permissive",
56
+ message=message,
57
+ suggestion=suggestion,
58
+ line_number=statement.line_number,
59
+ )
60
+ )
61
+
62
+ return issues
@@ -0,0 +1,131 @@
1
+ """Wildcard resource check - detects Resource: '*' in IAM policies."""
2
+
3
+ from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
4
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
5
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
6
+ from iam_validator.core.models import Statement, ValidationIssue
7
+
8
+
9
+ class WildcardResourceCheck(PolicyCheck):
10
+ """Checks for wildcard resources (Resource: '*') which grant access to all resources."""
11
+
12
+ @property
13
+ def check_id(self) -> str:
14
+ return "wildcard_resource"
15
+
16
+ @property
17
+ def description(self) -> str:
18
+ return "Checks for wildcard resources (*)"
19
+
20
+ @property
21
+ def default_severity(self) -> str:
22
+ return "medium"
23
+
24
+ async def execute(
25
+ self,
26
+ statement: Statement,
27
+ statement_idx: int,
28
+ fetcher: AWSServiceFetcher,
29
+ config: CheckConfig,
30
+ ) -> list[ValidationIssue]:
31
+ """Execute wildcard resource check on a statement."""
32
+ issues = []
33
+
34
+ # Only check Allow statements
35
+ if statement.effect != "Allow":
36
+ return issues
37
+
38
+ actions = statement.get_actions()
39
+ resources = statement.get_resources()
40
+
41
+ # Check for wildcard resource (Resource: "*")
42
+ if "*" in resources:
43
+ # Check if all actions are in the allowed_wildcards list
44
+ # allowed_wildcards works by expanding wildcard patterns (like "ec2:Describe*")
45
+ # to all matching AWS actions using the AWS API, then checking if the policy's
46
+ # actions are in that expanded list. This ensures only validated AWS actions
47
+ # are allowed with Resource: "*".
48
+ allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(config, fetcher)
49
+
50
+ # Check if ALL actions (excluding full wildcard "*") are in the expanded list
51
+ non_wildcard_actions = [a for a in actions if a != "*"]
52
+
53
+ if allowed_wildcards_expanded and non_wildcard_actions:
54
+ # Check if all actions are in the expanded allowed list (exact match)
55
+ all_actions_allowed = all(
56
+ action in allowed_wildcards_expanded for action in non_wildcard_actions
57
+ )
58
+
59
+ # If all actions are in the expanded list, skip the wildcard resource warning
60
+ if all_actions_allowed:
61
+ # All actions are safe, Resource: "*" is acceptable
62
+ return issues
63
+
64
+ # Flag the issue if actions are not all allowed or no allowed_wildcards configured
65
+ message = config.config.get("message", "Statement applies to all resources (*)")
66
+ suggestion_text = config.config.get(
67
+ "suggestion", "Replace wildcard with specific resource ARNs"
68
+ )
69
+ example = config.config.get("example", "")
70
+
71
+ # Combine suggestion + example
72
+ suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
73
+
74
+ issues.append(
75
+ ValidationIssue(
76
+ severity=self.get_severity(config),
77
+ statement_sid=statement.sid,
78
+ statement_index=statement_idx,
79
+ issue_type="overly_permissive",
80
+ message=message,
81
+ suggestion=suggestion,
82
+ line_number=statement.line_number,
83
+ )
84
+ )
85
+
86
+ return issues
87
+
88
+ async def _get_expanded_allowed_wildcards(
89
+ self, config: CheckConfig, fetcher: AWSServiceFetcher
90
+ ) -> frozenset[str]:
91
+ """Get and expand allowed_wildcards configuration.
92
+
93
+ This method retrieves wildcard patterns from the allowed_wildcards config
94
+ and expands them using the AWS API to get all matching actual AWS actions.
95
+
96
+ How it works:
97
+ 1. Retrieves patterns from config (e.g., ["ec2:Describe*", "s3:List*"])
98
+ 2. Expands each pattern using AWS API:
99
+ - "ec2:Describe*" → ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
100
+ - "s3:List*" → ["s3:ListBucket", "s3:ListObjects", ...]
101
+ 3. Returns a set of all expanded actions
102
+
103
+ This allows you to:
104
+ - Specify patterns like "ec2:Describe*" in config
105
+ - Have the validator allow specific actions like "ec2:DescribeInstances" with Resource: "*"
106
+ - Ensure only real AWS actions (validated via API) are allowed
107
+
108
+ Example:
109
+ Config: allowed_wildcards: ["ec2:Describe*"]
110
+ Expands to: ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
111
+ Policy: "Action": ["ec2:DescribeInstances"], "Resource": "*"
112
+ Result: ✅ Allowed (ec2:DescribeInstances is in expanded list)
113
+
114
+ Args:
115
+ config: The check configuration
116
+ fetcher: AWS service fetcher for expanding wildcards via AWS API
117
+
118
+ Returns:
119
+ A frozenset of all expanded action names from the configured patterns
120
+ """
121
+ patterns_to_expand = config.config.get("allowed_wildcards", [])
122
+
123
+ # If no patterns configured, return empty set
124
+ if not patterns_to_expand or not isinstance(patterns_to_expand, list):
125
+ return frozenset()
126
+
127
+ # Expand the wildcard patterns using the AWS API
128
+ # This converts patterns like "ec2:Describe*" to actual AWS actions
129
+ expanded_actions = await expand_wildcard_actions(patterns_to_expand, fetcher)
130
+
131
+ return frozenset(expanded_actions)
@@ -9,7 +9,7 @@ from rich.table import Table
9
9
 
10
10
  from iam_validator.commands.base import Command
11
11
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
12
- from iam_validator.core.config_loader import ConfigLoader
12
+ from iam_validator.core.config.config_loader import ConfigLoader
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
  console = Console()
@@ -9,20 +9,15 @@ from pathlib import Path
9
9
 
10
10
  import httpx
11
11
  from rich.console import Console
12
- from rich.progress import (
13
- BarColumn,
14
- Progress,
15
- TaskID,
16
- TextColumn,
17
- TimeRemainingColumn,
18
- )
12
+ from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeRemainingColumn
19
13
 
20
14
  from iam_validator.commands.base import Command
15
+ from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
21
16
 
22
17
  logger = logging.getLogger(__name__)
23
18
  console = Console()
24
19
 
25
- BASE_URL = "https://servicereference.us-east-1.amazonaws.com/"
20
+ BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
26
21
  DEFAULT_OUTPUT_DIR = Path("aws_services")
27
22
 
28
23
 
@@ -37,6 +37,10 @@ Examples:
37
37
  # Validate multiple paths (files and directories)
38
38
  iam-validator validate --path policy1.json --path ./policies/ --path ./more-policies/
39
39
 
40
+ # Read policy from stdin
41
+ cat policy.json | iam-validator validate --stdin
42
+ echo '{"Version":"2012-10-17","Statement":[...]}' | iam-validator validate --stdin
43
+
40
44
  # Use custom checks from a directory
41
45
  iam-validator validate --path ./policies/ --custom-checks-dir ./my-checks
42
46
 
@@ -61,15 +65,23 @@ Examples:
61
65
 
62
66
  def add_arguments(self, parser: argparse.ArgumentParser) -> None:
63
67
  """Add validate command arguments."""
64
- parser.add_argument(
68
+ # Create mutually exclusive group for input sources
69
+ input_group = parser.add_mutually_exclusive_group(required=True)
70
+
71
+ input_group.add_argument(
65
72
  "--path",
66
73
  "-p",
67
- required=True,
68
74
  action="append",
69
75
  dest="paths",
70
76
  help="Path to IAM policy file or directory (can be specified multiple times)",
71
77
  )
72
78
 
79
+ input_group.add_argument(
80
+ "--stdin",
81
+ action="store_true",
82
+ help="Read policy from stdin (JSON format)",
83
+ )
84
+
73
85
  parser.add_argument(
74
86
  "--format",
75
87
  "-f",
@@ -166,6 +178,18 @@ Examples:
166
178
  help="Number of policies to process per batch (default: 10, only with --stream)",
167
179
  )
168
180
 
181
+ parser.add_argument(
182
+ "--no-summary",
183
+ action="store_true",
184
+ help="Hide Executive Summary section in enhanced format output",
185
+ )
186
+
187
+ parser.add_argument(
188
+ "--no-severity-breakdown",
189
+ action="store_true",
190
+ help="Hide Issue Severity Breakdown section in enhanced format output",
191
+ )
192
+
169
193
  async def execute(self, args: argparse.Namespace) -> int:
170
194
  """Execute the validate command."""
171
195
  # Check if streaming mode is enabled
@@ -186,15 +210,36 @@ Examples:
186
210
 
187
211
  async def _execute_batch(self, args: argparse.Namespace) -> int:
188
212
  """Execute validation by loading all policies at once (original behavior)."""
189
- # Load policies from all specified paths
213
+ # Load policies from all specified paths or stdin
190
214
  loader = PolicyLoader()
191
- policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
192
215
 
193
- if not policies:
194
- logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
195
- return 1
216
+ if args.stdin:
217
+ # Read from stdin
218
+ import json
219
+ import sys
220
+
221
+ stdin_content = sys.stdin.read()
222
+ if not stdin_content.strip():
223
+ logging.error("No policy data provided on stdin")
224
+ return 1
225
+
226
+ try:
227
+ policy_data = json.loads(stdin_content)
228
+ # Create a synthetic policy entry
229
+ policies = [("stdin", policy_data)]
230
+ logging.info("Loaded policy from stdin")
231
+ except json.JSONDecodeError as e:
232
+ logging.error(f"Invalid JSON from stdin: {e}")
233
+ return 1
234
+ else:
235
+ # Load from paths
236
+ policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
237
+
238
+ if not policies:
239
+ logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
240
+ return 1
196
241
 
197
- logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
242
+ logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
198
243
 
199
244
  # Validate policies
200
245
  use_registry = not getattr(args, "no_registry", False)
@@ -229,7 +274,14 @@ Examples:
229
274
  print(generator.generate_github_comment(report))
230
275
  else:
231
276
  # Use formatter registry for other formats (enhanced, html, csv, sarif)
232
- output_content = generator.format_report(report, args.format)
277
+ # Pass options for enhanced format
278
+ format_options = {}
279
+ if args.format == "enhanced":
280
+ format_options["show_summary"] = not getattr(args, "no_summary", False)
281
+ format_options["show_severity_breakdown"] = not getattr(
282
+ args, "no_severity_breakdown", False
283
+ )
284
+ output_content = generator.format_report(report, args.format, **format_options)
233
285
  if args.output:
234
286
  with open(args.output, "w", encoding="utf-8") as f:
235
287
  f.write(output_content)
@@ -239,7 +291,7 @@ Examples:
239
291
 
240
292
  # Post to GitHub if configured
241
293
  if args.github_comment or getattr(args, "github_review", False):
242
- from iam_validator.core.config_loader import ConfigLoader
294
+ from iam_validator.core.config.config_loader import ConfigLoader
243
295
  from iam_validator.core.pr_commenter import PRCommenter
244
296
 
245
297
  # Load config to get fail_on_severity setting
@@ -348,7 +400,14 @@ Examples:
348
400
  print(generator.generate_github_comment(report))
349
401
  else:
350
402
  # Use formatter registry for other formats (enhanced, html, csv, sarif)
351
- output_content = generator.format_report(report, args.format)
403
+ # Pass options for enhanced format
404
+ format_options = {}
405
+ if args.format == "enhanced":
406
+ format_options["show_summary"] = not getattr(args, "no_summary", False)
407
+ format_options["show_severity_breakdown"] = not getattr(
408
+ args, "no_severity_breakdown", False
409
+ )
410
+ output_content = generator.format_report(report, args.format, **format_options)
352
411
  if args.output:
353
412
  with open(args.output, "w", encoding="utf-8") as f:
354
413
  f.write(output_content)
@@ -358,7 +417,7 @@ Examples:
358
417
 
359
418
  # Post summary comment to GitHub (if requested and not already posted per-file reviews)
360
419
  if args.github_comment:
361
- from iam_validator.core.config_loader import ConfigLoader
420
+ from iam_validator.core.config.config_loader import ConfigLoader
362
421
  from iam_validator.core.pr_commenter import PRCommenter
363
422
 
364
423
  # Load config to get fail_on_severity setting
@@ -410,7 +469,7 @@ Examples:
410
469
  This provides progressive feedback in PRs as files are processed.
411
470
  """
412
471
  try:
413
- from iam_validator.core.config_loader import ConfigLoader
472
+ from iam_validator.core.config.config_loader import ConfigLoader
414
473
  from iam_validator.core.pr_commenter import PRCommenter
415
474
 
416
475
  async with GitHubIntegration() as github:
@@ -27,64 +27,18 @@ import os
27
27
  import re
28
28
  import sys
29
29
  import time
30
- from collections import OrderedDict
31
30
  from pathlib import Path
32
31
  from typing import Any
33
32
 
34
33
  import httpx
35
34
 
35
+ from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
36
36
  from iam_validator.core.models import ServiceDetail, ServiceInfo
37
+ from iam_validator.utils.cache import LRUCache
37
38
 
38
39
  logger = logging.getLogger(__name__)
39
40
 
40
41
 
41
- class LRUCache:
42
- """Thread-safe LRU cache implementation with TTL support."""
43
-
44
- def __init__(self, maxsize: int = 128, ttl: int = 3600):
45
- """Initialize LRU cache.
46
-
47
- Args:
48
- maxsize: Maximum number of items in cache
49
- ttl: Time to live in seconds (default: 1 hour)
50
- """
51
- self.cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
52
- self.maxsize = maxsize
53
- self.ttl = ttl
54
- self._lock = asyncio.Lock()
55
-
56
- async def get(self, key: str) -> Any | None:
57
- """Get item from cache if not expired."""
58
- async with self._lock:
59
- if key in self.cache:
60
- value, timestamp = self.cache[key]
61
- if time.time() - timestamp < self.ttl:
62
- # Move to end (most recently used)
63
- self.cache.move_to_end(key)
64
- return value
65
- else:
66
- # Expired, remove it
67
- del self.cache[key]
68
- return None
69
-
70
- async def set(self, key: str, value: Any) -> None:
71
- """Set item in cache with current timestamp."""
72
- async with self._lock:
73
- if key in self.cache:
74
- # Move to end if exists
75
- self.cache.move_to_end(key)
76
- elif len(self.cache) >= self.maxsize:
77
- # Remove least recently used
78
- self.cache.popitem(last=False)
79
-
80
- self.cache[key] = (value, time.time())
81
-
82
- async def clear(self) -> None:
83
- """Clear the cache."""
84
- async with self._lock:
85
- self.cache.clear()
86
-
87
-
88
42
  class CompiledPatterns:
89
43
  """Pre-compiled regex patterns for validation."""
90
44
 
@@ -119,9 +73,71 @@ class CompiledPatterns:
119
73
 
120
74
 
121
75
  class AWSServiceFetcher:
122
- """Fetches AWS service information from the AWS service reference API with enhanced performance features."""
123
-
124
- BASE_URL = "https://servicereference.us-east-1.amazonaws.com/"
76
+ """Fetches AWS service information from the AWS service reference API with enhanced performance features.
77
+
78
+ This class provides a comprehensive interface for retrieving AWS service metadata,
79
+ including actions, resources, and condition keys. It includes multiple layers of
80
+ caching and optimization for high-performance policy validation.
81
+
82
+ Features:
83
+ - Multi-layer caching (memory LRU + disk with TTL)
84
+ - Service pre-fetching for common AWS services
85
+ - Request batching and coalescing
86
+ - Offline mode support with local AWS service files
87
+ - HTTP/2 connection pooling
88
+ - Automatic retry with exponential backoff
89
+
90
+ Example:
91
+ >>> async with AWSServiceFetcher() as fetcher:
92
+ ... # Fetch service list
93
+ ... services = await fetcher.fetch_services()
94
+ ...
95
+ ... # Fetch specific service details
96
+ ... s3_service = await fetcher.fetch_service_by_name("s3")
97
+ ...
98
+ ... # Validate actions
99
+ ... is_valid = await fetcher.validate_action("s3:GetObject", s3_service)
100
+
101
+ Method Organization:
102
+ Lifecycle Management:
103
+ - __init__: Initialize fetcher with configuration
104
+ - __aenter__, __aexit__: Context manager support
105
+
106
+ Caching (Private):
107
+ - _get_cache_directory: Determine cache location
108
+ - _get_cache_path: Generate cache file path
109
+ - _read_from_cache: Read from disk cache
110
+ - _write_to_cache: Write to disk cache
111
+ - clear_caches: Clear all caches
112
+
113
+ HTTP Operations (Private):
114
+ - _make_request: Core HTTP request handler
115
+ - _make_request_with_batching: Request coalescing
116
+ - _prefetch_common_services: Pre-load common services
117
+
118
+ File I/O (Private):
119
+ - _load_services_from_file: Load service list from local file
120
+ - _load_service_from_file: Load service details from local file
121
+
122
+ Public API - Fetching:
123
+ - fetch_services: Get list of all AWS services
124
+ - fetch_service_by_name: Get details for one service
125
+ - fetch_multiple_services: Batch fetch multiple services
126
+
127
+ Public API - Validation:
128
+ - validate_action: Check if action exists in service
129
+ - validate_arn: Validate ARN format
130
+ - validate_condition_key: Check condition key validity
131
+
132
+ Public API - Parsing:
133
+ - parse_action: Split action into service and name
134
+ - _match_wildcard_action: Match wildcard patterns
135
+
136
+ Utilities:
137
+ - get_stats: Get cache statistics
138
+ """
139
+
140
+ BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
125
141
 
126
142
  # Common AWS services to pre-fetch
127
143
  # All other services will be fetched on-demand (lazy loading if found in policies)
@@ -796,11 +812,26 @@ class AWSServiceFetcher:
796
812
  return True, None
797
813
 
798
814
  async def validate_condition_key(
799
- self, action: str, condition_key: str
800
- ) -> tuple[bool, str | None]:
801
- """Validate condition key with optimized caching."""
815
+ self, action: str, condition_key: str, resources: list[str] | None = None
816
+ ) -> tuple[bool, str | None, str | None]:
817
+ """
818
+ Validate condition key against action and optionally resource types.
819
+
820
+ Args:
821
+ action: IAM action (e.g., "s3:GetObject")
822
+ condition_key: Condition key to validate (e.g., "s3:prefix")
823
+ resources: Optional list of resource ARNs to validate against
824
+
825
+ Returns:
826
+ Tuple of (is_valid, error_message, warning_message)
827
+ - is_valid: True if key is valid (even with warning)
828
+ - error_message: Error message if invalid (is_valid=False)
829
+ - warning_message: Warning message if valid but not recommended
830
+ """
802
831
  try:
803
- from iam_validator.core.aws_global_conditions import get_global_conditions
832
+ from iam_validator.core.config.aws_global_conditions import (
833
+ get_global_conditions,
834
+ )
804
835
 
805
836
  service_prefix, action_name = self.parse_action(action)
806
837
 
@@ -814,6 +845,7 @@ class AWSServiceFetcher:
814
845
  return (
815
846
  False,
816
847
  f"Invalid AWS global condition key: '{condition_key}'.",
848
+ None,
817
849
  )
818
850
 
819
851
  # Fetch service detail (cached)
@@ -821,7 +853,7 @@ class AWSServiceFetcher:
821
853
 
822
854
  # Check service-specific condition keys
823
855
  if condition_key in service_detail.condition_keys:
824
- return True, None
856
+ return True, None, None
825
857
 
826
858
  # Check action-specific condition keys
827
859
  if action_name in service_detail.actions:
@@ -830,29 +862,47 @@ class AWSServiceFetcher:
830
862
  action_detail.action_condition_keys
831
863
  and condition_key in action_detail.action_condition_keys
832
864
  ):
833
- return True, None
865
+ return True, None, None
866
+
867
+ # Check resource-specific condition keys
868
+ # Get resource types required by this action
869
+ if resources and action_detail.resources:
870
+ for res_req in action_detail.resources:
871
+ resource_name = res_req.get("Name", "")
872
+ if not resource_name:
873
+ continue
874
+
875
+ # Look up resource type definition
876
+ resource_type = service_detail.resources.get(resource_name)
877
+ if resource_type and resource_type.condition_keys:
878
+ if condition_key in resource_type.condition_keys:
879
+ return True, None, None
834
880
 
835
881
  # If it's a global key but the action has specific condition keys defined,
836
- # check if the global key is explicitly listed in the action's supported keys
882
+ # AWS allows it but the key may not be available in every request context
837
883
  if is_global_key and action_detail.action_condition_keys is not None:
838
- return (
839
- False,
840
- f"Condition key '{condition_key}' is not supported by action '{action}'. "
841
- f"This action has a specific set of supported condition keys.",
884
+ warning_msg = (
885
+ f"Global condition key '{condition_key}' is used with action '{action}'. "
886
+ f"While global condition keys can be used across all AWS services, "
887
+ f"the key may not be available in every request context. "
888
+ f"Verify that '{condition_key}' is available for this specific action's request context. "
889
+ f"Consider using '*IfExists' operators (e.g., StringEqualsIfExists) if the key might be missing."
842
890
  )
891
+ return True, None, warning_msg
843
892
 
844
893
  # If it's a global key and action doesn't define specific keys, allow it
845
894
  if is_global_key:
846
- return True, None
895
+ return True, None, None
847
896
 
848
897
  return (
849
898
  False,
850
899
  f"Condition key '{condition_key}' is not valid for action '{action}'",
900
+ None,
851
901
  )
852
902
 
853
903
  except Exception as e:
854
904
  logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
855
- return False, f"Failed to validate condition key: {str(e)}"
905
+ return False, f"Failed to validate condition key: {str(e)}", None
856
906
 
857
907
  async def clear_caches(self) -> None:
858
908
  """Clear all caches (memory and disk)."""