iam-policy-validator 1.7.0__py3-none-any.whl → 1.7.2__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/METADATA +428 -0
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +37 -36
- iam_validator/__version__.py +4 -2
- iam_validator/checks/action_condition_enforcement.py +22 -13
- iam_validator/checks/action_resource_matching.py +70 -36
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +8 -6
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/principal_validation.py +24 -20
- iam_validator/checks/sensitive_action.py +3 -9
- iam_validator/checks/service_wildcard.py +2 -8
- iam_validator/checks/sid_uniqueness.py +1 -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 +2 -8
- iam_validator/checks/wildcard_resource.py +2 -8
- iam_validator/commands/validate.py +2 -2
- iam_validator/core/aws_fetcher.py +115 -22
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +16 -7
- iam_validator/core/constants.py +57 -0
- iam_validator/core/formatters/console.py +10 -1
- iam_validator/core/formatters/csv.py +2 -1
- iam_validator/core/formatters/enhanced.py +42 -8
- iam_validator/core/formatters/markdown.py +2 -1
- iam_validator/core/models.py +22 -7
- iam_validator/core/policy_checks.py +5 -4
- iam_validator/core/policy_loader.py +71 -14
- iam_validator/core/report.py +65 -24
- iam_validator/integrations/github_integration.py +4 -5
- iam_validator/utils/__init__.py +4 -0
- iam_validator/utils/regex.py +7 -8
- iam_validator/utils/terminal.py +22 -0
- iam_policy_validator-1.7.0.dist-info/METADATA +0 -1057
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -27,11 +27,13 @@ import os
|
|
|
27
27
|
import re
|
|
28
28
|
import sys
|
|
29
29
|
import time
|
|
30
|
+
from dataclasses import dataclass
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
from typing import Any
|
|
32
33
|
|
|
33
34
|
import httpx
|
|
34
35
|
|
|
36
|
+
from iam_validator.core import constants
|
|
35
37
|
from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
|
|
36
38
|
from iam_validator.core.models import ServiceDetail, ServiceInfo
|
|
37
39
|
from iam_validator.utils.cache import LRUCache
|
|
@@ -39,6 +41,23 @@ from iam_validator.utils.cache import LRUCache
|
|
|
39
41
|
logger = logging.getLogger(__name__)
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
@dataclass
|
|
45
|
+
class ConditionKeyValidationResult:
|
|
46
|
+
"""Result of condition key validation.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
is_valid: True if the condition key is valid for the action
|
|
50
|
+
error_message: Short error message if invalid (shown prominently)
|
|
51
|
+
warning_message: Warning message if valid but not recommended
|
|
52
|
+
suggestion: Detailed suggestion with valid keys (shown in collapsible section)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
is_valid: bool
|
|
56
|
+
error_message: str | None = None
|
|
57
|
+
warning_message: str | None = None
|
|
58
|
+
suggestion: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
42
61
|
class CompiledPatterns:
|
|
43
62
|
"""Pre-compiled regex patterns for validation.
|
|
44
63
|
|
|
@@ -199,10 +218,10 @@ class AWSServiceFetcher:
|
|
|
199
218
|
|
|
200
219
|
def __init__(
|
|
201
220
|
self,
|
|
202
|
-
timeout: float =
|
|
221
|
+
timeout: float = constants.DEFAULT_HTTP_TIMEOUT_SECONDS,
|
|
203
222
|
retries: int = 3,
|
|
204
223
|
enable_cache: bool = True,
|
|
205
|
-
cache_ttl: int =
|
|
224
|
+
cache_ttl: int = constants.DEFAULT_CACHE_TTL_SECONDS,
|
|
206
225
|
memory_cache_size: int = 256,
|
|
207
226
|
connection_pool_size: int = 50,
|
|
208
227
|
keepalive_connections: int = 20,
|
|
@@ -303,7 +322,7 @@ class AWSServiceFetcher:
|
|
|
303
322
|
limits=httpx.Limits(
|
|
304
323
|
max_keepalive_connections=self.keepalive_connections,
|
|
305
324
|
max_connections=self.connection_pool_size,
|
|
306
|
-
keepalive_expiry=
|
|
325
|
+
keepalive_expiry=constants.DEFAULT_HTTP_TIMEOUT_SECONDS, # Keep connections alive
|
|
307
326
|
),
|
|
308
327
|
http2=True, # Enable HTTP/2 for multiplexing
|
|
309
328
|
)
|
|
@@ -658,7 +677,7 @@ class AWSServiceFetcher:
|
|
|
658
677
|
return service_detail
|
|
659
678
|
except FileNotFoundError:
|
|
660
679
|
pass
|
|
661
|
-
raise ValueError(f"Service
|
|
680
|
+
raise ValueError(f"Service `{service_name}` not found in {self.aws_services_dir}")
|
|
662
681
|
|
|
663
682
|
# Fetch service list and find URL from API
|
|
664
683
|
services = await self.fetch_services()
|
|
@@ -676,7 +695,7 @@ class AWSServiceFetcher:
|
|
|
676
695
|
|
|
677
696
|
return service_detail
|
|
678
697
|
|
|
679
|
-
raise ValueError(f"Service
|
|
698
|
+
raise ValueError(f"Service `{service_name}` not found")
|
|
680
699
|
|
|
681
700
|
async def fetch_multiple_services(self, service_names: list[str]) -> dict[str, ServiceDetail]:
|
|
682
701
|
"""Fetch multiple services concurrently with optimized batching."""
|
|
@@ -823,7 +842,7 @@ class AWSServiceFetcher:
|
|
|
823
842
|
|
|
824
843
|
async def validate_condition_key(
|
|
825
844
|
self, action: str, condition_key: str, resources: list[str] | None = None
|
|
826
|
-
) ->
|
|
845
|
+
) -> ConditionKeyValidationResult:
|
|
827
846
|
"""
|
|
828
847
|
Validate condition key against action and optionally resource types.
|
|
829
848
|
|
|
@@ -833,10 +852,11 @@ class AWSServiceFetcher:
|
|
|
833
852
|
resources: Optional list of resource ARNs to validate against
|
|
834
853
|
|
|
835
854
|
Returns:
|
|
836
|
-
|
|
855
|
+
ConditionKeyValidationResult with:
|
|
837
856
|
- is_valid: True if key is valid (even with warning)
|
|
838
|
-
- error_message:
|
|
857
|
+
- error_message: Short error message if invalid (shown prominently)
|
|
839
858
|
- warning_message: Warning message if valid but not recommended
|
|
859
|
+
- suggestion: Detailed suggestion with valid keys (shown in collapsible section)
|
|
840
860
|
"""
|
|
841
861
|
try:
|
|
842
862
|
from iam_validator.core.config.aws_global_conditions import (
|
|
@@ -852,10 +872,9 @@ class AWSServiceFetcher:
|
|
|
852
872
|
if global_conditions.is_valid_global_key(condition_key):
|
|
853
873
|
is_global_key = True
|
|
854
874
|
else:
|
|
855
|
-
return (
|
|
856
|
-
False,
|
|
857
|
-
f"Invalid AWS global condition key:
|
|
858
|
-
None,
|
|
875
|
+
return ConditionKeyValidationResult(
|
|
876
|
+
is_valid=False,
|
|
877
|
+
error_message=f"Invalid AWS global condition key: `{condition_key}`.",
|
|
859
878
|
)
|
|
860
879
|
|
|
861
880
|
# Fetch service detail (cached)
|
|
@@ -863,7 +882,7 @@ class AWSServiceFetcher:
|
|
|
863
882
|
|
|
864
883
|
# Check service-specific condition keys
|
|
865
884
|
if condition_key in service_detail.condition_keys:
|
|
866
|
-
return True
|
|
885
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
867
886
|
|
|
868
887
|
# Check action-specific condition keys
|
|
869
888
|
if action_name in service_detail.actions:
|
|
@@ -872,7 +891,7 @@ class AWSServiceFetcher:
|
|
|
872
891
|
action_detail.action_condition_keys
|
|
873
892
|
and condition_key in action_detail.action_condition_keys
|
|
874
893
|
):
|
|
875
|
-
return True
|
|
894
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
876
895
|
|
|
877
896
|
# Check resource-specific condition keys
|
|
878
897
|
# Get resource types required by this action
|
|
@@ -886,7 +905,7 @@ class AWSServiceFetcher:
|
|
|
886
905
|
resource_type = service_detail.resources.get(resource_name)
|
|
887
906
|
if resource_type and resource_type.condition_keys:
|
|
888
907
|
if condition_key in resource_type.condition_keys:
|
|
889
|
-
return True
|
|
908
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
890
909
|
|
|
891
910
|
# If it's a global key but the action has specific condition keys defined,
|
|
892
911
|
# AWS allows it but the key may not be available in every request context
|
|
@@ -898,21 +917,95 @@ class AWSServiceFetcher:
|
|
|
898
917
|
f"Verify that '{condition_key}' is available for this specific action's request context. "
|
|
899
918
|
f"Consider using '*IfExists' operators (e.g., StringEqualsIfExists) if the key might be missing."
|
|
900
919
|
)
|
|
901
|
-
return True,
|
|
920
|
+
return ConditionKeyValidationResult(is_valid=True, warning_message=warning_msg)
|
|
902
921
|
|
|
903
922
|
# If it's a global key and action doesn't define specific keys, allow it
|
|
904
923
|
if is_global_key:
|
|
905
|
-
return True
|
|
924
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
925
|
+
|
|
926
|
+
# Short error message
|
|
927
|
+
error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
|
|
928
|
+
|
|
929
|
+
# Collect valid condition keys for this action
|
|
930
|
+
valid_keys = set()
|
|
906
931
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
932
|
+
# Add service-level condition keys
|
|
933
|
+
if service_detail.condition_keys:
|
|
934
|
+
if isinstance(service_detail.condition_keys, dict):
|
|
935
|
+
valid_keys.update(service_detail.condition_keys.keys())
|
|
936
|
+
elif isinstance(service_detail.condition_keys, list):
|
|
937
|
+
valid_keys.update(service_detail.condition_keys)
|
|
938
|
+
|
|
939
|
+
# Add action-specific condition keys
|
|
940
|
+
if action_name in service_detail.actions:
|
|
941
|
+
action_detail = service_detail.actions[action_name]
|
|
942
|
+
if action_detail.action_condition_keys:
|
|
943
|
+
if isinstance(action_detail.action_condition_keys, dict):
|
|
944
|
+
valid_keys.update(action_detail.action_condition_keys.keys())
|
|
945
|
+
elif isinstance(action_detail.action_condition_keys, list):
|
|
946
|
+
valid_keys.update(action_detail.action_condition_keys)
|
|
947
|
+
|
|
948
|
+
# Add resource-specific condition keys
|
|
949
|
+
if action_detail.resources:
|
|
950
|
+
for res_req in action_detail.resources:
|
|
951
|
+
resource_name = res_req.get("Name", "")
|
|
952
|
+
if resource_name:
|
|
953
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
954
|
+
if resource_type and resource_type.condition_keys:
|
|
955
|
+
if isinstance(resource_type.condition_keys, dict):
|
|
956
|
+
valid_keys.update(resource_type.condition_keys.keys())
|
|
957
|
+
elif isinstance(resource_type.condition_keys, list):
|
|
958
|
+
valid_keys.update(resource_type.condition_keys)
|
|
959
|
+
|
|
960
|
+
# Build detailed suggestion with valid keys (goes in collapsible section)
|
|
961
|
+
suggestion_parts = []
|
|
962
|
+
|
|
963
|
+
if valid_keys:
|
|
964
|
+
# Sort and limit to first 10 keys for readability
|
|
965
|
+
sorted_keys = sorted(valid_keys)
|
|
966
|
+
suggestion_parts.append("**Valid condition keys for this action:**")
|
|
967
|
+
if len(sorted_keys) <= 10:
|
|
968
|
+
for key in sorted_keys:
|
|
969
|
+
suggestion_parts.append(f"- `{key}`")
|
|
970
|
+
else:
|
|
971
|
+
for key in sorted_keys[:10]:
|
|
972
|
+
suggestion_parts.append(f"- `{key}`")
|
|
973
|
+
suggestion_parts.append(f"- ... and {len(sorted_keys) - 10} more")
|
|
974
|
+
|
|
975
|
+
suggestion_parts.append("")
|
|
976
|
+
suggestion_parts.append(
|
|
977
|
+
"**Global condition keys** (e.g., `aws:ResourceOrgID`, `aws:RequestedRegion`, `aws:SourceIp`, `aws:SourceVpce`) "
|
|
978
|
+
"can also be used with any AWS action"
|
|
979
|
+
)
|
|
980
|
+
else:
|
|
981
|
+
# No action-specific keys - mention global keys
|
|
982
|
+
suggestion_parts.append(
|
|
983
|
+
"This action does not have specific condition keys defined.\n\n"
|
|
984
|
+
"However, you can use **global condition keys** such as:\n"
|
|
985
|
+
"- `aws:RequestedRegion`\n"
|
|
986
|
+
"- `aws:SourceIp`\n"
|
|
987
|
+
"- `aws:SourceVpce`\n"
|
|
988
|
+
"- `aws:UserAgent`\n"
|
|
989
|
+
"- `aws:CurrentTime`\n"
|
|
990
|
+
"- `aws:SecureTransport`\n"
|
|
991
|
+
"- `aws:PrincipalArn`\n"
|
|
992
|
+
"- And many others"
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
suggestion = "\n".join(suggestion_parts)
|
|
996
|
+
|
|
997
|
+
return ConditionKeyValidationResult(
|
|
998
|
+
is_valid=False,
|
|
999
|
+
error_message=error_msg,
|
|
1000
|
+
suggestion=suggestion,
|
|
911
1001
|
)
|
|
912
1002
|
|
|
913
1003
|
except Exception as e:
|
|
914
1004
|
logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
|
|
915
|
-
return
|
|
1005
|
+
return ConditionKeyValidationResult(
|
|
1006
|
+
is_valid=False,
|
|
1007
|
+
error_message=f"Failed to validate condition key: {str(e)}",
|
|
1008
|
+
)
|
|
916
1009
|
|
|
917
1010
|
async def clear_caches(self) -> None:
|
|
918
1011
|
"""Clear all caches (memory and disk)."""
|
|
@@ -5,6 +5,7 @@ Loads and parses configuration from YAML files, environment variables,
|
|
|
5
5
|
and command-line arguments.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import importlib
|
|
8
9
|
import importlib.util
|
|
9
10
|
import inspect
|
|
10
11
|
import logging
|
|
@@ -314,8 +315,6 @@ class ConfigLoader:
|
|
|
314
315
|
module_name, class_name = parts
|
|
315
316
|
|
|
316
317
|
# Import the module
|
|
317
|
-
import importlib
|
|
318
|
-
|
|
319
318
|
module = importlib.import_module(module_name)
|
|
320
319
|
check_class = getattr(module, class_name)
|
|
321
320
|
|
|
@@ -16,6 +16,7 @@ Benefits of code-first approach:
|
|
|
16
16
|
- 5-10x faster than YAML parsing
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
from iam_validator.core import constants
|
|
19
20
|
from iam_validator.core.config.category_suggestions import get_category_suggestions
|
|
20
21
|
from iam_validator.core.config.condition_requirements import CONDITION_REQUIREMENTS
|
|
21
22
|
from iam_validator.core.config.principal_requirements import (
|
|
@@ -70,11 +71,11 @@ DEFAULT_CONFIG = {
|
|
|
70
71
|
# Cache AWS service definitions locally (persists between runs)
|
|
71
72
|
"cache_enabled": True,
|
|
72
73
|
# Cache TTL in hours (default: 168 = 7 days)
|
|
73
|
-
"cache_ttl_hours":
|
|
74
|
+
"cache_ttl_hours": constants.DEFAULT_CACHE_TTL_HOURS,
|
|
74
75
|
# Severity levels that cause validation to fail
|
|
75
76
|
# IAM Validity: error, warning, info
|
|
76
77
|
# Security: critical, high, medium, low
|
|
77
|
-
"fail_on_severity":
|
|
78
|
+
"fail_on_severity": list(constants.HIGH_SEVERITY_LEVELS),
|
|
78
79
|
},
|
|
79
80
|
# ========================================================================
|
|
80
81
|
# AWS IAM Validation Checks (17 checks total)
|
|
@@ -188,7 +189,7 @@ DEFAULT_CONFIG = {
|
|
|
188
189
|
"enabled": True,
|
|
189
190
|
"severity": "error", # IAM validity error
|
|
190
191
|
"description": "Validates ARN format for resources",
|
|
191
|
-
"arn_pattern":
|
|
192
|
+
"arn_pattern": constants.DEFAULT_ARN_VALIDATION_PATTERN,
|
|
192
193
|
},
|
|
193
194
|
# ========================================================================
|
|
194
195
|
# 9. PRINCIPAL VALIDATION
|
|
@@ -389,6 +390,18 @@ DEFAULT_CONFIG = {
|
|
|
389
390
|
# Services that are allowed to use wildcards (default: logs, cloudwatch, xray)
|
|
390
391
|
# See: iam_validator/core/config/wildcards.py
|
|
391
392
|
"allowed_services": list(DEFAULT_SERVICE_WILDCARDS),
|
|
393
|
+
"message": "Service wildcard '{action}' grants all permissions for the {service} service",
|
|
394
|
+
"suggestion": (
|
|
395
|
+
"Replace '{action}' with specific actions needed for your use case to follow least-privilege principle.\n"
|
|
396
|
+
"Find valid {service} actions: https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
|
|
397
|
+
),
|
|
398
|
+
"example": (
|
|
399
|
+
"Replace:\n"
|
|
400
|
+
' "Action": ["{action}"]\n'
|
|
401
|
+
"\n"
|
|
402
|
+
"With specific actions:\n"
|
|
403
|
+
' "Action": ["{service}:Describe*", "{service}:List*"]\n'
|
|
404
|
+
),
|
|
392
405
|
},
|
|
393
406
|
# ========================================================================
|
|
394
407
|
# 16. SENSITIVE ACTION
|
|
@@ -482,10 +495,6 @@ DEFAULT_CONFIG = {
|
|
|
482
495
|
# - prevent_public_ip: Prevents 0.0.0.0/0 IP ranges
|
|
483
496
|
#
|
|
484
497
|
# See: iam_validator/core/config/condition_requirements.py
|
|
485
|
-
# Python API:
|
|
486
|
-
# from iam_validator.core.config import CONDITION_REQUIREMENTS
|
|
487
|
-
# import copy
|
|
488
|
-
# requirements = copy.deepcopy(CONDITION_REQUIREMENTS)
|
|
489
498
|
"action_condition_enforcement": {
|
|
490
499
|
"enabled": True,
|
|
491
500
|
"severity": "high", # Default severity (can be overridden per-requirement)
|
iam_validator/core/constants.py
CHANGED
|
@@ -62,6 +62,21 @@ DEFAULT_CONFIG_FILENAMES = [
|
|
|
62
62
|
".iam-validator.yml",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# Severity Levels
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# Severity level groupings for filtering and categorization
|
|
69
|
+
# Used across formatters and report generation
|
|
70
|
+
|
|
71
|
+
# High severity issues that typically fail validation
|
|
72
|
+
HIGH_SEVERITY_LEVELS = ("error", "critical", "high")
|
|
73
|
+
|
|
74
|
+
# Medium severity issues (warnings)
|
|
75
|
+
MEDIUM_SEVERITY_LEVELS = ("warning", "medium")
|
|
76
|
+
|
|
77
|
+
# Low severity issues (informational)
|
|
78
|
+
LOW_SEVERITY_LEVELS = ("info", "low")
|
|
79
|
+
|
|
65
80
|
# ============================================================================
|
|
66
81
|
# GitHub Integration
|
|
67
82
|
# ============================================================================
|
|
@@ -72,3 +87,45 @@ BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
|
72
87
|
# HTML comment markers for identifying bot-generated content (for cleanup/updates)
|
|
73
88
|
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
74
89
|
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
90
|
+
|
|
91
|
+
# GitHub comment size limits
|
|
92
|
+
# GitHub's actual limit is 65536 characters, but we use a smaller limit for safety
|
|
93
|
+
GITHUB_MAX_COMMENT_LENGTH = 65000 # Maximum single comment length
|
|
94
|
+
GITHUB_COMMENT_SPLIT_LIMIT = 60000 # Target size when splitting into multiple parts
|
|
95
|
+
|
|
96
|
+
# Comment size estimation parameters (used for multi-part comment splitting)
|
|
97
|
+
COMMENT_BASE_OVERHEAD_CHARS = 2000 # Base overhead for headers/footers
|
|
98
|
+
COMMENT_CHARS_PER_ISSUE_ESTIMATE = 500 # Average characters per issue
|
|
99
|
+
COMMENT_CONTINUATION_OVERHEAD_CHARS = 200 # Overhead for continuation markers
|
|
100
|
+
FORMATTING_SAFETY_BUFFER = 100 # Safety buffer for formatting calculations
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Console Display Settings
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
# Panel width for formatted console output
|
|
107
|
+
CONSOLE_PANEL_WIDTH = 100
|
|
108
|
+
|
|
109
|
+
# Rich console color styles
|
|
110
|
+
CONSOLE_HEADER_COLOR = "bright_blue"
|
|
111
|
+
|
|
112
|
+
# ============================================================================
|
|
113
|
+
# Cache and Timeout Settings
|
|
114
|
+
# ============================================================================
|
|
115
|
+
|
|
116
|
+
# Cache TTL (Time To Live) - 7 days
|
|
117
|
+
DEFAULT_CACHE_TTL_HOURS = 168 # 7 days in hours
|
|
118
|
+
DEFAULT_CACHE_TTL_SECONDS = 604800 # 7 days in seconds (168 * 3600)
|
|
119
|
+
|
|
120
|
+
# HTTP request timeout in seconds
|
|
121
|
+
DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
|
|
122
|
+
|
|
123
|
+
# Time conversion constants
|
|
124
|
+
SECONDS_PER_HOUR = 3600
|
|
125
|
+
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# AWS Documentation URLs
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
# AWS Service Authorization Reference (for finding valid actions, resources, and condition keys)
|
|
131
|
+
AWS_SERVICE_AUTH_REF_URL = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
|
|
@@ -39,8 +39,17 @@ class ConsoleFormatter(OutputFormatter):
|
|
|
39
39
|
color = kwargs.get("color", True)
|
|
40
40
|
|
|
41
41
|
# Capture the output from print_console_report
|
|
42
|
+
from iam_validator.utils import get_terminal_width
|
|
43
|
+
|
|
42
44
|
string_buffer = StringIO()
|
|
43
|
-
|
|
45
|
+
# Get terminal width for proper table column spacing
|
|
46
|
+
terminal_width = get_terminal_width()
|
|
47
|
+
console = Console(
|
|
48
|
+
file=string_buffer,
|
|
49
|
+
force_terminal=color,
|
|
50
|
+
width=terminal_width,
|
|
51
|
+
legacy_windows=False,
|
|
52
|
+
)
|
|
44
53
|
|
|
45
54
|
# Create a generator instance with our custom console
|
|
46
55
|
generator = ReportGenerator()
|
|
@@ -4,6 +4,7 @@ import csv
|
|
|
4
4
|
import io
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from iam_validator.core import constants
|
|
7
8
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
8
9
|
from iam_validator.core.models import ValidationReport
|
|
9
10
|
|
|
@@ -58,7 +59,7 @@ class CSVFormatter(OutputFormatter):
|
|
|
58
59
|
1
|
|
59
60
|
for r in report.results
|
|
60
61
|
for i in r.issues
|
|
61
|
-
if i.severity in
|
|
62
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
62
63
|
)
|
|
63
64
|
warnings = sum(
|
|
64
65
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
@@ -10,6 +10,7 @@ from rich.text import Text
|
|
|
10
10
|
from rich.tree import Tree
|
|
11
11
|
|
|
12
12
|
from iam_validator.__version__ import __version__
|
|
13
|
+
from iam_validator.core import constants
|
|
13
14
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
14
15
|
from iam_validator.core.models import PolicyValidationResult, ValidationReport
|
|
15
16
|
|
|
@@ -50,8 +51,14 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
50
51
|
show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
|
|
51
52
|
|
|
52
53
|
# Use StringIO to capture Rich console output
|
|
54
|
+
from iam_validator.utils import get_terminal_width
|
|
55
|
+
|
|
53
56
|
string_buffer = StringIO()
|
|
54
|
-
|
|
57
|
+
# Get terminal width for proper text wrapping
|
|
58
|
+
terminal_width = get_terminal_width()
|
|
59
|
+
console = Console(
|
|
60
|
+
file=string_buffer, force_terminal=color, width=terminal_width, legacy_windows=False
|
|
61
|
+
)
|
|
55
62
|
|
|
56
63
|
# Header with title
|
|
57
64
|
console.print()
|
|
@@ -60,7 +67,14 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
60
67
|
style="bold cyan",
|
|
61
68
|
justify="center",
|
|
62
69
|
)
|
|
63
|
-
console.print(
|
|
70
|
+
console.print(
|
|
71
|
+
Panel(
|
|
72
|
+
title,
|
|
73
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
74
|
+
padding=(1, 0),
|
|
75
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
64
78
|
console.print()
|
|
65
79
|
|
|
66
80
|
# Executive Summary with progress bars (optional)
|
|
@@ -73,7 +87,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
73
87
|
self._print_severity_breakdown(console, report)
|
|
74
88
|
|
|
75
89
|
console.print()
|
|
76
|
-
console.print(Rule(title="[bold]Detailed Results", style=
|
|
90
|
+
console.print(Rule(title="[bold]Detailed Results", style=constants.CONSOLE_HEADER_COLOR))
|
|
77
91
|
console.print()
|
|
78
92
|
|
|
79
93
|
# Detailed results using tree structure
|
|
@@ -140,8 +154,9 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
140
154
|
Panel(
|
|
141
155
|
metrics_table,
|
|
142
156
|
title="📊 Executive Summary",
|
|
143
|
-
border_style=
|
|
157
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
144
158
|
padding=(1, 2),
|
|
159
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
145
160
|
)
|
|
146
161
|
)
|
|
147
162
|
|
|
@@ -225,7 +240,12 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
225
240
|
)
|
|
226
241
|
|
|
227
242
|
console.print(
|
|
228
|
-
Panel(
|
|
243
|
+
Panel(
|
|
244
|
+
severity_table,
|
|
245
|
+
title="🎯 Issue Severity Breakdown",
|
|
246
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
247
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
248
|
+
)
|
|
229
249
|
)
|
|
230
250
|
|
|
231
251
|
def _format_policy_result_modern(
|
|
@@ -247,7 +267,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
247
267
|
elif result.is_valid and result.issues:
|
|
248
268
|
# Valid IAM policy but has security findings
|
|
249
269
|
# Check severity to determine the appropriate status
|
|
250
|
-
has_critical = any(i.severity in
|
|
270
|
+
has_critical = any(i.severity in constants.HIGH_SEVERITY_LEVELS for i in result.issues)
|
|
251
271
|
if has_critical:
|
|
252
272
|
icon = "⚠️"
|
|
253
273
|
color = "red"
|
|
@@ -386,6 +406,13 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
386
406
|
suggestion_text.append(issue.suggestion, style="italic yellow")
|
|
387
407
|
msg_node.add(suggestion_text)
|
|
388
408
|
|
|
409
|
+
# Example (if present, show with indentation)
|
|
410
|
+
if issue.example:
|
|
411
|
+
msg_node.add(Text("Example:", style="bold cyan"))
|
|
412
|
+
# Show example code with syntax highlighting
|
|
413
|
+
example_text = Text(issue.example, style="dim")
|
|
414
|
+
msg_node.add(example_text)
|
|
415
|
+
|
|
389
416
|
def _print_final_status(self, console: Console, report: ValidationReport) -> None:
|
|
390
417
|
"""Print final status panel."""
|
|
391
418
|
if report.invalid_policies == 0 and report.total_issues == 0:
|
|
@@ -400,7 +427,7 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
400
427
|
# Valid IAM policies but may have security findings
|
|
401
428
|
# Check if there are critical/high security issues
|
|
402
429
|
has_critical = any(
|
|
403
|
-
i.severity in
|
|
430
|
+
i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
404
431
|
for r in report.results
|
|
405
432
|
for i in r.issues
|
|
406
433
|
)
|
|
@@ -437,4 +464,11 @@ class EnhancedFormatter(OutputFormatter):
|
|
|
437
464
|
final_text.append("\n\n")
|
|
438
465
|
final_text.append(message)
|
|
439
466
|
|
|
440
|
-
console.print(
|
|
467
|
+
console.print(
|
|
468
|
+
Panel(
|
|
469
|
+
final_text,
|
|
470
|
+
border_style=border_color,
|
|
471
|
+
padding=(1, 2),
|
|
472
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
473
|
+
)
|
|
474
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Markdown formatter - placeholder for existing functionality."""
|
|
2
2
|
|
|
3
|
+
from iam_validator.core import constants
|
|
3
4
|
from iam_validator.core.formatters.base import OutputFormatter
|
|
4
5
|
from iam_validator.core.models import ValidationReport
|
|
5
6
|
|
|
@@ -34,7 +35,7 @@ class MarkdownFormatter(OutputFormatter):
|
|
|
34
35
|
1
|
|
35
36
|
for r in report.results
|
|
36
37
|
for i in r.issues
|
|
37
|
-
if i.severity in
|
|
38
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
38
39
|
)
|
|
39
40
|
warnings = sum(
|
|
40
41
|
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
iam_validator/core/models.py
CHANGED
|
@@ -8,6 +8,8 @@ from typing import Any, ClassVar, Literal
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
10
|
|
|
11
|
+
from iam_validator.core import constants
|
|
12
|
+
|
|
11
13
|
# Policy Type Constants
|
|
12
14
|
PolicyType = Literal[
|
|
13
15
|
"IDENTITY_POLICY",
|
|
@@ -89,9 +91,8 @@ class ServiceDetail(BaseModel):
|
|
|
89
91
|
resources_list: list[ResourceType] = Field(default_factory=list, alias="Resources")
|
|
90
92
|
condition_keys_list: list[ConditionKey] = Field(default_factory=list, alias="ConditionKeys")
|
|
91
93
|
|
|
92
|
-
def model_post_init(self, __context: Any) -> None:
|
|
94
|
+
def model_post_init(self, __context: Any, /) -> None:
|
|
93
95
|
"""Convert lists to dictionaries for easier lookup."""
|
|
94
|
-
del __context # Unused
|
|
95
96
|
# Convert actions list to dict
|
|
96
97
|
self.actions = {action.name: action for action in self.actions_list}
|
|
97
98
|
# Convert resources list to dict
|
|
@@ -104,7 +105,7 @@ class ServiceDetail(BaseModel):
|
|
|
104
105
|
class Statement(BaseModel):
|
|
105
106
|
"""IAM policy statement."""
|
|
106
107
|
|
|
107
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
108
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
108
109
|
|
|
109
110
|
sid: str | None = Field(default=None, alias="Sid")
|
|
110
111
|
effect: str = Field(alias="Effect")
|
|
@@ -134,7 +135,7 @@ class Statement(BaseModel):
|
|
|
134
135
|
class IAMPolicy(BaseModel):
|
|
135
136
|
"""IAM policy document."""
|
|
136
137
|
|
|
137
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
138
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
138
139
|
|
|
139
140
|
version: str = Field(alias="Version")
|
|
140
141
|
statement: list[Statement] = Field(alias="Statement")
|
|
@@ -161,6 +162,7 @@ class ValidationIssue(BaseModel):
|
|
|
161
162
|
resource: str | None = None
|
|
162
163
|
condition_key: str | None = None
|
|
163
164
|
suggestion: str | None = None
|
|
165
|
+
example: str | None = None # Code example (JSON/YAML) - formatted separately for GitHub
|
|
164
166
|
line_number: int | None = None # Line number in the policy file (if available)
|
|
165
167
|
|
|
166
168
|
# Severity level constants (ClassVar to avoid Pydantic treating them as fields)
|
|
@@ -225,8 +227,8 @@ class ValidationIssue(BaseModel):
|
|
|
225
227
|
|
|
226
228
|
# Add identifier for bot comment cleanup (HTML comment - not visible to users)
|
|
227
229
|
if include_identifier:
|
|
228
|
-
parts.append("
|
|
229
|
-
parts.append("
|
|
230
|
+
parts.append(f"{constants.REVIEW_IDENTIFIER}\n")
|
|
231
|
+
parts.append(f"{constants.BOT_IDENTIFIER}\n")
|
|
230
232
|
|
|
231
233
|
# Build statement context for better navigation
|
|
232
234
|
statement_context = f"Statement[{self.statement_index}]"
|
|
@@ -241,7 +243,9 @@ class ValidationIssue(BaseModel):
|
|
|
241
243
|
parts.append(self.message)
|
|
242
244
|
|
|
243
245
|
# Put additional details in collapsible section if there are any
|
|
244
|
-
has_details = bool(
|
|
246
|
+
has_details = bool(
|
|
247
|
+
self.action or self.resource or self.condition_key or self.suggestion or self.example
|
|
248
|
+
)
|
|
245
249
|
|
|
246
250
|
if has_details:
|
|
247
251
|
parts.append("")
|
|
@@ -266,6 +270,14 @@ class ValidationIssue(BaseModel):
|
|
|
266
270
|
parts.append("**💡 Suggested Fix:**")
|
|
267
271
|
parts.append("")
|
|
268
272
|
parts.append(self.suggestion)
|
|
273
|
+
parts.append("")
|
|
274
|
+
|
|
275
|
+
# Add example if present (formatted as JSON code block for GitHub)
|
|
276
|
+
if self.example:
|
|
277
|
+
parts.append("**Example:**")
|
|
278
|
+
parts.append("```json")
|
|
279
|
+
parts.append(self.example)
|
|
280
|
+
parts.append("```")
|
|
269
281
|
|
|
270
282
|
parts.append("")
|
|
271
283
|
parts.append("</details>")
|
|
@@ -298,6 +310,9 @@ class ValidationReport(BaseModel):
|
|
|
298
310
|
validity_issues: int = 0 # Count of IAM validity issues (error/warning/info)
|
|
299
311
|
security_issues: int = 0 # Count of security issues (critical/high/medium/low)
|
|
300
312
|
results: list[PolicyValidationResult] = Field(default_factory=list)
|
|
313
|
+
parsing_errors: list[tuple[str, str]] = Field(
|
|
314
|
+
default_factory=list
|
|
315
|
+
) # (file_path, error_message)
|
|
301
316
|
|
|
302
317
|
def get_summary(self) -> str:
|
|
303
318
|
"""Generate a human-readable summary."""
|