iam-policy-validator 1.7.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.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
- iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
- iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +43 -0
- iam_validator/checks/action_condition_enforcement.py +884 -0
- iam_validator/checks/action_resource_matching.py +441 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +92 -0
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +71 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/policy_size.py +147 -0
- iam_validator/checks/policy_type_validation.py +305 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/resource_validation.py +138 -0
- iam_validator/checks/sensitive_action.py +254 -0
- iam_validator/checks/service_wildcard.py +107 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/checks/wildcard_action.py +67 -0
- iam_validator/checks/wildcard_resource.py +135 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +531 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +600 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +940 -0
- iam_validator/core/check_registry.py +607 -0
- iam_validator/core/cli.py +134 -0
- 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/config_loader.py +472 -0
- 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/constants.py +74 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +440 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/models.py +327 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +424 -0
- iam_validator/core/report.py +872 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +815 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +382 -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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Post-to-PR command for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from iam_validator.commands.base import Command
|
|
6
|
+
from iam_validator.core.pr_commenter import post_report_to_pr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostToPRCommand(Command):
|
|
10
|
+
"""Command to post a validation report to a GitHub PR."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "post-to-pr"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def help(self) -> str:
|
|
18
|
+
return "Post a JSON report to a GitHub PR"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def epilog(self) -> str:
|
|
22
|
+
return """
|
|
23
|
+
Examples:
|
|
24
|
+
# Post report with line comments
|
|
25
|
+
iam-validator post-to-pr --report report.json
|
|
26
|
+
|
|
27
|
+
# Post only summary comment
|
|
28
|
+
iam-validator post-to-pr --report report.json --no-review
|
|
29
|
+
|
|
30
|
+
# Post only line comments (no summary)
|
|
31
|
+
iam-validator post-to-pr --report report.json --no-summary
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
35
|
+
"""Add post-to-pr command arguments."""
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--report",
|
|
38
|
+
"-r",
|
|
39
|
+
required=True,
|
|
40
|
+
help="Path to JSON report file",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--create-review",
|
|
45
|
+
action="store_true",
|
|
46
|
+
default=True,
|
|
47
|
+
help="Create line-specific review comments (default: True)",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--no-review",
|
|
52
|
+
action="store_false",
|
|
53
|
+
dest="create_review",
|
|
54
|
+
help="Don't create line-specific review comments",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--add-summary",
|
|
59
|
+
action="store_true",
|
|
60
|
+
default=True,
|
|
61
|
+
help="Add summary comment (default: True)",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--no-summary",
|
|
66
|
+
action="store_false",
|
|
67
|
+
dest="add_summary",
|
|
68
|
+
help="Don't add summary comment",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--config",
|
|
73
|
+
"-c",
|
|
74
|
+
help="Path to configuration file (for fail_on_severity setting)",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
78
|
+
"""Execute the post-to-pr command."""
|
|
79
|
+
success = await post_report_to_pr(
|
|
80
|
+
args.report,
|
|
81
|
+
create_review=args.create_review,
|
|
82
|
+
add_summary=args.add_summary,
|
|
83
|
+
config_path=getattr(args, "config", None),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return 0 if success else 1
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""Validate command for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from iam_validator.commands.base import Command
|
|
9
|
+
from iam_validator.core.models import PolicyType, ValidationReport
|
|
10
|
+
from iam_validator.core.policy_checks import validate_policies
|
|
11
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
12
|
+
from iam_validator.core.report import ReportGenerator
|
|
13
|
+
from iam_validator.integrations.github_integration import GitHubIntegration
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidateCommand(Command):
|
|
17
|
+
"""Command to validate IAM policies."""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "validate"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def help(self) -> str:
|
|
25
|
+
return "Validate IAM policies"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def epilog(self) -> str:
|
|
29
|
+
return """
|
|
30
|
+
Examples:
|
|
31
|
+
# Validate a single policy file
|
|
32
|
+
iam-validator validate --path policy.json
|
|
33
|
+
|
|
34
|
+
# Validate all policies in a directory
|
|
35
|
+
iam-validator validate --path ./policies/
|
|
36
|
+
|
|
37
|
+
# Validate multiple paths (files and directories)
|
|
38
|
+
iam-validator validate --path policy1.json --path ./policies/ --path ./more-policies/
|
|
39
|
+
|
|
40
|
+
# Read policy from stdin
|
|
41
|
+
cat policy.json | iam-validator validate --stdin
|
|
42
|
+
echo '{"Version":"2012-10-17","Statement":[...]}' | iam-validator validate --stdin
|
|
43
|
+
|
|
44
|
+
# Use custom checks from a directory
|
|
45
|
+
iam-validator validate --path ./policies/ --custom-checks-dir ./my-checks
|
|
46
|
+
|
|
47
|
+
# Generate JSON output
|
|
48
|
+
iam-validator validate --path ./policies/ --format json --output report.json
|
|
49
|
+
|
|
50
|
+
# Validate resource policies (S3 bucket policies, SNS topics, etc.)
|
|
51
|
+
iam-validator validate --path ./bucket-policies/ --policy-type RESOURCE_POLICY
|
|
52
|
+
|
|
53
|
+
# GitHub integration - all options (PR comment + review comments + job summary)
|
|
54
|
+
iam-validator validate --path ./policies/ --github-comment --github-review --github-summary
|
|
55
|
+
|
|
56
|
+
# Only line-specific review comments (clean, minimal)
|
|
57
|
+
iam-validator validate --path ./policies/ --github-review
|
|
58
|
+
|
|
59
|
+
# Only PR summary comment
|
|
60
|
+
iam-validator validate --path ./policies/ --github-comment
|
|
61
|
+
|
|
62
|
+
# Only GitHub Actions job summary
|
|
63
|
+
iam-validator validate --path ./policies/ --github-summary
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
67
|
+
"""Add validate command arguments."""
|
|
68
|
+
# Create mutually exclusive group for input sources
|
|
69
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
70
|
+
|
|
71
|
+
input_group.add_argument(
|
|
72
|
+
"--path",
|
|
73
|
+
"-p",
|
|
74
|
+
action="append",
|
|
75
|
+
dest="paths",
|
|
76
|
+
help="Path to IAM policy file or directory (can be specified multiple times)",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
input_group.add_argument(
|
|
80
|
+
"--stdin",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="Read policy from stdin (JSON format)",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--format",
|
|
87
|
+
"-f",
|
|
88
|
+
choices=["console", "enhanced", "json", "markdown", "html", "csv", "sarif"],
|
|
89
|
+
default="console",
|
|
90
|
+
help="Output format (default: console). Use 'enhanced' for modern visual output with Rich library",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--output",
|
|
95
|
+
"-o",
|
|
96
|
+
help="Output file path (for json/markdown/html/csv/sarif formats)",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--no-recursive",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Don't recursively search directories",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--fail-on-warnings",
|
|
107
|
+
action="store_true",
|
|
108
|
+
help="Fail validation if warnings are found (default: only fail on errors)",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--policy-type",
|
|
113
|
+
"-t",
|
|
114
|
+
choices=[
|
|
115
|
+
"IDENTITY_POLICY",
|
|
116
|
+
"RESOURCE_POLICY",
|
|
117
|
+
"SERVICE_CONTROL_POLICY",
|
|
118
|
+
"RESOURCE_CONTROL_POLICY",
|
|
119
|
+
],
|
|
120
|
+
default="IDENTITY_POLICY",
|
|
121
|
+
help="Type of IAM policy being validated (default: IDENTITY_POLICY). "
|
|
122
|
+
"Enables policy-type-specific validation (e.g., requiring Principal for resource policies, "
|
|
123
|
+
"strict RCP requirements for resource control policies)",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--github-comment",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Post summary comment to PR conversation",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--github-review",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="Create line-specific review comments on PR files",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--github-summary",
|
|
140
|
+
action="store_true",
|
|
141
|
+
help="Write summary to GitHub Actions job summary (visible in Actions tab)",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--verbose",
|
|
146
|
+
"-v",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="Enable verbose logging",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--config",
|
|
153
|
+
"-c",
|
|
154
|
+
help="Path to configuration file (default: auto-discover iam-validator.yaml)",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--custom-checks-dir",
|
|
159
|
+
help="Path to directory containing custom checks for auto-discovery",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--no-registry",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Use legacy validation (disable check registry system)",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--stream",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Process files one-by-one (memory efficient, progressive feedback)",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--batch-size",
|
|
176
|
+
type=int,
|
|
177
|
+
default=10,
|
|
178
|
+
help="Number of policies to process per batch (default: 10, only with --stream)",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--no-summary",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="Hide Executive Summary section in enhanced format output",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--no-severity-breakdown",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="Hide Issue Severity Breakdown section in enhanced format output",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
194
|
+
"""Execute the validate command."""
|
|
195
|
+
# Check if streaming mode is enabled
|
|
196
|
+
use_stream = getattr(args, "stream", False)
|
|
197
|
+
|
|
198
|
+
# Auto-enable streaming for CI environments or large policy sets
|
|
199
|
+
# to provide progressive feedback
|
|
200
|
+
if not use_stream and os.getenv("CI"):
|
|
201
|
+
logging.info(
|
|
202
|
+
"CI environment detected, enabling streaming mode for progressive feedback"
|
|
203
|
+
)
|
|
204
|
+
use_stream = True
|
|
205
|
+
|
|
206
|
+
if use_stream:
|
|
207
|
+
return await self._execute_streaming(args)
|
|
208
|
+
else:
|
|
209
|
+
return await self._execute_batch(args)
|
|
210
|
+
|
|
211
|
+
async def _execute_batch(self, args: argparse.Namespace) -> int:
|
|
212
|
+
"""Execute validation by loading all policies at once (original behavior)."""
|
|
213
|
+
# Load policies from all specified paths or stdin
|
|
214
|
+
loader = PolicyLoader()
|
|
215
|
+
|
|
216
|
+
if args.stdin:
|
|
217
|
+
# Read from stdin
|
|
218
|
+
import json
|
|
219
|
+
import sys
|
|
220
|
+
|
|
221
|
+
stdin_content = sys.stdin.read()
|
|
222
|
+
if not stdin_content.strip():
|
|
223
|
+
logging.error("No policy data provided on stdin")
|
|
224
|
+
return 1
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
policy_data = json.loads(stdin_content)
|
|
228
|
+
# Create a synthetic policy entry
|
|
229
|
+
policies = [("stdin", policy_data)]
|
|
230
|
+
logging.info("Loaded policy from stdin")
|
|
231
|
+
except json.JSONDecodeError as e:
|
|
232
|
+
logging.error(f"Invalid JSON from stdin: {e}")
|
|
233
|
+
return 1
|
|
234
|
+
else:
|
|
235
|
+
# Load from paths
|
|
236
|
+
policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
|
|
237
|
+
|
|
238
|
+
if not policies:
|
|
239
|
+
logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
|
|
243
|
+
|
|
244
|
+
# Validate policies
|
|
245
|
+
use_registry = not getattr(args, "no_registry", False)
|
|
246
|
+
config_path = getattr(args, "config", None)
|
|
247
|
+
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
248
|
+
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
249
|
+
results = await validate_policies(
|
|
250
|
+
policies,
|
|
251
|
+
config_path=config_path,
|
|
252
|
+
use_registry=use_registry,
|
|
253
|
+
custom_checks_dir=custom_checks_dir,
|
|
254
|
+
policy_type=policy_type,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Generate report
|
|
258
|
+
generator = ReportGenerator()
|
|
259
|
+
report = generator.generate_report(results)
|
|
260
|
+
|
|
261
|
+
# Output results
|
|
262
|
+
if args.format is None:
|
|
263
|
+
# Default: use classic console output (direct Rich printing)
|
|
264
|
+
generator.print_console_report(report)
|
|
265
|
+
elif args.format == "json":
|
|
266
|
+
if args.output:
|
|
267
|
+
generator.save_json_report(report, args.output)
|
|
268
|
+
else:
|
|
269
|
+
print(generator.generate_json_report(report))
|
|
270
|
+
elif args.format == "markdown":
|
|
271
|
+
if args.output:
|
|
272
|
+
generator.save_markdown_report(report, args.output)
|
|
273
|
+
else:
|
|
274
|
+
print(generator.generate_github_comment(report))
|
|
275
|
+
else:
|
|
276
|
+
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
277
|
+
# Pass options for enhanced format
|
|
278
|
+
format_options = {}
|
|
279
|
+
if args.format == "enhanced":
|
|
280
|
+
format_options["show_summary"] = not getattr(args, "no_summary", False)
|
|
281
|
+
format_options["show_severity_breakdown"] = not getattr(
|
|
282
|
+
args, "no_severity_breakdown", False
|
|
283
|
+
)
|
|
284
|
+
output_content = generator.format_report(report, args.format, **format_options)
|
|
285
|
+
if args.output:
|
|
286
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
287
|
+
f.write(output_content)
|
|
288
|
+
logging.info(f"Saved {args.format.upper()} report to {args.output}")
|
|
289
|
+
else:
|
|
290
|
+
print(output_content)
|
|
291
|
+
|
|
292
|
+
# Post to GitHub if configured
|
|
293
|
+
if args.github_comment or getattr(args, "github_review", False):
|
|
294
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
295
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
296
|
+
|
|
297
|
+
# Load config to get fail_on_severity setting
|
|
298
|
+
config = ConfigLoader.load_config(config_path)
|
|
299
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
300
|
+
|
|
301
|
+
async with GitHubIntegration() as github:
|
|
302
|
+
commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
|
|
303
|
+
success = await commenter.post_findings_to_pr(
|
|
304
|
+
report,
|
|
305
|
+
create_review=getattr(args, "github_review", False),
|
|
306
|
+
add_summary_comment=args.github_comment,
|
|
307
|
+
)
|
|
308
|
+
if not success:
|
|
309
|
+
logging.error("Failed to post to GitHub PR")
|
|
310
|
+
|
|
311
|
+
# Write to GitHub Actions job summary if configured
|
|
312
|
+
if getattr(args, "github_summary", False):
|
|
313
|
+
self._write_github_actions_summary(report)
|
|
314
|
+
|
|
315
|
+
# Return exit code based on validation results
|
|
316
|
+
if args.fail_on_warnings:
|
|
317
|
+
return 0 if report.total_issues == 0 else 1
|
|
318
|
+
else:
|
|
319
|
+
return 0 if report.invalid_policies == 0 else 1
|
|
320
|
+
|
|
321
|
+
async def _execute_streaming(self, args: argparse.Namespace) -> int:
|
|
322
|
+
"""Execute validation by streaming policies one-by-one.
|
|
323
|
+
|
|
324
|
+
This provides:
|
|
325
|
+
- Lower memory usage
|
|
326
|
+
- Progressive feedback (see results as they come)
|
|
327
|
+
- Partial results if errors occur
|
|
328
|
+
- Better for CI/CD pipelines
|
|
329
|
+
"""
|
|
330
|
+
loader = PolicyLoader()
|
|
331
|
+
generator = ReportGenerator()
|
|
332
|
+
use_registry = not getattr(args, "no_registry", False)
|
|
333
|
+
config_path = getattr(args, "config", None)
|
|
334
|
+
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
335
|
+
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
336
|
+
|
|
337
|
+
all_results = []
|
|
338
|
+
total_processed = 0
|
|
339
|
+
|
|
340
|
+
# Clean up old review comments at the start (before posting any new ones)
|
|
341
|
+
if getattr(args, "github_review", False):
|
|
342
|
+
await self._cleanup_old_comments()
|
|
343
|
+
|
|
344
|
+
logging.info(f"Starting streaming validation from {len(args.paths)} path(s)")
|
|
345
|
+
|
|
346
|
+
# Process policies one at a time
|
|
347
|
+
for file_path, policy in loader.stream_from_paths(
|
|
348
|
+
args.paths, recursive=not args.no_recursive
|
|
349
|
+
):
|
|
350
|
+
total_processed += 1
|
|
351
|
+
logging.info(f"[{total_processed}] Processing: {file_path}")
|
|
352
|
+
|
|
353
|
+
# Validate single policy
|
|
354
|
+
results = await validate_policies(
|
|
355
|
+
[(file_path, policy)],
|
|
356
|
+
config_path=config_path,
|
|
357
|
+
use_registry=use_registry,
|
|
358
|
+
custom_checks_dir=custom_checks_dir,
|
|
359
|
+
policy_type=policy_type,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if results:
|
|
363
|
+
result = results[0]
|
|
364
|
+
all_results.append(result)
|
|
365
|
+
|
|
366
|
+
# Print immediate feedback for this file
|
|
367
|
+
if args.format == "console":
|
|
368
|
+
if result.is_valid:
|
|
369
|
+
logging.info(f" â {file_path}: Valid")
|
|
370
|
+
else:
|
|
371
|
+
logging.warning(f" â {file_path}: {len(result.issues)} issue(s) found")
|
|
372
|
+
# Note: validation_success tracks overall status
|
|
373
|
+
|
|
374
|
+
# Post to GitHub immediately for this file (progressive PR comments)
|
|
375
|
+
if getattr(args, "github_review", False):
|
|
376
|
+
await self._post_file_review(result, args)
|
|
377
|
+
|
|
378
|
+
if total_processed == 0:
|
|
379
|
+
logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
logging.info(f"\nCompleted validation of {total_processed} policies")
|
|
383
|
+
|
|
384
|
+
# Generate final summary report
|
|
385
|
+
report = generator.generate_report(all_results)
|
|
386
|
+
|
|
387
|
+
# Output final results
|
|
388
|
+
if args.format == "console":
|
|
389
|
+
# Classic console output (direct Rich printing from report.py)
|
|
390
|
+
generator.print_console_report(report)
|
|
391
|
+
elif args.format == "json":
|
|
392
|
+
if args.output:
|
|
393
|
+
generator.save_json_report(report, args.output)
|
|
394
|
+
else:
|
|
395
|
+
print(generator.generate_json_report(report))
|
|
396
|
+
elif args.format == "markdown":
|
|
397
|
+
if args.output:
|
|
398
|
+
generator.save_markdown_report(report, args.output)
|
|
399
|
+
else:
|
|
400
|
+
print(generator.generate_github_comment(report))
|
|
401
|
+
else:
|
|
402
|
+
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
403
|
+
# Pass options for enhanced format
|
|
404
|
+
format_options = {}
|
|
405
|
+
if args.format == "enhanced":
|
|
406
|
+
format_options["show_summary"] = not getattr(args, "no_summary", False)
|
|
407
|
+
format_options["show_severity_breakdown"] = not getattr(
|
|
408
|
+
args, "no_severity_breakdown", False
|
|
409
|
+
)
|
|
410
|
+
output_content = generator.format_report(report, args.format, **format_options)
|
|
411
|
+
if args.output:
|
|
412
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
413
|
+
f.write(output_content)
|
|
414
|
+
logging.info(f"Saved {args.format.upper()} report to {args.output}")
|
|
415
|
+
else:
|
|
416
|
+
print(output_content)
|
|
417
|
+
|
|
418
|
+
# Post summary comment to GitHub (if requested and not already posted per-file reviews)
|
|
419
|
+
if args.github_comment:
|
|
420
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
421
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
422
|
+
|
|
423
|
+
# Load config to get fail_on_severity setting
|
|
424
|
+
config = ConfigLoader.load_config(config_path)
|
|
425
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
426
|
+
|
|
427
|
+
async with GitHubIntegration() as github:
|
|
428
|
+
commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
|
|
429
|
+
success = await commenter.post_findings_to_pr(
|
|
430
|
+
report,
|
|
431
|
+
create_review=False, # Already posted per-file reviews in streaming mode
|
|
432
|
+
add_summary_comment=True,
|
|
433
|
+
)
|
|
434
|
+
if not success:
|
|
435
|
+
logging.error("Failed to post summary to GitHub PR")
|
|
436
|
+
|
|
437
|
+
# Write to GitHub Actions job summary if configured
|
|
438
|
+
if getattr(args, "github_summary", False):
|
|
439
|
+
self._write_github_actions_summary(report)
|
|
440
|
+
|
|
441
|
+
# Return exit code based on validation results
|
|
442
|
+
if args.fail_on_warnings:
|
|
443
|
+
return 0 if report.total_issues == 0 else 1
|
|
444
|
+
else:
|
|
445
|
+
return 0 if report.invalid_policies == 0 else 1
|
|
446
|
+
|
|
447
|
+
async def _cleanup_old_comments(self) -> None:
|
|
448
|
+
"""Clean up old bot review comments from previous validation runs.
|
|
449
|
+
|
|
450
|
+
This ensures the PR stays clean without duplicate/stale comments.
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
454
|
+
|
|
455
|
+
async with GitHubIntegration() as github:
|
|
456
|
+
if not github.is_configured():
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
logging.info("Cleaning up old review comments from previous runs...")
|
|
460
|
+
deleted = await github.cleanup_bot_review_comments(PRCommenter.REVIEW_IDENTIFIER)
|
|
461
|
+
if deleted > 0:
|
|
462
|
+
logging.info(f"Removed {deleted} old comment(s)")
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logging.warning(f"Failed to cleanup old comments: {e}")
|
|
465
|
+
|
|
466
|
+
async def _post_file_review(self, result, args: argparse.Namespace) -> None:
|
|
467
|
+
"""Post review comments for a single file immediately.
|
|
468
|
+
|
|
469
|
+
This provides progressive feedback in PRs as files are processed.
|
|
470
|
+
"""
|
|
471
|
+
try:
|
|
472
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
473
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
474
|
+
|
|
475
|
+
async with GitHubIntegration() as github:
|
|
476
|
+
if not github.is_configured():
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Load config to get fail_on_severity setting
|
|
480
|
+
config_path = getattr(args, "config", None)
|
|
481
|
+
config = ConfigLoader.load_config(config_path)
|
|
482
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
483
|
+
|
|
484
|
+
# In streaming mode, don't cleanup comments (we want to keep earlier files)
|
|
485
|
+
# Cleanup will happen once at the end
|
|
486
|
+
commenter = PRCommenter(
|
|
487
|
+
github,
|
|
488
|
+
cleanup_old_comments=False,
|
|
489
|
+
fail_on_severities=fail_on_severities,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Create a mini-report for just this file
|
|
493
|
+
generator = ReportGenerator()
|
|
494
|
+
mini_report = generator.generate_report([result])
|
|
495
|
+
|
|
496
|
+
# Post line-specific comments
|
|
497
|
+
await commenter.post_findings_to_pr(
|
|
498
|
+
mini_report,
|
|
499
|
+
create_review=True,
|
|
500
|
+
add_summary_comment=False, # Summary comes later
|
|
501
|
+
)
|
|
502
|
+
except Exception as e:
|
|
503
|
+
logging.warning(f"Failed to post review for {result.policy_file}: {e}")
|
|
504
|
+
|
|
505
|
+
def _write_github_actions_summary(self, report: ValidationReport) -> None:
|
|
506
|
+
"""Write a high-level summary to GitHub Actions job summary.
|
|
507
|
+
|
|
508
|
+
This appears in the Actions tab and provides a quick overview without all details.
|
|
509
|
+
Uses GITHUB_STEP_SUMMARY environment variable.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
report: Validation report to summarize
|
|
513
|
+
"""
|
|
514
|
+
summary_file = os.getenv("GITHUB_STEP_SUMMARY")
|
|
515
|
+
if not summary_file:
|
|
516
|
+
logging.warning(
|
|
517
|
+
"--github-summary specified but GITHUB_STEP_SUMMARY env var not found. "
|
|
518
|
+
"This feature only works in GitHub Actions."
|
|
519
|
+
)
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
# Generate high-level summary (no detailed issue list)
|
|
524
|
+
summary_parts = []
|
|
525
|
+
|
|
526
|
+
# Header with status
|
|
527
|
+
if report.total_issues == 0:
|
|
528
|
+
summary_parts.append("# â
IAM Policy Validation - Passed")
|
|
529
|
+
elif report.invalid_policies > 0:
|
|
530
|
+
summary_parts.append("# â IAM Policy Validation - Failed")
|
|
531
|
+
else:
|
|
532
|
+
summary_parts.append("# â ī¸ IAM Policy Validation - Security Issues Found")
|
|
533
|
+
|
|
534
|
+
summary_parts.append("")
|
|
535
|
+
|
|
536
|
+
# Summary table
|
|
537
|
+
summary_parts.append("## Summary")
|
|
538
|
+
summary_parts.append("")
|
|
539
|
+
summary_parts.append("| Metric | Count |")
|
|
540
|
+
summary_parts.append("|--------|-------|")
|
|
541
|
+
summary_parts.append(f"| Total Policies | {report.total_policies} |")
|
|
542
|
+
summary_parts.append(f"| Valid Policies | {report.valid_policies} |")
|
|
543
|
+
summary_parts.append(f"| Invalid Policies | {report.invalid_policies} |")
|
|
544
|
+
summary_parts.append(
|
|
545
|
+
f"| Policies with Security Issues | {report.policies_with_security_issues} |"
|
|
546
|
+
)
|
|
547
|
+
summary_parts.append(f"| **Total Issues** | **{report.total_issues}** |")
|
|
548
|
+
|
|
549
|
+
# Issue breakdown by severity if there are issues
|
|
550
|
+
if report.total_issues > 0:
|
|
551
|
+
summary_parts.append("")
|
|
552
|
+
summary_parts.append("## đ Issues by Severity")
|
|
553
|
+
summary_parts.append("")
|
|
554
|
+
|
|
555
|
+
# Count issues by severity
|
|
556
|
+
severity_counts: dict[str, int] = {}
|
|
557
|
+
for result in report.results:
|
|
558
|
+
for issue in result.issues:
|
|
559
|
+
severity_counts[issue.severity] = severity_counts.get(issue.severity, 0) + 1
|
|
560
|
+
|
|
561
|
+
# Sort by severity rank (highest first)
|
|
562
|
+
from iam_validator.core.models import ValidationIssue
|
|
563
|
+
|
|
564
|
+
sorted_severities = sorted(
|
|
565
|
+
severity_counts.items(),
|
|
566
|
+
key=lambda x: ValidationIssue.SEVERITY_RANK.get(x[0], 0),
|
|
567
|
+
reverse=True,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
summary_parts.append("| Severity | Count |")
|
|
571
|
+
summary_parts.append("|----------|-------|")
|
|
572
|
+
for severity, count in sorted_severities:
|
|
573
|
+
emoji = {
|
|
574
|
+
"error": "â",
|
|
575
|
+
"critical": "đ´",
|
|
576
|
+
"high": "đ ",
|
|
577
|
+
"warning": "â ī¸",
|
|
578
|
+
"medium": "đĄ",
|
|
579
|
+
"low": "đĩ",
|
|
580
|
+
"info": "âšī¸",
|
|
581
|
+
}.get(severity, "âĸ")
|
|
582
|
+
summary_parts.append(f"| {emoji} {severity.upper()} | {count} |")
|
|
583
|
+
|
|
584
|
+
# Add footer with links
|
|
585
|
+
summary_parts.append("")
|
|
586
|
+
summary_parts.append("---")
|
|
587
|
+
summary_parts.append("")
|
|
588
|
+
summary_parts.append(
|
|
589
|
+
"đ For detailed findings, check the PR comments or review the workflow logs."
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Write to summary file (append mode)
|
|
593
|
+
with open(summary_file, "a", encoding="utf-8") as f:
|
|
594
|
+
f.write("\n".join(summary_parts))
|
|
595
|
+
f.write("\n")
|
|
596
|
+
|
|
597
|
+
logging.info("Wrote summary to GitHub Actions job summary")
|
|
598
|
+
|
|
599
|
+
except Exception as e:
|
|
600
|
+
logging.warning(f"Failed to write GitHub Actions summary: {e}")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Core validation modules."""
|
|
2
|
+
|
|
3
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
4
|
+
from iam_validator.core.policy_checks import PolicyValidator, validate_policies
|
|
5
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
6
|
+
from iam_validator.core.report import ReportGenerator
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AWSServiceFetcher",
|
|
10
|
+
"PolicyValidator",
|
|
11
|
+
"validate_policies",
|
|
12
|
+
"PolicyLoader",
|
|
13
|
+
"ReportGenerator",
|
|
14
|
+
]
|