iam-policy-validator 1.14.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.
Files changed (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -0
@@ -0,0 +1,64 @@
1
+ """Markdown formatter - placeholder for existing functionality."""
2
+
3
+ from iam_validator.core import constants
4
+ from iam_validator.core.formatters.base import OutputFormatter
5
+ from iam_validator.core.models import ValidationReport
6
+
7
+
8
+ class MarkdownFormatter(OutputFormatter):
9
+ """Markdown formatter for GitHub comments and documentation."""
10
+
11
+ @property
12
+ def format_id(self) -> str:
13
+ return "markdown"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "GitHub-flavored markdown for PR comments"
18
+
19
+ @property
20
+ def file_extension(self) -> str:
21
+ return "md"
22
+
23
+ @property
24
+ def content_type(self) -> str:
25
+ return "text/markdown"
26
+
27
+ def format(self, report: ValidationReport, **kwargs) -> str:
28
+ """Format as Markdown.
29
+
30
+ Note: The primary markdown generation is handled by ReportGenerator.generate_github_comment().
31
+ This is a simplified formatter for the registry system.
32
+ """
33
+ # Count issues by severity - support both IAM validity and security severities
34
+ errors = sum(
35
+ 1
36
+ for r in report.results
37
+ for i in r.issues
38
+ if i.severity in constants.HIGH_SEVERITY_LEVELS
39
+ )
40
+ warnings = sum(
41
+ 1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
42
+ )
43
+ infos = sum(1 for r in report.results for i in r.issues if i.severity in ("info", "low"))
44
+
45
+ output = [
46
+ "# IAM Policy Validation Report\n",
47
+ "## Summary",
48
+ f"**Total Policies:** {report.total_policies}",
49
+ f"**Valid (IAM):** {report.valid_policies} ✅",
50
+ f"**Invalid (IAM):** {report.invalid_policies} ❌",
51
+ f"**Security Findings:** {report.policies_with_security_issues} ⚠️",
52
+ "",
53
+ "## Issue Breakdown",
54
+ f"**Total Issues:** {report.total_issues}",
55
+ f"**Validity Issues:** {report.validity_issues} (error/warning/info)",
56
+ f"**Security Issues:** {report.security_issues} (critical/high/medium/low)",
57
+ "",
58
+ "## Legacy Severity Counts",
59
+ f"**Errors:** {errors}",
60
+ f"**Warnings:** {warnings}",
61
+ f"**Info:** {infos}\n",
62
+ ]
63
+
64
+ return "\n".join(output)
@@ -0,0 +1,251 @@
1
+ """SARIF (Static Analysis Results Interchange Format) formatter for GitHub integration."""
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+ from iam_validator.core.formatters.base import OutputFormatter
8
+ from iam_validator.core.models import ValidationIssue, ValidationReport
9
+
10
+
11
+ class SARIFFormatter(OutputFormatter):
12
+ """Formats validation results in SARIF format for GitHub code scanning."""
13
+
14
+ @property
15
+ def format_id(self) -> str:
16
+ return "sarif"
17
+
18
+ @property
19
+ def description(self) -> str:
20
+ return "SARIF format for GitHub code scanning integration"
21
+
22
+ @property
23
+ def file_extension(self) -> str:
24
+ return "sarif"
25
+
26
+ @property
27
+ def content_type(self) -> str:
28
+ return "application/sarif+json"
29
+
30
+ def format(self, report: ValidationReport, **kwargs) -> str:
31
+ """Format report as SARIF.
32
+
33
+ Args:
34
+ report: The validation report
35
+ **kwargs: Additional options like 'tool_version'
36
+
37
+ Returns:
38
+ SARIF JSON string
39
+ """
40
+ sarif = self._create_sarif_output(report, **kwargs)
41
+ return json.dumps(sarif, indent=2)
42
+
43
+ def _create_sarif_output(self, report: ValidationReport, **kwargs) -> dict[str, Any]:
44
+ """Create SARIF output structure."""
45
+ tool_version = kwargs.get("tool_version", "1.0.0")
46
+
47
+ # Map severity levels to SARIF - support both IAM validity and security severities
48
+ severity_map = {
49
+ "error": "error",
50
+ "critical": "error",
51
+ "high": "error",
52
+ "warning": "warning",
53
+ "medium": "warning",
54
+ "info": "note",
55
+ "low": "note",
56
+ }
57
+
58
+ # Create SARIF structure
59
+ sarif = {
60
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
61
+ "version": "2.1.0",
62
+ "runs": [
63
+ {
64
+ "tool": {
65
+ "driver": {
66
+ "name": "IAM Validator",
67
+ "version": tool_version,
68
+ "informationUri": "https://github.com/boogy/iam-validator",
69
+ "rules": self._create_rules(),
70
+ }
71
+ },
72
+ "results": self._create_results(report, severity_map),
73
+ "invocations": [
74
+ {
75
+ "executionSuccessful": len([r for r in report.results if r.is_valid])
76
+ > 0,
77
+ "endTimeUtc": datetime.now(timezone.utc).isoformat(),
78
+ }
79
+ ],
80
+ }
81
+ ],
82
+ }
83
+
84
+ return sarif
85
+
86
+ def _create_rules(self) -> list[dict[str, Any]]:
87
+ """Create SARIF rules definitions."""
88
+ return [
89
+ {
90
+ "id": "invalid-action",
91
+ "shortDescription": {"text": "Invalid IAM Action"},
92
+ "fullDescription": {"text": "The specified IAM action does not exist in AWS"},
93
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html",
94
+ "defaultConfiguration": {"level": "error"},
95
+ },
96
+ {
97
+ "id": "invalid-condition-key",
98
+ "shortDescription": {"text": "Invalid Condition Key"},
99
+ "fullDescription": {
100
+ "text": "The specified condition key is not valid for this action"
101
+ },
102
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
103
+ "defaultConfiguration": {"level": "error"},
104
+ },
105
+ {
106
+ "id": "invalid-resource",
107
+ "shortDescription": {"text": "Invalid Resource ARN"},
108
+ "fullDescription": {"text": "The resource ARN format is invalid"},
109
+ "helpUri": "https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html",
110
+ "defaultConfiguration": {"level": "error"},
111
+ },
112
+ {
113
+ "id": "duplicate-sid",
114
+ "shortDescription": {"text": "Duplicate Statement ID"},
115
+ "fullDescription": {
116
+ "text": "Multiple statements use the same Statement ID (Sid), which can cause confusion"
117
+ },
118
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html",
119
+ "defaultConfiguration": {"level": "error"},
120
+ },
121
+ {
122
+ "id": "overly-permissive",
123
+ "shortDescription": {"text": "Overly Permissive Policy"},
124
+ "fullDescription": {
125
+ "text": "Policy grants overly broad permissions using wildcards in actions or resources"
126
+ },
127
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege",
128
+ "defaultConfiguration": {"level": "warning"},
129
+ },
130
+ {
131
+ "id": "missing-condition",
132
+ "shortDescription": {"text": "Missing Condition Restrictions"},
133
+ "fullDescription": {
134
+ "text": "Sensitive actions should include condition restrictions to limit when they can be used"
135
+ },
136
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-policy-conditions",
137
+ "defaultConfiguration": {"level": "warning"},
138
+ },
139
+ {
140
+ "id": "missing-required-condition",
141
+ "shortDescription": {"text": "Missing Required Condition"},
142
+ "fullDescription": {
143
+ "text": "Specific actions require certain conditions to prevent privilege escalation or security issues"
144
+ },
145
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html",
146
+ "defaultConfiguration": {"level": "error"},
147
+ },
148
+ {
149
+ "id": "invalid-principal",
150
+ "shortDescription": {"text": "Invalid Principal"},
151
+ "fullDescription": {
152
+ "text": "The specified principal is invalid or improperly formatted"
153
+ },
154
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html",
155
+ "defaultConfiguration": {"level": "error"},
156
+ },
157
+ {
158
+ "id": "general-issue",
159
+ "shortDescription": {"text": "IAM Policy Issue"},
160
+ "fullDescription": {"text": "General IAM policy validation issue"},
161
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html",
162
+ "defaultConfiguration": {"level": "warning"},
163
+ },
164
+ ]
165
+
166
+ def _create_results(
167
+ self, report: ValidationReport, severity_map: dict[str, str]
168
+ ) -> list[dict[str, Any]]:
169
+ """Create SARIF results from validation issues."""
170
+ results = []
171
+
172
+ for policy_result in report.results:
173
+ if not policy_result.issues:
174
+ continue
175
+
176
+ for issue in policy_result.issues:
177
+ result = {
178
+ "ruleId": self._get_rule_id(issue),
179
+ "level": severity_map.get(issue.severity, "note"),
180
+ "message": {"text": issue.message},
181
+ "locations": [
182
+ {
183
+ "physicalLocation": {
184
+ "artifactLocation": {
185
+ "uri": policy_result.policy_file,
186
+ "uriBaseId": "SRCROOT",
187
+ },
188
+ "region": {
189
+ "startLine": issue.line_number or 1,
190
+ "startColumn": 1,
191
+ },
192
+ }
193
+ }
194
+ ],
195
+ }
196
+
197
+ # Add fix suggestions if available
198
+ if issue.suggestion:
199
+ result["fixes"] = [
200
+ {
201
+ "description": {"text": issue.suggestion},
202
+ }
203
+ ]
204
+
205
+ results.append(result)
206
+
207
+ return results
208
+
209
+ def _get_rule_id(self, issue: ValidationIssue) -> str:
210
+ """Map issue to SARIF rule ID.
211
+
212
+ Uses the issue_type field directly, converting underscores to hyphens
213
+ for SARIF rule ID format. Falls back to heuristic matching for unknown types.
214
+ """
215
+ # Map common issue types directly
216
+ issue_type_map = {
217
+ "invalid_action": "invalid-action",
218
+ "invalid_condition_key": "invalid-condition-key",
219
+ "invalid_resource": "invalid-resource",
220
+ "duplicate_sid": "duplicate-sid",
221
+ "overly_permissive": "overly-permissive",
222
+ "missing_condition": "missing-condition",
223
+ "missing_required_condition": "missing-required-condition",
224
+ "invalid_principal": "invalid-principal",
225
+ }
226
+
227
+ # Try direct mapping from issue_type
228
+ if issue.issue_type in issue_type_map:
229
+ return issue_type_map[issue.issue_type]
230
+
231
+ # Fallback: heuristic matching based on message
232
+ message_lower = issue.message.lower()
233
+
234
+ if "action" in message_lower and "not found" in message_lower:
235
+ return "invalid-action"
236
+ elif "condition key" in message_lower:
237
+ return "invalid-condition-key"
238
+ elif "duplicate" in message_lower and "sid" in message_lower:
239
+ return "duplicate-sid"
240
+ elif "wildcard" in message_lower or "overly permissive" in message_lower:
241
+ return "overly-permissive"
242
+ elif "missing" in message_lower and "condition" in message_lower:
243
+ if "required" in message_lower:
244
+ return "missing-required-condition"
245
+ return "missing-condition"
246
+ elif "principal" in message_lower:
247
+ return "invalid-principal"
248
+ elif "resource" in message_lower or "arn" in message_lower:
249
+ return "invalid-resource"
250
+ else:
251
+ return "general-issue"
@@ -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)