iam-policy-validator 1.10.1__py3-none-any.whl → 1.10.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iam-policy-validator
3
- Version: 1.10.1
3
+ Version: 1.10.3
4
4
  Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
5
5
  Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
6
6
  Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
@@ -1,6 +1,6 @@
1
1
  iam_validator/__init__.py,sha256=xHdUASOxFHwEXfT_GSr_KrkLlnxZ-pAAr1wW1PwAGko,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=SRRdMee5oIovcL25-xNSjSxoHM9bzGQhwmx_U4YhnXQ,362
3
+ iam_validator/__version__.py,sha256=UMITyOhvLRTs5TZWk4U4b0IKHWjVN05qKIVWOKq-Gx8,374
4
4
  iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
5
5
  iam_validator/checks/action_condition_enforcement.py,sha256=0dCH_xX-Xc0uLxtNeRjrpNjWYbdWQRzO1XNcLTSn6sI,51698
6
6
  iam_validator/checks/action_resource_matching.py,sha256=WiGJmCIJfx5yituMjZxpKmk-99N6nK20ueN02ddy9oM,19296
@@ -20,7 +20,7 @@ iam_validator/checks/set_operator_validation.py,sha256=FyxZ7qWlp9-ABzZaRRkxRP_Hw
20
20
  iam_validator/checks/sid_uniqueness.py,sha256=vfpk88b9G9OApxtrotABI2mPXvGd_C_X4gJKeqIURlk,5968
21
21
  iam_validator/checks/trust_policy_validation.py,sha256=a8Sm2xu3gFOHLd7rXDl-ibqiLEmg5c-dyWv1lK2i6HA,17816
22
22
  iam_validator/checks/wildcard_action.py,sha256=CyURgURDt2fQT2468LK813RupQ3WWvpmvLVLjUZf9QQ,1960
23
- iam_validator/checks/wildcard_resource.py,sha256=AidyyKMQL3PxLI6Zd-iFiiI6BnvSle4ATLwDXUmV3jQ,5404
23
+ iam_validator/checks/wildcard_resource.py,sha256=lRNZN7f3ZQrvnbGdVDCefUQF8lESIMoXVfhIgpln3mM,6679
24
24
  iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
25
25
  iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
26
26
  iam_validator/checks/utils/sensitive_action_matcher.py,sha256=qDXcJa_2sCJu9pBbjDlI7x5lPtLRc6jQCpKPMheCOJQ,11215
@@ -50,18 +50,18 @@ iam_validator/core/report.py,sha256=kzSeWnT1LqWZVA5pqKKz-maVowXVj0djdoShfRhhpz4,
50
50
  iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
51
51
  iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
52
52
  iam_validator/core/aws_service/client.py,sha256=Zv7rIpEFdUCDXKGp3migPDkj8L5eZltgrGe64M2t2Ko,7336
53
- iam_validator/core/aws_service/fetcher.py,sha256=X4iI6fiLj4l9f3W6_J0E58lSP26UsBhE9gu2pzmx7Bw,22641
53
+ iam_validator/core/aws_service/fetcher.py,sha256=s_o8h6ua9gPdiV9-ElNs7YY0HlyoP0Ewtl71hTrhsZA,23340
54
54
  iam_validator/core/aws_service/parsers.py,sha256=gJzR7HCD8ItCWCCbguTQIZpPEdj2rdMwC7LPhu7ve14,5174
55
55
  iam_validator/core/aws_service/patterns.py,sha256=gGc55Tn-EJ3cmcWtmYAZROUajKYz7DaMchYWGEhHpC0,1726
56
56
  iam_validator/core/aws_service/storage.py,sha256=PrfKdvF60IL7E_8xYs_XwFoAJPRcVYw57FVLHCoqwVk,10429
57
- iam_validator/core/aws_service/validators.py,sha256=AY0BjydskXoesEzUShH4gZKp6gtSX7s1rCLP_iOZQMc,16493
57
+ iam_validator/core/aws_service/validators.py,sha256=L9XRJdGmR-vZ1r0bj5SCznULyKEY_G1OAjij7-kOZPM,16463
58
58
  iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
59
59
  iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
60
60
  iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
61
61
  iam_validator/core/config/category_suggestions.py,sha256=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
62
62
  iam_validator/core/config/condition_requirements.py,sha256=qauIP73HFnOw1dchUeFpg1x7Y7QWkILo3GfxV_dxdQo,7696
63
63
  iam_validator/core/config/config_loader.py,sha256=qKD8aR8YAswaFf68pnYJLFNwKznvcc6lNxSQWU3i6SY,17713
