iam-policy-validator 1.13.1__py3-none-any.whl → 1.14.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.
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/METADATA +1 -1
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/RECORD +45 -39
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +6 -0
- iam_validator/checks/action_resource_matching.py +12 -12
- iam_validator/checks/action_validation.py +1 -0
- iam_validator/checks/condition_key_validation.py +2 -0
- iam_validator/checks/condition_type_mismatch.py +3 -0
- iam_validator/checks/full_wildcard.py +1 -0
- iam_validator/checks/mfa_condition_check.py +2 -0
- iam_validator/checks/policy_structure.py +9 -0
- iam_validator/checks/policy_type_validation.py +11 -0
- iam_validator/checks/principal_validation.py +5 -0
- iam_validator/checks/resource_validation.py +4 -0
- iam_validator/checks/sensitive_action.py +1 -0
- iam_validator/checks/service_wildcard.py +6 -3
- iam_validator/checks/set_operator_validation.py +3 -0
- iam_validator/checks/sid_uniqueness.py +2 -0
- iam_validator/checks/trust_policy_validation.py +3 -0
- iam_validator/checks/utils/__init__.py +16 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/wildcard_action.py +1 -0
- iam_validator/checks/wildcard_resource.py +231 -4
- iam_validator/commands/analyze.py +19 -1
- iam_validator/commands/completion.py +6 -2
- iam_validator/commands/validate.py +231 -12
- iam_validator/core/aws_service/fetcher.py +21 -9
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/config_loader.py +199 -0
- iam_validator/core/config/defaults.py +25 -0
- iam_validator/core/constants.py +1 -0
- iam_validator/core/diff_parser.py +8 -4
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/sarif.py +370 -128
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/models.py +54 -4
- iam_validator/core/policy_loader.py +313 -4
- iam_validator/core/pr_commenter.py +223 -22
- iam_validator/core/report.py +22 -6
- iam_validator/integrations/github_integration.py +881 -123
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -262,6 +262,7 @@ class TrustPolicyValidationCheck(PolicyCheck):
|
|
|
262
262
|
example=self._get_example_for_action(
|
|
263
263
|
action, allowed_types[0] if allowed_types else "AWS"
|
|
264
264
|
),
|
|
265
|
+
field_name="principal",
|
|
265
266
|
)
|
|
266
267
|
)
|
|
267
268
|
|
|
@@ -312,6 +313,7 @@ class TrustPolicyValidationCheck(PolicyCheck):
|
|
|
312
313
|
f"Expected pattern: `{provider_pattern}`\n"
|
|
313
314
|
f"Found: `{principal}`",
|
|
314
315
|
example=self._get_provider_example(provider_type),
|
|
316
|
+
field_name="principal",
|
|
315
317
|
)
|
|
316
318
|
)
|
|
317
319
|
|
|
@@ -378,6 +380,7 @@ class TrustPolicyValidationCheck(PolicyCheck):
|
|
|
378
380
|
f"Missing: `{missing_list}`\n\n"
|
|
379
381
|
f"{rule.get('description', '')}",
|
|
380
382
|
example=self._get_condition_example(action, required_conditions[0]),
|
|
383
|
+
field_name="condition",
|
|
381
384
|
)
|
|
382
385
|
)
|
|
383
386
|
|
|
@@ -1 +1,17 @@
|
|
|
1
1
|
"""Utility modules for IAM policy checks."""
|
|
2
|
+
|
|
3
|
+
from iam_validator.checks.utils.action_parser import (
|
|
4
|
+
ParsedAction,
|
|
5
|
+
extract_service,
|
|
6
|
+
get_action_case_insensitive,
|
|
7
|
+
is_wildcard_action,
|
|
8
|
+
parse_action,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ParsedAction",
|
|
13
|
+
"extract_service",
|
|
14
|
+
"get_action_case_insensitive",
|
|
15
|
+
"is_wildcard_action",
|
|
16
|
+
"parse_action",
|
|
17
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Action parsing utility for IAM policy validation.
|
|
2
|
+
|
|
3
|
+
This module provides a consistent way to parse AWS IAM action names
|
|
4
|
+
(format: service:ActionName) across all validation checks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
# Type variable for generic dictionary value lookup
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ParsedAction:
|
|
16
|
+
"""Represents a parsed AWS IAM action.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
service: The AWS service prefix (e.g., "s3", "ec2", "iam")
|
|
20
|
+
action_name: The action name (e.g., "GetObject", "DescribeInstances")
|
|
21
|
+
has_wildcard: True if the service or action contains "*"
|
|
22
|
+
original: The original action string as provided
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
service: str
|
|
26
|
+
action_name: str
|
|
27
|
+
has_wildcard: bool
|
|
28
|
+
original: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_action(action: str) -> ParsedAction | None:
|
|
32
|
+
"""Parse an AWS IAM action string into its components.
|
|
33
|
+
|
|
34
|
+
AWS IAM actions follow the format "service:ActionName" where:
|
|
35
|
+
- service is the AWS service prefix (case-insensitive, typically lowercase)
|
|
36
|
+
- ActionName is the specific API action (PascalCase or camelCase)
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
action: The action string to parse (e.g., "s3:GetObject", "ec2:*")
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
ParsedAction if the action is valid, None if malformed.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> parse_action("s3:GetObject")
|
|
46
|
+
ParsedAction(service="s3", action_name="GetObject", has_wildcard=False, original="s3:GetObject")
|
|
47
|
+
|
|
48
|
+
>>> parse_action("ec2:Describe*")
|
|
49
|
+
ParsedAction(service="ec2", action_name="Describe*", has_wildcard=True, original="ec2:Describe*")
|
|
50
|
+
|
|
51
|
+
>>> parse_action("InvalidAction")
|
|
52
|
+
None
|
|
53
|
+
|
|
54
|
+
>>> parse_action("*")
|
|
55
|
+
None
|
|
56
|
+
"""
|
|
57
|
+
# Handle full wildcard - not a parseable service:action
|
|
58
|
+
if action == "*":
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Must contain exactly one colon separating service and action
|
|
62
|
+
if ":" not in action:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Split on first colon only (action names can theoretically contain colons)
|
|
66
|
+
parts = action.split(":", 1)
|
|
67
|
+
if len(parts) != 2:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
service, action_name = parts
|
|
71
|
+
|
|
72
|
+
# Both service and action name must be non-empty
|
|
73
|
+
if not service or not action_name:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
return ParsedAction(
|
|
77
|
+
service=service,
|
|
78
|
+
action_name=action_name,
|
|
79
|
+
has_wildcard="*" in service or "*" in action_name,
|
|
80
|
+
original=action,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_wildcard_action(action: str) -> bool:
|
|
85
|
+
"""Check if an action contains a wildcard.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
action: The action string to check
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if the action is "*" or contains "*" in service or action name
|
|
92
|
+
"""
|
|
93
|
+
if action == "*":
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
parsed = parse_action(action)
|
|
97
|
+
return parsed.has_wildcard if parsed else False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def extract_service(action: str) -> str | None:
|
|
101
|
+
"""Extract the service prefix from an action string.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
action: The action string (e.g., "s3:GetObject")
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The service prefix (e.g., "s3") or None if the action is malformed
|
|
108
|
+
"""
|
|
109
|
+
if action == "*":
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
parsed = parse_action(action)
|
|
113
|
+
return parsed.service if parsed else None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_action_case_insensitive(actions_dict: dict[str, T], action_name: str) -> T | None:
|
|
117
|
+
"""Look up an action in a dictionary using case-insensitive matching.
|
|
118
|
+
|
|
119
|
+
AWS action names are case-insensitive, but our service definitions may have
|
|
120
|
+
canonical casing. This function tries exact match first, then falls back
|
|
121
|
+
to case-insensitive lookup.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
actions_dict: Dictionary mapping action names to values (e.g., ActionDetail)
|
|
125
|
+
action_name: The action name to look up
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The value if found, None otherwise
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> actions = {"GetObject": detail, "PutObject": detail2}
|
|
132
|
+
>>> get_action_case_insensitive(actions, "GetObject") # Exact match
|
|
133
|
+
detail
|
|
134
|
+
>>> get_action_case_insensitive(actions, "getobject") # Case-insensitive
|
|
135
|
+
detail
|
|
136
|
+
>>> get_action_case_insensitive(actions, "Unknown")
|
|
137
|
+
None
|
|
138
|
+
"""
|
|
139
|
+
# Try exact match first (most common case)
|
|
140
|
+
if action_name in actions_dict:
|
|
141
|
+
return actions_dict[action_name]
|
|
142
|
+
|
|
143
|
+
# Fall back to case-insensitive lookup
|
|
144
|
+
action_name_lower = action_name.lower()
|
|
145
|
+
for key, value in actions_dict.items():
|
|
146
|
+
if key.lower() == action_name_lower:
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
return None
|
|
@@ -1,11 +1,73 @@
|
|
|
1
1
|
"""Wildcard resource check - detects Resource: '*' in IAM policies."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
3
5
|
from typing import ClassVar
|
|
4
6
|
|
|
7
|
+
from iam_validator.checks.utils.action_parser import get_action_case_insensitive, parse_action
|
|
5
8
|
from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
|
|
6
9
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
7
10
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
8
|
-
from iam_validator.core.models import Statement, ValidationIssue
|
|
11
|
+
from iam_validator.core.models import ActionDetail, ServiceDetail, Statement, ValidationIssue
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Module-level cache for action resource support lookups.
|
|
16
|
+
# Maps action name (e.g., "s3:GetObject") to whether it supports resource-level permissions.
|
|
17
|
+
# True = supports resources (should be flagged for wildcard)
|
|
18
|
+
# False = doesn't support resources (wildcard is appropriate)
|
|
19
|
+
# None = unknown (be conservative, assume it supports resources)
|
|
20
|
+
_action_resource_support_cache: dict[str, bool | None] = {}
|
|
21
|
+
|
|
22
|
+
# Module-level cache for action access level lookups.
|
|
23
|
+
# Maps action name (e.g., "s3:ListBuckets") to its access level.
|
|
24
|
+
# "list" = list-level action (safe with wildcards)
|
|
25
|
+
# Other values or None = unknown
|
|
26
|
+
_action_access_level_cache: dict[str, str | None] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_access_level(action_detail: ActionDetail) -> str:
|
|
30
|
+
"""Derive access level from action annotations.
|
|
31
|
+
|
|
32
|
+
AWS API provides Properties dict with boolean flags instead of AccessLevel string.
|
|
33
|
+
We derive the access level from these flags.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
action_detail: Action detail object with annotations
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Access level string: "permissions-management", "tagging", "write", "list", or "read"
|
|
40
|
+
"""
|
|
41
|
+
if not action_detail.annotations:
|
|
42
|
+
return "unknown"
|
|
43
|
+
|
|
44
|
+
props = action_detail.annotations.get("Properties", {})
|
|
45
|
+
if not props:
|
|
46
|
+
return "unknown"
|
|
47
|
+
|
|
48
|
+
# Check flags in priority order
|
|
49
|
+
if props.get("IsPermissionManagement"):
|
|
50
|
+
return "permissions-management"
|
|
51
|
+
if props.get("IsTaggingOnly"):
|
|
52
|
+
return "tagging"
|
|
53
|
+
if props.get("IsWrite"):
|
|
54
|
+
return "write"
|
|
55
|
+
if props.get("IsList"):
|
|
56
|
+
return "list"
|
|
57
|
+
|
|
58
|
+
# Default to read if none of the above
|
|
59
|
+
return "read"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def clear_resource_support_cache() -> None:
|
|
63
|
+
"""Clear the action resource support and access level caches.
|
|
64
|
+
|
|
65
|
+
This is primarily useful for testing to ensure a clean state between tests.
|
|
66
|
+
In production, the cache persists for the lifetime of the process, which is
|
|
67
|
+
beneficial as AWS action definitions don't change frequently.
|
|
68
|
+
"""
|
|
69
|
+
_action_resource_support_cache.clear()
|
|
70
|
+
_action_access_level_cache.clear()
|
|
9
71
|
|
|
10
72
|
|
|
11
73
|
class WildcardResourceCheck(PolicyCheck):
|
|
@@ -34,6 +96,18 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
34
96
|
|
|
35
97
|
# Check for wildcard resource (Resource: "*")
|
|
36
98
|
if "*" in resources:
|
|
99
|
+
# First, filter out actions that don't support resource-level permissions
|
|
100
|
+
# These actions legitimately require Resource: "*"
|
|
101
|
+
actions_requiring_specific_resources = await self._filter_actions_requiring_resources(
|
|
102
|
+
actions, fetcher
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# If all actions don't support resources, wildcard is appropriate - no issue
|
|
106
|
+
if not actions_requiring_specific_resources:
|
|
107
|
+
return issues
|
|
108
|
+
|
|
109
|
+
# Use filtered actions for the rest of the check
|
|
110
|
+
actions = actions_requiring_specific_resources
|
|
37
111
|
# Check if all actions are in the allowed_wildcards list
|
|
38
112
|
# allowed_wildcards works by expanding wildcard patterns (like "ec2:Describe*")
|
|
39
113
|
# to all matching AWS actions using the AWS API, then checking if the policy's
|
|
@@ -78,9 +152,25 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
78
152
|
return issues
|
|
79
153
|
|
|
80
154
|
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
81
|
-
message
|
|
82
|
-
|
|
83
|
-
|
|
155
|
+
# Build a helpful message showing which actions require specific resources
|
|
156
|
+
custom_message = config.config.get("message")
|
|
157
|
+
if custom_message:
|
|
158
|
+
message = custom_message
|
|
159
|
+
else:
|
|
160
|
+
# Build default message with action list
|
|
161
|
+
# Note: actions_requiring_specific_resources is guaranteed non-empty here
|
|
162
|
+
# because we return early above if it's empty
|
|
163
|
+
sorted_actions = sorted(actions_requiring_specific_resources)
|
|
164
|
+
if len(sorted_actions) <= 5:
|
|
165
|
+
action_list = ", ".join(f"`{a}`" for a in sorted_actions)
|
|
166
|
+
else:
|
|
167
|
+
action_list = ", ".join(f"`{a}`" for a in sorted_actions[:5])
|
|
168
|
+
action_list += f" (+{len(sorted_actions) - 5} more)"
|
|
169
|
+
message = (
|
|
170
|
+
f'Statement applies to all resources `"*"`. '
|
|
171
|
+
f"Actions that support resource-level permissions: {action_list}"
|
|
172
|
+
)
|
|
173
|
+
|
|
84
174
|
suggestion = config.config.get(
|
|
85
175
|
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
86
176
|
)
|
|
@@ -96,6 +186,7 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
96
186
|
suggestion=suggestion,
|
|
97
187
|
example=example if example else None,
|
|
98
188
|
line_number=statement.line_number,
|
|
189
|
+
field_name="resource",
|
|
99
190
|
)
|
|
100
191
|
)
|
|
101
192
|
|
|
@@ -145,3 +236,139 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
145
236
|
expanded_actions = await expand_wildcard_actions(patterns_to_expand, fetcher)
|
|
146
237
|
|
|
147
238
|
return frozenset(expanded_actions)
|
|
239
|
+
|
|
240
|
+
async def _filter_actions_requiring_resources(
|
|
241
|
+
self, actions: list[str], fetcher: AWSServiceFetcher
|
|
242
|
+
) -> list[str]:
|
|
243
|
+
"""Filter actions to only those that should be flagged for wildcard resources.
|
|
244
|
+
|
|
245
|
+
This method filters out actions that legitimately use Resource: "*":
|
|
246
|
+
1. Actions that don't support resource-level permissions (e.g., sts:GetCallerIdentity)
|
|
247
|
+
2. List-level actions (e.g., s3:ListBuckets) - these only enumerate resources
|
|
248
|
+
and are not dangerous with wildcards
|
|
249
|
+
|
|
250
|
+
Examples of actions filtered out:
|
|
251
|
+
- iam:ListUsers (list-level, must use Resource: "*")
|
|
252
|
+
- sts:GetCallerIdentity (must use Resource: "*")
|
|
253
|
+
- ec2:DescribeInstances (must use Resource: "*")
|
|
254
|
+
- s3:ListAllMyBuckets (list-level)
|
|
255
|
+
|
|
256
|
+
This method uses a module-level cache to avoid repeated lookups and
|
|
257
|
+
fetches all required services in parallel for better performance.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
actions: List of actions from the policy statement
|
|
261
|
+
fetcher: AWS service fetcher for looking up action definitions
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of actions that should be flagged for wildcard resource usage
|
|
265
|
+
"""
|
|
266
|
+
actions_requiring_resources = []
|
|
267
|
+
# Actions that need service lookup, grouped by service
|
|
268
|
+
service_actions: dict[str, list[tuple[str, str]]] = {} # service -> [(action, action_name)]
|
|
269
|
+
|
|
270
|
+
for action in actions:
|
|
271
|
+
# Full wildcard "*" - keep it (it's too broad to determine)
|
|
272
|
+
if action == "*":
|
|
273
|
+
actions_requiring_resources.append(action)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Parse action using the utility
|
|
277
|
+
parsed = parse_action(action)
|
|
278
|
+
if not parsed:
|
|
279
|
+
# Malformed action - keep it (be conservative)
|
|
280
|
+
actions_requiring_resources.append(action)
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Wildcard in service or action name - keep it (can't determine resource support)
|
|
284
|
+
if parsed.has_wildcard:
|
|
285
|
+
actions_requiring_resources.append(action)
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
service = parsed.service
|
|
289
|
+
action_name = parsed.action_name
|
|
290
|
+
|
|
291
|
+
# Check module-level caches first
|
|
292
|
+
if action in _action_resource_support_cache and action in _action_access_level_cache:
|
|
293
|
+
cached_resource_support = _action_resource_support_cache[action]
|
|
294
|
+
cached_access_level = _action_access_level_cache[action]
|
|
295
|
+
|
|
296
|
+
# Skip list-level actions - they're safe with wildcards
|
|
297
|
+
if cached_access_level == "list":
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if cached_resource_support is True or cached_resource_support is None:
|
|
301
|
+
# Supports resources or unknown - include it
|
|
302
|
+
actions_requiring_resources.append(action)
|
|
303
|
+
# If False, action doesn't support resources - skip it
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Group actions by service for parallel fetching
|
|
307
|
+
if service not in service_actions:
|
|
308
|
+
service_actions[service] = []
|
|
309
|
+
service_actions[service].append((action, action_name))
|
|
310
|
+
|
|
311
|
+
# If no services to look up, return early
|
|
312
|
+
if not service_actions:
|
|
313
|
+
return actions_requiring_resources
|
|
314
|
+
|
|
315
|
+
# Fetch all services in parallel
|
|
316
|
+
services = list(service_actions.keys())
|
|
317
|
+
results = await asyncio.gather(
|
|
318
|
+
*[fetcher.fetch_service_by_name(s) for s in services],
|
|
319
|
+
return_exceptions=True,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Build service cache from successful results
|
|
323
|
+
service_cache: dict[str, ServiceDetail | None] = {}
|
|
324
|
+
for service, result in zip(services, results):
|
|
325
|
+
if isinstance(result, BaseException):
|
|
326
|
+
logger.debug(f"Could not look up service {service}: {result}")
|
|
327
|
+
# Mark service as failed - will keep all its actions (conservative)
|
|
328
|
+
service_cache[service] = None
|
|
329
|
+
else:
|
|
330
|
+
# Result is ServiceDetail when not an exception
|
|
331
|
+
service_cache[service] = result
|
|
332
|
+
|
|
333
|
+
# Process actions using cached service data
|
|
334
|
+
for service, action_list in service_actions.items():
|
|
335
|
+
service_detail = service_cache.get(service)
|
|
336
|
+
|
|
337
|
+
if not service_detail:
|
|
338
|
+
# Unknown service - keep all its actions (be conservative)
|
|
339
|
+
for action, _ in action_list:
|
|
340
|
+
_action_resource_support_cache[action] = None # Cache as unknown
|
|
341
|
+
_action_access_level_cache[action] = None # Cache as unknown
|
|
342
|
+
actions_requiring_resources.append(action)
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
for action, action_name in action_list:
|
|
346
|
+
# Use case-insensitive lookup since AWS actions are case-insensitive
|
|
347
|
+
action_detail = get_action_case_insensitive(service_detail.actions, action_name)
|
|
348
|
+
if not action_detail:
|
|
349
|
+
# Unknown action - keep it (be conservative)
|
|
350
|
+
_action_resource_support_cache[action] = None # Cache as unknown
|
|
351
|
+
_action_access_level_cache[action] = None # Cache as unknown
|
|
352
|
+
actions_requiring_resources.append(action)
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# Get action's access level and cache it
|
|
356
|
+
access_level = _get_access_level(action_detail)
|
|
357
|
+
_action_access_level_cache[action] = access_level
|
|
358
|
+
|
|
359
|
+
# Skip list-level actions - they only enumerate resources and are safe with wildcards
|
|
360
|
+
if access_level == "list":
|
|
361
|
+
_action_resource_support_cache[action] = False # Mark as not needing resources
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Check if action supports resource-level permissions
|
|
365
|
+
# action_detail.resources is empty for actions that don't support resources
|
|
366
|
+
supports_resources = bool(action_detail.resources)
|
|
367
|
+
_action_resource_support_cache[action] = supports_resources # Cache result
|
|
368
|
+
|
|
369
|
+
if supports_resources:
|
|
370
|
+
# Action supports resources - should be flagged for wildcard
|
|
371
|
+
actions_requiring_resources.append(action)
|
|
372
|
+
# Else: action doesn't support resources, Resource: "*" is appropriate
|
|
373
|
+
|
|
374
|
+
return actions_requiring_resources
|
|
@@ -387,10 +387,28 @@ Examples:
|
|
|
387
387
|
|
|
388
388
|
# Post to GitHub if configured
|
|
389
389
|
if args.github_comment:
|
|
390
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
390
391
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
391
392
|
|
|
393
|
+
# Load config to get fail_on_severity, severity_labels, and ignore settings
|
|
394
|
+
config_path = getattr(args, "config", None)
|
|
395
|
+
config = ConfigLoader.load_config(config_path)
|
|
396
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
397
|
+
severity_labels = config.get_setting("severity_labels", {})
|
|
398
|
+
|
|
399
|
+
# Get ignore settings from config
|
|
400
|
+
ignore_settings = config.get_setting("ignore_settings", {})
|
|
401
|
+
enable_ignore = ignore_settings.get("enabled", True)
|
|
402
|
+
allowed_users = ignore_settings.get("allowed_users", [])
|
|
403
|
+
|
|
392
404
|
async with GitHubIntegration() as github:
|
|
393
|
-
commenter = PRCommenter(
|
|
405
|
+
commenter = PRCommenter(
|
|
406
|
+
github,
|
|
407
|
+
fail_on_severities=fail_on_severities,
|
|
408
|
+
severity_labels=severity_labels,
|
|
409
|
+
enable_codeowners_ignore=enable_ignore,
|
|
410
|
+
allowed_ignore_users=allowed_users,
|
|
411
|
+
)
|
|
394
412
|
success = await commenter.post_findings_to_pr(
|
|
395
413
|
validation_report,
|
|
396
414
|
create_review=getattr(args, "github_review", False),
|
|
@@ -247,7 +247,7 @@ _iam_validator_completion() {{
|
|
|
247
247
|
return 0
|
|
248
248
|
;;
|
|
249
249
|
validate)
|
|
250
|
-
opts="--path -p --stdin --format -f --output -o --no-recursive --fail-on-warnings --policy-type -t --github-comment --github-review --github-summary --verbose -v --config -c --custom-checks-dir --aws-services-dir --stream --batch-size --summary --severity-breakdown"
|
|
250
|
+
opts="--path -p --stdin --format -f --output -o --no-recursive --fail-on-warnings --policy-type -t --github-comment --github-review --github-summary --verbose -v --config -c --custom-checks-dir --aws-services-dir --stream --batch-size --summary --severity-breakdown --allow-owner-ignore --no-owner-ignore --ci --ci-output"
|
|
251
251
|
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
|
252
252
|
return 0
|
|
253
253
|
;;
|
|
@@ -376,7 +376,11 @@ _iam_validator() {{
|
|
|
376
376
|
'--stream[Process files one-by-one]' \\
|
|
377
377
|
'--batch-size[Policies per batch]:number:' \\
|
|
378
378
|
'--summary[Show Executive Summary section]' \\
|
|
379
|
-
'--severity-breakdown[Show Issue Severity Breakdown section]'
|
|
379
|
+
'--severity-breakdown[Show Issue Severity Breakdown section]' \\
|
|
380
|
+
'--allow-owner-ignore[Allow CODEOWNERS to ignore findings]' \\
|
|
381
|
+
'--no-owner-ignore[Disable CODEOWNERS ignore feature]' \\
|
|
382
|
+
'--ci[CI mode - print enhanced output, write JSON to file]' \\
|
|
383
|
+
'--ci-output[Output file for JSON report in CI mode]:file:_files'
|
|
380
384
|
;;
|
|
381
385
|
post-to-pr)
|
|
382
386
|
_arguments \\
|