iam-policy-validator 1.14.7__py3-none-any.whl → 1.15.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.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/METADATA +16 -11
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/RECORD +41 -28
- iam_policy_validator-1.15.1.dist-info/entry_points.txt +4 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_validation.py +91 -27
- iam_validator/checks/not_action_not_resource.py +163 -0
- iam_validator/checks/resource_validation.py +132 -81
- iam_validator/checks/wildcard_resource.py +136 -6
- iam_validator/commands/__init__.py +3 -0
- iam_validator/commands/cache.py +66 -24
- iam_validator/commands/completion.py +94 -15
- iam_validator/commands/mcp.py +210 -0
- iam_validator/commands/query.py +489 -65
- iam_validator/core/aws_service/__init__.py +5 -1
- iam_validator/core/aws_service/cache.py +20 -0
- iam_validator/core/aws_service/fetcher.py +180 -11
- iam_validator/core/aws_service/storage.py +14 -6
- iam_validator/core/aws_service/validators.py +68 -51
- iam_validator/core/check_registry.py +100 -35
- iam_validator/core/config/aws_global_conditions.py +18 -9
- iam_validator/core/config/check_documentation.py +104 -51
- iam_validator/core/config/config_loader.py +39 -3
- iam_validator/core/config/defaults.py +6 -0
- iam_validator/core/constants.py +11 -4
- iam_validator/core/models.py +39 -14
- iam_validator/mcp/__init__.py +162 -0
- iam_validator/mcp/models.py +118 -0
- iam_validator/mcp/server.py +2928 -0
- iam_validator/mcp/session_config.py +319 -0
- iam_validator/mcp/templates/__init__.py +79 -0
- iam_validator/mcp/templates/builtin.py +856 -0
- iam_validator/mcp/tools/__init__.py +72 -0
- iam_validator/mcp/tools/generation.py +888 -0
- iam_validator/mcp/tools/org_config_tools.py +263 -0
- iam_validator/mcp/tools/query.py +395 -0
- iam_validator/mcp/tools/validation.py +376 -0
- iam_validator/sdk/__init__.py +2 -0
- iam_validator/sdk/policy_utils.py +31 -5
- iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,6 +15,7 @@ from dataclasses import dataclass, field
|
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
17
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
18
|
+
from iam_validator.core.config.check_documentation import CheckDocumentationRegistry
|
|
18
19
|
from iam_validator.core.ignore_patterns import IgnorePatternMatcher
|
|
19
20
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
20
21
|
|
|
@@ -22,6 +23,28 @@ if TYPE_CHECKING:
|
|
|
22
23
|
from iam_validator.core.models import IAMPolicy
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _inject_documentation(issue: ValidationIssue, check_id: str) -> None:
|
|
27
|
+
"""Inject documentation fields from CheckDocumentationRegistry into an issue.
|
|
28
|
+
|
|
29
|
+
This populates risk_explanation, documentation_url, remediation_steps, and
|
|
30
|
+
risk_category from the centralized documentation registry if not already set.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
issue: The validation issue to enhance
|
|
34
|
+
check_id: The check ID to look up documentation for
|
|
35
|
+
"""
|
|
36
|
+
doc = CheckDocumentationRegistry.get(check_id)
|
|
37
|
+
if doc:
|
|
38
|
+
if issue.risk_explanation is None:
|
|
39
|
+
issue.risk_explanation = doc.risk_explanation
|
|
40
|
+
if issue.documentation_url is None:
|
|
41
|
+
issue.documentation_url = doc.documentation_url
|
|
42
|
+
if issue.remediation_steps is None:
|
|
43
|
+
issue.remediation_steps = doc.remediation_steps
|
|
44
|
+
if issue.risk_category is None:
|
|
45
|
+
issue.risk_category = doc.risk_category
|
|
46
|
+
|
|
47
|
+
|
|
25
48
|
@dataclass
|
|
26
49
|
class CheckConfig:
|
|
27
50
|
"""Configuration for a single check."""
|
|
@@ -32,28 +55,53 @@ class CheckConfig:
|
|
|
32
55
|
config: dict[str, Any] = field(default_factory=dict) # Check-specific config
|
|
33
56
|
description: str = ""
|
|
34
57
|
root_config: dict[str, Any] = field(default_factory=dict) # Full config for cross-check access
|
|
35
|
-
ignore_patterns: list[dict[str, Any]] = field(default_factory=list) #
|
|
58
|
+
ignore_patterns: list[dict[str, Any]] = field(default_factory=list) # Ignore patterns
|
|
59
|
+
hide_severities: frozenset[str] | None = None # Severities to hide from output
|
|
36
60
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
Configuration fields:
|
|
62
|
+
|
|
63
|
+
ignore_patterns: List of patterns to ignore findings.
|
|
64
|
+
Each pattern is a dict with optional fields:
|
|
65
|
+
- filepath: Regex to match file path
|
|
66
|
+
- action: Regex to match action name
|
|
67
|
+
- resource: Regex to match resource
|
|
68
|
+
- sid: Exact SID to match (or regex if ends with .*)
|
|
69
|
+
- condition_key: Regex to match condition key
|
|
70
|
+
|
|
71
|
+
Multiple fields in one pattern = AND logic
|
|
72
|
+
Multiple patterns = OR logic (any pattern matches → ignore)
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
ignore_patterns:
|
|
76
|
+
- filepath: "test/.*|examples/.*"
|
|
77
|
+
- filepath: "policies/readonly-.*"
|
|
78
|
+
action: ".*:(Get|List|Describe).*"
|
|
79
|
+
- sid: "AllowReadOnlyAccess"
|
|
80
|
+
|
|
81
|
+
hide_severities: Set of severity levels to hide from output.
|
|
82
|
+
Issues with these severities will be filtered out and not shown
|
|
83
|
+
in any output (console, JSON, SARIF, GitHub PR comments, etc.).
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
hide_severities: frozenset(["low", "info"])
|
|
55
87
|
"""
|
|
56
88
|
|
|
89
|
+
def should_show_severity(self, severity: str) -> bool:
|
|
90
|
+
"""Check if a severity level should be shown in output.
|
|
91
|
+
|
|
92
|
+
Returns False if severity is in hide_severities, True otherwise.
|
|
93
|
+
This is used to filter out low-priority findings to reduce noise.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
severity: The severity level to check
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if the severity should be shown, False if it should be hidden
|
|
100
|
+
"""
|
|
101
|
+
if self.hide_severities and severity in self.hide_severities:
|
|
102
|
+
return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
57
105
|
def should_ignore(self, issue: ValidationIssue, filepath: str = "") -> bool:
|
|
58
106
|
"""
|
|
59
107
|
Check if issue should be ignored based on ignore patterns.
|
|
@@ -425,13 +473,17 @@ class CheckRegistry:
|
|
|
425
473
|
config = self.get_config(check.check_id)
|
|
426
474
|
if config:
|
|
427
475
|
issues = await check.execute(statement, statement_idx, fetcher, config)
|
|
428
|
-
# Inject check_id into each issue
|
|
476
|
+
# Inject check_id and documentation into each issue
|
|
429
477
|
for issue in issues:
|
|
430
478
|
if issue.check_id is None:
|
|
431
479
|
issue.check_id = check.check_id
|
|
432
|
-
|
|
480
|
+
_inject_documentation(issue, check.check_id)
|
|
481
|
+
# Filter issues based on ignore_patterns and hide_severities
|
|
433
482
|
filtered_issues = [
|
|
434
|
-
issue
|
|
483
|
+
issue
|
|
484
|
+
for issue in issues
|
|
485
|
+
if not config.should_ignore(issue, filepath)
|
|
486
|
+
and config.should_show_severity(issue.severity)
|
|
435
487
|
]
|
|
436
488
|
all_issues.extend(filtered_issues)
|
|
437
489
|
return all_issues
|
|
@@ -449,7 +501,7 @@ class CheckRegistry:
|
|
|
449
501
|
# Wait for all checks to complete
|
|
450
502
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
451
503
|
|
|
452
|
-
# Collect all issues, handling any exceptions and applying
|
|
504
|
+
# Collect all issues, handling any exceptions and applying filters
|
|
453
505
|
all_issues = []
|
|
454
506
|
for idx, result in enumerate(results):
|
|
455
507
|
if isinstance(result, Exception):
|
|
@@ -459,13 +511,17 @@ class CheckRegistry:
|
|
|
459
511
|
elif isinstance(result, list):
|
|
460
512
|
check = enabled_checks[idx]
|
|
461
513
|
config = configs[idx]
|
|
462
|
-
# Inject check_id into each issue
|
|
514
|
+
# Inject check_id and documentation into each issue
|
|
463
515
|
for issue in result:
|
|
464
516
|
if issue.check_id is None:
|
|
465
517
|
issue.check_id = check.check_id
|
|
466
|
-
|
|
518
|
+
_inject_documentation(issue, check.check_id)
|
|
519
|
+
# Filter issues based on ignore_patterns and hide_severities
|
|
467
520
|
filtered_issues = [
|
|
468
|
-
issue
|
|
521
|
+
issue
|
|
522
|
+
for issue in result
|
|
523
|
+
if not config.should_ignore(issue, filepath)
|
|
524
|
+
and config.should_show_severity(issue.severity)
|
|
469
525
|
]
|
|
470
526
|
all_issues.extend(filtered_issues)
|
|
471
527
|
|
|
@@ -499,7 +555,7 @@ class CheckRegistry:
|
|
|
499
555
|
try:
|
|
500
556
|
issues = await check.execute(statement, statement_idx, fetcher, config)
|
|
501
557
|
all_issues.extend(issues)
|
|
502
|
-
except Exception as e:
|
|
558
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
503
559
|
print(f"Warning: Check '{check.check_id}' failed: {e}")
|
|
504
560
|
|
|
505
561
|
return all_issues
|
|
@@ -551,18 +607,20 @@ class CheckRegistry:
|
|
|
551
607
|
policy_type=policy_type,
|
|
552
608
|
**kwargs,
|
|
553
609
|
)
|
|
554
|
-
# Inject check_id into each issue
|
|
610
|
+
# Inject check_id and documentation into each issue
|
|
555
611
|
for issue in issues:
|
|
556
612
|
if issue.check_id is None:
|
|
557
613
|
issue.check_id = check.check_id
|
|
558
|
-
|
|
614
|
+
_inject_documentation(issue, check.check_id)
|
|
615
|
+
# Filter issues based on ignore_patterns and hide_severities
|
|
559
616
|
filtered_issues = [
|
|
560
617
|
issue
|
|
561
618
|
for issue in issues
|
|
562
619
|
if not config.should_ignore(issue, policy_file)
|
|
620
|
+
and config.should_show_severity(issue.severity)
|
|
563
621
|
]
|
|
564
622
|
all_issues.extend(filtered_issues)
|
|
565
|
-
except Exception as e:
|
|
623
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
566
624
|
print(f"Warning: Check '{check.check_id}' failed: {e}")
|
|
567
625
|
return all_issues
|
|
568
626
|
|
|
@@ -581,7 +639,7 @@ class CheckRegistry:
|
|
|
581
639
|
# Wait for all checks to complete
|
|
582
640
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
583
641
|
|
|
584
|
-
# Collect all issues, handling any exceptions and applying
|
|
642
|
+
# Collect all issues, handling any exceptions and applying filters
|
|
585
643
|
for idx, result in enumerate(results):
|
|
586
644
|
if isinstance(result, Exception):
|
|
587
645
|
# Log error but continue with other checks
|
|
@@ -590,13 +648,17 @@ class CheckRegistry:
|
|
|
590
648
|
elif isinstance(result, list):
|
|
591
649
|
check = policy_level_checks[idx]
|
|
592
650
|
config = configs[idx]
|
|
593
|
-
# Inject check_id into each issue
|
|
651
|
+
# Inject check_id and documentation into each issue
|
|
594
652
|
for issue in result:
|
|
595
653
|
if issue.check_id is None:
|
|
596
654
|
issue.check_id = check.check_id
|
|
597
|
-
|
|
655
|
+
_inject_documentation(issue, check.check_id)
|
|
656
|
+
# Filter issues based on ignore_patterns and hide_severities
|
|
598
657
|
filtered_issues = [
|
|
599
|
-
issue
|
|
658
|
+
issue
|
|
659
|
+
for issue in result
|
|
660
|
+
if not config.should_ignore(issue, policy_file)
|
|
661
|
+
and config.should_show_severity(issue.severity)
|
|
600
662
|
]
|
|
601
663
|
all_issues.extend(filtered_issues)
|
|
602
664
|
|
|
@@ -623,7 +685,7 @@ def create_default_registry(
|
|
|
623
685
|
|
|
624
686
|
if include_builtin_checks:
|
|
625
687
|
# Import and register built-in checks
|
|
626
|
-
from iam_validator import checks
|
|
688
|
+
from iam_validator import checks # pylint: disable=import-outside-toplevel
|
|
627
689
|
|
|
628
690
|
# 0. FUNDAMENTAL STRUCTURE (Must run FIRST - validates basic policy structure)
|
|
629
691
|
registry.register(
|
|
@@ -655,6 +717,9 @@ def create_default_registry(
|
|
|
655
717
|
registry.register(checks.WildcardResourceCheck()) # Wildcard resource detection
|
|
656
718
|
registry.register(checks.FullWildcardCheck()) # Full wildcard (*) detection
|
|
657
719
|
registry.register(checks.ServiceWildcardCheck()) # Service-level wildcard detection
|
|
720
|
+
registry.register(
|
|
721
|
+
checks.NotActionNotResourceCheck()
|
|
722
|
+
) # NotAction/NotResource pattern detection
|
|
658
723
|
|
|
659
724
|
# 6. SECURITY - ADVANCED (Sensitive actions and condition enforcement)
|
|
660
725
|
registry.register(
|
|
@@ -71,18 +71,27 @@ AWS_GLOBAL_CONDITION_KEYS = {
|
|
|
71
71
|
"aws:ViaAWSService": "Bool", # Whether AWS service made the request
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
# Global condition keys that restrict resource scope.
|
|
75
|
+
# These conditions are always valid for all services and directly constrain
|
|
76
|
+
# which resources can be accessed, making them suitable for lowering severity
|
|
77
|
+
# when used with wildcard resources.
|
|
78
|
+
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-resourceaccount
|
|
79
|
+
GLOBAL_RESOURCE_SCOPING_CONDITION_KEYS = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"aws:ResourceAccount", # Limits to specific AWS account(s)
|
|
82
|
+
"aws:ResourceOrgID", # Limits to specific AWS Organization
|
|
83
|
+
"aws:ResourceOrgPaths", # Limits to specific OU paths
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
74
87
|
# Patterns that should be recognized (wildcards and tag-based keys)
|
|
75
|
-
#
|
|
88
|
+
# IMPORTANT: aws:RequestTag and aws:ResourceTag are NOT global condition keys!
|
|
89
|
+
# They are action-specific or resource-specific and must be explicitly listed in
|
|
90
|
+
# the action's ActionConditionKeys or the resource's ConditionKeys.
|
|
91
|
+
# Only aws:PrincipalTag is a true global condition key.
|
|
92
|
+
#
|
|
76
93
|
# Uses centralized tag key character class from constants
|
|
77
94
|
AWS_CONDITION_KEY_PATTERNS = [
|
|
78
|
-
{
|
|
79
|
-
"pattern": rf"^aws:RequestTag/[{AWS_TAG_KEY_ALLOWED_CHARS}]+$",
|
|
80
|
-
"description": "Tag keys in the request (for tag-based access control)",
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"pattern": rf"^aws:ResourceTag/[{AWS_TAG_KEY_ALLOWED_CHARS}]+$",
|
|
84
|
-
"description": "Tags on the resource being accessed",
|
|
85
|
-
},
|
|
86
95
|
{
|
|
87
96
|
"pattern": rf"^aws:PrincipalTag/[{AWS_TAG_KEY_ALLOWED_CHARS}]+$",
|
|
88
97
|
"description": "Tags attached to the principal making the request",
|
|
@@ -9,6 +9,18 @@ Used to enhance ValidationIssue objects with actionable guidance.
|
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from typing import ClassVar
|
|
11
11
|
|
|
12
|
+
# Risk category icons for display in PR comments
|
|
13
|
+
RISK_CATEGORY_ICONS = {
|
|
14
|
+
"privilege_escalation": "🔐",
|
|
15
|
+
"data_exfiltration": "📤",
|
|
16
|
+
"denial_of_service": "🚫",
|
|
17
|
+
"resource_exposure": "🌐",
|
|
18
|
+
"credential_exposure": "🔑",
|
|
19
|
+
"compliance": "📋",
|
|
20
|
+
"configuration": "⚙️",
|
|
21
|
+
"validation": "✅",
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
|
|
13
25
|
@dataclass
|
|
14
26
|
class CheckDocumentation:
|
|
@@ -19,12 +31,14 @@ class CheckDocumentation:
|
|
|
19
31
|
risk_explanation: Why this issue is a security risk
|
|
20
32
|
documentation_url: Link to relevant AWS docs or runbook
|
|
21
33
|
remediation_steps: Step-by-step fix guidance
|
|
34
|
+
risk_category: Category of risk (e.g., "privilege_escalation", "data_exfiltration")
|
|
22
35
|
"""
|
|
23
36
|
|
|
24
37
|
check_id: str
|
|
25
38
|
risk_explanation: str
|
|
26
39
|
documentation_url: str
|
|
27
40
|
remediation_steps: list[str] = field(default_factory=list)
|
|
41
|
+
risk_category: str | None = None
|
|
28
42
|
|
|
29
43
|
|
|
30
44
|
class CheckDocumentationRegistry:
|
|
@@ -80,15 +94,16 @@ CheckDocumentationRegistry.register(
|
|
|
80
94
|
CheckDocumentation(
|
|
81
95
|
check_id="action_validation",
|
|
82
96
|
risk_explanation=(
|
|
83
|
-
"Invalid
|
|
97
|
+
"Invalid `Action`s may silently fail to grant intended permissions, "
|
|
84
98
|
"or indicate a typo that could expose unintended access."
|
|
85
99
|
),
|
|
86
100
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
87
101
|
remediation_steps=[
|
|
88
|
-
"Verify the
|
|
89
|
-
"Use the IAM policy simulator to test your intended permissions",
|
|
90
|
-
"Check for common typos (e.g.,
|
|
102
|
+
"Verify the `Action` name against AWS documentation for the target service",
|
|
103
|
+
"Use the AWS IAM policy simulator to test your intended permissions",
|
|
104
|
+
"Check for common typos (e.g., `S3` vs `s3`, `GetObjects` vs `GetObject`)",
|
|
91
105
|
],
|
|
106
|
+
risk_category="validation",
|
|
92
107
|
)
|
|
93
108
|
)
|
|
94
109
|
|
|
@@ -96,15 +111,16 @@ CheckDocumentationRegistry.register(
|
|
|
96
111
|
CheckDocumentation(
|
|
97
112
|
check_id="condition_key_validation",
|
|
98
113
|
risk_explanation=(
|
|
99
|
-
"Invalid condition keys are silently ignored by IAM, meaning your "
|
|
114
|
+
"Invalid condition keys are silently ignored by AWS IAM, meaning your "
|
|
100
115
|
"intended access restrictions may not be enforced."
|
|
101
116
|
),
|
|
102
117
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_condition-keys.html",
|
|
103
118
|
remediation_steps=[
|
|
104
|
-
"Verify the
|
|
105
|
-
"Check AWS documentation for the correct key name and format",
|
|
106
|
-
"Use global condition keys (aws
|
|
119
|
+
"Verify the `Condition` key exists for the target service",
|
|
120
|
+
"Check AWS documentation for the correct `Condition` key name and format for the target service",
|
|
121
|
+
"Use global condition keys (`aws:*`) for cross-service restrictions",
|
|
107
122
|
],
|
|
123
|
+
risk_category="validation",
|
|
108
124
|
)
|
|
109
125
|
)
|
|
110
126
|
|
|
@@ -112,15 +128,16 @@ CheckDocumentationRegistry.register(
|
|
|
112
128
|
CheckDocumentation(
|
|
113
129
|
check_id="condition_type_mismatch",
|
|
114
130
|
risk_explanation=(
|
|
115
|
-
"Using the wrong condition operator type (e.g., StringEquals with a "
|
|
131
|
+
"Using the wrong condition operator type (e.g., `StringEquals` with a "
|
|
116
132
|
"numeric value) may cause unexpected behavior or silent failures."
|
|
117
133
|
),
|
|
118
134
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_condition_operators.html",
|
|
119
135
|
remediation_steps=[
|
|
120
|
-
"Match the condition operator to the key's data type",
|
|
121
|
-
"Use String operators for string keys, Numeric for numbers, Date for timestamps",
|
|
122
|
-
"Consider using IfExists variants for optional conditions",
|
|
136
|
+
"Match the condition operator to the `Condition` key's data type",
|
|
137
|
+
"Use `String` operators for string keys, `Numeric` for numbers, `Date` for timestamps",
|
|
138
|
+
"Consider using `IfExists` variants for optional conditions",
|
|
123
139
|
],
|
|
140
|
+
risk_category="validation",
|
|
124
141
|
)
|
|
125
142
|
)
|
|
126
143
|
|
|
@@ -128,15 +145,16 @@ CheckDocumentationRegistry.register(
|
|
|
128
145
|
CheckDocumentation(
|
|
129
146
|
check_id="resource_validation",
|
|
130
147
|
risk_explanation=(
|
|
131
|
-
"Invalid
|
|
148
|
+
"Invalid `Resource` ARNs may silently fail to match intended resources, "
|
|
132
149
|
"leaving permissions ineffective or overly broad."
|
|
133
150
|
),
|
|
134
151
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
135
152
|
remediation_steps=[
|
|
136
|
-
"Verify ARN format matches the target service's documentation",
|
|
153
|
+
"Verify `Resource` ARN format matches the target service's documentation",
|
|
137
154
|
"Ensure region and account ID are correct or use wildcards intentionally",
|
|
138
|
-
"Test the policy with IAM policy simulator before deployment",
|
|
155
|
+
"Test the policy with AWS IAM policy simulator before deployment",
|
|
139
156
|
],
|
|
157
|
+
risk_category="validation",
|
|
140
158
|
)
|
|
141
159
|
)
|
|
142
160
|
|
|
@@ -153,6 +171,7 @@ CheckDocumentationRegistry.register(
|
|
|
153
171
|
"Use descriptive SIDs that indicate the statement's purpose",
|
|
154
172
|
"Consider a naming convention like 'AllowS3ReadAccess' or 'DenyPublicAccess'",
|
|
155
173
|
],
|
|
174
|
+
risk_category="compliance",
|
|
156
175
|
)
|
|
157
176
|
)
|
|
158
177
|
|
|
@@ -161,15 +180,16 @@ CheckDocumentationRegistry.register(
|
|
|
161
180
|
check_id="policy_size",
|
|
162
181
|
risk_explanation=(
|
|
163
182
|
"Policies exceeding AWS size limits cannot be attached to IAM entities. "
|
|
164
|
-
"Inline policies have a 2KB limit, managed policies have a 6KB limit."
|
|
183
|
+
"Inline policies have a 2KB limit, managed policies have a 6KB limit (for the entire policy document)."
|
|
165
184
|
),
|
|
166
185
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_iam-quotas.html",
|
|
167
186
|
remediation_steps=[
|
|
168
187
|
"Split large policies into multiple smaller policies",
|
|
169
188
|
"Use managed policies instead of inline policies for larger permissions",
|
|
170
|
-
"Remove redundant statements or consolidate similar
|
|
189
|
+
"Remove redundant statements or consolidate similar `Action`s",
|
|
171
190
|
"Consider using permission boundaries or SCPs for broad restrictions",
|
|
172
191
|
],
|
|
192
|
+
risk_category="validation",
|
|
173
193
|
)
|
|
174
194
|
)
|
|
175
195
|
|
|
@@ -177,15 +197,16 @@ CheckDocumentationRegistry.register(
|
|
|
177
197
|
CheckDocumentation(
|
|
178
198
|
check_id="policy_structure",
|
|
179
199
|
risk_explanation=(
|
|
180
|
-
"Malformed policy structure will cause IAM to reject the policy entirely, "
|
|
200
|
+
"Malformed policy structure will cause AWS IAM to reject the policy entirely, "
|
|
181
201
|
"preventing any permissions from being granted."
|
|
182
202
|
),
|
|
183
203
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_grammar.html",
|
|
184
204
|
remediation_steps=[
|
|
185
205
|
"Verify the policy follows AWS IAM policy grammar",
|
|
186
|
-
"Ensure all required elements (Version
|
|
187
|
-
"Check that Effect
|
|
206
|
+
"Ensure all required elements (`Version`, `Statement`) are present",
|
|
207
|
+
"Check that `Effect`, `Action`, and `Resource` are properly formatted",
|
|
188
208
|
],
|
|
209
|
+
risk_category="validation",
|
|
189
210
|
)
|
|
190
211
|
)
|
|
191
212
|
|
|
@@ -193,15 +214,16 @@ CheckDocumentationRegistry.register(
|
|
|
193
214
|
CheckDocumentation(
|
|
194
215
|
check_id="set_operator_validation",
|
|
195
216
|
risk_explanation=(
|
|
196
|
-
"Invalid ForAllValues
|
|
217
|
+
"Invalid `ForAllValues`/`ForAnyValue` operators may cause conditions to "
|
|
197
218
|
"behave unexpectedly, potentially granting or denying unintended access."
|
|
198
219
|
),
|
|
199
220
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_multi-value-conditions.html",
|
|
200
221
|
remediation_steps=[
|
|
201
|
-
"Use ForAllValues when ALL values must match the condition",
|
|
202
|
-
"Use ForAnyValue when ANY value matching is sufficient",
|
|
203
|
-
"Consider the empty set behavior: ForAllValues returns true for empty sets",
|
|
222
|
+
"Use `ForAllValues` when ALL values must match the condition",
|
|
223
|
+
"Use `ForAnyValue` when ANY value matching is sufficient",
|
|
224
|
+
"Consider the empty set behavior: `ForAllValues` returns true for empty sets",
|
|
204
225
|
],
|
|
226
|
+
risk_category="validation",
|
|
205
227
|
)
|
|
206
228
|
)
|
|
207
229
|
|
|
@@ -214,10 +236,11 @@ CheckDocumentationRegistry.register(
|
|
|
214
236
|
),
|
|
215
237
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/id_credentials_mfa_configure-api-require.html",
|
|
216
238
|
remediation_steps=[
|
|
217
|
-
"Add
|
|
218
|
-
"Consider using
|
|
239
|
+
"Add `aws:MultiFactorAuthPresent`: `true` condition for sensitive actions",
|
|
240
|
+
"Consider using `aws:MultiFactorAuthAge` to require recent MFA",
|
|
219
241
|
"Ensure MFA is enforced at the identity level as well as policy level",
|
|
220
242
|
],
|
|
243
|
+
risk_category="credential_exposure",
|
|
221
244
|
)
|
|
222
245
|
)
|
|
223
246
|
|
|
@@ -234,6 +257,7 @@ CheckDocumentationRegistry.register(
|
|
|
234
257
|
"Use specific principals instead of wildcards where possible",
|
|
235
258
|
"For service principals, use the canonical format (e.g., 's3.amazonaws.com')",
|
|
236
259
|
],
|
|
260
|
+
risk_category="resource_exposure",
|
|
237
261
|
)
|
|
238
262
|
)
|
|
239
263
|
|
|
@@ -246,10 +270,11 @@ CheckDocumentationRegistry.register(
|
|
|
246
270
|
),
|
|
247
271
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/access_policies.html",
|
|
248
272
|
remediation_steps=[
|
|
249
|
-
"Identity policies: Don't include Principal element",
|
|
250
|
-
"Resource policies: Include Principal element",
|
|
251
|
-
"SCPs: Use only Allow statements with specific conditions",
|
|
273
|
+
"Identity policies: Don't include `Principal` element",
|
|
274
|
+
"Resource policies: Include `Principal` element",
|
|
275
|
+
"SCPs: Use only `Allow` statements with specific conditions",
|
|
252
276
|
],
|
|
277
|
+
risk_category="configuration",
|
|
253
278
|
)
|
|
254
279
|
)
|
|
255
280
|
|
|
@@ -257,15 +282,16 @@ CheckDocumentationRegistry.register(
|
|
|
257
282
|
CheckDocumentation(
|
|
258
283
|
check_id="action_resource_matching",
|
|
259
284
|
risk_explanation=(
|
|
260
|
-
"
|
|
285
|
+
"`Action`s that don't support the specified `Resource`s will silently fail, "
|
|
261
286
|
"resulting in permissions that don't work as intended."
|
|
262
287
|
),
|
|
263
288
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
264
289
|
remediation_steps=[
|
|
265
290
|
"Check AWS documentation for supported resource types per action",
|
|
266
|
-
"Use
|
|
291
|
+
"Use `*` (wildcard) for actions that don't support resource-level permissions",
|
|
267
292
|
"Split statements when actions require different resource types",
|
|
268
293
|
],
|
|
294
|
+
risk_category="validation",
|
|
269
295
|
)
|
|
270
296
|
)
|
|
271
297
|
|
|
@@ -278,11 +304,12 @@ CheckDocumentationRegistry.register(
|
|
|
278
304
|
),
|
|
279
305
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/id_roles_create_for-user.html",
|
|
280
306
|
remediation_steps=[
|
|
281
|
-
"Restrict Principal to specific accounts/roles/users",
|
|
307
|
+
"Restrict `Principal` to specific accounts/roles/users (e.g., `arn:aws:iam::123456789012:role/foo`)",
|
|
282
308
|
"Add conditions to limit who can assume the role",
|
|
283
|
-
"Avoid wildcards in Principal unless absolutely necessary",
|
|
309
|
+
"Avoid wildcards in `Principal` unless absolutely necessary",
|
|
284
310
|
"Use ExternalId for cross-account role assumption",
|
|
285
311
|
],
|
|
312
|
+
risk_category="privilege_escalation",
|
|
286
313
|
)
|
|
287
314
|
)
|
|
288
315
|
|
|
@@ -293,16 +320,16 @@ CheckDocumentationRegistry.register(
|
|
|
293
320
|
CheckDocumentation(
|
|
294
321
|
check_id="wildcard_action",
|
|
295
322
|
risk_explanation=(
|
|
296
|
-
"Wildcard actions (e.g.,
|
|
323
|
+
"Wildcard actions (e.g., `s3:*`) grant all current AND future permissions "
|
|
297
324
|
"for a service, violating least privilege and increasing attack surface."
|
|
298
325
|
),
|
|
299
326
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
300
327
|
remediation_steps=[
|
|
301
|
-
"Replace wildcards with specific
|
|
302
|
-
"Use action groups like
|
|
303
|
-
"
|
|
304
|
-
"Review and reduce permissions periodically",
|
|
328
|
+
"Replace wildcards with specific `Action` lists needed for the use case",
|
|
329
|
+
"Use action groups like `s3:Get*` for read-only access",
|
|
330
|
+
"Review and reduce permissions periodically if not needed",
|
|
305
331
|
],
|
|
332
|
+
risk_category="privilege_escalation",
|
|
306
333
|
)
|
|
307
334
|
)
|
|
308
335
|
|
|
@@ -310,15 +337,17 @@ CheckDocumentationRegistry.register(
|
|
|
310
337
|
CheckDocumentation(
|
|
311
338
|
check_id="wildcard_resource",
|
|
312
339
|
risk_explanation=(
|
|
313
|
-
"Wildcard resources (
|
|
340
|
+
"Wildcard resources (`*`) grant access to ALL resources of a type, "
|
|
314
341
|
"including resources created in the future, violating least privilege."
|
|
315
342
|
),
|
|
316
343
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
317
344
|
remediation_steps=[
|
|
318
|
-
"Specify exact
|
|
319
|
-
"Use resource tags and conditions for dynamic access control",
|
|
345
|
+
"Specify exact `Resource` ARNs when possible",
|
|
346
|
+
"Use resource tags and conditions for dynamic access control (ABAC)",
|
|
320
347
|
"Limit scope to specific accounts, regions, or resource prefixes",
|
|
348
|
+
"Use `aws:ResourceAccount`, `aws:ResourceOrgID`, or `aws:ResourceOrgPaths` conditions to restrict scope",
|
|
321
349
|
],
|
|
350
|
+
risk_category="resource_exposure",
|
|
322
351
|
)
|
|
323
352
|
)
|
|
324
353
|
|
|
@@ -336,6 +365,7 @@ CheckDocumentationRegistry.register(
|
|
|
336
365
|
"Implement permission boundaries to limit maximum possible permissions",
|
|
337
366
|
"Consider using service control policies (SCPs) as guardrails",
|
|
338
367
|
],
|
|
368
|
+
risk_category="privilege_escalation",
|
|
339
369
|
)
|
|
340
370
|
)
|
|
341
371
|
|
|
@@ -343,15 +373,16 @@ CheckDocumentationRegistry.register(
|
|
|
343
373
|
CheckDocumentation(
|
|
344
374
|
check_id="service_wildcard",
|
|
345
375
|
risk_explanation=(
|
|
346
|
-
"Service-level wildcards (e.g.,
|
|
376
|
+
"Service-level wildcards (e.g., `iam:*`) grant all permissions for "
|
|
347
377
|
"an entire service, including destructive and privilege escalation actions."
|
|
348
378
|
),
|
|
349
379
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
350
380
|
remediation_steps=[
|
|
351
381
|
"Replace with specific actions required for the use case",
|
|
352
382
|
"Use AWS managed policies for common patterns",
|
|
353
|
-
"Consider permission boundaries to limit sensitive actions",
|
|
383
|
+
"Consider permission boundaries to limit sensitive actions and enforce least privilege",
|
|
354
384
|
],
|
|
385
|
+
risk_category="privilege_escalation",
|
|
355
386
|
)
|
|
356
387
|
)
|
|
357
388
|
|
|
@@ -359,16 +390,16 @@ CheckDocumentationRegistry.register(
|
|
|
359
390
|
CheckDocumentation(
|
|
360
391
|
check_id="sensitive_action",
|
|
361
392
|
risk_explanation=(
|
|
362
|
-
"Sensitive actions (e.g., iam
|
|
393
|
+
"Sensitive actions (e.g., `iam:*`, `sts:AssumeRole`, `kms:Decrypt`) can lead "
|
|
363
394
|
"to privilege escalation, data exfiltration, or account compromise."
|
|
364
395
|
),
|
|
365
396
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
366
397
|
remediation_steps=[
|
|
367
398
|
"Add conditions to restrict when these actions can be used",
|
|
368
|
-
"Require
|
|
369
|
-
"Limit to specific resources where possible",
|
|
370
|
-
"Implement monitoring and alerting for sensitive action usage",
|
|
399
|
+
"Require Attribute Based Access Control (ABAC) for sensitive operations",
|
|
400
|
+
"Limit to specific resources and accounts where possible",
|
|
371
401
|
],
|
|
402
|
+
risk_category="privilege_escalation",
|
|
372
403
|
)
|
|
373
404
|
)
|
|
374
405
|
|
|
@@ -377,14 +408,36 @@ CheckDocumentationRegistry.register(
|
|
|
377
408
|
check_id="action_condition_enforcement",
|
|
378
409
|
risk_explanation=(
|
|
379
410
|
"Certain sensitive actions should always have conditions to prevent "
|
|
380
|
-
"misuse, such as
|
|
411
|
+
"misuse, such as Account/Organization boundaries, VPC/VPCe restrictions, MFA requirements, or time-based access."
|
|
381
412
|
),
|
|
382
413
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_condition.html",
|
|
383
414
|
remediation_steps=[
|
|
384
415
|
"Add appropriate conditions based on the action type",
|
|
385
|
-
"Use aws:
|
|
386
|
-
"Use aws:
|
|
387
|
-
"Use aws:
|
|
416
|
+
"Use `aws:ResourceAccount` and `aws:PrincipalAccount` for account-restricted actions",
|
|
417
|
+
"Use `aws:ResourceOrgID` and `aws:PrincipalOrgID` for organization-restricted actions",
|
|
418
|
+
"Use `aws:SourceVpc` or `aws:SourceVpce` for VPC-restricted actions",
|
|
419
|
+
"Use `aws:SourceIp` for network-restricted actions",
|
|
420
|
+
"Use `aws:RequestedRegion` to limit geographic scope",
|
|
421
|
+
],
|
|
422
|
+
risk_category="compliance",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
CheckDocumentationRegistry.register(
|
|
427
|
+
CheckDocumentation(
|
|
428
|
+
check_id="not_action_not_resource",
|
|
429
|
+
risk_explanation=(
|
|
430
|
+
"`NotAction` and `NotResource` grant permissions by exclusion rather than "
|
|
431
|
+
"inclusion. This can accidentally grant far more access than intended, "
|
|
432
|
+
"including access to actions and resources created in the future."
|
|
433
|
+
),
|
|
434
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_notaction.html",
|
|
435
|
+
remediation_steps=[
|
|
436
|
+
"Replace `NotAction` with explicit `Action` lists when possible",
|
|
437
|
+
"Replace `NotResource` with specific `Resource` ARNs",
|
|
438
|
+
"If `NotAction` is required, add strict conditions (`aws:SourceIp`, `aws:SourceVpc`, `aws:SourceVpce`, `aws:ResourceAccount`, `aws:ResourceOrgID`, `aws:RequestedRegion`, etc.)",
|
|
439
|
+
"Document why exclusion-based permissions are necessary",
|
|
388
440
|
],
|
|
441
|
+
risk_category="privilege_escalation",
|
|
389
442
|
)
|
|
390
443
|
)
|