iam-policy-validator 1.7.1__py3-none-any.whl → 1.8.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.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
- iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
- iam_validator/__version__.py +4 -2
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +81 -36
- iam_validator/checks/action_resource_matching.py +75 -37
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +10 -8
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +86 -150
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +9 -11
- iam_validator/checks/service_wildcard.py +4 -10
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +5 -9
- iam_validator/checks/wildcard_resource.py +5 -9
- iam_validator/commands/validate.py +8 -14
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +159 -64
- iam_validator/core/check_registry.py +83 -79
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +74 -59
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/constants.py +57 -0
- iam_validator/core/formatters/console.py +10 -1
- iam_validator/core/formatters/csv.py +2 -1
- iam_validator/core/formatters/enhanced.py +42 -8
- iam_validator/core/formatters/markdown.py +2 -1
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +35 -10
- iam_validator/core/policy_checks.py +34 -474
- iam_validator/core/policy_loader.py +98 -18
- iam_validator/core/report.py +65 -24
- iam_validator/integrations/github_integration.py +4 -5
- iam_validator/utils/__init__.py +4 -0
- iam_validator/utils/terminal.py +22 -0
- iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
@@ -62,6 +62,21 @@ DEFAULT_CONFIG_FILENAMES = [
|
|
|
62
62
|
".iam-validator.yml",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# Severity Levels
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# Severity level groupings for filtering and categorization
|
|
69
|
+
# Used across formatters and report generation
|
|
70
|
+
|
|
71
|
+
# High severity issues that typically fail validation
|
|
72
|
+
HIGH_SEVERITY_LEVELS = ("error", "critical", "high")
|
|
73
|
+
|
|
74
|
+
# Medium severity issues (warnings)
|
|
75
|
+
MEDIUM_SEVERITY_LEVELS = ("warning", "medium")
|
|
76
|
+
|
|
77
|
+
# Low severity issues (informational)
|
|
78
|
+
LOW_SEVERITY_LEVELS = ("info", "low")
|
|
79
|
+
|
|
65
80
|
# ============================================================================
|
|
66
81
|
# GitHub Integration
|
|
67
82
|
# ============================================================================
|
|
@@ -72,3 +87,45 @@ BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
|
72
87
|
# HTML comment markers for identifying bot-generated content (for cleanup/updates)
|
|
73
88
|
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
74
89
|
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
90
|
+
|
|
91
|
+
# GitHub comment size limits
|
|
92
|
+
# GitHub's actual limit is 65536 characters, but we use a smaller limit for safety
|
|
93
|
+
GITHUB_MAX_COMMENT_LENGTH = 65000 # Maximum single comment length
|
|
94
|
+
GITHUB_COMMENT_SPLIT_LIMIT = 60000 # Target size when splitting into multiple parts
|
|
95
|
+
|
|
96
|
+
# Comment size estimation parameters (used for multi-part comment splitting)
|
|
97
|
+
COMMENT_BASE_OVERHEAD_CHARS = 2000 # Base overhead for headers/footers
|
|
98
|
+
COMMENT_CHARS_PER_ISSUE_ESTIMATE = 500 # Average characters per issue
|
|
99
|
+
COMMENT_CONTINUATION_OVERHEAD_CHARS = 200 # Overhead for continuation markers
|
|
100
|
+
FORMATTING_SAFETY_BUFFER = 100 # Safety buffer for formatting calculations
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Console Display Settings
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
# Panel width for formatted console output
|
|
107
|
+
CONSOLE_PANEL_WIDTH = 100
|
|
108
|
+
|
|
109
|
+
# Rich console color styles
|
|
110
|
+
CONSOLE_HEADER_COLOR = "bright_blue"
|
|
111
|
+
|
|
112
|
+
# ============================================================================
|
|
113
|
+
# Cache and Timeout Settings
|
|
114
|
+
# ============================================================================
|
|
115
|
+
|
|
116
|
+
# Cache TTL (Time To Live) - 7 days
|
|
117
|
+
DEFAULT_CACHE_TTL_HOURS = 168 # 7 days in hours
|
|
118
|
+
DEFAULT_CACHE_TTL_SECONDS = 604800 # 7 days in seconds (168 * 3600)
|
|
119
|
+
|
|
120
|
+
# HTTP request timeout in seconds
|
|
121
|
+
DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
|
|
122
|
+
|
|
123
|
+
# Time conversion constants
|
|
124
|
+
SECONDS_PER_HOUR = 3600
|
|
125
|
+
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# AWS Documentation URLs
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
# AWS Service Authorization Reference (for finding valid actions, resources, and condition keys)
|
|
131
|
+
AWS_SERVICE_AUTH_REF_URL = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
|
|
@@ -39,8 +39,17 @@ class ConsoleFormatter(OutputFormatter):
|
|
|
39
39
|
color = kwargs.get("color", True)
|
|
40
40
|
|
|
41
41
|
# Capture the output from print_console_report
|
|
42
|
+
from iam_validator.utils import get_terminal_width
|
|
43
|
+
|
|
42
44
|
string_buffer = StringIO()
|
|
43
|
-
|
|
45
|
+
# Get terminal width for proper table column spacing
|
|
46
|
+
terminal_width = get_terminal_width()
|
|
47
|
+
console = Console(
|
|
48
|
+
file=string_buffer,
|
|
49
|
+
force_terminal=color,
|
|
50
|
+
width=terminal_width,
|
|
51
|
+
legacy_windows=False,
|
|
52
|
+
)
|
|
44
53
|
|
|
45
54
|
# Create a generator instance with our custom console
|
|
46
55
|
generator = ReportGenerator()
|
|
@@ -4,6 +4,7 @@ import csv
|
|
|
4
4
|
import io
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from iam_validator.core import constants
|
|
7
8
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
8
9
|
from iam_validator.core.models import ValidationReport
|
|
9
10
|
|
|
@@ -58,7 +59,7 @@ class CSVFormatter(OutputFormatter):
|
|
|
58
59
|
1
|
|
59
60
|
for r in report.results
|
|
60
61
|
for i in r.issues
|
|
61
|
-
if i.severity in
|
|
62
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
62
63
|
)
|
|
63
64
|
warnings = sum(
|
|
64
65
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
@@ -10,6 +10,7 @@ from rich.text import Text
|
|
|
10
10
|
from rich.tree import Tree
|
|
11
11
|
|
|
12
12
|
from iam_validator.__version__ import __version__
|
|
13
|
+
from iam_validator.core import constants
|
|
13
14
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
14
15
|
from iam_validator.core.models import PolicyValidationResult, ValidationReport
|
|
15
16
|
|
|
@@ -50,8 +51,14 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
50
51
|
show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
|
|
51
52
|
|
|
52
53
|
# Use StringIO to capture Rich console output
|
|
54
|
+
from iam_validator.utils import get_terminal_width
|
|
55
|
+
|
|
53
56
|
string_buffer = StringIO()
|
|
54
|
-
|
|
57
|
+
# Get terminal width for proper text wrapping
|
|
58
|
+
terminal_width = get_terminal_width()
|
|
59
|
+
console = Console(
|
|
60
|
+
file=string_buffer, force_terminal=color, width=terminal_width, legacy_windows=False
|
|
61
|
+
)
|
|
55
62
|
|
|
56
63
|
# Header with title
|
|
57
64
|
console.print()
|
|
@@ -60,7 +67,14 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
60
67
|
style="bold cyan",
|
|
61
68
|
justify="center",
|
|
62
69
|
)
|
|
63
|
-
console.print(
|
|
70
|
+
console.print(
|
|
71
|
+
Panel(
|
|
72
|
+
title,
|
|
73
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
74
|
+
padding=(1, 0),
|
|
75
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
64
78
|
console.print()
|
|
65
79
|
|
|
66
80
|
# Executive Summary with progress bars (optional)
|
|
@@ -73,7 +87,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
73
87
|
self._print_severity_breakdown(console, report)
|
|
74
88
|
|
|
75
89
|
console.print()
|
|
76
|
-
console.print(Rule(title="[bold]Detailed Results", style=
|
|
90
|
+
console.print(Rule(title="[bold]Detailed Results", style=constants.CONSOLE_HEADER_COLOR))
|
|
77
91
|
console.print()
|
|
78
92
|
|
|
79
93
|
# Detailed results using tree structure
|
|
@@ -140,8 +154,9 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
140
154
|
Panel(
|
|
141
155
|
metrics_table,
|
|
142
156
|
title="📊 Executive Summary",
|
|
143
|
-
border_style=
|
|
157
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
144
158
|
padding=(1, 2),
|
|
159
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
145
160
|
)
|
|
146
161
|
)
|
|
147
162
|
|
|
@@ -225,7 +240,12 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
225
240
|
)
|
|
226
241
|
|
|
227
242
|
console.print(
|
|
228
|
-
Panel(
|
|
243
|
+
Panel(
|
|
244
|
+
severity_table,
|
|
245
|
+
title="🎯 Issue Severity Breakdown",
|
|
246
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
247
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
248
|
+
)
|
|
229
249
|
)
|
|
230
250
|
|
|
231
251
|
def _format_policy_result_modern(
|
|
@@ -247,7 +267,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
247
267
|
elif result.is_valid and result.issues:
|
|
248
268
|
# Valid IAM policy but has security findings
|
|
249
269
|
# Check severity to determine the appropriate status
|
|
250
|
-
has_critical = any(i.severity in
|
|
270
|
+
has_critical = any(i.severity in constants.HIGH_SEVERITY_LEVELS for i in result.issues)
|
|
251
271
|
if has_critical:
|
|
252
272
|
icon = "⚠️"
|
|
253
273
|
color = "red"
|
|
@@ -386,6 +406,13 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
386
406
|
suggestion_text.append(issue.suggestion, style="italic yellow")
|
|
387
407
|
msg_node.add(suggestion_text)
|
|
388
408
|
|
|
409
|
+
# Example (if present, show with indentation)
|
|
410
|
+
if issue.example:
|
|
411
|
+
msg_node.add(Text("Example:", style="bold cyan"))
|
|
412
|
+
# Show example code with syntax highlighting
|
|
413
|
+
example_text = Text(issue.example, style="dim")
|
|
414
|
+
msg_node.add(example_text)
|
|
415
|
+
|
|
389
416
|
def _print_final_status(self, console: Console, report: ValidationReport) -> None:
|
|
390
417
|
"""Print final status panel."""
|
|
391
418
|
if report.invalid_policies == 0 and report.total_issues == 0:
|
|
@@ -400,7 +427,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
400
427
|
# Valid IAM policies but may have security findings
|
|
401
428
|
# Check if there are critical/high security issues
|
|
402
429
|
has_critical = any(
|
|
403
|
-
i.severity in
|
|
430
|
+
i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
404
431
|
for r in report.results
|
|
405
432
|
for i in r.issues
|
|
406
433
|
)
|
|
@@ -437,4 +464,11 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
437
464
|
final_text.append("\n\n")
|
|
438
465
|
final_text.append(message)
|
|
439
466
|
|
|
440
|
-
console.print(
|
|
467
|
+
console.print(
|
|
468
|
+
Panel(
|
|
469
|
+
final_text,
|
|
470
|
+
border_style=border_color,
|
|
471
|
+
padding=(1, 2),
|
|
472
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
473
|
+
)
|
|
474
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Markdown formatter - placeholder for existing functionality."""
|
|
2
2
|
|
|
3
|
+
from iam_validator.core import constants
|
|
3
4
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
4
5
|
from iam_validator.core.models import ValidationReport
|
|
5
6
|
|
|
@@ -34,7 +35,7 @@ class MarkdownFormatter(OutputFormatter):
|
|
|
34
35
|
1
|
|
35
36
|
for r in report.results
|
|
36
37
|
for i in r.issues
|
|
37
|
-
if i.severity in
|
|
38
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
38
39
|
)
|
|
39
40
|
warnings = sum(
|
|
40
41
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
@@ -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)
|