iam-policy-validator 1.9.0__py3-none-any.whl → 1.10.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.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/METADATA +1 -1
- {iam_policy_validator-1.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/RECORD +13 -12
- iam_validator/__version__.py +1 -1
- iam_validator/commands/validate.py +14 -4
- iam_validator/core/aws_service/validators.py +5 -2
- iam_validator/core/config/defaults.py +10 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +6 -6
- iam_validator/core/pr_commenter.py +34 -7
- iam_validator/sdk/policy_utils.py +34 -8
- {iam_policy_validator-1.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iam-policy-validator
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.1
|
|
4
4
|
Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
|
|
5
5
|
Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
|
|
6
6
|
Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
iam_validator/__init__.py,sha256=xHdUASOxFHwEXfT_GSr_KrkLlnxZ-pAAr1wW1PwAGko,693
|
|
2
2
|
iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
|
|
3
|
-
iam_validator/__version__.py,sha256=
|
|
3
|
+
iam_validator/__version__.py,sha256=SRRdMee5oIovcL25-xNSjSxoHM9bzGQhwmx_U4YhnXQ,362
|
|
4
4
|
iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
|
|
5
5
|
iam_validator/checks/action_condition_enforcement.py,sha256=0dCH_xX-Xc0uLxtNeRjrpNjWYbdWQRzO1XNcLTSn6sI,51698
|
|
6
6
|
iam_validator/checks/action_resource_matching.py,sha256=WiGJmCIJfx5yituMjZxpKmk-99N6nK20ueN02ddy9oM,19296
|
|
@@ -31,7 +31,7 @@ iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gz
|
|
|
31
31
|
iam_validator/commands/cache.py,sha256=llfyQzPE5Azd5YcW0ohYcYjF_OCyiQ1GoJQ982t71lQ,14294
|
|
32
32
|
iam_validator/commands/download_services.py,sha256=KKz3ybMLT8DQUf9aFZ0tilJ-o1b6PE8Pf1pC4K6cT8I,9175
|
|
33
33
|
iam_validator/commands/post_to_pr.py,sha256=CvUXs2xvO-UhluxdfNM6F0TCWD8hDBEOiYw60fm1Dms,2363
|
|
34
|
-
iam_validator/commands/validate.py,sha256=
|
|
34
|
+
iam_validator/commands/validate.py,sha256=cvrgYagYm7W29MYsitZsLcttIIqVKQMRm-bCGY7N3fU,24355
|
|
35
35
|
iam_validator/core/__init__.py,sha256=hYXkSbxplKzhM6dqrVzV4M3k7GKLsZbgExypxKq74gs,376
|
|
36
36
|
iam_validator/core/access_analyzer.py,sha256=mtMaY-FnKjKEVITky_9ywZe1FaCAm61ElRv5Z_ZeC7E,24562
|
|
37
37
|
iam_validator/core/access_analyzer_report.py,sha256=UMm2RNGj2rAKav1zsCw_htQZZRwRC0jjayd2zvKma1A,24896
|
|
@@ -41,10 +41,11 @@ iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,384
|
|
|
41
41
|
iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
|
|
42
42
|
iam_validator/core/constants.py,sha256=cVBPgbXr4ALltH_NTSKsgBi6wmndLnOyUWhyBx0ZwrM,6113
|
|
43
43
|
iam_validator/core/ignore_patterns.py,sha256=pZqDJBtkbck-85QK5eFPM5ZOPEKs3McRh3avqiCT5z0,10398
|
|
44
|
-
iam_validator/core/
|
|
44
|
+
iam_validator/core/label_manager.py,sha256=48CRASWg98wyjfVF_1pUzj6dm9itzmG7SeIWf0TSUfc,7502
|
|
45
|
+
iam_validator/core/models.py,sha256=yQ5iBTffdAzx88h8RyVCCmBg6kkD2zg5_lb-qLdjy3w,13386
|
|
45
46
|
iam_validator/core/policy_checks.py,sha256=FNVuS2GTffwCjjrlupVIazC172gSxKYAAT_ObV6Apbo,8803
|
|
46
47
|
iam_validator/core/policy_loader.py,sha256=2KJnXzGg3g9pDXWZHk3DO0xpZnZZ-wXWFEOdQ_naJ8s,17862
|
|
47
|
-
iam_validator/core/pr_commenter.py,sha256=
|
|
48
|
+
iam_validator/core/pr_commenter.py,sha256=NTKoSmjvspYX2rbl3Xn8d611XkTNSfYlGUY0zBHBP4g,16801
|
|
48
49
|
iam_validator/core/report.py,sha256=kzSeWnT1LqWZVA5pqKKz-maVowXVj0djdoShfRhhpz4,35899
|
|
49
50
|
iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
|
|
50
51
|
iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
|
|
@@ -53,14 +54,14 @@ iam_validator/core/aws_service/fetcher.py,sha256=X4iI6fiLj4l9f3W6_J0E58lSP26UsBh
|
|
|
53
54
|
iam_validator/core/aws_service/parsers.py,sha256=gJzR7HCD8ItCWCCbguTQIZpPEdj2rdMwC7LPhu7ve14,5174
|
|
54
55
|
iam_validator/core/aws_service/patterns.py,sha256=gGc55Tn-EJ3cmcWtmYAZROUajKYz7DaMchYWGEhHpC0,1726
|
|
55
56
|
iam_validator/core/aws_service/storage.py,sha256=PrfKdvF60IL7E_8xYs_XwFoAJPRcVYw57FVLHCoqwVk,10429
|
|
56
|
-
iam_validator/core/aws_service/validators.py,sha256=
|
|
57
|
+
iam_validator/core/aws_service/validators.py,sha256=AY0BjydskXoesEzUShH4gZKp6gtSX7s1rCLP_iOZQMc,16493
|
|
57
58
|
iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
|
|
58
59
|
iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
|
|
59
60
|
iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
|
|
60
61
|
iam_validator/core/config/category_suggestions.py,sha256=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
|
|
61
62
|
iam_validator/core/config/condition_requirements.py,sha256=qauIP73HFnOw1dchUeFpg1x7Y7QWkILo3GfxV_dxdQo,7696
|
|
62
63
|
iam_validator/core/config/config_loader.py,sha256=qKD8aR8YAswaFf68pnYJLFNwKznvcc6lNxSQWU3i6SY,17713
|
|
63
|
-
iam_validator/core/config/defaults.py,sha256=
|
|
64
|
+
iam_validator/core/config/defaults.py,sha256=qpFP534dgCQ-vjCdhkK7ZslDoTm9Ftgy20qmYZsSYUI,28637
|
|
64
65
|
iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
|
|
65
66
|
iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
|
|
66
67
|
iam_validator/core/config/service_principals.py,sha256=8pys5H_yycVJ9KTyimAKFYBg83Aol2Iri53wiHjtnEM,3959
|
|
@@ -82,14 +83,14 @@ iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urch
|
|
|
82
83
|
iam_validator/sdk/context.py,sha256=FvAEyUa_s7tHWoSdgjSkzHf1CLlYpAEmLZANxs2IJ4A,6826
|
|
83
84
|
iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
|
|
84
85
|
iam_validator/sdk/helpers.py,sha256=sjfK0na_Fo7O8GhEVhl44rVHqOdw6nAKkBL4FVL-QdU,5697
|
|
85
|
-
iam_validator/sdk/policy_utils.py,sha256=
|
|
86
|
+
iam_validator/sdk/policy_utils.py,sha256=Fh-QElhmPypzSJuF9rcrY7y46Gz3hQu3-yN5b1_mSHY,13579
|
|
86
87
|
iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
|
|
87
88
|
iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYuCs,1021
|
|
88
89
|
iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
|
|
89
90
|
iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
|
|
90
91
|
iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
|
|
91
|
-
iam_policy_validator-1.
|
|
92
|
-
iam_policy_validator-1.
|
|
93
|
-
iam_policy_validator-1.
|
|
94
|
-
iam_policy_validator-1.
|
|
95
|
-
iam_policy_validator-1.
|
|
92
|
+
iam_policy_validator-1.10.1.dist-info/METADATA,sha256=lIMmE1Y6TX34sh3stfe9J2_q5ATb9fYZqVqOupYNcL8,19070
|
|
93
|
+
iam_policy_validator-1.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
94
|
+
iam_policy_validator-1.10.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
|
|
95
|
+
iam_policy_validator-1.10.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
96
|
+
iam_policy_validator-1.10.1.dist-info/RECORD,,
|
iam_validator/__version__.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This file is the single source of truth for the package version.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
__version__ = "1.
|
|
6
|
+
__version__ = "1.10.1"
|
|
7
7
|
# Parse version, handling pre-release suffixes like -rc, -alpha, -beta
|
|
8
8
|
_version_base = __version__.split("-")[0] # Remove pre-release suffix if present
|
|
9
9
|
__version_info__ = tuple(int(part) for part in _version_base.split("."))
|
|
@@ -302,12 +302,17 @@ Examples:
|
|
|
302
302
|
from iam_validator.core.config.config_loader import ConfigLoader
|
|
303
303
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
304
304
|
|
|
305
|
-
# Load config to get fail_on_severity
|
|
305
|
+
# Load config to get fail_on_severity and severity_labels settings
|
|
306
306
|
config = ConfigLoader.load_config(config_path)
|
|
307
307
|
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
308
|
+
severity_labels = config.get_setting("severity_labels", {})
|
|
308
309
|
|
|
309
310
|
async with GitHubIntegration() as github:
|
|
310
|
-
commenter = PRCommenter(
|
|
311
|
+
commenter = PRCommenter(
|
|
312
|
+
github,
|
|
313
|
+
fail_on_severities=fail_on_severities,
|
|
314
|
+
severity_labels=severity_labels,
|
|
315
|
+
)
|
|
311
316
|
success = await commenter.post_findings_to_pr(
|
|
312
317
|
report,
|
|
313
318
|
create_review=getattr(args, "github_review", False),
|
|
@@ -426,12 +431,17 @@ Examples:
|
|
|
426
431
|
from iam_validator.core.config.config_loader import ConfigLoader
|
|
427
432
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
428
433
|
|
|
429
|
-
# Load config to get fail_on_severity
|
|
434
|
+
# Load config to get fail_on_severity and severity_labels settings
|
|
430
435
|
config = ConfigLoader.load_config(config_path)
|
|
431
436
|
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
437
|
+
severity_labels = config.get_setting("severity_labels", {})
|
|
432
438
|
|
|
433
439
|
async with GitHubIntegration() as github:
|
|
434
|
-
commenter = PRCommenter(
|
|
440
|
+
commenter = PRCommenter(
|
|
441
|
+
github,
|
|
442
|
+
fail_on_severities=fail_on_severities,
|
|
443
|
+
severity_labels=severity_labels,
|
|
444
|
+
)
|
|
435
445
|
success = await commenter.post_findings_to_pr(
|
|
436
446
|
report,
|
|
437
447
|
create_review=False, # Already posted per-file reviews in streaming mode
|
|
@@ -280,9 +280,12 @@ class ServiceValidator:
|
|
|
280
280
|
"- `aws:RequestedRegion`\n"
|
|
281
281
|
"- `aws:SourceIp`\n"
|
|
282
282
|
"- `aws:SourceVpce`\n"
|
|
283
|
-
"- `aws:
|
|
283
|
+
"- `aws:ResourceOrgID`\n"
|
|
284
|
+
"- `aws:PrincipalOrgID`\n"
|
|
285
|
+
"- `aws:SourceAccount`\n"
|
|
286
|
+
"- `aws:PrincipalAccount`\n"
|
|
284
287
|
"- `aws:CurrentTime`\n"
|
|
285
|
-
"- `aws:
|
|
288
|
+
"- `aws:ResourceAccount`\n"
|
|
286
289
|
"- `aws:PrincipalArn`\n"
|
|
287
290
|
"- And many others"
|
|
288
291
|
)
|
|
@@ -75,6 +75,16 @@ DEFAULT_CONFIG = {
|
|
|
75
75
|
# IAM Validity: error, warning, info
|
|
76
76
|
# Security: critical, high, medium, low
|
|
77
77
|
"fail_on_severity": list(constants.HIGH_SEVERITY_LEVELS),
|
|
78
|
+
# GitHub PR label mapping based on severity findings
|
|
79
|
+
# When issues with these severities are found, apply the corresponding labels
|
|
80
|
+
# If no issues with these severities exist, remove the labels if present
|
|
81
|
+
# Supports both single labels and lists of labels per severity
|
|
82
|
+
# Examples:
|
|
83
|
+
# Single label per severity: {"error": "iam-validity-error", "critical": "security-critical"}
|
|
84
|
+
# Multiple labels per severity: {"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-review"]}
|
|
85
|
+
# Mixed: {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
|
|
86
|
+
# Default: {} (disabled)
|
|
87
|
+
"severity_labels": {},
|
|
78
88
|
},
|
|
79
89
|
# ========================================================================
|
|
80
90
|
# AWS IAM Validation Checks (17 checks total)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Label Manager for GitHub PR Labels based on Severity Findings.
|
|
2
|
+
|
|
3
|
+
This module manages GitHub PR labels based on IAM policy validation severity findings.
|
|
4
|
+
When validation finds issues with specific severities, it applies corresponding labels.
|
|
5
|
+
When those severities are not found, it removes the labels if present.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from iam_validator.core.models import PolicyValidationResult, ValidationReport
|
|
13
|
+
from iam_validator.integrations.github_integration import GitHubIntegration
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LabelManager:
|
|
19
|
+
"""Manages GitHub PR labels based on severity findings."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
github: "GitHubIntegration",
|
|
24
|
+
severity_labels: dict[str, str | list[str]] | None = None,
|
|
25
|
+
):
|
|
26
|
+
"""Initialize label manager.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
github: GitHubIntegration instance for API calls
|
|
30
|
+
severity_labels: Mapping of severity levels to label name(s)
|
|
31
|
+
Supports both single labels and lists of labels per severity.
|
|
32
|
+
Examples:
|
|
33
|
+
- Single label per severity:
|
|
34
|
+
{"error": "iam-validity-error", "critical": "security-critical"}
|
|
35
|
+
- Multiple labels per severity:
|
|
36
|
+
{"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-security-review"]}
|
|
37
|
+
- Mixed:
|
|
38
|
+
{"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
|
|
39
|
+
"""
|
|
40
|
+
self.github = github
|
|
41
|
+
self.severity_labels = severity_labels or {}
|
|
42
|
+
|
|
43
|
+
def is_enabled(self) -> bool:
|
|
44
|
+
"""Check if label management is enabled.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if severity_labels is configured and GitHub is configured
|
|
48
|
+
"""
|
|
49
|
+
return bool(self.severity_labels) and self.github.is_configured()
|
|
50
|
+
|
|
51
|
+
def _get_severities_in_results(self, results: list["PolicyValidationResult"]) -> set[str]:
|
|
52
|
+
"""Extract all severity levels found in validation results.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
results: List of PolicyValidationResult objects
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Set of severity levels found (e.g., {"error", "critical", "high"})
|
|
59
|
+
"""
|
|
60
|
+
severities = set()
|
|
61
|
+
for result in results:
|
|
62
|
+
for issue in result.issues:
|
|
63
|
+
severities.add(issue.severity)
|
|
64
|
+
return severities
|
|
65
|
+
|
|
66
|
+
def _get_severities_in_report(self, report: "ValidationReport") -> set[str]:
|
|
67
|
+
"""Extract all severity levels found in validation report.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
report: ValidationReport object
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Set of severity levels found (e.g., {"error", "critical", "high"})
|
|
74
|
+
"""
|
|
75
|
+
return self._get_severities_in_results(report.results)
|
|
76
|
+
|
|
77
|
+
def _determine_labels_to_apply(self, found_severities: set[str]) -> set[str]:
|
|
78
|
+
"""Determine which labels should be applied based on found severities.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
found_severities: Set of severity levels found in validation
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Set of label names to apply
|
|
85
|
+
"""
|
|
86
|
+
labels_to_apply = set()
|
|
87
|
+
for severity, labels in self.severity_labels.items():
|
|
88
|
+
if severity in found_severities:
|
|
89
|
+
# Support both single labels and lists of labels
|
|
90
|
+
if isinstance(labels, list):
|
|
91
|
+
labels_to_apply.update(labels)
|
|
92
|
+
else:
|
|
93
|
+
labels_to_apply.add(labels)
|
|
94
|
+
return labels_to_apply
|
|
95
|
+
|
|
96
|
+
def _determine_labels_to_remove(self, found_severities: set[str]) -> set[str]:
|
|
97
|
+
"""Determine which labels should be removed based on missing severities.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
found_severities: Set of severity levels found in validation
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Set of label names to remove
|
|
104
|
+
"""
|
|
105
|
+
labels_to_remove = set()
|
|
106
|
+
for severity, labels in self.severity_labels.items():
|
|
107
|
+
if severity not in found_severities:
|
|
108
|
+
# Support both single labels and lists of labels
|
|
109
|
+
if isinstance(labels, list):
|
|
110
|
+
labels_to_remove.update(labels)
|
|
111
|
+
else:
|
|
112
|
+
labels_to_remove.add(labels)
|
|
113
|
+
return labels_to_remove
|
|
114
|
+
|
|
115
|
+
async def manage_labels_from_results(
|
|
116
|
+
self, results: list["PolicyValidationResult"]
|
|
117
|
+
) -> tuple[bool, int, int]:
|
|
118
|
+
"""Manage PR labels based on validation results.
|
|
119
|
+
|
|
120
|
+
This method will:
|
|
121
|
+
1. Determine which severity levels are present in the results
|
|
122
|
+
2. Add labels for severities that are found
|
|
123
|
+
3. Remove labels for severities that are not found
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
results: List of PolicyValidationResult objects
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Tuple of (success, labels_added, labels_removed)
|
|
130
|
+
"""
|
|
131
|
+
if not self.is_enabled():
|
|
132
|
+
logger.debug("Label management not enabled (no severity_labels configured)")
|
|
133
|
+
return (True, 0, 0)
|
|
134
|
+
|
|
135
|
+
# Get all severities found in results
|
|
136
|
+
found_severities = self._get_severities_in_results(results)
|
|
137
|
+
logger.debug(f"Found severities in results: {found_severities}")
|
|
138
|
+
|
|
139
|
+
# Determine which labels to apply/remove
|
|
140
|
+
labels_to_apply = self._determine_labels_to_apply(found_severities)
|
|
141
|
+
labels_to_remove = self._determine_labels_to_remove(found_severities)
|
|
142
|
+
|
|
143
|
+
logger.debug(f"Labels to apply: {labels_to_apply}")
|
|
144
|
+
logger.debug(f"Labels to remove: {labels_to_remove}")
|
|
145
|
+
|
|
146
|
+
# Get current labels on PR
|
|
147
|
+
current_labels = set(await self.github.get_labels())
|
|
148
|
+
logger.debug(f"Current PR labels: {current_labels}")
|
|
149
|
+
|
|
150
|
+
# Filter: only add labels that aren't already present
|
|
151
|
+
labels_to_add = labels_to_apply - current_labels
|
|
152
|
+
|
|
153
|
+
# Filter: only remove labels that are currently present
|
|
154
|
+
labels_to_actually_remove = labels_to_remove & current_labels
|
|
155
|
+
|
|
156
|
+
success = True
|
|
157
|
+
added_count = 0
|
|
158
|
+
removed_count = 0
|
|
159
|
+
|
|
160
|
+
# Add new labels
|
|
161
|
+
if labels_to_add:
|
|
162
|
+
logger.info(f"Adding labels to PR: {labels_to_add}")
|
|
163
|
+
if await self.github.add_labels(list(labels_to_add)):
|
|
164
|
+
added_count = len(labels_to_add)
|
|
165
|
+
else:
|
|
166
|
+
logger.error("Failed to add labels to PR")
|
|
167
|
+
success = False
|
|
168
|
+
|
|
169
|
+
# Remove old labels
|
|
170
|
+
for label in labels_to_actually_remove:
|
|
171
|
+
logger.info(f"Removing label from PR: {label}")
|
|
172
|
+
if await self.github.remove_label(label):
|
|
173
|
+
removed_count += 1
|
|
174
|
+
else:
|
|
175
|
+
logger.error(f"Failed to remove label: {label}")
|
|
176
|
+
success = False
|
|
177
|
+
|
|
178
|
+
if added_count > 0 or removed_count > 0:
|
|
179
|
+
logger.info(f"Label management complete: added {added_count}, removed {removed_count}")
|
|
180
|
+
else:
|
|
181
|
+
logger.debug("No label changes needed")
|
|
182
|
+
|
|
183
|
+
return (success, added_count, removed_count)
|
|
184
|
+
|
|
185
|
+
async def manage_labels_from_report(self, report: "ValidationReport") -> tuple[bool, int, int]:
|
|
186
|
+
"""Manage PR labels based on validation report.
|
|
187
|
+
|
|
188
|
+
This is a convenience method that extracts results from the report
|
|
189
|
+
and calls manage_labels_from_results().
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
report: ValidationReport object
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Tuple of (success, labels_added, labels_removed)
|
|
196
|
+
"""
|
|
197
|
+
return await self.manage_labels_from_results(report.results)
|
iam_validator/core/models.py
CHANGED
|
@@ -31,7 +31,7 @@ class ServiceInfo(BaseModel):
|
|
|
31
31
|
class ActionDetail(BaseModel):
|
|
32
32
|
"""Details about an AWS IAM action."""
|
|
33
33
|
|
|
34
|
-
model_config = ConfigDict(
|
|
34
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
|
35
35
|
|
|
36
36
|
name: str = Field(alias="Name")
|
|
37
37
|
action_condition_keys: list[str] | None = Field(
|
|
@@ -45,7 +45,7 @@ class ActionDetail(BaseModel):
|
|
|
45
45
|
class ResourceType(BaseModel):
|
|
46
46
|
"""Details about an AWS resource type."""
|
|
47
47
|
|
|
48
|
-
model_config = ConfigDict(
|
|
48
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
|
49
49
|
|
|
50
50
|
name: str = Field(alias="Name")
|
|
51
51
|
arn_formats: list[str] | None = Field(default=None, alias="ARNFormats")
|
|
@@ -68,7 +68,7 @@ class ResourceType(BaseModel):
|
|
|
68
68
|
class ConditionKey(BaseModel):
|
|
69
69
|
"""Details about an AWS condition key."""
|
|
70
70
|
|
|
71
|
-
model_config = ConfigDict(
|
|
71
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
|
72
72
|
|
|
73
73
|
name: str = Field(alias="Name")
|
|
74
74
|
description: str | None = Field(default=None, alias="Description")
|
|
@@ -78,7 +78,7 @@ class ConditionKey(BaseModel):
|
|
|
78
78
|
class ServiceDetail(BaseModel):
|
|
79
79
|
"""Detailed information about an AWS service."""
|
|
80
80
|
|
|
81
|
-
model_config = ConfigDict(
|
|
81
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
|
82
82
|
|
|
83
83
|
name: str = Field(alias="Name")
|
|
84
84
|
prefix: str | None = None # Not always present in API response
|
|
@@ -106,7 +106,7 @@ class ServiceDetail(BaseModel):
|
|
|
106
106
|
class Statement(BaseModel):
|
|
107
107
|
"""IAM policy statement."""
|
|
108
108
|
|
|
109
|
-
model_config = ConfigDict(
|
|
109
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, extra="allow")
|
|
110
110
|
|
|
111
111
|
sid: str | None = Field(default=None, alias="Sid")
|
|
112
112
|
effect: str | None = Field(default=None, alias="Effect")
|
|
@@ -136,7 +136,7 @@ class Statement(BaseModel):
|
|
|
136
136
|
class IAMPolicy(BaseModel):
|
|
137
137
|
"""IAM policy document."""
|
|
138
138
|
|
|
139
|
-
model_config = ConfigDict(
|
|
139
|
+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, extra="allow")
|
|
140
140
|
|
|
141
141
|
version: str | None = Field(default=None, alias="Version")
|
|
142
142
|
statement: list[Statement] | None = Field(default=None, alias="Statement")
|
|
@@ -13,7 +13,9 @@ from iam_validator.core.constants import (
|
|
|
13
13
|
REVIEW_IDENTIFIER,
|
|
14
14
|
SUMMARY_IDENTIFIER,
|
|
15
15
|
)
|
|
16
|
+
from iam_validator.core.label_manager import LabelManager
|
|
16
17
|
from iam_validator.core.models import ValidationIssue, ValidationReport
|
|
18
|
+
from iam_validator.core.report import ReportGenerator
|
|
17
19
|
from iam_validator.integrations.github_integration import GitHubIntegration, ReviewEvent
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
@@ -32,6 +34,7 @@ class PRCommenter:
|
|
|
32
34
|
github: GitHubIntegration | None = None,
|
|
33
35
|
cleanup_old_comments: bool = True,
|
|
34
36
|
fail_on_severities: list[str] | None = None,
|
|
37
|
+
severity_labels: dict[str, str | list[str]] | None = None,
|
|
35
38
|
):
|
|
36
39
|
"""Initialize PR commenter.
|
|
37
40
|
|
|
@@ -40,16 +43,24 @@ class PRCommenter:
|
|
|
40
43
|
cleanup_old_comments: Whether to clean up old bot comments before posting new ones
|
|
41
44
|
fail_on_severities: List of severity levels that should trigger REQUEST_CHANGES
|
|
42
45
|
(e.g., ["error", "critical", "high"])
|
|
46
|
+
severity_labels: Mapping of severity levels to label name(s) for automatic label management
|
|
47
|
+
Supports both single labels and lists of labels per severity.
|
|
48
|
+
Examples:
|
|
49
|
+
- Single: {"error": "iam-validity-error", "critical": "security-critical"}
|
|
50
|
+
- Multiple: {"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-review"]}
|
|
51
|
+
- Mixed: {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
|
|
43
52
|
"""
|
|
44
53
|
self.github = github
|
|
45
54
|
self.cleanup_old_comments = cleanup_old_comments
|
|
46
55
|
self.fail_on_severities = fail_on_severities or ["error", "critical"]
|
|
56
|
+
self.severity_labels = severity_labels or {}
|
|
47
57
|
|
|
48
58
|
async def post_findings_to_pr(
|
|
49
59
|
self,
|
|
50
60
|
report: ValidationReport,
|
|
51
61
|
create_review: bool = True,
|
|
52
62
|
add_summary_comment: bool = True,
|
|
63
|
+
manage_labels: bool = True,
|
|
53
64
|
) -> bool:
|
|
54
65
|
"""Post validation findings to a PR.
|
|
55
66
|
|
|
@@ -57,6 +68,7 @@ class PRCommenter:
|
|
|
57
68
|
report: Validation report with findings
|
|
58
69
|
create_review: Whether to create a PR review with line comments
|
|
59
70
|
add_summary_comment: Whether to add a summary comment
|
|
71
|
+
manage_labels: Whether to manage PR labels based on severity findings
|
|
60
72
|
|
|
61
73
|
Returns:
|
|
62
74
|
True if successful, False otherwise
|
|
@@ -81,8 +93,6 @@ class PRCommenter:
|
|
|
81
93
|
|
|
82
94
|
# Post summary comment (potentially as multiple parts)
|
|
83
95
|
if add_summary_comment:
|
|
84
|
-
from iam_validator.core.report import ReportGenerator
|
|
85
|
-
|
|
86
96
|
generator = ReportGenerator()
|
|
87
97
|
comment_parts = generator.generate_github_comment_parts(report)
|
|
88
98
|
|
|
@@ -104,6 +114,18 @@ class PRCommenter:
|
|
|
104
114
|
logger.error("Failed to post review comments")
|
|
105
115
|
success = False
|
|
106
116
|
|
|
117
|
+
# Manage PR labels based on severity findings
|
|
118
|
+
if manage_labels and self.severity_labels:
|
|
119
|
+
label_manager = LabelManager(self.github, self.severity_labels)
|
|
120
|
+
label_success, added, removed = await label_manager.manage_labels_from_report(report)
|
|
121
|
+
|
|
122
|
+
if not label_success:
|
|
123
|
+
logger.error("Failed to manage PR labels")
|
|
124
|
+
success = False
|
|
125
|
+
else:
|
|
126
|
+
if added > 0 or removed > 0:
|
|
127
|
+
logger.info(f"Label management: added {added}, removed {removed}")
|
|
128
|
+
|
|
107
129
|
return success
|
|
108
130
|
|
|
109
131
|
async def _post_review_comments(self, report: ValidationReport) -> bool:
|
|
@@ -288,7 +310,7 @@ class PRCommenter:
|
|
|
288
310
|
|
|
289
311
|
return mapping
|
|
290
312
|
|
|
291
|
-
except Exception as e:
|
|
313
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
292
314
|
logger.warning(f"Could not parse {policy_file} for line mapping: {e}")
|
|
293
315
|
return {}
|
|
294
316
|
|
|
@@ -369,7 +391,7 @@ class PRCommenter:
|
|
|
369
391
|
|
|
370
392
|
return None
|
|
371
393
|
|
|
372
|
-
except Exception as e:
|
|
394
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
373
395
|
logger.debug(f"Could not search {policy_file}: {e}")
|
|
374
396
|
return None
|
|
375
397
|
|
|
@@ -398,15 +420,20 @@ async def post_report_to_pr(
|
|
|
398
420
|
|
|
399
421
|
report = ValidationReport.model_validate(report_data)
|
|
400
422
|
|
|
401
|
-
# Load config to get fail_on_severity
|
|
423
|
+
# Load config to get fail_on_severity and severity_labels settings
|
|
402
424
|
from iam_validator.core.config.config_loader import ConfigLoader
|
|
403
425
|
|
|
404
426
|
config = ConfigLoader.load_config(config_path)
|
|
405
427
|
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
428
|
+
severity_labels = config.get_setting("severity_labels", {})
|
|
406
429
|
|
|
407
430
|
# Post to PR
|
|
408
431
|
async with GitHubIntegration() as github:
|
|
409
|
-
commenter = PRCommenter(
|
|
432
|
+
commenter = PRCommenter(
|
|
433
|
+
github,
|
|
434
|
+
fail_on_severities=fail_on_severities,
|
|
435
|
+
severity_labels=severity_labels,
|
|
436
|
+
)
|
|
410
437
|
return await commenter.post_findings_to_pr(
|
|
411
438
|
report,
|
|
412
439
|
create_review=create_review,
|
|
@@ -419,6 +446,6 @@ async def post_report_to_pr(
|
|
|
419
446
|
except json.JSONDecodeError as e:
|
|
420
447
|
logger.error(f"Invalid JSON in report file: {e}")
|
|
421
448
|
return False
|
|
422
|
-
except Exception as e:
|
|
449
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
423
450
|
logger.error(f"Failed to post report to PR: {e}")
|
|
424
451
|
return False
|
|
@@ -63,9 +63,13 @@ def normalize_policy(policy: IAMPolicy) -> IAMPolicy:
|
|
|
63
63
|
"""
|
|
64
64
|
# Pydantic model already handles this via Field(alias="Statement")
|
|
65
65
|
# which expects a list, but we can ensure it's always a list
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
66
|
+
if policy.statement is None:
|
|
67
|
+
statements: list[Statement] = []
|
|
68
|
+
elif isinstance(policy.statement, list):
|
|
69
|
+
statements = policy.statement
|
|
70
|
+
else:
|
|
71
|
+
# Single statement - wrap in list
|
|
72
|
+
statements = [policy.statement]
|
|
69
73
|
|
|
70
74
|
# Normalize actions and resources in each statement
|
|
71
75
|
normalized_statements: list[Statement] = []
|
|
@@ -118,6 +122,9 @@ def extract_actions(policy: IAMPolicy) -> list[str]:
|
|
|
118
122
|
"""
|
|
119
123
|
actions = set()
|
|
120
124
|
|
|
125
|
+
if policy.statement is None:
|
|
126
|
+
return []
|
|
127
|
+
|
|
121
128
|
for stmt in policy.statement:
|
|
122
129
|
# Handle Action field
|
|
123
130
|
if stmt.action:
|
|
@@ -150,6 +157,9 @@ def extract_resources(policy: IAMPolicy) -> list[str]:
|
|
|
150
157
|
"""
|
|
151
158
|
resources = set()
|
|
152
159
|
|
|
160
|
+
if policy.statement is None:
|
|
161
|
+
return []
|
|
162
|
+
|
|
153
163
|
for stmt in policy.statement:
|
|
154
164
|
# Handle Resource field
|
|
155
165
|
if stmt.resource:
|
|
@@ -181,7 +191,10 @@ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
|
|
|
181
191
|
>>> keys = extract_condition_keys(policy)
|
|
182
192
|
>>> print(f"Policy uses condition keys: {', '.join(keys)}")
|
|
183
193
|
"""
|
|
184
|
-
condition_keys = set()
|
|
194
|
+
condition_keys: set[str] = set()
|
|
195
|
+
|
|
196
|
+
if policy.statement is None:
|
|
197
|
+
return []
|
|
185
198
|
|
|
186
199
|
for stmt in policy.statement:
|
|
187
200
|
if stmt.condition:
|
|
@@ -216,6 +229,9 @@ def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statemen
|
|
|
216
229
|
|
|
217
230
|
matching_statements = []
|
|
218
231
|
|
|
232
|
+
if policy.statement is None:
|
|
233
|
+
return []
|
|
234
|
+
|
|
219
235
|
for stmt in policy.statement:
|
|
220
236
|
stmt_actions = stmt.get_actions()
|
|
221
237
|
|
|
@@ -250,6 +266,9 @@ def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Stat
|
|
|
250
266
|
|
|
251
267
|
matching_statements = []
|
|
252
268
|
|
|
269
|
+
if policy.statement is None:
|
|
270
|
+
return []
|
|
271
|
+
|
|
253
272
|
for stmt in policy.statement:
|
|
254
273
|
stmt_resources = stmt.get_resources()
|
|
255
274
|
|
|
@@ -286,7 +305,8 @@ def merge_policies(*policies: IAMPolicy) -> IAMPolicy:
|
|
|
286
305
|
|
|
287
306
|
all_statements: list[Statement] = []
|
|
288
307
|
for policy in policies:
|
|
289
|
-
|
|
308
|
+
if policy.statement is not None:
|
|
309
|
+
all_statements.extend(policy.statement)
|
|
290
310
|
|
|
291
311
|
# Use capitalized field names (aliases) for Pydantic model construction
|
|
292
312
|
return IAMPolicy(
|
|
@@ -318,8 +338,9 @@ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
|
|
|
318
338
|
condition_keys = extract_condition_keys(policy)
|
|
319
339
|
|
|
320
340
|
# Count allow vs deny statements
|
|
321
|
-
|
|
322
|
-
|
|
341
|
+
statements = policy.statement or []
|
|
342
|
+
allow_count = sum(1 for s in statements if s.effect and s.effect.lower() == "allow")
|
|
343
|
+
deny_count = sum(1 for s in statements if s.effect and s.effect.lower() == "deny")
|
|
323
344
|
|
|
324
345
|
# Check for wildcards
|
|
325
346
|
has_wildcard_actions = any("*" in action for action in actions)
|
|
@@ -327,7 +348,7 @@ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
|
|
|
327
348
|
|
|
328
349
|
return {
|
|
329
350
|
"version": policy.version,
|
|
330
|
-
"statement_count": len(
|
|
351
|
+
"statement_count": len(statements),
|
|
331
352
|
"allow_statements": allow_count,
|
|
332
353
|
"deny_statements": deny_count,
|
|
333
354
|
"action_count": len(actions),
|
|
@@ -396,6 +417,8 @@ def is_resource_policy(policy: IAMPolicy) -> bool:
|
|
|
396
417
|
>>> if is_resource_policy(policy):
|
|
397
418
|
... print("This is an S3 bucket policy or similar")
|
|
398
419
|
"""
|
|
420
|
+
if policy.statement is None:
|
|
421
|
+
return False
|
|
399
422
|
return any(stmt.principal is not None for stmt in policy.statement)
|
|
400
423
|
|
|
401
424
|
|
|
@@ -414,6 +437,9 @@ def has_public_access(policy: IAMPolicy) -> bool:
|
|
|
414
437
|
>>> if has_public_access(policy):
|
|
415
438
|
... print("WARNING: This policy allows public access!")
|
|
416
439
|
"""
|
|
440
|
+
if policy.statement is None:
|
|
441
|
+
return False
|
|
442
|
+
|
|
417
443
|
for stmt in policy.statement:
|
|
418
444
|
if stmt.principal == "*":
|
|
419
445
|
return True
|
|
File without changes
|
{iam_policy_validator-1.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.9.0.dist-info → iam_policy_validator-1.10.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|