iam-policy-validator 1.6.0__py3-none-any.whl → 1.7.1__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/METADATA +429 -0
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/RECORD +32 -31
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +3 -1
- iam_validator/checks/action_resource_matching.py +23 -6
- iam_validator/checks/full_wildcard.py +5 -1
- iam_validator/checks/policy_size.py +3 -7
- iam_validator/checks/policy_type_validation.py +9 -3
- iam_validator/checks/principal_validation.py +1 -1
- iam_validator/checks/resource_validation.py +54 -24
- iam_validator/checks/sensitive_action.py +5 -1
- iam_validator/checks/service_wildcard.py +3 -1
- iam_validator/checks/utils/sensitive_action_matcher.py +1 -2
- iam_validator/checks/utils/wildcard_expansion.py +1 -2
- iam_validator/checks/wildcard_action.py +7 -2
- iam_validator/checks/wildcard_resource.py +5 -1
- iam_validator/commands/analyze.py +98 -1
- iam_validator/commands/validate.py +4 -2
- iam_validator/core/access_analyzer.py +5 -0
- iam_validator/core/access_analyzer_report.py +2 -5
- iam_validator/core/aws_fetcher.py +14 -4
- iam_validator/core/config/config_loader.py +3 -6
- iam_validator/core/constants.py +74 -0
- iam_validator/core/models.py +29 -13
- iam_validator/core/pr_commenter.py +104 -18
- iam_validator/core/report.py +49 -36
- iam_validator/integrations/github_integration.py +21 -1
- iam_validator/sdk/arn_matching.py +108 -0
- iam_validator/utils/regex.py +7 -8
- iam_policy_validator-1.6.0.dist-info/METADATA +0 -1050
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING
|
|
|
16
16
|
|
|
17
17
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
18
18
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
19
|
+
from iam_validator.core.constants import AWS_POLICY_SIZE_LIMITS
|
|
19
20
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
@@ -25,13 +26,8 @@ if TYPE_CHECKING:
|
|
|
25
26
|
class PolicySizeCheck(PolicyCheck):
|
|
26
27
|
"""Validates that IAM policies don't exceed AWS size limits."""
|
|
27
28
|
|
|
28
|
-
# AWS IAM policy size limits (
|
|
29
|
-
DEFAULT_LIMITS =
|
|
30
|
-
"managed": 6144,
|
|
31
|
-
"inline_user": 2048,
|
|
32
|
-
"inline_group": 5120,
|
|
33
|
-
"inline_role": 10240,
|
|
34
|
-
}
|
|
29
|
+
# AWS IAM policy size limits (loaded from constants module)
|
|
30
|
+
DEFAULT_LIMITS = AWS_POLICY_SIZE_LIMITS
|
|
35
31
|
|
|
36
32
|
@property
|
|
37
33
|
def check_id(self) -> str:
|
|
@@ -71,6 +71,7 @@ async def execute_policy(
|
|
|
71
71
|
line_number=statement.line_number,
|
|
72
72
|
suggestion="Add a Principal element to specify who can access this resource.\n"
|
|
73
73
|
"Example:\n"
|
|
74
|
+
"```json\n"
|
|
74
75
|
"{\n"
|
|
75
76
|
' "Effect": "Allow",\n'
|
|
76
77
|
' "Principal": {\n'
|
|
@@ -78,7 +79,8 @@ async def execute_policy(
|
|
|
78
79
|
" },\n"
|
|
79
80
|
' "Action": "s3:GetObject",\n'
|
|
80
81
|
' "Resource": "arn:aws:s3:::bucket/*"\n'
|
|
81
|
-
"}"
|
|
82
|
+
"}\n"
|
|
83
|
+
"```",
|
|
82
84
|
)
|
|
83
85
|
)
|
|
84
86
|
|
|
@@ -101,11 +103,13 @@ async def execute_policy(
|
|
|
101
103
|
line_number=statement.line_number,
|
|
102
104
|
suggestion="Remove the Principal element from this identity policy statement.\n"
|
|
103
105
|
"Example:\n"
|
|
106
|
+
"```json\n"
|
|
104
107
|
"{\n"
|
|
105
108
|
' "Effect": "Allow",\n'
|
|
106
109
|
' "Action": "s3:GetObject",\n'
|
|
107
110
|
' "Resource": "arn:aws:s3:::bucket/*"\n'
|
|
108
|
-
"}"
|
|
111
|
+
"}\n"
|
|
112
|
+
"```",
|
|
109
113
|
)
|
|
110
114
|
)
|
|
111
115
|
|
|
@@ -127,6 +131,7 @@ async def execute_policy(
|
|
|
127
131
|
line_number=statement.line_number,
|
|
128
132
|
suggestion="Remove the Principal element from this SCP statement.\n"
|
|
129
133
|
"Example:\n"
|
|
134
|
+
"```json\n"
|
|
130
135
|
"{\n"
|
|
131
136
|
' "Effect": "Deny",\n'
|
|
132
137
|
' "Action": "ec2:*",\n'
|
|
@@ -136,7 +141,8 @@ async def execute_policy(
|
|
|
136
141
|
' "ec2:Region": ["us-east-1", "us-west-2"]\n'
|
|
137
142
|
" }\n"
|
|
138
143
|
" }\n"
|
|
139
|
-
"}"
|
|
144
|
+
"}\n"
|
|
145
|
+
"```",
|
|
140
146
|
)
|
|
141
147
|
)
|
|
142
148
|
|
|
@@ -668,7 +668,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
668
668
|
|
|
669
669
|
# Build example based on condition key type
|
|
670
670
|
if example:
|
|
671
|
-
parts.append(f"Example:\n{example}")
|
|
671
|
+
parts.append(f"Example:\n```json\n{example}\n```")
|
|
672
672
|
else:
|
|
673
673
|
# Auto-generate example
|
|
674
674
|
example_lines = ['Add to "Condition" block:', f' "{operator}": {{']
|
|
@@ -4,15 +4,17 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
6
6
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
7
|
+
from iam_validator.core.constants import DEFAULT_ARN_VALIDATION_PATTERN, MAX_ARN_LENGTH
|
|
7
8
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
9
|
+
from iam_validator.sdk.arn_matching import (
|
|
10
|
+
has_template_variables,
|
|
11
|
+
normalize_template_variables,
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class ResourceValidationCheck(PolicyCheck):
|
|
11
16
|
"""Validates ARN format for resources."""
|
|
12
17
|
|
|
13
|
-
# Maximum allowed length for ARN to prevent ReDoS attacks
|
|
14
|
-
MAX_ARN_LENGTH = 2048 # AWS max ARN length is ~2048 characters
|
|
15
|
-
|
|
16
18
|
@property
|
|
17
19
|
def check_id(self) -> str:
|
|
18
20
|
return "resource_validation"
|
|
@@ -42,17 +44,21 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
42
44
|
|
|
43
45
|
# Get ARN pattern from config, or use default
|
|
44
46
|
# Pattern allows wildcards (*) in region and account fields
|
|
45
|
-
arn_pattern_str = config.config.get(
|
|
46
|
-
"arn_pattern",
|
|
47
|
-
r"^arn:(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f):[a-z0-9\-]+:[a-z0-9\-*]*:[0-9*]*:.+$",
|
|
48
|
-
)
|
|
47
|
+
arn_pattern_str = config.config.get("arn_pattern", DEFAULT_ARN_VALIDATION_PATTERN)
|
|
49
48
|
|
|
50
|
-
# Compile pattern
|
|
49
|
+
# Compile pattern
|
|
51
50
|
try:
|
|
52
51
|
arn_pattern = re.compile(arn_pattern_str)
|
|
53
52
|
except re.error:
|
|
54
|
-
# Fallback to
|
|
55
|
-
arn_pattern =
|
|
53
|
+
# Fallback to default pattern if custom pattern is invalid
|
|
54
|
+
arn_pattern = re.compile(DEFAULT_ARN_VALIDATION_PATTERN)
|
|
55
|
+
|
|
56
|
+
# Check if template variable support is enabled (default: true)
|
|
57
|
+
# Try global settings first, then check-specific config
|
|
58
|
+
allow_template_variables = config.root_config.get("settings", {}).get(
|
|
59
|
+
"allow_template_variables",
|
|
60
|
+
config.config.get("allow_template_variables", True),
|
|
61
|
+
)
|
|
56
62
|
|
|
57
63
|
for resource in resources:
|
|
58
64
|
# Skip wildcard resources (handled by security checks)
|
|
@@ -60,14 +66,14 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
60
66
|
continue
|
|
61
67
|
|
|
62
68
|
# Validate ARN length to prevent ReDoS attacks
|
|
63
|
-
if len(resource) >
|
|
69
|
+
if len(resource) > MAX_ARN_LENGTH:
|
|
64
70
|
issues.append(
|
|
65
71
|
ValidationIssue(
|
|
66
72
|
severity=self.get_severity(config),
|
|
67
73
|
statement_sid=statement_sid,
|
|
68
74
|
statement_index=statement_idx,
|
|
69
75
|
issue_type="invalid_resource",
|
|
70
|
-
message=f"Resource ARN exceeds maximum length ({len(resource)} > {
|
|
76
|
+
message=f"Resource ARN exceeds maximum length ({len(resource)} > {MAX_ARN_LENGTH}): {resource[:100]}...",
|
|
71
77
|
resource=resource[:100] + "...",
|
|
72
78
|
suggestion="ARN is too long and may be invalid",
|
|
73
79
|
line_number=line_number,
|
|
@@ -75,21 +81,45 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
75
81
|
)
|
|
76
82
|
continue
|
|
77
83
|
|
|
84
|
+
# Check if resource contains template variables
|
|
85
|
+
has_templates = has_template_variables(resource)
|
|
86
|
+
|
|
87
|
+
# If template variables are found and allowed, normalize them for validation
|
|
88
|
+
validation_resource = resource
|
|
89
|
+
if has_templates and allow_template_variables:
|
|
90
|
+
validation_resource = normalize_template_variables(resource)
|
|
91
|
+
|
|
78
92
|
# Validate ARN format
|
|
79
93
|
try:
|
|
80
|
-
if not arn_pattern.match(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
if not arn_pattern.match(validation_resource):
|
|
95
|
+
# If original resource had templates and normalization didn't help,
|
|
96
|
+
# provide a more informative message
|
|
97
|
+
if has_templates and allow_template_variables:
|
|
98
|
+
issues.append(
|
|
99
|
+
ValidationIssue(
|
|
100
|
+
severity=self.get_severity(config),
|
|
101
|
+
statement_sid=statement_sid,
|
|
102
|
+
statement_index=statement_idx,
|
|
103
|
+
issue_type="invalid_resource",
|
|
104
|
+
message=f"Invalid ARN format even after normalizing template variables: {resource}",
|
|
105
|
+
resource=resource,
|
|
106
|
+
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource (template variables like ${aws_account_id} are supported)",
|
|
107
|
+
line_number=line_number,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
issues.append(
|
|
112
|
+
ValidationIssue(
|
|
113
|
+
severity=self.get_severity(config),
|
|
114
|
+
statement_sid=statement_sid,
|
|
115
|
+
statement_index=statement_idx,
|
|
116
|
+
issue_type="invalid_resource",
|
|
117
|
+
message=f"Invalid ARN format: {resource}",
|
|
118
|
+
resource=resource,
|
|
119
|
+
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
|
|
120
|
+
line_number=line_number,
|
|
121
|
+
)
|
|
91
122
|
)
|
|
92
|
-
)
|
|
93
123
|
except Exception:
|
|
94
124
|
# If regex matching fails (shouldn't happen with length check), treat as invalid
|
|
95
125
|
issues.append(
|
|
@@ -143,7 +143,11 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
143
143
|
)
|
|
144
144
|
|
|
145
145
|
# Combine suggestion + example
|
|
146
|
-
suggestion =
|
|
146
|
+
suggestion = (
|
|
147
|
+
f"{suggestion_text}\n\nExample:\n```json\n{example}\n```"
|
|
148
|
+
if example
|
|
149
|
+
else suggestion_text
|
|
150
|
+
)
|
|
147
151
|
|
|
148
152
|
# Determine severity based on the highest severity action in the list
|
|
149
153
|
# If single action, use its category severity
|
|
@@ -69,7 +69,9 @@ class ServiceWildcardCheck(PolicyCheck):
|
|
|
69
69
|
|
|
70
70
|
# Combine suggestion + example
|
|
71
71
|
suggestion = (
|
|
72
|
-
f"{suggestion_text}\nExample:\n{example}"
|
|
72
|
+
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
73
|
+
if example
|
|
74
|
+
else suggestion_text
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
issues.append(
|
|
@@ -11,7 +11,6 @@ Performance optimizations:
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
from functools import lru_cache
|
|
14
|
-
from re import Pattern
|
|
15
14
|
|
|
16
15
|
from iam_validator.core.check_registry import CheckConfig
|
|
17
16
|
from iam_validator.core.config.sensitive_actions import get_sensitive_actions
|
|
@@ -69,7 +68,7 @@ DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
|
69
68
|
|
|
70
69
|
# Global regex pattern cache for performance
|
|
71
70
|
@lru_cache(maxsize=256)
|
|
72
|
-
def compile_pattern(pattern: str) -> Pattern[str] | None:
|
|
71
|
+
def compile_pattern(pattern: str) -> re.Pattern[str] | None:
|
|
73
72
|
"""Compile and cache regex patterns.
|
|
74
73
|
|
|
75
74
|
Args:
|
|
@@ -6,7 +6,6 @@ to their actual action names using the AWS Service Reference API.
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
from functools import lru_cache
|
|
9
|
-
from re import Pattern
|
|
10
9
|
|
|
11
10
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
12
11
|
|
|
@@ -14,7 +13,7 @@ from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
|
14
13
|
# Global cache for compiled wildcard patterns (shared across checks)
|
|
15
14
|
# Using lru_cache for O(1) pattern reuse and 20-30x performance improvement
|
|
16
15
|
@lru_cache(maxsize=512)
|
|
17
|
-
def compile_wildcard_pattern(pattern: str) -> Pattern[str]:
|
|
16
|
+
def compile_wildcard_pattern(pattern: str) -> re.Pattern[str]:
|
|
18
17
|
"""Compile and cache wildcard patterns for O(1) reuse.
|
|
19
18
|
|
|
20
19
|
Args:
|
|
@@ -40,12 +40,17 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
40
40
|
if "*" in actions:
|
|
41
41
|
message = config.config.get("message", "Statement allows all actions (*)")
|
|
42
42
|
suggestion_text = config.config.get(
|
|
43
|
-
"suggestion",
|
|
43
|
+
"suggestion",
|
|
44
|
+
"Replace wildcard with specific actions needed for your use case",
|
|
44
45
|
)
|
|
45
46
|
example = config.config.get("example", "")
|
|
46
47
|
|
|
47
48
|
# Combine suggestion + example
|
|
48
|
-
suggestion =
|
|
49
|
+
suggestion = (
|
|
50
|
+
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
51
|
+
if example
|
|
52
|
+
else suggestion_text
|
|
53
|
+
)
|
|
49
54
|
|
|
50
55
|
issues.append(
|
|
51
56
|
ValidationIssue(
|
|
@@ -69,7 +69,11 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
69
69
|
example = config.config.get("example", "")
|
|
70
70
|
|
|
71
71
|
# Combine suggestion + example
|
|
72
|
-
suggestion =
|
|
72
|
+
suggestion = (
|
|
73
|
+
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
74
|
+
if example
|
|
75
|
+
else suggestion_text
|
|
76
|
+
)
|
|
73
77
|
|
|
74
78
|
issues.append(
|
|
75
79
|
ValidationIssue(
|
|
@@ -136,6 +136,12 @@ Examples:
|
|
|
136
136
|
help="Create line-specific review comments on PR (requires --github-comment)",
|
|
137
137
|
)
|
|
138
138
|
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--github-summary",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Write validation summary to GitHub Actions job summary (works for all workflow runs)",
|
|
143
|
+
)
|
|
144
|
+
|
|
139
145
|
parser.add_argument(
|
|
140
146
|
"--run-all-checks",
|
|
141
147
|
action="store_true",
|
|
@@ -265,6 +271,10 @@ Examples:
|
|
|
265
271
|
if not success:
|
|
266
272
|
logging.error("Failed to post Access Analyzer results to GitHub PR")
|
|
267
273
|
|
|
274
|
+
# Write to GitHub Actions job summary if configured
|
|
275
|
+
if getattr(args, "github_summary", False):
|
|
276
|
+
self._write_github_actions_summary(report)
|
|
277
|
+
|
|
268
278
|
# Determine exit code based on validation results
|
|
269
279
|
if args.fail_on_warnings:
|
|
270
280
|
exit_code = 0 if report.total_findings == 0 else 1
|
|
@@ -405,7 +415,8 @@ Examples:
|
|
|
405
415
|
if not github.is_configured():
|
|
406
416
|
logging.error(
|
|
407
417
|
"GitHub integration not configured. "
|
|
408
|
-
"
|
|
418
|
+
"Required: GITHUB_TOKEN, GITHUB_REPOSITORY, and GITHUB_PR_NUMBER environment variables. "
|
|
419
|
+
"Ensure your workflow is triggered by a pull_request event."
|
|
409
420
|
)
|
|
410
421
|
return False
|
|
411
422
|
|
|
@@ -432,3 +443,89 @@ Examples:
|
|
|
432
443
|
logging.error("Failed to post Access Analyzer results to PR")
|
|
433
444
|
|
|
434
445
|
return success
|
|
446
|
+
|
|
447
|
+
def _write_github_actions_summary(self, report: AccessAnalyzerReport) -> None:
|
|
448
|
+
"""Write a high-level summary to GitHub Actions job summary.
|
|
449
|
+
|
|
450
|
+
This appears in the Actions tab and provides a quick overview of Access Analyzer results.
|
|
451
|
+
Uses GITHUB_STEP_SUMMARY environment variable.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
report: Access Analyzer report to summarize
|
|
455
|
+
"""
|
|
456
|
+
import os
|
|
457
|
+
|
|
458
|
+
summary_file = os.getenv("GITHUB_STEP_SUMMARY")
|
|
459
|
+
if not summary_file:
|
|
460
|
+
logging.warning(
|
|
461
|
+
"--github-summary specified but GITHUB_STEP_SUMMARY env var not found. "
|
|
462
|
+
"This feature only works in GitHub Actions."
|
|
463
|
+
)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
# Generate high-level summary
|
|
468
|
+
summary_parts = []
|
|
469
|
+
|
|
470
|
+
# Header with status
|
|
471
|
+
if report.total_findings == 0:
|
|
472
|
+
summary_parts.append("# ✅ IAM Policy Validation (Access Analyzer) - Passed")
|
|
473
|
+
elif report.total_errors > 0:
|
|
474
|
+
summary_parts.append("# ❌ IAM Policy Validation (Access Analyzer) - Failed")
|
|
475
|
+
else:
|
|
476
|
+
summary_parts.append("# ⚠️ IAM Policy Validation (Access Analyzer) - Issues Found")
|
|
477
|
+
|
|
478
|
+
summary_parts.append("")
|
|
479
|
+
|
|
480
|
+
# Summary table
|
|
481
|
+
summary_parts.append("## Summary")
|
|
482
|
+
summary_parts.append("")
|
|
483
|
+
summary_parts.append("| Metric | Count |")
|
|
484
|
+
summary_parts.append("|--------|-------|")
|
|
485
|
+
summary_parts.append(f"| Total Policies Analyzed | {report.total_policies} |")
|
|
486
|
+
summary_parts.append(f"| Policies with Findings | {report.policies_with_findings} |")
|
|
487
|
+
summary_parts.append(f"| Total Findings | {report.total_findings} |")
|
|
488
|
+
summary_parts.append(f"| Errors | {report.total_errors} |")
|
|
489
|
+
summary_parts.append(f"| Warnings | {report.total_warnings} |")
|
|
490
|
+
summary_parts.append(f"| Suggestions | {report.total_suggestions} |")
|
|
491
|
+
|
|
492
|
+
# Finding breakdown by type if there are findings
|
|
493
|
+
if report.total_findings > 0:
|
|
494
|
+
summary_parts.append("")
|
|
495
|
+
summary_parts.append("## 📊 Findings by Type")
|
|
496
|
+
summary_parts.append("")
|
|
497
|
+
|
|
498
|
+
# Count findings by type
|
|
499
|
+
finding_types: dict[str, int] = {}
|
|
500
|
+
for result in report.results:
|
|
501
|
+
for finding in result.findings:
|
|
502
|
+
finding_type = finding.finding_type
|
|
503
|
+
finding_types[finding_type] = finding_types.get(finding_type, 0) + 1
|
|
504
|
+
|
|
505
|
+
# Sort by count (highest first)
|
|
506
|
+
sorted_types = sorted(finding_types.items(), key=lambda x: x[1], reverse=True)
|
|
507
|
+
|
|
508
|
+
summary_parts.append("| Finding Type | Count |")
|
|
509
|
+
summary_parts.append("|--------------|-------|")
|
|
510
|
+
for finding_type, count in sorted_types:
|
|
511
|
+
summary_parts.append(f"| {finding_type} | {count} |")
|
|
512
|
+
|
|
513
|
+
# Add footer with links
|
|
514
|
+
summary_parts.append("")
|
|
515
|
+
summary_parts.append("---")
|
|
516
|
+
summary_parts.append("")
|
|
517
|
+
summary_parts.append(
|
|
518
|
+
"📝 For detailed findings, check the PR comments or review the workflow logs."
|
|
519
|
+
)
|
|
520
|
+
summary_parts.append("")
|
|
521
|
+
summary_parts.append("*Powered by AWS IAM Access Analyzer*")
|
|
522
|
+
|
|
523
|
+
# Write to summary file (append mode)
|
|
524
|
+
with open(summary_file, "a", encoding="utf-8") as f:
|
|
525
|
+
f.write("\n".join(summary_parts))
|
|
526
|
+
f.write("\n")
|
|
527
|
+
|
|
528
|
+
logging.info("Wrote Access Analyzer summary to GitHub Actions job summary")
|
|
529
|
+
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logging.warning(f"Failed to write GitHub Actions summary: {e}")
|
|
@@ -484,7 +484,9 @@ Examples:
|
|
|
484
484
|
# In streaming mode, don't cleanup comments (we want to keep earlier files)
|
|
485
485
|
# Cleanup will happen once at the end
|
|
486
486
|
commenter = PRCommenter(
|
|
487
|
-
github,
|
|
487
|
+
github,
|
|
488
|
+
cleanup_old_comments=False,
|
|
489
|
+
fail_on_severities=fail_on_severities,
|
|
488
490
|
)
|
|
489
491
|
|
|
490
492
|
# Create a mini-report for just this file
|
|
@@ -547,7 +549,7 @@ Examples:
|
|
|
547
549
|
# Issue breakdown by severity if there are issues
|
|
548
550
|
if report.total_issues > 0:
|
|
549
551
|
summary_parts.append("")
|
|
550
|
-
summary_parts.append("## Issues by Severity")
|
|
552
|
+
summary_parts.append("## 📊 Issues by Severity")
|
|
551
553
|
summary_parts.append("")
|
|
552
554
|
|
|
553
555
|
# Count issues by severity
|
|
@@ -197,6 +197,11 @@ class AccessAnalyzerReport:
|
|
|
197
197
|
"""Total number of suggestions across all policies."""
|
|
198
198
|
return sum(r.suggestion_count for r in self.results)
|
|
199
199
|
|
|
200
|
+
@property
|
|
201
|
+
def policies_with_findings(self) -> int:
|
|
202
|
+
"""Number of policies that have at least one finding."""
|
|
203
|
+
return sum(1 for r in self.results if r.findings)
|
|
204
|
+
|
|
200
205
|
|
|
201
206
|
class AccessAnalyzerValidator:
|
|
202
207
|
"""Validates IAM policies using AWS IAM Access Analyzer."""
|
|
@@ -338,11 +338,8 @@ class AccessAnalyzerReportFormatter:
|
|
|
338
338
|
"---",
|
|
339
339
|
"",
|
|
340
340
|
"<div align='center'>",
|
|
341
|
-
"",
|
|
342
|
-
"
|
|
343
|
-
"",
|
|
344
|
-
"_Powered by AWS IAM Access Analyzer_",
|
|
345
|
-
"",
|
|
341
|
+
"🤖 <em>Generated by <strong>IAM Policy Validator</strong></em><br>",
|
|
342
|
+
"<sub>Powered by AWS IAM Access Analyzer</sub>",
|
|
346
343
|
"</div>",
|
|
347
344
|
]
|
|
348
345
|
footer_content = "\n".join(footer_lines)
|
|
@@ -40,18 +40,28 @@ logger = logging.getLogger(__name__)
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class CompiledPatterns:
|
|
43
|
-
"""Pre-compiled regex patterns for validation.
|
|
43
|
+
"""Pre-compiled regex patterns for validation.
|
|
44
|
+
|
|
45
|
+
This class implements the Singleton pattern to ensure patterns are compiled only once
|
|
46
|
+
and reused across all instances for better performance.
|
|
47
|
+
"""
|
|
44
48
|
|
|
45
49
|
_instance = None
|
|
50
|
+
_initialized = False
|
|
46
51
|
|
|
47
52
|
def __new__(cls) -> "CompiledPatterns":
|
|
48
53
|
if cls._instance is None:
|
|
49
54
|
cls._instance = super().__new__(cls)
|
|
50
|
-
cls._instance._initialize()
|
|
51
55
|
return cls._instance
|
|
52
56
|
|
|
53
|
-
def
|
|
54
|
-
"""Initialize compiled patterns."""
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
"""Initialize compiled patterns (only once due to Singleton pattern)."""
|
|
59
|
+
# Only initialize once, even if __init__ is called multiple times
|
|
60
|
+
if CompiledPatterns._initialized:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
CompiledPatterns._initialized = True
|
|
64
|
+
|
|
55
65
|
# ARN validation pattern
|
|
56
66
|
self.arn_pattern = re.compile(
|
|
57
67
|
r"^arn:(?P<partition>(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f)):"
|
|
@@ -16,6 +16,7 @@ import yaml
|
|
|
16
16
|
|
|
17
17
|
from iam_validator.core.check_registry import CheckConfig, CheckRegistry, PolicyCheck
|
|
18
18
|
from iam_validator.core.config.defaults import get_default_config
|
|
19
|
+
from iam_validator.core.constants import DEFAULT_CONFIG_FILENAMES
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -127,12 +128,8 @@ class ValidatorConfig:
|
|
|
127
128
|
class ConfigLoader:
|
|
128
129
|
"""Loads configuration from various sources."""
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"iam-validator.yml",
|
|
133
|
-
".iam-validator.yaml", # Support hidden files as fallback
|
|
134
|
-
".iam-validator.yml",
|
|
135
|
-
]
|
|
131
|
+
# Load default config names from constants module
|
|
132
|
+
DEFAULT_CONFIG_NAMES = DEFAULT_CONFIG_FILENAMES
|
|
136
133
|
|
|
137
134
|
@staticmethod
|
|
138
135
|
def find_config_file(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core constants for IAM Policy Validator.
|
|
3
|
+
|
|
4
|
+
This module defines constants used across the validator to ensure consistency
|
|
5
|
+
and provide a single source of truth for shared values. These constants are
|
|
6
|
+
based on AWS service limits and documentation.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
- AWS IAM Policy Size Limits: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html
|
|
10
|
+
- AWS ARN Format: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# ARN Validation
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
# ARN Validation Pattern
|
|
18
|
+
# This pattern is specifically designed for validation and allows wildcards (*) in region and account fields
|
|
19
|
+
# Unlike the parsing pattern in CompiledPatterns, this is more lenient for validation purposes
|
|
20
|
+
# Supports all AWS partitions: aws, aws-cn, aws-us-gov, aws-eusc, aws-iso*
|
|
21
|
+
DEFAULT_ARN_VALIDATION_PATTERN = r"^arn:(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f):[a-z0-9\-]+:[a-z0-9\-*]*:[0-9*]*:.+$"
|
|
22
|
+
|
|
23
|
+
# Maximum allowed ARN length to prevent ReDoS attacks
|
|
24
|
+
# AWS maximum ARN length is approximately 2048 characters
|
|
25
|
+
MAX_ARN_LENGTH = 2048
|
|
26
|
+
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# AWS IAM Policy Size Limits
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# These limits are enforced by AWS and policies exceeding them will be rejected
|
|
31
|
+
# Note: AWS does not count whitespace when calculating policy size
|
|
32
|
+
|
|
33
|
+
# Managed policy maximum size (characters, excluding whitespace)
|
|
34
|
+
MAX_MANAGED_POLICY_SIZE = 6144
|
|
35
|
+
|
|
36
|
+
# Inline policy maximum size for IAM users (characters, excluding whitespace)
|
|
37
|
+
MAX_INLINE_USER_POLICY_SIZE = 2048
|
|
38
|
+
|
|
39
|
+
# Inline policy maximum size for IAM groups (characters, excluding whitespace)
|
|
40
|
+
MAX_INLINE_GROUP_POLICY_SIZE = 5120
|
|
41
|
+
|
|
42
|
+
# Inline policy maximum size for IAM roles (characters, excluding whitespace)
|
|
43
|
+
MAX_INLINE_ROLE_POLICY_SIZE = 10240
|
|
44
|
+
|
|
45
|
+
# Policy size limits dictionary (for backward compatibility and easy lookup)
|
|
46
|
+
AWS_POLICY_SIZE_LIMITS = {
|
|
47
|
+
"managed": MAX_MANAGED_POLICY_SIZE,
|
|
48
|
+
"inline_user": MAX_INLINE_USER_POLICY_SIZE,
|
|
49
|
+
"inline_group": MAX_INLINE_GROUP_POLICY_SIZE,
|
|
50
|
+
"inline_role": MAX_INLINE_ROLE_POLICY_SIZE,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# Configuration Defaults
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
# Default configuration file names (searched in order)
|
|
58
|
+
DEFAULT_CONFIG_FILENAMES = [
|
|
59
|
+
"iam-validator.yaml",
|
|
60
|
+
"iam-validator.yml",
|
|
61
|
+
".iam-validator.yaml",
|
|
62
|
+
".iam-validator.yml",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# GitHub Integration
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
# Bot identifier for GitHub comments and reviews
|
|
70
|
+
BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
71
|
+
|
|
72
|
+
# HTML comment markers for identifying bot-generated content (for cleanup/updates)
|
|
73
|
+
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
74
|
+
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|