iam-policy-validator 1.7.2__py3-none-any.whl → 1.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +127 -6
- iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
- iam_validator/__init__.py +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +559 -207
- iam_validator/checks/action_resource_matching.py +12 -15
- iam_validator/checks/action_validation.py +7 -13
- iam_validator/checks/condition_key_validation.py +7 -13
- iam_validator/checks/condition_type_mismatch.py +15 -22
- iam_validator/checks/full_wildcard.py +9 -13
- iam_validator/checks/mfa_condition_check.py +8 -17
- iam_validator/checks/policy_size.py +6 -39
- iam_validator/checks/policy_structure.py +547 -0
- iam_validator/checks/policy_type_validation.py +61 -46
- iam_validator/checks/principal_validation.py +71 -148
- iam_validator/checks/resource_validation.py +13 -20
- iam_validator/checks/sensitive_action.py +15 -18
- iam_validator/checks/service_wildcard.py +8 -14
- iam_validator/checks/set_operator_validation.py +21 -28
- iam_validator/checks/sid_uniqueness.py +16 -42
- iam_validator/checks/trust_policy_validation.py +506 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +2 -2
- iam_validator/checks/wildcard_action.py +9 -13
- iam_validator/checks/wildcard_resource.py +9 -13
- iam_validator/commands/cache.py +4 -3
- iam_validator/commands/validate.py +15 -9
- iam_validator/core/__init__.py +2 -3
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +24 -1028
- 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 +612 -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 +379 -0
- iam_validator/core/check_registry.py +165 -93
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/defaults.py +58 -52
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/constants.py +17 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +38 -475
- iam_validator/core/policy_loader.py +27 -4
- iam_validator/sdk/__init__.py +1 -1
- iam_validator/sdk/context.py +1 -1
- iam_validator/sdk/helpers.py +1 -1
- iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,11 +10,12 @@ This module provides a pluggable check system that allows:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
-
from abc import ABC
|
|
13
|
+
from abc import ABC
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
|
-
from iam_validator.core.
|
|
17
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
18
|
+
from iam_validator.core.ignore_patterns import IgnorePatternMatcher
|
|
18
19
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
@@ -36,107 +37,64 @@ class CheckConfig:
|
|
|
36
37
|
List of patterns to ignore findings.
|
|
37
38
|
|
|
38
39
|
Each pattern is a dict with optional fields:
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
40
|
+
- filepath: Regex to match file path
|
|
41
|
+
- action: Regex to match action name
|
|
42
|
+
- resource: Regex to match resource
|
|
43
|
+
- sid: Exact SID to match (or regex if ends with .*)
|
|
44
|
+
- condition_key: Regex to match condition key
|
|
44
45
|
|
|
45
46
|
Multiple fields in one pattern = AND logic
|
|
46
47
|
Multiple patterns = OR logic (any pattern matches → ignore)
|
|
47
48
|
|
|
48
49
|
Example:
|
|
49
50
|
ignore_patterns:
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
-
|
|
51
|
+
- filepath: "test/.*|examples/.*"
|
|
52
|
+
- filepath: "policies/readonly-.*"
|
|
53
|
+
action: ".*:(Get|List|Describe).*"
|
|
54
|
+
- sid: "AllowReadOnlyAccess"
|
|
54
55
|
"""
|
|
55
56
|
|
|
56
57
|
def should_ignore(self, issue: ValidationIssue, filepath: str = "") -> bool:
|
|
57
58
|
"""
|
|
58
59
|
Check if issue should be ignored based on ignore patterns.
|
|
59
60
|
|
|
61
|
+
Uses centralized IgnorePatternMatcher for high-performance filtering
|
|
62
|
+
with cached compiled regex patterns.
|
|
63
|
+
|
|
60
64
|
Args:
|
|
61
65
|
issue: The validation issue to check
|
|
62
66
|
filepath: Path to the policy file
|
|
63
67
|
|
|
64
68
|
Returns:
|
|
65
69
|
True if the issue should be ignored
|
|
66
|
-
"""
|
|
67
|
-
if not self.ignore_patterns:
|
|
68
|
-
return False
|
|
69
|
-
|
|
70
|
-
import re
|
|
71
|
-
|
|
72
|
-
for pattern in self.ignore_patterns:
|
|
73
|
-
if self._matches_pattern(pattern, issue, filepath, re):
|
|
74
|
-
return True
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
Performance:
|
|
72
|
+
- Cached regex compilation (LRU cache)
|
|
73
|
+
- Early exit optimization
|
|
74
|
+
"""
|
|
75
|
+
return IgnorePatternMatcher.should_ignore_issue(issue, filepath, self.ignore_patterns)
|
|
77
76
|
|
|
78
|
-
def
|
|
79
|
-
self,
|
|
80
|
-
pattern: dict[str, Any],
|
|
81
|
-
issue: ValidationIssue,
|
|
82
|
-
filepath: str,
|
|
83
|
-
re_module: Any,
|
|
84
|
-
) -> bool:
|
|
77
|
+
def filter_actions(self, actions: frozenset[str]) -> frozenset[str]:
|
|
85
78
|
"""
|
|
86
|
-
|
|
79
|
+
Filter actions based on action ignore patterns.
|
|
80
|
+
|
|
81
|
+
Uses centralized IgnorePatternMatcher for high-performance filtering
|
|
82
|
+
with cached compiled regex patterns.
|
|
87
83
|
|
|
88
|
-
|
|
84
|
+
This is useful for checks that need to filter a set of actions before
|
|
85
|
+
creating ValidationIssues (e.g., sensitive_action check).
|
|
89
86
|
|
|
90
87
|
Args:
|
|
91
|
-
|
|
92
|
-
issue: ValidationIssue to check
|
|
93
|
-
filepath: Path to policy file
|
|
94
|
-
re_module: re module for regex matching
|
|
88
|
+
actions: Set of actions to filter
|
|
95
89
|
|
|
96
90
|
Returns:
|
|
97
|
-
|
|
91
|
+
Filtered set of actions (actions matching ignore patterns removed)
|
|
92
|
+
|
|
93
|
+
Performance:
|
|
94
|
+
- Cached regex compilation (LRU cache)
|
|
95
|
+
- Early exit per action on first match
|
|
98
96
|
"""
|
|
99
|
-
|
|
100
|
-
actual_value = None
|
|
101
|
-
|
|
102
|
-
if field_name == "filepath_regex":
|
|
103
|
-
actual_value = filepath
|
|
104
|
-
elif field_name == "action_matches":
|
|
105
|
-
actual_value = issue.action or ""
|
|
106
|
-
elif field_name == "resource_matches":
|
|
107
|
-
actual_value = issue.resource or ""
|
|
108
|
-
elif field_name == "statement_sid":
|
|
109
|
-
# For SID, support both exact match and regex
|
|
110
|
-
if isinstance(regex_pattern, str) and "*" in regex_pattern:
|
|
111
|
-
# Treat as regex if contains wildcard
|
|
112
|
-
actual_value = issue.statement_sid or ""
|
|
113
|
-
else:
|
|
114
|
-
# Exact match
|
|
115
|
-
if issue.statement_sid != regex_pattern:
|
|
116
|
-
return False
|
|
117
|
-
continue
|
|
118
|
-
elif field_name == "condition_key_matches":
|
|
119
|
-
actual_value = issue.condition_key or ""
|
|
120
|
-
else:
|
|
121
|
-
# Unknown field, skip
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
# Check regex match (case-insensitive)
|
|
125
|
-
if actual_value is None:
|
|
126
|
-
return False
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
if not re_module.search(
|
|
130
|
-
str(regex_pattern),
|
|
131
|
-
str(actual_value),
|
|
132
|
-
re_module.IGNORECASE,
|
|
133
|
-
):
|
|
134
|
-
return False
|
|
135
|
-
except re_module.error:
|
|
136
|
-
# Invalid regex, don't match
|
|
137
|
-
return False
|
|
138
|
-
|
|
139
|
-
return True # All fields matched
|
|
97
|
+
return IgnorePatternMatcher.filter_actions(actions, self.ignore_patterns)
|
|
140
98
|
|
|
141
99
|
|
|
142
100
|
class PolicyCheck(ABC):
|
|
@@ -145,38 +103,101 @@ class PolicyCheck(ABC):
|
|
|
145
103
|
|
|
146
104
|
To create a custom check:
|
|
147
105
|
1. Inherit from this class
|
|
148
|
-
2. Implement check_id
|
|
149
|
-
3.
|
|
106
|
+
2. Implement check_id and description (required)
|
|
107
|
+
3. Implement either execute() OR execute_policy() (or both)
|
|
108
|
+
4. Register with CheckRegistry
|
|
150
109
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
110
|
+
Two ways to define check_id and description:
|
|
111
|
+
|
|
112
|
+
Option 1 - Class attributes (simpler, recommended for static values):
|
|
113
|
+
from typing import ClassVar
|
|
114
|
+
|
|
115
|
+
class MyCheck(PolicyCheck):
|
|
116
|
+
check_id: ClassVar[str] = "my_check"
|
|
117
|
+
description: ClassVar[str] = "My check description"
|
|
155
118
|
|
|
156
119
|
async def execute(self, statement, statement_idx, fetcher, config):
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
Note: ClassVar annotation is required for Pylance type checker compatibility.
|
|
123
|
+
|
|
124
|
+
Option 2 - Property decorators (more flexible, supports dynamic values):
|
|
125
|
+
class MyCheck(PolicyCheck):
|
|
126
|
+
@property
|
|
127
|
+
def check_id(self) -> str:
|
|
128
|
+
return "my_check"
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def description(self) -> str:
|
|
132
|
+
return "My check description"
|
|
133
|
+
|
|
134
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
Statement-level check example:
|
|
138
|
+
from typing import ClassVar
|
|
139
|
+
|
|
140
|
+
class MyStatementCheck(PolicyCheck):
|
|
141
|
+
check_id: ClassVar[str] = "my_statement_check"
|
|
142
|
+
description: ClassVar[str] = "Validates individual statements"
|
|
143
|
+
|
|
144
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
145
|
+
issues = []
|
|
146
|
+
# Your validation logic here
|
|
147
|
+
return issues
|
|
148
|
+
|
|
149
|
+
Policy-level check example:
|
|
150
|
+
from typing import ClassVar
|
|
151
|
+
|
|
152
|
+
class MyPolicyCheck(PolicyCheck):
|
|
153
|
+
check_id: ClassVar[str] = "my_policy_check"
|
|
154
|
+
description: ClassVar[str] = "Validates entire policy"
|
|
155
|
+
|
|
156
|
+
async def execute_policy(self, policy, policy_file, fetcher, config, **kwargs):
|
|
157
157
|
issues = []
|
|
158
158
|
# Your validation logic here
|
|
159
159
|
return issues
|
|
160
160
|
"""
|
|
161
161
|
|
|
162
162
|
@property
|
|
163
|
-
@abstractmethod
|
|
164
163
|
def check_id(self) -> str:
|
|
165
164
|
"""Unique identifier for this check (e.g., 'action_validation')."""
|
|
166
|
-
|
|
165
|
+
raise NotImplementedError("Subclasses must define check_id")
|
|
167
166
|
|
|
168
167
|
@property
|
|
169
|
-
@abstractmethod
|
|
170
168
|
def description(self) -> str:
|
|
171
169
|
"""Human-readable description of what this check does."""
|
|
172
|
-
|
|
170
|
+
raise NotImplementedError("Subclasses must define description")
|
|
173
171
|
|
|
174
172
|
@property
|
|
175
173
|
def default_severity(self) -> str:
|
|
176
174
|
"""Default severity level for issues found by this check."""
|
|
177
175
|
return "warning"
|
|
178
176
|
|
|
179
|
-
|
|
177
|
+
def __init_subclass__(cls, **kwargs):
|
|
178
|
+
"""
|
|
179
|
+
Validate that subclasses override at least one execution method.
|
|
180
|
+
|
|
181
|
+
This ensures checks implement either execute() OR execute_policy() (or both).
|
|
182
|
+
If neither is overridden, the check would never produce any results.
|
|
183
|
+
"""
|
|
184
|
+
super().__init_subclass__(**kwargs)
|
|
185
|
+
|
|
186
|
+
# Skip validation for abstract classes
|
|
187
|
+
if ABC in cls.__bases__:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Check if at least one method is overridden
|
|
191
|
+
has_execute = cls.execute is not PolicyCheck.execute
|
|
192
|
+
has_execute_policy = cls.execute_policy is not PolicyCheck.execute_policy
|
|
193
|
+
|
|
194
|
+
if not has_execute and not has_execute_policy:
|
|
195
|
+
raise TypeError(
|
|
196
|
+
f"Check '{cls.__name__}' must override at least one of: "
|
|
197
|
+
"execute() for statement-level checks, or "
|
|
198
|
+
"execute_policy() for policy-level checks"
|
|
199
|
+
)
|
|
200
|
+
|
|
180
201
|
async def execute(
|
|
181
202
|
self,
|
|
182
203
|
statement: Statement,
|
|
@@ -187,6 +208,10 @@ class PolicyCheck(ABC):
|
|
|
187
208
|
"""
|
|
188
209
|
Execute the check on a policy statement.
|
|
189
210
|
|
|
211
|
+
This method is called for statement-level checks. If your check only needs
|
|
212
|
+
to examine the entire policy (not individual statements), you can leave this
|
|
213
|
+
as the default implementation and override execute_policy() instead.
|
|
214
|
+
|
|
190
215
|
Args:
|
|
191
216
|
statement: The IAM policy statement to check
|
|
192
217
|
statement_idx: Index of the statement in the policy
|
|
@@ -196,7 +221,8 @@ class PolicyCheck(ABC):
|
|
|
196
221
|
Returns:
|
|
197
222
|
List of ValidationIssue objects found by this check
|
|
198
223
|
"""
|
|
199
|
-
|
|
224
|
+
del statement, statement_idx, fetcher, config # Unused in default implementation
|
|
225
|
+
return []
|
|
200
226
|
|
|
201
227
|
async def execute_policy(
|
|
202
228
|
self,
|
|
@@ -399,6 +425,10 @@ class CheckRegistry:
|
|
|
399
425
|
config = self.get_config(check.check_id)
|
|
400
426
|
if config:
|
|
401
427
|
issues = await check.execute(statement, statement_idx, fetcher, config)
|
|
428
|
+
# Inject check_id into each issue
|
|
429
|
+
for issue in issues:
|
|
430
|
+
if issue.check_id is None:
|
|
431
|
+
issue.check_id = check.check_id
|
|
402
432
|
# Filter issues based on ignore_patterns
|
|
403
433
|
filtered_issues = [
|
|
404
434
|
issue for issue in issues if not config.should_ignore(issue, filepath)
|
|
@@ -427,7 +457,12 @@ class CheckRegistry:
|
|
|
427
457
|
check = enabled_checks[idx]
|
|
428
458
|
print(f"Warning: Check '{check.check_id}' failed: {result}")
|
|
429
459
|
elif isinstance(result, list):
|
|
460
|
+
check = enabled_checks[idx]
|
|
430
461
|
config = configs[idx]
|
|
462
|
+
# Inject check_id into each issue
|
|
463
|
+
for issue in result:
|
|
464
|
+
if issue.check_id is None:
|
|
465
|
+
issue.check_id = check.check_id
|
|
431
466
|
# Filter issues based on ignore_patterns
|
|
432
467
|
filtered_issues = [
|
|
433
468
|
issue for issue in result if not config.should_ignore(issue, filepath)
|
|
@@ -475,6 +510,7 @@ class CheckRegistry:
|
|
|
475
510
|
policy_file: str,
|
|
476
511
|
fetcher: AWSServiceFetcher,
|
|
477
512
|
policy_type: str = "IDENTITY_POLICY",
|
|
513
|
+
**kwargs,
|
|
478
514
|
) -> list[ValidationIssue]:
|
|
479
515
|
"""
|
|
480
516
|
Execute all enabled policy-level checks.
|
|
@@ -487,6 +523,7 @@ class CheckRegistry:
|
|
|
487
523
|
policy_file: Path to the policy file (for context/reporting)
|
|
488
524
|
fetcher: AWS service fetcher for API calls
|
|
489
525
|
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
526
|
+
**kwargs: Additional arguments to pass to checks (e.g., raw_policy_dict)
|
|
490
527
|
|
|
491
528
|
Returns:
|
|
492
529
|
List of all ValidationIssue objects from all policy-level checks
|
|
@@ -507,34 +544,61 @@ class CheckRegistry:
|
|
|
507
544
|
if config:
|
|
508
545
|
try:
|
|
509
546
|
issues = await check.execute_policy(
|
|
510
|
-
policy,
|
|
547
|
+
policy,
|
|
548
|
+
policy_file,
|
|
549
|
+
fetcher,
|
|
550
|
+
config,
|
|
551
|
+
policy_type=policy_type,
|
|
552
|
+
**kwargs,
|
|
511
553
|
)
|
|
512
|
-
|
|
554
|
+
# Inject check_id into each issue
|
|
555
|
+
for issue in issues:
|
|
556
|
+
if issue.check_id is None:
|
|
557
|
+
issue.check_id = check.check_id
|
|
558
|
+
# Filter issues based on ignore_patterns
|
|
559
|
+
filtered_issues = [
|
|
560
|
+
issue
|
|
561
|
+
for issue in issues
|
|
562
|
+
if not config.should_ignore(issue, policy_file)
|
|
563
|
+
]
|
|
564
|
+
all_issues.extend(filtered_issues)
|
|
513
565
|
except Exception as e:
|
|
514
566
|
print(f"Warning: Check '{check.check_id}' failed: {e}")
|
|
515
567
|
return all_issues
|
|
516
568
|
|
|
517
569
|
# Execute all policy-level checks in parallel
|
|
518
570
|
tasks = []
|
|
571
|
+
configs = []
|
|
519
572
|
for check in policy_level_checks:
|
|
520
573
|
config = self.get_config(check.check_id)
|
|
521
574
|
if config:
|
|
522
575
|
task = check.execute_policy(
|
|
523
|
-
policy, policy_file, fetcher, config, policy_type=policy_type
|
|
576
|
+
policy, policy_file, fetcher, config, policy_type=policy_type, **kwargs
|
|
524
577
|
)
|
|
525
578
|
tasks.append(task)
|
|
579
|
+
configs.append(config)
|
|
526
580
|
|
|
527
581
|
# Wait for all checks to complete
|
|
528
582
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
529
583
|
|
|
530
|
-
# Collect all issues, handling any exceptions
|
|
584
|
+
# Collect all issues, handling any exceptions and applying ignore_patterns
|
|
531
585
|
for idx, result in enumerate(results):
|
|
532
586
|
if isinstance(result, Exception):
|
|
533
587
|
# Log error but continue with other checks
|
|
534
588
|
check = policy_level_checks[idx]
|
|
535
589
|
print(f"Warning: Check '{check.check_id}' failed: {result}")
|
|
536
590
|
elif isinstance(result, list):
|
|
537
|
-
|
|
591
|
+
check = policy_level_checks[idx]
|
|
592
|
+
config = configs[idx]
|
|
593
|
+
# Inject check_id into each issue
|
|
594
|
+
for issue in result:
|
|
595
|
+
if issue.check_id is None:
|
|
596
|
+
issue.check_id = check.check_id
|
|
597
|
+
# Filter issues based on ignore_patterns
|
|
598
|
+
filtered_issues = [
|
|
599
|
+
issue for issue in result if not config.should_ignore(issue, policy_file)
|
|
600
|
+
]
|
|
601
|
+
all_issues.extend(filtered_issues)
|
|
538
602
|
|
|
539
603
|
return all_issues
|
|
540
604
|
|
|
@@ -561,6 +625,11 @@ def create_default_registry(
|
|
|
561
625
|
# Import and register built-in checks
|
|
562
626
|
from iam_validator import checks
|
|
563
627
|
|
|
628
|
+
# 0. FUNDAMENTAL STRUCTURE (Must run FIRST - validates basic policy structure)
|
|
629
|
+
registry.register(
|
|
630
|
+
checks.PolicyStructureCheck()
|
|
631
|
+
) # Policy-level: Validates required fields, conflicts, valid values
|
|
632
|
+
|
|
564
633
|
# 1. POLICY STRUCTURE (Checks that examine the entire policy, not individual statements)
|
|
565
634
|
registry.register(
|
|
566
635
|
checks.SidUniquenessCheck()
|
|
@@ -600,6 +669,9 @@ def create_default_registry(
|
|
|
600
669
|
registry.register(
|
|
601
670
|
checks.PrincipalValidationCheck()
|
|
602
671
|
) # Principal validation (resource policies)
|
|
672
|
+
registry.register(
|
|
673
|
+
checks.TrustPolicyValidationCheck()
|
|
674
|
+
) # Trust policy validation (role assumption policies)
|
|
603
675
|
|
|
604
676
|
# Note: policy_type_validation is a standalone function (not a class-based check)
|
|
605
677
|
# and is called separately in the validation flow
|
|
@@ -54,23 +54,75 @@ IAM_PASS_ROLE_REQUIREMENT: Final[dict[str, Any]] = {
|
|
|
54
54
|
S3_WRITE_ORG_ID: Final[dict[str, Any]] = {
|
|
55
55
|
"actions": ["s3:PutObject"],
|
|
56
56
|
"severity": "medium",
|
|
57
|
-
"required_conditions":
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
57
|
+
"required_conditions": {
|
|
58
|
+
"any_of": [
|
|
59
|
+
# Option 1: Use organization-level control with ResourceOrgID
|
|
60
|
+
{
|
|
61
|
+
"all_of": [
|
|
62
|
+
{
|
|
63
|
+
"condition_key": "aws:ResourceOrgID",
|
|
64
|
+
"description": "Restrict S3 write actions to resources within your AWS Organization",
|
|
65
|
+
"expected_value": "${aws:PrincipalOrgID}",
|
|
66
|
+
"example": (
|
|
67
|
+
"{\n"
|
|
68
|
+
' "Condition": {\n'
|
|
69
|
+
' "StringEquals": {\n'
|
|
70
|
+
' "aws:ResourceOrgID": "${aws:PrincipalOrgID}",\n'
|
|
71
|
+
' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
|
|
72
|
+
" }\n"
|
|
73
|
+
" }\n"
|
|
74
|
+
"}"
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"condition_key": "aws:ResourceAccount",
|
|
79
|
+
"description": "Ensure the S3 resource belongs to the same AWS account as the principal",
|
|
80
|
+
"expected_value": "${aws:PrincipalAccount}",
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
# Option 2: Use organization path-based control
|
|
85
|
+
{
|
|
86
|
+
"all_of": [
|
|
87
|
+
{
|
|
88
|
+
"condition_key": "aws:ResourceOrgPaths",
|
|
89
|
+
"description": "Restrict S3 write actions to resources within your AWS Organization path",
|
|
90
|
+
"expected_value": "${aws:PrincipalOrgPaths}",
|
|
91
|
+
"example": (
|
|
92
|
+
"{\n"
|
|
93
|
+
' "Condition": {\n'
|
|
94
|
+
' "StringEquals": {\n'
|
|
95
|
+
' "aws:ResourceOrgPaths": "${aws:PrincipalOrgPaths}",\n'
|
|
96
|
+
' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
|
|
97
|
+
" }\n"
|
|
98
|
+
" }\n"
|
|
99
|
+
"}"
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"condition_key": "aws:ResourceAccount",
|
|
104
|
+
"description": "Ensure the S3 resource belongs to the same AWS account as the principal",
|
|
105
|
+
"expected_value": "${aws:PrincipalAccount}",
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
# Option 3: Account-only control (less restrictive, but still secure)
|
|
110
|
+
{
|
|
111
|
+
"condition_key": "aws:ResourceAccount",
|
|
112
|
+
"description": "Restrict S3 write actions to resources within the same AWS account",
|
|
113
|
+
"expected_value": "${aws:PrincipalAccount}",
|
|
114
|
+
"example": (
|
|
115
|
+
"{\n"
|
|
116
|
+
' "Condition": {\n'
|
|
117
|
+
' "StringEquals": {\n'
|
|
118
|
+
' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
|
|
119
|
+
" }\n"
|
|
120
|
+
" }\n"
|
|
121
|
+
"}"
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
74
126
|
}
|
|
75
127
|
|
|
76
128
|
# IP Restrictions - Source IP requirements
|