iam-policy-validator 1.5.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.
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +9 -3
- iam_validator/checks/action_condition_enforcement.py +164 -2
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +3 -1
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/sensitive_action.py +78 -6
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/validate.py +44 -11
- iam_validator/core/aws_fetcher.py +89 -52
- iam_validator/core/check_registry.py +165 -21
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +13 -15
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +104 -0
- iam_validator/core/config/condition_requirements.py +5 -385
- iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
- iam_validator/core/config/defaults.py +187 -54
- iam_validator/core/config/sensitive_actions.py +620 -81
- iam_validator/core/models.py +14 -1
- iam_validator/core/policy_checks.py +4 -4
- iam_validator/core/pr_commenter.py +1 -1
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +274 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/core/aws_global_conditions.py +0 -137
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -38,6 +38,31 @@ def _get_default_sensitive_actions() -> frozenset[str]:
|
|
|
38
38
|
return _DEFAULT_SENSITIVE_ACTIONS_CACHE
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def get_sensitive_actions_by_categories(categories: list[str] | None = None) -> frozenset[str]:
|
|
42
|
+
"""
|
|
43
|
+
Get sensitive actions filtered by categories.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
categories: List of category IDs to include. If None, returns all actions.
|
|
47
|
+
Valid categories: 'credential_exposure', 'data_access',
|
|
48
|
+
'priv_esc', 'resource_exposure'
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Frozenset of sensitive actions matching the specified categories
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
>>> # Get all sensitive actions (default behavior)
|
|
55
|
+
>>> all_actions = get_sensitive_actions_by_categories()
|
|
56
|
+
|
|
57
|
+
>>> # Get only privilege escalation actions
|
|
58
|
+
>>> priv_esc = get_sensitive_actions_by_categories(['priv_esc'])
|
|
59
|
+
|
|
60
|
+
>>> # Get credential exposure and data access actions
|
|
61
|
+
>>> sensitive = get_sensitive_actions_by_categories(['credential_exposure', 'data_access'])
|
|
62
|
+
"""
|
|
63
|
+
return get_sensitive_actions(categories)
|
|
64
|
+
|
|
65
|
+
|
|
41
66
|
# Export for backward compatibility
|
|
42
67
|
DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
43
68
|
|
|
@@ -79,7 +104,16 @@ def check_sensitive_actions(
|
|
|
79
104
|
- Uses lazy-loaded defaults (only loaded on first use)
|
|
80
105
|
- O(1) frozenset lookups for action matching
|
|
81
106
|
"""
|
|
82
|
-
if
|
|
107
|
+
# Check if categories are specified in config
|
|
108
|
+
categories = config.config.get("categories")
|
|
109
|
+
if categories is not None:
|
|
110
|
+
# If categories is an empty list, disable the check
|
|
111
|
+
if len(categories) == 0:
|
|
112
|
+
return False, []
|
|
113
|
+
# Get sensitive actions filtered by categories
|
|
114
|
+
default_actions = get_sensitive_actions_by_categories(categories)
|
|
115
|
+
elif default_actions is None:
|
|
116
|
+
# Use all categories if no specific categories configured
|
|
83
117
|
default_actions = _get_default_sensitive_actions()
|
|
84
118
|
|
|
85
119
|
# Filter out wildcards
|
iam_validator/commands/cache.py
CHANGED
|
@@ -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()
|
|
@@ -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
|
-
|
|
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",
|
|
@@ -198,15 +210,36 @@ Examples:
|
|
|
198
210
|
|
|
199
211
|
async def _execute_batch(self, args: argparse.Namespace) -> int:
|
|
200
212
|
"""Execute validation by loading all policies at once (original behavior)."""
|
|
201
|
-
# Load policies from all specified paths
|
|
213
|
+
# Load policies from all specified paths or stdin
|
|
202
214
|
loader = PolicyLoader()
|
|
203
|
-
policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
|
|
204
215
|
|
|
205
|
-
if
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
208
241
|
|
|
209
|
-
|
|
242
|
+
logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
|
|
210
243
|
|
|
211
244
|
# Validate policies
|
|
212
245
|
use_registry = not getattr(args, "no_registry", False)
|
|
@@ -258,7 +291,7 @@ Examples:
|
|
|
258
291
|
|
|
259
292
|
# Post to GitHub if configured
|
|
260
293
|
if args.github_comment or getattr(args, "github_review", False):
|
|
261
|
-
from iam_validator.core.config_loader import ConfigLoader
|
|
294
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
262
295
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
263
296
|
|
|
264
297
|
# Load config to get fail_on_severity setting
|
|
@@ -384,7 +417,7 @@ Examples:
|
|
|
384
417
|
|
|
385
418
|
# Post summary comment to GitHub (if requested and not already posted per-file reviews)
|
|
386
419
|
if args.github_comment:
|
|
387
|
-
from iam_validator.core.config_loader import ConfigLoader
|
|
420
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
388
421
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
389
422
|
|
|
390
423
|
# Load config to get fail_on_severity setting
|
|
@@ -436,7 +469,7 @@ Examples:
|
|
|
436
469
|
This provides progressive feedback in PRs as files are processed.
|
|
437
470
|
"""
|
|
438
471
|
try:
|
|
439
|
-
from iam_validator.core.config_loader import ConfigLoader
|
|
472
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
440
473
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
441
474
|
|
|
442
475
|
async with GitHubIntegration() as github:
|
|
@@ -27,7 +27,6 @@ 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
|
|
|
@@ -35,57 +34,11 @@ import httpx
|
|
|
35
34
|
|
|
36
35
|
from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
|
|
37
36
|
from iam_validator.core.models import ServiceDetail, ServiceInfo
|
|
37
|
+
from iam_validator.utils.cache import LRUCache
|
|
38
38
|
|
|
39
39
|
logger = logging.getLogger(__name__)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
class LRUCache:
|
|
43
|
-
"""Thread-safe LRU cache implementation with TTL support."""
|
|
44
|
-
|
|
45
|
-
def __init__(self, maxsize: int = 128, ttl: int = 3600):
|
|
46
|
-
"""Initialize LRU cache.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
maxsize: Maximum number of items in cache
|
|
50
|
-
ttl: Time to live in seconds (default: 1 hour)
|
|
51
|
-
"""
|
|
52
|
-
self.cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
|
|
53
|
-
self.maxsize = maxsize
|
|
54
|
-
self.ttl = ttl
|
|
55
|
-
self._lock = asyncio.Lock()
|
|
56
|
-
|
|
57
|
-
async def get(self, key: str) -> Any | None:
|
|
58
|
-
"""Get item from cache if not expired."""
|
|
59
|
-
async with self._lock:
|
|
60
|
-
if key in self.cache:
|
|
61
|
-
value, timestamp = self.cache[key]
|
|
62
|
-
if time.time() - timestamp < self.ttl:
|
|
63
|
-
# Move to end (most recently used)
|
|
64
|
-
self.cache.move_to_end(key)
|
|
65
|
-
return value
|
|
66
|
-
else:
|
|
67
|
-
# Expired, remove it
|
|
68
|
-
del self.cache[key]
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
async def set(self, key: str, value: Any) -> None:
|
|
72
|
-
"""Set item in cache with current timestamp."""
|
|
73
|
-
async with self._lock:
|
|
74
|
-
if key in self.cache:
|
|
75
|
-
# Move to end if exists
|
|
76
|
-
self.cache.move_to_end(key)
|
|
77
|
-
elif len(self.cache) >= self.maxsize:
|
|
78
|
-
# Remove least recently used
|
|
79
|
-
self.cache.popitem(last=False)
|
|
80
|
-
|
|
81
|
-
self.cache[key] = (value, time.time())
|
|
82
|
-
|
|
83
|
-
async def clear(self) -> None:
|
|
84
|
-
"""Clear the cache."""
|
|
85
|
-
async with self._lock:
|
|
86
|
-
self.cache.clear()
|
|
87
|
-
|
|
88
|
-
|
|
89
42
|
class CompiledPatterns:
|
|
90
43
|
"""Pre-compiled regex patterns for validation."""
|
|
91
44
|
|
|
@@ -120,7 +73,69 @@ class CompiledPatterns:
|
|
|
120
73
|
|
|
121
74
|
|
|
122
75
|
class AWSServiceFetcher:
|
|
123
|
-
"""Fetches AWS service information from the AWS service reference API with enhanced performance features.
|
|
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
|
+
"""
|
|
124
139
|
|
|
125
140
|
BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
|
|
126
141
|
|
|
@@ -797,9 +812,15 @@ class AWSServiceFetcher:
|
|
|
797
812
|
return True, None
|
|
798
813
|
|
|
799
814
|
async def validate_condition_key(
|
|
800
|
-
self, action: str, condition_key: str
|
|
815
|
+
self, action: str, condition_key: str, resources: list[str] | None = None
|
|
801
816
|
) -> tuple[bool, str | None, str | None]:
|
|
802
|
-
"""
|
|
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
|
|
803
824
|
|
|
804
825
|
Returns:
|
|
805
826
|
Tuple of (is_valid, error_message, warning_message)
|
|
@@ -808,7 +829,9 @@ class AWSServiceFetcher:
|
|
|
808
829
|
- warning_message: Warning message if valid but not recommended
|
|
809
830
|
"""
|
|
810
831
|
try:
|
|
811
|
-
from iam_validator.core.aws_global_conditions import
|
|
832
|
+
from iam_validator.core.config.aws_global_conditions import (
|
|
833
|
+
get_global_conditions,
|
|
834
|
+
)
|
|
812
835
|
|
|
813
836
|
service_prefix, action_name = self.parse_action(action)
|
|
814
837
|
|
|
@@ -841,6 +864,20 @@ class AWSServiceFetcher:
|
|
|
841
864
|
):
|
|
842
865
|
return True, None, None
|
|
843
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
|
|
880
|
+
|
|
844
881
|
# If it's a global key but the action has specific condition keys defined,
|
|
845
882
|
# AWS allows it but the key may not be available in every request context
|
|
846
883
|
if is_global_key and action_detail.action_condition_keys is not None:
|
|
@@ -31,6 +31,112 @@ class CheckConfig:
|
|
|
31
31
|
config: dict[str, Any] = field(default_factory=dict) # Check-specific config
|
|
32
32
|
description: str = ""
|
|
33
33
|
root_config: dict[str, Any] = field(default_factory=dict) # Full config for cross-check access
|
|
34
|
+
ignore_patterns: list[dict[str, Any]] = field(default_factory=list) # NEW: Ignore patterns
|
|
35
|
+
"""
|
|
36
|
+
List of patterns to ignore findings.
|
|
37
|
+
|
|
38
|
+
Each pattern is a dict with optional fields:
|
|
39
|
+
- filepath_regex: Regex to match file path
|
|
40
|
+
- action_matches: Regex to match action name
|
|
41
|
+
- resource_matches: Regex to match resource
|
|
42
|
+
- statement_sid: Exact SID to match (or regex if ends with .*)
|
|
43
|
+
- condition_key_matches: Regex to match condition key
|
|
44
|
+
|
|
45
|
+
Multiple fields in one pattern = AND logic
|
|
46
|
+
Multiple patterns = OR logic (any pattern matches → ignore)
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
ignore_patterns:
|
|
50
|
+
- filepath_regex: "test/.*|examples/.*"
|
|
51
|
+
- filepath_regex: "policies/readonly-.*"
|
|
52
|
+
action_matches: ".*:(Get|List|Describe).*"
|
|
53
|
+
- statement_sid: "AllowReadOnlyAccess"
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def should_ignore(self, issue: ValidationIssue, filepath: str = "") -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if issue should be ignored based on ignore patterns.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
issue: The validation issue to check
|
|
62
|
+
filepath: Path to the policy file
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if the issue should be ignored
|
|
66
|
+
"""
|
|
67
|
+
if not self.ignore_patterns:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
import re
|
|
71
|
+
|
|
72
|
+
for pattern in self.ignore_patterns:
|
|
73
|
+
if self._matches_pattern(pattern, issue, filepath, re):
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _matches_pattern(
|
|
79
|
+
self,
|
|
80
|
+
pattern: dict[str, Any],
|
|
81
|
+
issue: ValidationIssue,
|
|
82
|
+
filepath: str,
|
|
83
|
+
re_module: Any,
|
|
84
|
+
) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Check if issue matches a single ignore pattern.
|
|
87
|
+
|
|
88
|
+
All fields in pattern must match (AND logic).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
pattern: Pattern dict with optional fields
|
|
92
|
+
issue: ValidationIssue to check
|
|
93
|
+
filepath: Path to policy file
|
|
94
|
+
re_module: re module for regex matching
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if all fields in pattern match the issue
|
|
98
|
+
"""
|
|
99
|
+
for field_name, regex_pattern in pattern.items():
|
|
100
|
+
actual_value = None
|
|
101
|
+
|
|
102
|
+
if field_name == "filepath_regex":
|
|
103
|
+
actual_value = filepath
|
|
104
|
+
elif field_name == "action_matches":
|
|
105
|
+
actual_value = issue.action or ""
|
|
106
|
+
elif field_name == "resource_matches":
|
|
107
|
+
actual_value = issue.resource or ""
|
|
108
|
+
elif field_name == "statement_sid":
|
|
109
|
+
# For SID, support both exact match and regex
|
|
110
|
+
if isinstance(regex_pattern, str) and "*" in regex_pattern:
|
|
111
|
+
# Treat as regex if contains wildcard
|
|
112
|
+
actual_value = issue.statement_sid or ""
|
|
113
|
+
else:
|
|
114
|
+
# Exact match
|
|
115
|
+
if issue.statement_sid != regex_pattern:
|
|
116
|
+
return False
|
|
117
|
+
continue
|
|
118
|
+
elif field_name == "condition_key_matches":
|
|
119
|
+
actual_value = issue.condition_key or ""
|
|
120
|
+
else:
|
|
121
|
+
# Unknown field, skip
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Check regex match (case-insensitive)
|
|
125
|
+
if actual_value is None:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if not re_module.search(
|
|
130
|
+
str(regex_pattern),
|
|
131
|
+
str(actual_value),
|
|
132
|
+
re_module.IGNORECASE,
|
|
133
|
+
):
|
|
134
|
+
return False
|
|
135
|
+
except re_module.error:
|
|
136
|
+
# Invalid regex, don't match
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
return True # All fields matched
|
|
34
140
|
|
|
35
141
|
|
|
36
142
|
class PolicyCheck(ABC):
|
|
@@ -264,6 +370,7 @@ class CheckRegistry:
|
|
|
264
370
|
statement: Statement,
|
|
265
371
|
statement_idx: int,
|
|
266
372
|
fetcher: AWSServiceFetcher,
|
|
373
|
+
filepath: str = "",
|
|
267
374
|
) -> list[ValidationIssue]:
|
|
268
375
|
"""
|
|
269
376
|
Execute all enabled checks in parallel for maximum performance.
|
|
@@ -275,9 +382,10 @@ class CheckRegistry:
|
|
|
275
382
|
statement: The IAM policy statement to validate
|
|
276
383
|
statement_idx: Index of the statement in the policy
|
|
277
384
|
fetcher: AWS service fetcher for API calls
|
|
385
|
+
filepath: Path to the policy file (for ignore_patterns filtering)
|
|
278
386
|
|
|
279
387
|
Returns:
|
|
280
|
-
List of all ValidationIssue objects from all checks
|
|
388
|
+
List of all ValidationIssue objects from all checks (filtered by ignore_patterns)
|
|
281
389
|
"""
|
|
282
390
|
enabled_checks = self.get_enabled_checks()
|
|
283
391
|
|
|
@@ -291,21 +399,27 @@ class CheckRegistry:
|
|
|
291
399
|
config = self.get_config(check.check_id)
|
|
292
400
|
if config:
|
|
293
401
|
issues = await check.execute(statement, statement_idx, fetcher, config)
|
|
294
|
-
|
|
402
|
+
# Filter issues based on ignore_patterns
|
|
403
|
+
filtered_issues = [
|
|
404
|
+
issue for issue in issues if not config.should_ignore(issue, filepath)
|
|
405
|
+
]
|
|
406
|
+
all_issues.extend(filtered_issues)
|
|
295
407
|
return all_issues
|
|
296
408
|
|
|
297
409
|
# Execute all checks in parallel
|
|
298
410
|
tasks = []
|
|
411
|
+
configs = []
|
|
299
412
|
for check in enabled_checks:
|
|
300
413
|
config = self.get_config(check.check_id)
|
|
301
414
|
if config:
|
|
302
415
|
task = check.execute(statement, statement_idx, fetcher, config)
|
|
303
416
|
tasks.append(task)
|
|
417
|
+
configs.append(config)
|
|
304
418
|
|
|
305
419
|
# Wait for all checks to complete
|
|
306
420
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
307
421
|
|
|
308
|
-
# Collect all issues, handling any exceptions
|
|
422
|
+
# Collect all issues, handling any exceptions and applying ignore_patterns
|
|
309
423
|
all_issues = []
|
|
310
424
|
for idx, result in enumerate(results):
|
|
311
425
|
if isinstance(result, Exception):
|
|
@@ -313,7 +427,12 @@ class CheckRegistry:
|
|
|
313
427
|
check = enabled_checks[idx]
|
|
314
428
|
print(f"Warning: Check '{check.check_id}' failed: {result}")
|
|
315
429
|
elif isinstance(result, list):
|
|
316
|
-
|
|
430
|
+
config = configs[idx]
|
|
431
|
+
# Filter issues based on ignore_patterns
|
|
432
|
+
filtered_issues = [
|
|
433
|
+
issue for issue in result if not config.should_ignore(issue, filepath)
|
|
434
|
+
]
|
|
435
|
+
all_issues.extend(filtered_issues)
|
|
317
436
|
|
|
318
437
|
return all_issues
|
|
319
438
|
|
|
@@ -442,22 +561,47 @@ def create_default_registry(
|
|
|
442
561
|
# Import and register built-in checks
|
|
443
562
|
from iam_validator import checks
|
|
444
563
|
|
|
445
|
-
|
|
446
|
-
registry.register(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
registry.register(checks.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
registry.register(checks.
|
|
453
|
-
registry.register(checks.
|
|
454
|
-
registry.register(checks.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
registry.register(checks.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
#
|
|
461
|
-
|
|
564
|
+
# 1. POLICY STRUCTURE (Checks that examine the entire policy, not individual statements)
|
|
565
|
+
registry.register(
|
|
566
|
+
checks.SidUniquenessCheck()
|
|
567
|
+
) # Policy-level: Duplicate SID detection across statements
|
|
568
|
+
registry.register(checks.PolicySizeCheck()) # Policy-level: Size limit validation
|
|
569
|
+
|
|
570
|
+
# 2. IAM VALIDITY (AWS syntax validation - must pass before deeper checks)
|
|
571
|
+
registry.register(checks.ActionValidationCheck()) # Validate actions against AWS API
|
|
572
|
+
registry.register(checks.ResourceValidationCheck()) # Validate resource ARNs
|
|
573
|
+
registry.register(checks.ConditionKeyValidationCheck()) # Validate condition keys
|
|
574
|
+
|
|
575
|
+
# 3. TYPE VALIDATION (Condition operator type checking)
|
|
576
|
+
registry.register(checks.ConditionTypeMismatchCheck()) # Operator-value type compatibility
|
|
577
|
+
registry.register(checks.SetOperatorValidationCheck()) # ForAllValues/ForAnyValue usage
|
|
578
|
+
|
|
579
|
+
# 4. RESOURCE MATCHING (Action-resource relationship validation)
|
|
580
|
+
registry.register(
|
|
581
|
+
checks.ActionResourceMatchingCheck()
|
|
582
|
+
) # ARN type matching and resource constraints
|
|
583
|
+
|
|
584
|
+
# 5. SECURITY - WILDCARDS (Security best practices for wildcards)
|
|
585
|
+
registry.register(checks.WildcardActionCheck()) # Wildcard action detection
|
|
586
|
+
registry.register(checks.WildcardResourceCheck()) # Wildcard resource detection
|
|
587
|
+
registry.register(checks.FullWildcardCheck()) # Full wildcard (*) detection
|
|
588
|
+
registry.register(checks.ServiceWildcardCheck()) # Service-level wildcard detection
|
|
589
|
+
|
|
590
|
+
# 6. SECURITY - ADVANCED (Sensitive actions and condition enforcement)
|
|
591
|
+
registry.register(
|
|
592
|
+
checks.SensitiveActionCheck()
|
|
593
|
+
) # Policy-level: Privilege escalation detection (all_of across statements)
|
|
594
|
+
registry.register(
|
|
595
|
+
checks.ActionConditionEnforcementCheck()
|
|
596
|
+
) # Statement + Policy-level: Condition enforcement (any_of/all_of/none_of)
|
|
597
|
+
registry.register(checks.MFAConditionCheck()) # MFA anti-pattern detection
|
|
598
|
+
|
|
599
|
+
# 7. PRINCIPAL VALIDATION (Resource policy specific)
|
|
600
|
+
registry.register(
|
|
601
|
+
checks.PrincipalValidationCheck()
|
|
602
|
+
) # Principal validation (resource policies)
|
|
603
|
+
|
|
604
|
+
# Note: policy_type_validation is a standalone function (not a class-based check)
|
|
605
|
+
# and is called separately in the validation flow
|
|
462
606
|
|
|
463
607
|
return registry
|