iam-policy-validator 1.4.0__py3-none-any.whl → 1.6.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.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +106 -78
- iam_policy_validator-1.6.0.dist-info/RECORD +82 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +20 -4
- iam_validator/checks/action_condition_enforcement.py +165 -8
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +24 -2
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +67 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/principal_validation.py +497 -3
- iam_validator/checks/sensitive_action.py +250 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +74 -32
- iam_validator/checks/wildcard_action.py +62 -0
- iam_validator/checks/wildcard_resource.py +131 -0
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/download_services.py +3 -8
- iam_validator/commands/validate.py +72 -13
- iam_validator/core/aws_fetcher.py +114 -64
- iam_validator/core/check_registry.py +167 -29
- 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 +104 -0
- iam_validator/core/config/condition_requirements.py +155 -0
- iam_validator/core/{config_loader.py → config/config_loader.py} +32 -9
- iam_validator/core/config/defaults.py +523 -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 +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/formatters/enhanced.py +11 -5
- iam_validator/core/formatters/sarif.py +78 -14
- iam_validator/core/models.py +14 -1
- iam_validator/core/policy_checks.py +4 -4
- iam_validator/core/pr_commenter.py +1 -1
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +274 -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 +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
- iam_policy_validator-1.4.0.dist-info/RECORD +0 -56
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/checks/security_best_practices.py +0 -536
- iam_validator/core/aws_global_conditions.py +0 -137
- iam_validator/core/defaults.py +0 -393
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,13 +4,40 @@ Validates Principal elements in resource-based policies for security best practi
|
|
|
4
4
|
This check enforces:
|
|
5
5
|
- Blocked principals (e.g., public access via "*")
|
|
6
6
|
- Allowed principals whitelist (optional)
|
|
7
|
-
- Required conditions for specific principals
|
|
7
|
+
- Required conditions for specific principals (simple format)
|
|
8
|
+
- Rich condition requirements for principals (advanced format with all_of/any_of)
|
|
8
9
|
- Service principal validation
|
|
9
10
|
|
|
10
11
|
Only runs for RESOURCE_POLICY type policies.
|
|
12
|
+
|
|
13
|
+
Configuration supports TWO formats:
|
|
14
|
+
|
|
15
|
+
1. Simple format (backward compatible):
|
|
16
|
+
require_conditions_for:
|
|
17
|
+
"*": ["aws:SourceArn", "aws:SourceAccount"]
|
|
18
|
+
"arn:aws:iam::*:root": ["aws:PrincipalOrgID"]
|
|
19
|
+
|
|
20
|
+
2. Advanced format with rich condition requirements:
|
|
21
|
+
principal_condition_requirements:
|
|
22
|
+
- principals:
|
|
23
|
+
- "*"
|
|
24
|
+
severity: critical
|
|
25
|
+
required_conditions:
|
|
26
|
+
all_of:
|
|
27
|
+
- condition_key: "aws:SourceArn"
|
|
28
|
+
description: "Limit by source ARN"
|
|
29
|
+
- condition_key: "aws:SourceAccount"
|
|
30
|
+
|
|
31
|
+
- principals:
|
|
32
|
+
- "arn:aws:iam::*:root"
|
|
33
|
+
required_conditions:
|
|
34
|
+
- condition_key: "aws:PrincipalOrgID"
|
|
35
|
+
expected_value: "o-xxxxx"
|
|
36
|
+
operator: "StringEquals"
|
|
11
37
|
"""
|
|
12
38
|
|
|
13
39
|
import fnmatch
|
|
40
|
+
from typing import Any
|
|
14
41
|
|
|
15
42
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
16
43
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
@@ -22,7 +49,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
22
49
|
|
|
23
50
|
@property
|
|
24
51
|
def check_id(self) -> str:
|
|
25
|
-
return "
|
|
52
|
+
return "principal_validation"
|
|
26
53
|
|
|
27
54
|
@property
|
|
28
55
|
def description(self) -> str:
|
|
@@ -60,6 +87,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
60
87
|
blocked_principals = config.config.get("blocked_principals", ["*"])
|
|
61
88
|
allowed_principals = config.config.get("allowed_principals", [])
|
|
62
89
|
require_conditions_for = config.config.get("require_conditions_for", {})
|
|
90
|
+
principal_condition_requirements = config.config.get("principal_condition_requirements", [])
|
|
63
91
|
allowed_service_principals = config.config.get(
|
|
64
92
|
"allowed_service_principals",
|
|
65
93
|
[
|
|
@@ -114,7 +142,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
114
142
|
)
|
|
115
143
|
continue
|
|
116
144
|
|
|
117
|
-
# Check
|
|
145
|
+
# Check simple format: require_conditions_for (backward compatible)
|
|
118
146
|
required_conditions = self._get_required_conditions(principal, require_conditions_for)
|
|
119
147
|
if required_conditions:
|
|
120
148
|
missing_conditions = self._check_required_conditions(statement, required_conditions)
|
|
@@ -138,6 +166,13 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
138
166
|
)
|
|
139
167
|
)
|
|
140
168
|
|
|
169
|
+
# Check advanced format: principal_condition_requirements
|
|
170
|
+
if principal_condition_requirements:
|
|
171
|
+
condition_issues = self._validate_principal_condition_requirements(
|
|
172
|
+
statement, statement_idx, principals, principal_condition_requirements, config
|
|
173
|
+
)
|
|
174
|
+
issues.extend(condition_issues)
|
|
175
|
+
|
|
141
176
|
return issues
|
|
142
177
|
|
|
143
178
|
def _extract_principals(self, statement: Statement) -> list[str]:
|
|
@@ -280,3 +315,462 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
280
315
|
# Find missing keys
|
|
281
316
|
missing = [key for key in required_keys if key not in present_keys]
|
|
282
317
|
return missing
|
|
318
|
+
|
|
319
|
+
def _validate_principal_condition_requirements(
|
|
320
|
+
self,
|
|
321
|
+
statement: Statement,
|
|
322
|
+
statement_idx: int,
|
|
323
|
+
principals: list[str],
|
|
324
|
+
requirements: list[dict[str, Any]],
|
|
325
|
+
config: CheckConfig,
|
|
326
|
+
) -> list[ValidationIssue]:
|
|
327
|
+
"""Validate advanced principal condition requirements.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
statement: The statement to validate
|
|
331
|
+
statement_idx: Index of the statement
|
|
332
|
+
principals: List of principals from the statement
|
|
333
|
+
requirements: List of principal condition requirements
|
|
334
|
+
config: Check configuration
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of validation issues
|
|
338
|
+
"""
|
|
339
|
+
issues: list[ValidationIssue] = []
|
|
340
|
+
|
|
341
|
+
# Check each requirement rule
|
|
342
|
+
for requirement in requirements:
|
|
343
|
+
# Check if any principal matches this requirement
|
|
344
|
+
matching_principals = self._get_matching_principals(principals, requirement)
|
|
345
|
+
|
|
346
|
+
if not matching_principals:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
# Get required conditions from the requirement
|
|
350
|
+
required_conditions_config = requirement.get("required_conditions", [])
|
|
351
|
+
if not required_conditions_config:
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# Validate conditions using the same logic as action_condition_enforcement
|
|
355
|
+
condition_issues = self._validate_conditions(
|
|
356
|
+
statement,
|
|
357
|
+
statement_idx,
|
|
358
|
+
required_conditions_config,
|
|
359
|
+
matching_principals,
|
|
360
|
+
config,
|
|
361
|
+
requirement,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
issues.extend(condition_issues)
|
|
365
|
+
|
|
366
|
+
return issues
|
|
367
|
+
|
|
368
|
+
def _get_matching_principals(
|
|
369
|
+
self, principals: list[str], requirement: dict[str, Any]
|
|
370
|
+
) -> list[str]:
|
|
371
|
+
"""Get principals that match the requirement pattern.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
principals: List of principals from the statement
|
|
375
|
+
requirement: Principal condition requirement config
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of matching principals
|
|
379
|
+
"""
|
|
380
|
+
principal_patterns = requirement.get("principals", [])
|
|
381
|
+
if not principal_patterns:
|
|
382
|
+
return []
|
|
383
|
+
|
|
384
|
+
matching: list[str] = []
|
|
385
|
+
|
|
386
|
+
for principal in principals:
|
|
387
|
+
for pattern in principal_patterns:
|
|
388
|
+
# Special case: "*" pattern should only match literal "*"
|
|
389
|
+
if pattern == "*":
|
|
390
|
+
if principal == "*":
|
|
391
|
+
matching.append(principal)
|
|
392
|
+
elif fnmatch.fnmatch(principal, pattern):
|
|
393
|
+
matching.append(principal)
|
|
394
|
+
|
|
395
|
+
return matching
|
|
396
|
+
|
|
397
|
+
def _validate_conditions(
|
|
398
|
+
self,
|
|
399
|
+
statement: Statement,
|
|
400
|
+
statement_idx: int,
|
|
401
|
+
required_conditions_config: Any,
|
|
402
|
+
matching_principals: list[str],
|
|
403
|
+
config: CheckConfig,
|
|
404
|
+
requirement: dict[str, Any],
|
|
405
|
+
) -> list[ValidationIssue]:
|
|
406
|
+
"""Validate that required conditions are present.
|
|
407
|
+
|
|
408
|
+
Supports: simple list, all_of, any_of, none_of formats.
|
|
409
|
+
Similar to action_condition_enforcement logic.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
statement: The statement to validate
|
|
413
|
+
statement_idx: Index of the statement
|
|
414
|
+
required_conditions_config: Condition requirements config
|
|
415
|
+
matching_principals: Principals that matched
|
|
416
|
+
config: Check configuration
|
|
417
|
+
requirement: Parent requirement for severity override
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
List of validation issues
|
|
421
|
+
"""
|
|
422
|
+
issues: list[ValidationIssue] = []
|
|
423
|
+
|
|
424
|
+
# Handle simple list format (backward compatibility)
|
|
425
|
+
if isinstance(required_conditions_config, list):
|
|
426
|
+
for condition_requirement in required_conditions_config:
|
|
427
|
+
if not self._has_condition_requirement(statement, condition_requirement):
|
|
428
|
+
issues.append(
|
|
429
|
+
self._create_condition_issue(
|
|
430
|
+
statement,
|
|
431
|
+
statement_idx,
|
|
432
|
+
condition_requirement,
|
|
433
|
+
matching_principals,
|
|
434
|
+
config,
|
|
435
|
+
requirement,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
return issues
|
|
439
|
+
|
|
440
|
+
# Handle all_of/any_of/none_of format
|
|
441
|
+
if isinstance(required_conditions_config, dict):
|
|
442
|
+
all_of = required_conditions_config.get("all_of", [])
|
|
443
|
+
any_of = required_conditions_config.get("any_of", [])
|
|
444
|
+
none_of = required_conditions_config.get("none_of", [])
|
|
445
|
+
|
|
446
|
+
# Validate all_of: ALL conditions must be present
|
|
447
|
+
if all_of:
|
|
448
|
+
for condition_requirement in all_of:
|
|
449
|
+
if not self._has_condition_requirement(statement, condition_requirement):
|
|
450
|
+
issues.append(
|
|
451
|
+
self._create_condition_issue(
|
|
452
|
+
statement,
|
|
453
|
+
statement_idx,
|
|
454
|
+
condition_requirement,
|
|
455
|
+
matching_principals,
|
|
456
|
+
config,
|
|
457
|
+
requirement,
|
|
458
|
+
requirement_type="all_of",
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Validate any_of: At least ONE condition must be present
|
|
463
|
+
if any_of:
|
|
464
|
+
any_present = any(
|
|
465
|
+
self._has_condition_requirement(statement, cond_req) for cond_req in any_of
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if not any_present:
|
|
469
|
+
# Create a combined error for any_of
|
|
470
|
+
condition_keys = [cond.get("condition_key", "unknown") for cond in any_of]
|
|
471
|
+
severity = requirement.get("severity", self.get_severity(config))
|
|
472
|
+
issues.append(
|
|
473
|
+
ValidationIssue(
|
|
474
|
+
severity=severity,
|
|
475
|
+
statement_sid=statement.sid,
|
|
476
|
+
statement_index=statement_idx,
|
|
477
|
+
issue_type="missing_principal_condition_any_of",
|
|
478
|
+
message=(
|
|
479
|
+
f"Principals {matching_principals} require at least ONE of these conditions: "
|
|
480
|
+
f"{', '.join(condition_keys)}"
|
|
481
|
+
),
|
|
482
|
+
suggestion=self._build_any_of_suggestion(any_of),
|
|
483
|
+
line_number=statement.line_number,
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Validate none_of: NONE of these conditions should be present
|
|
488
|
+
if none_of:
|
|
489
|
+
for condition_requirement in none_of:
|
|
490
|
+
if self._has_condition_requirement(statement, condition_requirement):
|
|
491
|
+
issues.append(
|
|
492
|
+
self._create_none_of_condition_issue(
|
|
493
|
+
statement,
|
|
494
|
+
statement_idx,
|
|
495
|
+
condition_requirement,
|
|
496
|
+
matching_principals,
|
|
497
|
+
config,
|
|
498
|
+
requirement,
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return issues
|
|
503
|
+
|
|
504
|
+
def _has_condition_requirement(
|
|
505
|
+
self, statement: Statement, condition_requirement: dict[str, Any]
|
|
506
|
+
) -> bool:
|
|
507
|
+
"""Check if statement has the required condition.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
statement: The statement to check
|
|
511
|
+
condition_requirement: Condition requirement config
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
True if condition is present and matches requirements
|
|
515
|
+
"""
|
|
516
|
+
condition_key = condition_requirement.get("condition_key")
|
|
517
|
+
if not condition_key:
|
|
518
|
+
return True # No condition key specified, skip
|
|
519
|
+
|
|
520
|
+
operator = condition_requirement.get("operator")
|
|
521
|
+
expected_value = condition_requirement.get("expected_value")
|
|
522
|
+
|
|
523
|
+
return self._has_condition(statement, condition_key, operator, expected_value)
|
|
524
|
+
|
|
525
|
+
def _has_condition(
|
|
526
|
+
self,
|
|
527
|
+
statement: Statement,
|
|
528
|
+
condition_key: str,
|
|
529
|
+
operator: str | None = None,
|
|
530
|
+
expected_value: Any = None,
|
|
531
|
+
) -> bool:
|
|
532
|
+
"""Check if statement has the specified condition key.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
statement: The IAM policy statement
|
|
536
|
+
condition_key: The condition key to look for
|
|
537
|
+
operator: Optional specific operator (e.g., "StringEquals")
|
|
538
|
+
expected_value: Optional expected value for the condition
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
True if condition is present (and matches expected value if specified)
|
|
542
|
+
"""
|
|
543
|
+
if not statement.condition:
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
# If operator specified, only check that operator
|
|
547
|
+
operators_to_check = [operator] if operator else list(statement.condition.keys())
|
|
548
|
+
|
|
549
|
+
# Look through specified condition operators
|
|
550
|
+
for op in operators_to_check:
|
|
551
|
+
if op not in statement.condition:
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
conditions = statement.condition[op]
|
|
555
|
+
if isinstance(conditions, dict):
|
|
556
|
+
if condition_key in conditions:
|
|
557
|
+
# If no expected value specified, just presence is enough
|
|
558
|
+
if expected_value is None:
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
# Check if the value matches
|
|
562
|
+
actual_value = conditions[condition_key]
|
|
563
|
+
|
|
564
|
+
# Handle boolean values
|
|
565
|
+
if isinstance(expected_value, bool):
|
|
566
|
+
if isinstance(actual_value, bool):
|
|
567
|
+
return actual_value == expected_value
|
|
568
|
+
if isinstance(actual_value, str):
|
|
569
|
+
return actual_value.lower() == str(expected_value).lower()
|
|
570
|
+
|
|
571
|
+
# Handle exact matches
|
|
572
|
+
if actual_value == expected_value:
|
|
573
|
+
return True
|
|
574
|
+
|
|
575
|
+
# Handle list values (actual can be string or list)
|
|
576
|
+
if isinstance(expected_value, list):
|
|
577
|
+
if isinstance(actual_value, list):
|
|
578
|
+
return set(expected_value) == set(actual_value)
|
|
579
|
+
if actual_value in expected_value:
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
# Handle string matches for variable references like ${aws:PrincipalTag/owner}
|
|
583
|
+
if str(actual_value) == str(expected_value):
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
def _create_condition_issue(
|
|
589
|
+
self,
|
|
590
|
+
statement: Statement,
|
|
591
|
+
statement_idx: int,
|
|
592
|
+
condition_requirement: dict[str, Any],
|
|
593
|
+
matching_principals: list[str],
|
|
594
|
+
config: CheckConfig,
|
|
595
|
+
requirement: dict[str, Any],
|
|
596
|
+
requirement_type: str = "required",
|
|
597
|
+
) -> ValidationIssue:
|
|
598
|
+
"""Create a validation issue for a missing condition.
|
|
599
|
+
|
|
600
|
+
Severity precedence:
|
|
601
|
+
1. Individual condition requirement's severity (condition_requirement['severity'])
|
|
602
|
+
2. Parent requirement's severity (requirement['severity'])
|
|
603
|
+
3. Global check severity (config.severity)
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
statement: The statement being validated
|
|
607
|
+
statement_idx: Index of the statement
|
|
608
|
+
condition_requirement: The condition requirement config
|
|
609
|
+
matching_principals: Principals that matched
|
|
610
|
+
config: Check configuration
|
|
611
|
+
requirement: Parent requirement config
|
|
612
|
+
requirement_type: Type of requirement (required, all_of)
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
ValidationIssue
|
|
616
|
+
"""
|
|
617
|
+
condition_key = condition_requirement.get("condition_key", "unknown")
|
|
618
|
+
description = condition_requirement.get("description", "")
|
|
619
|
+
expected_value = condition_requirement.get("expected_value")
|
|
620
|
+
example = condition_requirement.get("example", "")
|
|
621
|
+
operator = condition_requirement.get("operator", "StringEquals")
|
|
622
|
+
|
|
623
|
+
message_prefix = "ALL required:" if requirement_type == "all_of" else "Required:"
|
|
624
|
+
|
|
625
|
+
# Determine severity with precedence: condition > requirement > global
|
|
626
|
+
severity = (
|
|
627
|
+
condition_requirement.get("severity")
|
|
628
|
+
or requirement.get("severity")
|
|
629
|
+
or self.get_severity(config)
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
return ValidationIssue(
|
|
633
|
+
severity=severity,
|
|
634
|
+
statement_sid=statement.sid,
|
|
635
|
+
statement_index=statement_idx,
|
|
636
|
+
issue_type="missing_principal_condition",
|
|
637
|
+
message=f"{message_prefix} Principal(s) {matching_principals} require condition '{condition_key}'",
|
|
638
|
+
suggestion=self._build_condition_suggestion(
|
|
639
|
+
condition_key, description, example, expected_value, operator
|
|
640
|
+
),
|
|
641
|
+
line_number=statement.line_number,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def _build_condition_suggestion(
|
|
645
|
+
self,
|
|
646
|
+
condition_key: str,
|
|
647
|
+
description: str,
|
|
648
|
+
example: str,
|
|
649
|
+
expected_value: Any = None,
|
|
650
|
+
operator: str = "StringEquals",
|
|
651
|
+
) -> str:
|
|
652
|
+
"""Build a helpful suggestion for adding the missing condition.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
condition_key: The condition key
|
|
656
|
+
description: Description of the condition
|
|
657
|
+
example: Example usage
|
|
658
|
+
expected_value: Expected value for the condition
|
|
659
|
+
operator: Condition operator
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Suggestion string
|
|
663
|
+
"""
|
|
664
|
+
parts = []
|
|
665
|
+
|
|
666
|
+
if description:
|
|
667
|
+
parts.append(description)
|
|
668
|
+
|
|
669
|
+
# Build example based on condition key type
|
|
670
|
+
if example:
|
|
671
|
+
parts.append(f"Example:\n{example}")
|
|
672
|
+
else:
|
|
673
|
+
# Auto-generate example
|
|
674
|
+
example_lines = ['Add to "Condition" block:', f' "{operator}": {{']
|
|
675
|
+
|
|
676
|
+
if isinstance(expected_value, list):
|
|
677
|
+
value_str = (
|
|
678
|
+
"["
|
|
679
|
+
+ ", ".join(
|
|
680
|
+
[
|
|
681
|
+
f'"{v}"' if not str(v).startswith("${") else f'"{v}"'
|
|
682
|
+
for v in expected_value
|
|
683
|
+
]
|
|
684
|
+
)
|
|
685
|
+
+ "]"
|
|
686
|
+
)
|
|
687
|
+
elif expected_value is not None:
|
|
688
|
+
# Don't quote if it's a variable reference like ${aws:PrincipalTag/owner}
|
|
689
|
+
if str(expected_value).startswith("${"):
|
|
690
|
+
value_str = f'"{expected_value}"'
|
|
691
|
+
elif isinstance(expected_value, bool):
|
|
692
|
+
value_str = str(expected_value).lower()
|
|
693
|
+
else:
|
|
694
|
+
value_str = f'"{expected_value}"'
|
|
695
|
+
else:
|
|
696
|
+
value_str = '"<value>"'
|
|
697
|
+
|
|
698
|
+
example_lines.append(f' "{condition_key}": {value_str}')
|
|
699
|
+
example_lines.append(" }")
|
|
700
|
+
|
|
701
|
+
parts.append("\n".join(example_lines))
|
|
702
|
+
|
|
703
|
+
return ". ".join(parts) if parts else f"Add condition: {condition_key}"
|
|
704
|
+
|
|
705
|
+
def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
|
|
706
|
+
"""Build suggestion for any_of conditions.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
any_of_conditions: List of condition requirements
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
Suggestion string
|
|
713
|
+
"""
|
|
714
|
+
suggestions = []
|
|
715
|
+
suggestions.append("Add at least ONE of these conditions:")
|
|
716
|
+
|
|
717
|
+
for i, cond in enumerate(any_of_conditions, 1):
|
|
718
|
+
condition_key = cond.get("condition_key", "unknown")
|
|
719
|
+
description = cond.get("description", "")
|
|
720
|
+
expected_value = cond.get("expected_value")
|
|
721
|
+
|
|
722
|
+
option = f"\nOption {i}: {condition_key}"
|
|
723
|
+
if description:
|
|
724
|
+
option += f" - {description}"
|
|
725
|
+
if expected_value is not None:
|
|
726
|
+
option += f" (value: {expected_value})"
|
|
727
|
+
|
|
728
|
+
suggestions.append(option)
|
|
729
|
+
|
|
730
|
+
return "".join(suggestions)
|
|
731
|
+
|
|
732
|
+
def _create_none_of_condition_issue(
|
|
733
|
+
self,
|
|
734
|
+
statement: Statement,
|
|
735
|
+
statement_idx: int,
|
|
736
|
+
condition_requirement: dict[str, Any],
|
|
737
|
+
matching_principals: list[str],
|
|
738
|
+
config: CheckConfig,
|
|
739
|
+
requirement: dict[str, Any],
|
|
740
|
+
) -> ValidationIssue:
|
|
741
|
+
"""Create a validation issue for a forbidden condition that is present.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
statement: The statement being validated
|
|
745
|
+
statement_idx: Index of the statement
|
|
746
|
+
condition_requirement: The condition requirement config
|
|
747
|
+
matching_principals: Principals that matched
|
|
748
|
+
config: Check configuration
|
|
749
|
+
requirement: Parent requirement config
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
ValidationIssue
|
|
753
|
+
"""
|
|
754
|
+
condition_key = condition_requirement.get("condition_key", "unknown")
|
|
755
|
+
description = condition_requirement.get("description", "")
|
|
756
|
+
expected_value = condition_requirement.get("expected_value")
|
|
757
|
+
|
|
758
|
+
message = f"FORBIDDEN: Principal(s) {matching_principals} must NOT have condition '{condition_key}'"
|
|
759
|
+
if expected_value is not None:
|
|
760
|
+
message += f" with value '{expected_value}'"
|
|
761
|
+
|
|
762
|
+
suggestion = f"Remove the '{condition_key}' condition from the statement"
|
|
763
|
+
if description:
|
|
764
|
+
suggestion += f". {description}"
|
|
765
|
+
|
|
766
|
+
severity = requirement.get("severity", self.get_severity(config))
|
|
767
|
+
|
|
768
|
+
return ValidationIssue(
|
|
769
|
+
severity=severity,
|
|
770
|
+
statement_sid=statement.sid,
|
|
771
|
+
statement_index=statement_idx,
|
|
772
|
+
issue_type="forbidden_principal_condition",
|
|
773
|
+
message=message,
|
|
774
|
+
suggestion=suggestion,
|
|
775
|
+
line_number=statement.line_number,
|
|
776
|
+
)
|