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.
- iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- 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 +641 -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 +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -0
- iam_validator/sdk/arn_matching.py +382 -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 +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- 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)
|