64
- iam_validator/core/config/defaults.py,sha256=qpFP534dgCQ-vjCdhkK7ZslDoTm9Ftgy20qmYZsSYUI,28637
64
+ iam_validator/core/config/defaults.py,sha256=bI0MC3x7q6TVDxJxFs0MeyVRMmANGyfOOdvOWQQAuKc,30103
65
65
  iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
66
66
  iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
67
67
  iam_validator/core/config/service_principals.py,sha256=8pys5H_yycVJ9KTyimAKFYBg83Aol2Iri53wiHjtnEM,3959
@@ -83,14 +83,14 @@ iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urch
83
83
  iam_validator/sdk/context.py,sha256=FvAEyUa_s7tHWoSdgjSkzHf1CLlYpAEmLZANxs2IJ4A,6826
84
84
  iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
85
85
  iam_validator/sdk/helpers.py,sha256=sjfK0na_Fo7O8GhEVhl44rVHqOdw6nAKkBL4FVL-QdU,5697
86
- iam_validator/sdk/policy_utils.py,sha256=Fh-QElhmPypzSJuF9rcrY7y46Gz3hQu3-yN5b1_mSHY,13579
86
+ iam_validator/sdk/policy_utils.py,sha256=bGdJ1X1aC72dVXXpAnAwyBpAiiX-qXvblpetY5BsjKU,13658
87
87
  iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
88
88
  iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYuCs,1021
89
89
  iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
90
90
  iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
91
91
  iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
92
- iam_policy_validator-1.10.1.dist-info/METADATA,sha256=lIMmE1Y6TX34sh3stfe9J2_q5ATb9fYZqVqOupYNcL8,19070
93
- iam_policy_validator-1.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- iam_policy_validator-1.10.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
95
- iam_policy_validator-1.10.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
96
- iam_policy_validator-1.10.1.dist-info/RECORD,,
92
+ iam_policy_validator-1.10.3.dist-info/METADATA,sha256=TNwd0AdEvphwtPx0E8AoHsNNyYzTcl6QUDT6dRqqyEE,19070
93
+ iam_policy_validator-1.10.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ iam_policy_validator-1.10.3.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
95
+ iam_policy_validator-1.10.3.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
96
+ iam_policy_validator-1.10.3.dist-info/RECORD,,
@@ -3,7 +3,7 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.10.1"
6
+ __version__ = "1.10.3"
7
7
  # Parse version, handling pre-release suffixes like -rc, -alpha, -beta
8
- _version_base = __version__.split("-")[0] # Remove pre-release suffix if present
8
+ _version_base = __version__.split("-", maxsplit=1)[0] # Remove pre-release suffix if present
9
9
  __version_info__ = tuple(int(part) for part in _version_base.split("."))
@@ -39,22 +39,44 @@ class WildcardResourceCheck(PolicyCheck):
39
39
  # to all matching AWS actions using the AWS API, then checking if the policy's
40
40
  # actions are in that expanded list. This ensures only validated AWS actions
41
41
  # are allowed with Resource: "*".
42
+ allowed_wildcards_config = config.config.get("allowed_wildcards", [])
42
43
  allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(config, fetcher)
43
44
 
44
45
  # Check if ALL actions (excluding full wildcard "*") are in the expanded list
45
46
  non_wildcard_actions = [a for a in actions if a != "*"]
46
47
 
