iam-policy-validator 1.7.2__py3-none-any.whl → 1.9.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.9.0.dist-info}/METADATA +127 -6
- iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
- iam_validator/__init__.py +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +559 -207
- iam_validator/checks/action_resource_matching.py +12 -15
- iam_validator/checks/action_validation.py +7 -13
- iam_validator/checks/condition_key_validation.py +7 -13
- iam_validator/checks/condition_type_mismatch.py +15 -22
- iam_validator/checks/full_wildcard.py +9 -13
- iam_validator/checks/mfa_condition_check.py +8 -17
- iam_validator/checks/policy_size.py +6 -39
- iam_validator/checks/policy_structure.py +547 -0
- iam_validator/checks/policy_type_validation.py +61 -46
- iam_validator/checks/principal_validation.py +71 -148
- iam_validator/checks/resource_validation.py +13 -20
- iam_validator/checks/sensitive_action.py +15 -18
- iam_validator/checks/service_wildcard.py +8 -14
- iam_validator/checks/set_operator_validation.py +21 -28
- iam_validator/checks/sid_uniqueness.py +16 -42
- iam_validator/checks/trust_policy_validation.py +506 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +2 -2
- iam_validator/checks/wildcard_action.py +9 -13
- iam_validator/checks/wildcard_resource.py +9 -13
- iam_validator/commands/cache.py +4 -3
- iam_validator/commands/validate.py +15 -9
- iam_validator/core/__init__.py +2 -3
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +24 -1028
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +612 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +379 -0
- iam_validator/core/check_registry.py +165 -93
- 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/constants.py +17 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +38 -475
- iam_validator/core/policy_loader.py +27 -4
- iam_validator/sdk/__init__.py +1 -1
- iam_validator/sdk/context.py +1 -1
- iam_validator/sdk/helpers.py +1 -1
- iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -22,7 +22,6 @@ from iam_validator.core.config.condition_requirements import CONDITION_REQUIREME
|
|
|
22
22
|
from iam_validator.core.config.principal_requirements import (
|
|
23
23
|
get_default_principal_requirements,
|
|
24
24
|
)
|
|
25
|
-
from iam_validator.core.config.service_principals import DEFAULT_SERVICE_PRINCIPALS
|
|
26
25
|
from iam_validator.core.config.wildcards import (
|
|
27
26
|
DEFAULT_ALLOWED_WILDCARDS,
|
|
28
27
|
DEFAULT_SERVICE_WILDCARDS,
|
|
@@ -195,61 +194,68 @@ DEFAULT_CONFIG = {
|
|
|
195
194
|
# 9. PRINCIPAL VALIDATION
|
|
196
195
|
# ========================================================================
|
|
197
196
|
# Validates Principal elements in resource-based policies
|
|
198
|
-
#
|
|
199
|
-
# Only runs when --policy-type RESOURCE_POLICY
|
|
197
|
+
# Applies to: S3 buckets, SNS topics, SQS queues, Lambda functions, etc.
|
|
198
|
+
# Only runs when: --policy-type RESOURCE_POLICY
|
|
200
199
|
#
|
|
201
|
-
#
|
|
200
|
+
# Three control mechanisms:
|
|
201
|
+
# 1. blocked_principals - Block specific principals (deny list)
|
|
202
|
+
# 2. allowed_principals - Allow only specific principals (whitelist mode)
|
|
203
|
+
# 3. principal_condition_requirements - Require conditions for principals
|
|
204
|
+
# 4. allowed_service_principals - Always allow AWS service principals
|
|
202
205
|
"principal_validation": {
|
|
203
206
|
"enabled": True,
|
|
204
207
|
"severity": "high", # Security issue, not IAM validity error
|
|
205
208
|
"description": "Validates Principal elements in resource policies for security best practices",
|
|
206
|
-
# blocked_principals:
|
|
207
|
-
# Default: ["*"] blocks public access
|
|
208
|
-
# Examples:
|
|
209
|
-
# ["*"] - Block public access
|
|
210
|
-
# ["*", "arn:aws:iam::*:root"] - Block public + all AWS accounts
|
|
209
|
+
# blocked_principals: Deny list - these principals are never allowed
|
|
210
|
+
# Default: ["*"] blocks public access
|
|
211
211
|
"blocked_principals": ["*"],
|
|
212
|
-
# allowed_principals:
|
|
213
|
-
#
|
|
214
|
-
# Examples:
|
|
215
|
-
# [] - Allow all (except blocked)
|
|
216
|
-
# ["arn:aws:iam::123456789012:root"] - Only allow specific account
|
|
217
|
-
# ["arn:aws:iam::*:role/OrgAccessRole"] - Allow specific role in any account
|
|
212
|
+
# allowed_principals: Whitelist mode - when set, ONLY these are allowed
|
|
213
|
+
# Default: [] allows all (except blocked)
|
|
218
214
|
"allowed_principals": [],
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
# Default:
|
|
222
|
-
# Examples:
|
|
223
|
-
# "*": ["aws:SourceArn"] - Public access must specify source ARN
|
|
224
|
-
# "arn:aws:iam::*:root": ["aws:PrincipalOrgID"] - Cross-account must be from org
|
|
225
|
-
"require_conditions_for": {
|
|
226
|
-
"*": [
|
|
227
|
-
"aws:SourceArn",
|
|
228
|
-
"aws:SourceAccount",
|
|
229
|
-
"aws:SourceVpce",
|
|
230
|
-
"aws:SourceIp",
|
|
231
|
-
"aws:SourceOrgID",
|
|
232
|
-
"aws:SourceOrgPaths",
|
|
233
|
-
],
|
|
234
|
-
},
|
|
235
|
-
# principal_condition_requirements: Advanced condition requirements for principals
|
|
236
|
-
# Similar to action_condition_enforcement but for principals
|
|
237
|
-
# Supports all_of/any_of/none_of logic with rich metadata
|
|
238
|
-
# Default: 2 critical requirements enabled (public_access, prevent_insecure_transport)
|
|
215
|
+
# principal_condition_requirements: Require conditions for specific principals
|
|
216
|
+
# Supports all_of/any_of/none_of logic like action_condition_enforcement
|
|
217
|
+
# Default: 2 enabled (public_access, prevent_insecure_transport)
|
|
239
218
|
# See: iam_validator/core/config/principal_requirements.py
|
|
240
|
-
# To customize requirements, use Python API:
|
|
241
|
-
# from iam_validator.core.config import get_principal_requirements_by_names
|
|
242
|
-
# requirements = get_principal_requirements_by_names(['public_access', 'cross_account_org'])
|
|
243
|
-
# To disable: set to empty list []
|
|
244
219
|
"principal_condition_requirements": get_default_principal_requirements(),
|
|
245
|
-
# allowed_service_principals: AWS service principals
|
|
246
|
-
# Default:
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
220
|
+
# allowed_service_principals: AWS service principals (*.amazonaws.com)
|
|
221
|
+
# Default: ["aws:*"] allows ALL AWS service principals
|
|
222
|
+
# Note: "aws:*" is different from "*" (public access)
|
|
223
|
+
"allowed_service_principals": ["aws:*"],
|
|
224
|
+
},
|
|
225
|
+
# ========================================================================
|
|
226
|
+
# 10. TRUST POLICY VALIDATION
|
|
227
|
+
# ========================================================================
|
|
228
|
+
# Validate trust policies (role assumption policies) for security best practices
|
|
229
|
+
# Ensures assume role actions have appropriate principals and conditions
|
|
230
|
+
#
|
|
231
|
+
# Key validations:
|
|
232
|
+
# - Action-Principal type matching (e.g., AssumeRoleWithSAML needs Federated)
|
|
233
|
+
# - Provider ARN format validation (SAML vs OIDC provider patterns)
|
|
234
|
+
# - Required conditions per assume method
|
|
235
|
+
#
|
|
236
|
+
# Complements principal_validation check (which validates principal allowlists/blocklists)
|
|
237
|
+
# This check focuses on action-principal coupling specific to trust policies
|
|
238
|
+
#
|
|
239
|
+
# Auto-detection: Only runs on statements with assume role actions
|
|
240
|
+
"trust_policy_validation": {
|
|
241
|
+
"enabled": True, # Enabled by default (auto-detects trust policies)
|
|
242
|
+
"severity": "high", # Security issue
|
|
243
|
+
"description": "Validates trust policies for role assumption security and action-principal coupling",
|
|
244
|
+
# validation_rules: Custom rules override defaults
|
|
245
|
+
# Default rules validate:
|
|
246
|
+
# - sts:AssumeRole → AWS or Service principals
|
|
247
|
+
# - sts:AssumeRoleWithSAML → Federated (SAML provider) with SAML:aud
|
|
248
|
+
# - sts:AssumeRoleWithWebIdentity → Federated (OIDC provider)
|
|
249
|
+
# Example custom rules:
|
|
250
|
+
# "validation_rules": {
|
|
251
|
+
# "sts:AssumeRole": {
|
|
252
|
+
# "allowed_principal_types": ["AWS"], # Only AWS, not Service
|
|
253
|
+
# "required_conditions": ["sts:ExternalId"], # Always require ExternalId
|
|
254
|
+
# }
|
|
255
|
+
# }
|
|
250
256
|
},
|
|
251
257
|
# ========================================================================
|
|
252
|
-
#
|
|
258
|
+
# 11. POLICY TYPE VALIDATION
|
|
253
259
|
# ========================================================================
|
|
254
260
|
# Validate policy type requirements (new in v1.3.0)
|
|
255
261
|
# Ensures policies conform to the declared type (IDENTITY vs RESOURCE_POLICY)
|
|
@@ -264,7 +270,7 @@ DEFAULT_CONFIG = {
|
|
|
264
270
|
"description": "Validates policies match declared type and enforces RCP requirements",
|
|
265
271
|
},
|
|
266
272
|
# ========================================================================
|
|
267
|
-
#
|
|
273
|
+
# 12. ACTION-RESOURCE MATCHING
|
|
268
274
|
# ========================================================================
|
|
269
275
|
# Validate action-resource matching
|
|
270
276
|
# Ensures resources match the required resource types for actions
|
|
@@ -304,7 +310,7 @@ DEFAULT_CONFIG = {
|
|
|
304
310
|
# See: iam_validator/core/config/sensitive_actions.py for sensitive actions
|
|
305
311
|
# ========================================================================
|
|
306
312
|
# ========================================================================
|
|
307
|
-
#
|
|
313
|
+
# 13. WILDCARD ACTION
|
|
308
314
|
# ========================================================================
|
|
309
315
|
# Check for wildcard actions (Action: "*")
|
|
310
316
|
# Flags statements that allow all actions
|
|
@@ -323,7 +329,7 @@ DEFAULT_CONFIG = {
|
|
|
323
329
|
),
|
|
324
330
|
},
|
|
325
331
|
# ========================================================================
|
|
326
|
-
#
|
|
332
|
+
# 14. WILDCARD RESOURCE
|
|
327
333
|
# ========================================================================
|
|
328
334
|
# Check for wildcard resources (Resource: "*")
|
|
329
335
|
# Flags statements that apply to all resources
|
|
@@ -350,7 +356,7 @@ DEFAULT_CONFIG = {
|
|
|
350
356
|
),
|
|
351
357
|
},
|
|
352
358
|
# ========================================================================
|
|
353
|
-
#
|
|
359
|
+
# 15. FULL WILDCARD (CRITICAL)
|
|
354
360
|
# ========================================================================
|
|
355
361
|
# Check for BOTH Action: "*" AND Resource: "*" (CRITICAL)
|
|
356
362
|
# This grants full administrative access (AdministratorAccess equivalent)
|
|
@@ -374,7 +380,7 @@ DEFAULT_CONFIG = {
|
|
|
374
380
|
),
|
|
375
381
|
},
|
|
376
382
|
# ========================================================================
|
|
377
|
-
#
|
|
383
|
+
# 16. SERVICE WILDCARD
|
|
378
384
|
# ========================================================================
|
|
379
385
|
# Check for service-level wildcards (e.g., "iam:*", "s3:*", "ec2:*")
|
|
380
386
|
# These grant ALL permissions for a service (often too permissive)
|
|
@@ -404,7 +410,7 @@ DEFAULT_CONFIG = {
|
|
|
404
410
|
),
|
|
405
411
|
},
|
|
406
412
|
# ========================================================================
|
|
407
|
-
#
|
|
413
|
+
# 17. SENSITIVE ACTION
|
|
408
414
|
# ========================================================================
|
|
409
415
|
# Check for sensitive actions without IAM conditions
|
|
410
416
|
# Sensitive actions: IAM changes, secrets access, destructive operations
|
|
@@ -479,7 +485,7 @@ DEFAULT_CONFIG = {
|
|
|
479
485
|
],
|
|
480
486
|
},
|
|
481
487
|
# ========================================================================
|
|
482
|
-
#
|
|
488
|
+
# 18. ACTION CONDITION ENFORCEMENT
|
|
483
489
|
# ========================================================================
|
|
484
490
|
# Enforce specific IAM condition requirements for actions
|
|
485
491
|
# Examples: iam:PassRole must specify iam:PassedToService,
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Service principals utilities for resource policy validation.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
This module provides:
|
|
5
|
+
- Default list of common AWS service principals
|
|
6
|
+
- Utility to check if a principal is any AWS service principal
|
|
7
|
+
- Functions to categorize service principals by type
|
|
8
|
+
|
|
9
|
+
Configuration:
|
|
10
|
+
- Use "*" in allowed_service_principals to allow ALL AWS service principals
|
|
11
|
+
- Use explicit list to restrict to specific services only
|
|
12
|
+
- AWS service principals end with .amazonaws.com or .amazonaws.com.cn
|
|
6
13
|
"""
|
|
7
14
|
|
|
8
15
|
from typing import Final
|
|
@@ -58,6 +65,36 @@ def is_allowed_service_principal(principal: str) -> bool:
|
|
|
58
65
|
return principal in DEFAULT_SERVICE_PRINCIPALS
|
|
59
66
|
|
|
60
67
|
|
|
68
|
+
def is_aws_service_principal(principal: str) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if a principal is an AWS service principal (any AWS service).
|
|
71
|
+
|
|
72
|
+
This checks if the principal matches the AWS service principal pattern.
|
|
73
|
+
AWS service principals typically end with ".amazonaws.com" or ".amazonaws.com.cn"
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
principal: Principal to check (e.g., "lambda.amazonaws.com", "s3.amazonaws.com.cn")
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if principal matches AWS service principal pattern
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> is_aws_service_principal("lambda.amazonaws.com")
|
|
83
|
+
True
|
|
84
|
+
>>> is_aws_service_principal("s3.amazonaws.com.cn")
|
|
85
|
+
True
|
|
86
|
+
>>> is_aws_service_principal("arn:aws:iam::123456789012:root")
|
|
87
|
+
False
|
|
88
|
+
>>> is_aws_service_principal("*")
|
|
89
|
+
False
|
|
90
|
+
"""
|
|
91
|
+
if not isinstance(principal, str):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# AWS service principals end with .amazonaws.com or .amazonaws.com.cn
|
|
95
|
+
return principal.endswith(".amazonaws.com") or principal.endswith(".amazonaws.com.cn")
|
|
96
|
+
|
|
97
|
+
|
|
61
98
|
def get_service_principals_by_category() -> dict[str, tuple[str, ...]]:
|
|
62
99
|
"""
|
|
63
100
|
Get service principals organized by service category.
|
iam_validator/core/constants.py
CHANGED
|
@@ -123,6 +123,23 @@ DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
|
|
|
123
123
|
# Time conversion constants
|
|
124
124
|
SECONDS_PER_HOUR = 3600
|
|
125
125
|
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# Policy Type Restrictions
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
# AWS services that support Resource Control Policies (RCP)
|
|
131
|
+
# These services can have wildcard actions in RCP policy statements
|
|
132
|
+
# Reference: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_rcps.html
|
|
133
|
+
RCP_SUPPORTED_SERVICES = frozenset(
|
|
134
|
+
{
|
|
135
|
+
"s3",
|
|
136
|
+
"sts",
|
|
137
|
+
"sqs",
|
|
138
|
+
"secretsmanager",
|
|
139
|
+
"kms",
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
126
143
|
# ============================================================================
|
|
127
144
|
# AWS Documentation URLs
|
|
128
145
|
# ============================================================================
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized ignore patterns utility with caching and performance optimization.
|
|
3
|
+
|
|
4
|
+
This module provides high-performance pattern matching for ignore_patterns across
|
|
5
|
+
all checks. Uses LRU caching and compiled regex patterns for optimal performance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from iam_validator.core.models import ValidationIssue
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Global regex pattern cache (shared across all checks for maximum efficiency)
|
|
16
|
+
@lru_cache(maxsize=512)
|
|
17
|
+
def compile_pattern(pattern: str) -> re.Pattern[str] | None:
|
|
18
|
+
"""
|
|
19
|
+
Compile and cache regex patterns.
|
|
20
|
+
|
|
21
|
+
Uses LRU cache to avoid recompiling the same patterns across multiple calls.
|
|
22
|
+
This is critical for performance when the same patterns are used repeatedly.
|
|
23
|
+
|
|
24
|
+
This is a public API function used across multiple modules for consistent
|
|
25
|
+
regex caching.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
pattern: Regex pattern string
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Compiled pattern or None if invalid
|
|
32
|
+
|
|
33
|
+
Performance:
|
|
34
|
+
- First call: O(n) compile time
|
|
35
|
+
- Cached calls: O(1) lookup
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return re.compile(str(pattern), re.IGNORECASE)
|
|
39
|
+
except re.error:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IgnorePatternMatcher:
|
|
44
|
+
"""
|
|
45
|
+
High-performance pattern matcher for ignore_patterns.
|
|
46
|
+
|
|
47
|
+
Features:
|
|
48
|
+
- Cached compiled regex patterns (LRU cache)
|
|
49
|
+
- Support for new (simple) and old (verbose) field names
|
|
50
|
+
- Efficient filtering with early exit optimization
|
|
51
|
+
- Field-specific validation logic
|
|
52
|
+
|
|
53
|
+
Thread-safe: Yes (regex compilation is cached globally)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Supported field name mappings (new -> old for backward compatibility)
|
|
57
|
+
FIELD_ALIASES = {
|
|
58
|
+
"filepath": "filepath_regex",
|
|
59
|
+
"action": "action_matches",
|
|
60
|
+
"resource": "resource_matches",
|
|
61
|
+
"sid": "statement_sid",
|
|
62
|
+
"condition_key": "condition_key_matches",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def should_ignore_issue(
|
|
67
|
+
issue: ValidationIssue,
|
|
68
|
+
filepath: str,
|
|
69
|
+
ignore_patterns: list[dict[str, Any]],
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Check if a ValidationIssue should be ignored based on patterns.
|
|
73
|
+
|
|
74
|
+
Pattern Matching Logic:
|
|
75
|
+
- Multiple fields in ONE pattern = AND logic (all must match)
|
|
76
|
+
- Multiple patterns = OR logic (any pattern matches → ignore)
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
issue: The validation issue to check
|
|
80
|
+
filepath: Path to the policy file
|
|
81
|
+
ignore_patterns: List of pattern dictionaries
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if the issue should be ignored
|
|
85
|
+
|
|
86
|
+
Performance:
|
|
87
|
+
- Early exit on first match (OR logic)
|
|
88
|
+
- Cached regex compilation
|
|
89
|
+
- O(p * f) where p=patterns, f=fields per pattern
|
|
90
|
+
"""
|
|
91
|
+
if not ignore_patterns:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
for pattern in ignore_patterns:
|
|
95
|
+
if IgnorePatternMatcher._matches_pattern(pattern, issue, filepath):
|
|
96
|
+
return True # Early exit on first match
|
|
97
|
+
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def filter_actions(
|
|
102
|
+
actions: frozenset[str],
|
|
103
|
+
ignore_patterns: list[dict[str, Any]],
|
|
104
|
+
) -> frozenset[str]:
|
|
105
|
+
"""
|
|
106
|
+
Filter actions based on action ignore patterns.
|
|
107
|
+
|
|
108
|
+
Only considers patterns that contain an "action" or "action_matches" field.
|
|
109
|
+
This is optimized for the sensitive_action check which needs to filter
|
|
110
|
+
actions before creating ValidationIssues.
|
|
111
|
+
|
|
112
|
+
Supports both single action patterns and lists:
|
|
113
|
+
- action: "s3:.*" # Single regex pattern
|
|
114
|
+
- action: ["s3:GetObject", "s3:PutObject"] # List of patterns
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
actions: Set of actions to filter
|
|
118
|
+
ignore_patterns: List of pattern dictionaries
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Filtered set of actions (actions matching patterns removed)
|
|
122
|
+
|
|
123
|
+
Performance:
|
|
124
|
+
- Extracts action patterns once: O(p) where p=patterns
|
|
125
|
+
- Filters with cached regex: O(a * p) where a=actions, p=patterns
|
|
126
|
+
- Early exit per action when match found
|
|
127
|
+
"""
|
|
128
|
+
if not ignore_patterns:
|
|
129
|
+
return actions
|
|
130
|
+
|
|
131
|
+
# Extract action patterns once (cache-friendly)
|
|
132
|
+
action_patterns = []
|
|
133
|
+
for pattern in ignore_patterns:
|
|
134
|
+
# Support both new and old field names
|
|
135
|
+
action_regex = pattern.get("action") or pattern.get("action_matches")
|
|
136
|
+
if action_regex:
|
|
137
|
+
# Support both single string and list of strings
|
|
138
|
+
if isinstance(action_regex, list):
|
|
139
|
+
action_patterns.extend(action_regex)
|
|
140
|
+
else:
|
|
141
|
+
action_patterns.append(action_regex)
|
|
142
|
+
|
|
143
|
+
if not action_patterns:
|
|
144
|
+
return actions
|
|
145
|
+
|
|
146
|
+
# Filter actions with compiled patterns (cached)
|
|
147
|
+
filtered = set()
|
|
148
|
+
for action in actions:
|
|
149
|
+
should_keep = True
|
|
150
|
+
for pattern_str in action_patterns:
|
|
151
|
+
compiled = compile_pattern(pattern_str)
|
|
152
|
+
if compiled and compiled.search(str(action)):
|
|
153
|
+
should_keep = False
|
|
154
|
+
break # Early exit on first match
|
|
155
|
+
|
|
156
|
+
if should_keep:
|
|
157
|
+
filtered.add(action)
|
|
158
|
+
|
|
159
|
+
return frozenset(filtered)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _matches_pattern(
|
|
163
|
+
pattern: dict[str, Any],
|
|
164
|
+
issue: ValidationIssue,
|
|
165
|
+
filepath: str,
|
|
166
|
+
) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check if issue matches a single ignore pattern.
|
|
169
|
+
|
|
170
|
+
All fields in pattern must match (AND logic).
|
|
171
|
+
For list-based fields (like action), ANY match from the list counts (OR logic).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
pattern: Pattern dict with optional fields
|
|
175
|
+
issue: ValidationIssue to check
|
|
176
|
+
filepath: Path to policy file
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if all fields in pattern match the issue
|
|
180
|
+
|
|
181
|
+
Performance:
|
|
182
|
+
- Early exit on first non-match (AND logic)
|
|
183
|
+
- Uses cached compiled patterns
|
|
184
|
+
"""
|
|
185
|
+
for field_name, regex_pattern in pattern.items():
|
|
186
|
+
# Get actual value from issue based on field name
|
|
187
|
+
actual_value = IgnorePatternMatcher._get_field_value(field_name, issue, filepath)
|
|
188
|
+
|
|
189
|
+
# Handle special case: SID with exact match (no regex)
|
|
190
|
+
if field_name in ("sid", "statement_sid"):
|
|
191
|
+
# Support both single string and list of strings
|
|
192
|
+
if isinstance(regex_pattern, list):
|
|
193
|
+
# List of SIDs - exact match or regex
|
|
194
|
+
matched = False
|
|
195
|
+
for single_sid in regex_pattern:
|
|
196
|
+
if isinstance(single_sid, str) and "*" not in single_sid:
|
|
197
|
+
# Exact match
|
|
198
|
+
if issue.statement_sid == single_sid:
|
|
199
|
+
matched = True
|
|
200
|
+
break
|
|
201
|
+
else:
|
|
202
|
+
# Regex match
|
|
203
|
+
compiled = compile_pattern(str(single_sid))
|
|
204
|
+
if compiled and compiled.search(str(issue.statement_sid or "")):
|
|
205
|
+
matched = True
|
|
206
|
+
break
|
|
207
|
+
if not matched:
|
|
208
|
+
return False
|
|
209
|
+
continue
|
|
210
|
+
elif isinstance(regex_pattern, str) and "*" not in regex_pattern:
|
|
211
|
+
# Single SID - exact match (not a regex)
|
|
212
|
+
if issue.statement_sid != regex_pattern:
|
|
213
|
+
return False # Early exit on non-match
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Regex match for all other cases
|
|
217
|
+
if actual_value is None:
|
|
218
|
+
return False # Early exit on missing value
|
|
219
|
+
|
|
220
|
+
# Support list of patterns (OR logic - any match succeeds)
|
|
221
|
+
if isinstance(regex_pattern, list):
|
|
222
|
+
matched = False
|
|
223
|
+
for single_pattern in regex_pattern:
|
|
224
|
+
compiled = compile_pattern(str(single_pattern))
|
|
225
|
+
if compiled and compiled.search(str(actual_value)):
|
|
226
|
+
matched = True
|
|
227
|
+
break # Found a match in the list
|
|
228
|
+
if not matched:
|
|
229
|
+
return False # None of the patterns matched
|
|
230
|
+
else:
|
|
231
|
+
# Single pattern
|
|
232
|
+
compiled = compile_pattern(str(regex_pattern))
|
|
233
|
+
if not compiled or not compiled.search(str(actual_value)):
|
|
234
|
+
return False # Early exit on non-match
|
|
235
|
+
|
|
236
|
+
return True # All fields matched
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _get_field_value(
|
|
240
|
+
field_name: str,
|
|
241
|
+
issue: ValidationIssue,
|
|
242
|
+
filepath: str,
|
|
243
|
+
) -> str | None:
|
|
244
|
+
"""
|
|
245
|
+
Extract field value from issue or filepath.
|
|
246
|
+
|
|
247
|
+
Supports both new (simple) and old (verbose) field names for
|
|
248
|
+
backward compatibility.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
field_name: Name of the field to extract
|
|
252
|
+
issue: ValidationIssue to extract from
|
|
253
|
+
filepath: Policy file path
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Field value as string, or None if field not recognized
|
|
257
|
+
"""
|
|
258
|
+
# Normalize field name (support both old and new names)
|
|
259
|
+
if field_name in ("filepath", "filepath_regex"):
|
|
260
|
+
return filepath
|
|
261
|
+
elif field_name in ("action", "action_matches"):
|
|
262
|
+
return issue.action or ""
|
|
263
|
+
elif field_name in ("resource", "resource_matches"):
|
|
264
|
+
return issue.resource or ""
|
|
265
|
+
elif field_name in ("sid", "statement_sid"):
|
|
266
|
+
return issue.statement_sid or ""
|
|
267
|
+
elif field_name in ("condition_key", "condition_key_matches"):
|
|
268
|
+
return issue.condition_key or ""
|
|
269
|
+
else:
|
|
270
|
+
# Unknown field - skip (don't fail)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# Convenience functions for backward compatibility
|
|
275
|
+
def should_ignore_issue(
|
|
276
|
+
issue: ValidationIssue,
|
|
277
|
+
filepath: str,
|
|
278
|
+
ignore_patterns: list[dict[str, Any]],
|
|
279
|
+
) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Convenience function for checking if an issue should be ignored.
|
|
282
|
+
|
|
283
|
+
See IgnorePatternMatcher.should_ignore_issue() for details.
|
|
284
|
+
"""
|
|
285
|
+
return IgnorePatternMatcher.should_ignore_issue(issue, filepath, ignore_patterns)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def filter_actions(
|
|
289
|
+
actions: frozenset[str],
|
|
290
|
+
ignore_patterns: list[dict[str, Any]],
|
|
291
|
+
) -> frozenset[str]:
|
|
292
|
+
"""
|
|
293
|
+
Convenience function for filtering actions.
|
|
294
|
+
|
|
295
|
+
See IgnorePatternMatcher.filter_actions() for details.
|
|
296
|
+
"""
|
|
297
|
+
return IgnorePatternMatcher.filter_actions(actions, ignore_patterns)
|
iam_validator/core/models.py
CHANGED
|
@@ -14,6 +14,7 @@ from iam_validator.core import constants
|
|
|
14
14
|
PolicyType = Literal[
|
|
15
15
|
"IDENTITY_POLICY",
|
|
16
16
|
"RESOURCE_POLICY",
|
|
17
|
+
"TRUST_POLICY", # Trust policies (role assumption policies - subtype of resource policies)
|
|
17
18
|
"SERVICE_CONTROL_POLICY",
|
|
18
19
|
"RESOURCE_CONTROL_POLICY",
|
|
19
20
|
]
|
|
@@ -105,10 +106,10 @@ class ServiceDetail(BaseModel):
|
|
|
105
106
|
class Statement(BaseModel):
|
|
106
107
|
"""IAM policy statement."""
|
|
107
108
|
|
|
108
|
-
model_config = ConfigDict(populate_by_name=True, extra="
|
|
109
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
109
110
|
|
|
110
111
|
sid: str | None = Field(default=None, alias="Sid")
|
|
111
|
-
effect: str = Field(alias="Effect")
|
|
112
|
+
effect: str | None = Field(default=None, alias="Effect")
|
|
112
113
|
action: list[str] | str | None = Field(default=None, alias="Action")
|
|
113
114
|
not_action: list[str] | str | None = Field(default=None, alias="NotAction")
|
|
114
115
|
resource: list[str] | str | None = Field(default=None, alias="Resource")
|
|
@@ -135,10 +136,10 @@ class Statement(BaseModel):
|
|
|
135
136
|
class IAMPolicy(BaseModel):
|
|
136
137
|
"""IAM policy document."""
|
|
137
138
|
|
|
138
|
-
model_config = ConfigDict(populate_by_name=True, extra="
|
|
139
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
139
140
|
|
|
140
|
-
version: str = Field(alias="Version")
|
|
141
|
-
statement: list[Statement] = Field(alias="Statement")
|
|
141
|
+
version: str | None = Field(default=None, alias="Version")
|
|
142
|
+
statement: list[Statement] | None = Field(default=None, alias="Statement")
|
|
142
143
|
id: str | None = Field(default=None, alias="Id")
|
|
143
144
|
|
|
144
145
|
|
|
@@ -164,6 +165,9 @@ class ValidationIssue(BaseModel):
|
|
|
164
165
|
suggestion: str | None = None
|
|
165
166
|
example: str | None = None # Code example (JSON/YAML) - formatted separately for GitHub
|
|
166
167
|
line_number: int | None = None # Line number in the policy file (if available)
|
|
168
|
+
check_id: str | None = (
|
|
169
|
+
None # Check that triggered this issue (e.g., "policy_size", "sensitive_action")
|
|
170
|
+
)
|
|
167
171
|
|
|
168
172
|
# Severity level constants (ClassVar to avoid Pydantic treating them as fields)
|
|
169
173
|
VALID_SEVERITIES: ClassVar[frozenset[str]] = frozenset(
|
|
@@ -282,6 +286,12 @@ class ValidationIssue(BaseModel):
|
|
|
282
286
|
parts.append("")
|
|
283
287
|
parts.append("</details>")
|
|
284
288
|
|
|
289
|
+
# Add check ID at the bottom if available
|
|
290
|
+
if self.check_id:
|
|
291
|
+
parts.append("")
|
|
292
|
+
parts.append("---")
|
|
293
|
+
parts.append(f"*Check: `{self.check_id}`*")
|
|
294
|
+
|
|
285
295
|
return "\n".join(parts)
|
|
286
296
|
|
|
287
297
|
|