47
- if allowed_wildcards_expanded and non_wildcard_actions:
48
- # Check if all actions are in the expanded allowed list (exact match)
49
- all_actions_allowed = all(
50
- action in allowed_wildcards_expanded for action in non_wildcard_actions
48
+ if (allowed_wildcards_config or allowed_wildcards_expanded) and non_wildcard_actions:
49
+ # Strategy 1: Check literal pattern match (fast path)
50
+ # If policy action matches config pattern literally, allow it
51
+ # Example: Policy has "iam:Get*", config has "iam:Get*" -> match
52
+ all_actions_allowed_literal = all(
53
+ action in allowed_wildcards_config for action in non_wildcard_actions
51
54
  )
52
55
 
53
- # If all actions are in the expanded list, skip the wildcard resource warning
54
- if all_actions_allowed:
55
- # All actions are safe, Resource: "*" is acceptable
56
+ if all_actions_allowed_literal:
57
+ # All actions match literally, Resource: "*" is acceptable
56
58
  return issues
57
59
 
60
+ # Strategy 2: Check expanded pattern match (comprehensive path)
61
+ # Expand both policy actions and config patterns, then compare
62
+ # Example: Policy has "iam:Get*" -> ["iam:GetUser", ...],
63
+ # config has "iam:Get*" -> ["iam:GetUser", ...] -> all match
64
+ if allowed_wildcards_expanded:
65
+ expanded_statement_actions = await expand_wildcard_actions(
66
+ non_wildcard_actions, fetcher
67
+ )
68
+
69
+ # Check if all expanded actions are in the expanded allowed list (exact match)
70
+ all_actions_allowed_expanded = all(
71
+ action in allowed_wildcards_expanded
72
+ for action in expanded_statement_actions
73
+ )
74
+
75
+ # If all actions are in the expanded list, skip the wildcard resource warning
76
+ if all_actions_allowed_expanded:
77
+ # All actions are safe, Resource: "*" is acceptable
78
+ return issues
79
+
58
80
  # Flag the issue if actions are not all allowed or no allowed_wildcards configured
59
81
  message = config.config.get(
60
82
  "message", 'Statement applies to all resources `"*"` (wildcard resource).'
@@ -233,8 +233,16 @@ class AWSServiceFetcher:
233
233
  await self._cache.set(services_cache_key, loaded_services)
234
234
  return loaded_services
235
235
 
236
- # Not in parsed cache, fetch the raw data from API
237
- data = await self._client.fetch(self.BASE_URL)
236
+ # Not in parsed cache, check disk cache then fetch from API
237
+ data = await self._cache.get(
238
+ f"raw:{self.BASE_URL}", url=self.BASE_URL, base_url=self.BASE_URL
239
+ )
240
+ if data is None:
241
+ data = await self._client.fetch(self.BASE_URL)
242
+ # Cache the raw data
243
+ await self._cache.set(
244
+ f"raw:{self.BASE_URL}", data, url=self.BASE_URL, base_url=self.BASE_URL
245
+ )
238
246
 
239
247
  if not isinstance(data, list):
240
248
  raise ValueError("Expected list of services from root endpoint")
@@ -247,7 +255,7 @@ class AWSServiceFetcher:
247
255
  if service and url:
248
256
  services.append(ServiceInfo(service=str(service), url=str(url)))
249
257
 
250
- # Cache the parsed services list (memory only - raw JSON already cached by client)
258
+ # Cache the parsed services list (memory only)
251
259
  await self._cache.set(services_cache_key, services)
252
260
 
253
261
  # Log only on first fetch (when parsed cache was empty)
@@ -312,13 +320,22 @@ class AWSServiceFetcher:
312
320
 
313
321
  for service in services:
314
322
  if service.service.lower() == service_name_lower:
315
- # Fetch service detail from API
316
- data = await self._client.fetch(service.url)
323
+ # Check disk cache first, then fetch from API
324
+ data = await self._cache.get(
325
+ f"raw:{service.url}", url=service.url, base_url=self.BASE_URL
326
+ )
327
+ if data is None:
328
+ # Fetch service detail from API
329
+ data = await self._client.fetch(service.url)
330
+ # Cache the raw data
331
+ await self._cache.set(
332
+ f"raw:{service.url}", data, url=service.url, base_url=self.BASE_URL
333
+ )
317
334
 
318
335
  # Validate and parse
319
336
  service_detail = ServiceDetail.model_validate(data)
320
337
 
321
- # Cache with service name as key (memory only - raw JSON already cached by client)
338
+ # Cache with service name as key (memory only)
322
339
  await self._cache.set(cache_key, service_detail)
323
340
 
324
341
  return service_detail
@@ -550,7 +567,7 @@ class AWSServiceFetcher:
550
567
  if action_pattern in ("*", "*:*"):
551
568
  return ["*"]
552
569
 
553
- service_prefix, action_name = self._parser.parse_action(action_pattern)
570
+ service_prefix, _ = self._parser.parse_action(action_pattern)
554
571
  service_detail = await self.fetch_service_by_name(service_prefix)
555
572
  available = list(service_detail.actions.keys())
556
573
  return self._parser.expand_wildcard_to_actions(action_pattern, available, service_prefix)
@@ -94,9 +94,7 @@ class ServiceValidator:
94
94
  if not allow_wildcards:
95
95
  return False, "Wildcard actions are not allowed", True
96
96
 
97
- has_matches, matched_actions = self._parser.match_wildcard_action(
98
- action_name, available_actions
99
- )
97
+ has_matches, _ = self._parser.match_wildcard_action(action_name, available_actions)
100
98
 
101
99
  if has_matches:
102
100
  # Wildcard is valid and matches at least one action
@@ -161,7 +159,7 @@ class ServiceValidator:
161
159
  get_global_conditions,
162
160
  )
163
161
 
164
- service_prefix, action_name = self._parser.parse_action(action)
162
+ _, action_name = self._parser.parse_action(action)
165
163
 
166
164
  # Check if it's a global condition key
167
165
  is_global_key = False
@@ -323,7 +321,7 @@ class ServiceValidator:
323
321
  >>> resources = validator.get_resources_for_action("s3:GetObject", service)
324
322
  """
325
323
  try:
326
- _, action_name = self._parser.parse_action(action)
324
+ _, action_name = self._parser.parse_action(action) # pylint: disable=unused-variable
327
325
 
328
326
  # Find the action (case-insensitive)
329
327
  action_detail = service_detail.actions.get(action_name)
@@ -344,13 +344,41 @@ DEFAULT_CONFIG = {
344
344
  # Check for wildcard resources (Resource: "*")
345
345
  # Flags statements that apply to all resources
346
346
  # Exception: Allowed if ALL actions are in allowed_wildcards list
347
+ #
348
+ # DUAL MATCHING STRATEGY:
349
+ # The check uses two complementary matching strategies for maximum flexibility:
350
+ #
351
+ # 1. LITERAL MATCH (Fast Path - no AWS API calls):
352
+ # Policy actions match config patterns exactly as strings
353
+ # Example: Policy "iam:Get*" matches config "iam:Get*" → PASS
354
+ #
355
+ # 2. EXPANDED MATCH (Comprehensive Path - uses AWS API):
356
+ # Both policy actions and config patterns expand to actual AWS actions
357
+ # Example: Policy "iam:GetUser" matches config "iam:Get*" (expanded) → PASS
358
+ #
359
+ # SUPPORTED SCENARIOS:
360
+ # Policy Action Config Pattern Match Type Result
361
+ # iam:Get* iam:Get* Literal ✅ Pass
362
+ # iam:GetUser iam:Get* Expanded ✅ Pass
363
+ # iam:Get*, iam:List* iam:Get*, iam:List* Literal ✅ Pass
364
+ # iam:Get*, iam:GetUser iam:Get* Literal ✅ Pass
365
+ # iam:Delete* iam:Get* None ❌ Fail
366
+ #
367
+ # PERFORMANCE TIP: Literal matching is faster (no AWS API expansion)
347
368
  "wildcard_resource": {
348
369
  "enabled": True,
349
370
  "severity": "medium", # Security issue
350
371
  "description": "Checks for wildcard resources (*)",
351
372
  # Allowed wildcard patterns for actions that can be used with Resource: "*"
373
+ # Supports BOTH literal matching and pattern expansion via AWS API
374
+ #
352
375
  # Default: 25 read-only patterns (Describe*, List*, Get*)
353
376
  # See: iam_validator/core/config/wildcards.py
377
+ #
378
+ # Examples:
379
+ # ["ec2:Describe*"] # Matches: ec2:Describe* (literal) OR ec2:DescribeInstances (expanded)
380
+ # ["iam:GetUser"] # Matches: iam:GetUser only
381
+ # ["s3:List*"] # Matches: s3:List* (literal) OR s3:ListBucket (expanded)
354
382
  "allowed_wildcards": list(DEFAULT_ALLOWED_WILDCARDS),
355
383
  "message": "Statement applies to all resources (*)",
356
384
  "suggestion": "Replace wildcard with specific resource ARNs",
@@ -199,7 +199,7 @@ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
199
199
  for stmt in policy.statement:
200
200
  if stmt.condition:
201
201
  # Condition format: {"StringEquals": {"aws:username": "johndoe"}}
202
- for operator, key_values in stmt.condition.items():
202
+ for _, key_values in stmt.condition.items():
203
203
  if isinstance(key_values, dict):
204
204
  condition_keys.update(key_values.keys())
205
205
 
@@ -225,7 +225,7 @@ def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statemen
225
225
  >>> for stmt in stmts:
226
226
  ... print(f"Statement {stmt.sid} allows s3:GetObject")
227
227
  """
228
- import fnmatch
228
+ import fnmatch # pylint: disable=import-outside-toplevel
229
229
 
230
230
  matching_statements = []
231
231
 
@@ -262,7 +262,7 @@ def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Stat
262
262
  >>> stmts = find_statements_with_resource(policy, "arn:aws:s3:::my-bucket/*")
263
263
  >>> print(f"Found {len(stmts)} statements with this resource")
264
264
  """
265
- import fnmatch
265
+ import fnmatch # pylint: disable=import-outside-toplevel
266
266
 
267
267
  matching_statements = []
268
268