iam-policy-validator 1.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -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 +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -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 +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -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 +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,830 @@
|
|
|
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
|
+
# Use offline mode with pre-downloaded AWS service definitions
|
|
48
|
+
iam-validator validate --path ./policies/ --aws-services-dir ./aws_services
|
|
49
|
+
|
|
50
|
+
# Generate JSON output
|
|
51
|
+
iam-validator validate --path ./policies/ --format json --output report.json
|
|
52
|
+
|
|
53
|
+
# Validate resource policies (S3 bucket policies, SNS topics, etc.)
|
|
54
|
+
iam-validator validate --path ./bucket-policies/ --policy-type RESOURCE_POLICY
|
|
55
|
+
|
|
56
|
+
# GitHub integration - all options (PR comment + review comments + job summary)
|
|
57
|
+
iam-validator validate --path ./policies/ --github-comment --github-review --github-summary
|
|
58
|
+
|
|
59
|
+
# Only line-specific review comments (clean, minimal)
|
|
60
|
+
iam-validator validate --path ./policies/ --github-review
|
|
61
|
+
|
|
62
|
+
# Only PR summary comment
|
|
63
|
+
iam-validator validate --path ./policies/ --github-comment
|
|
64
|
+
|
|
65
|
+
# Only GitHub Actions job summary
|
|
66
|
+
iam-validator validate --path ./policies/ --github-summary
|
|
67
|
+
|
|
68
|
+
# CI mode: show enhanced output in logs, save JSON to file
|
|
69
|
+
iam-validator validate --path ./policies/ --ci --github-review
|
|
70
|
+
iam-validator validate --path ./policies/ --ci --ci-output results.json
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
74
|
+
"""Add validate command arguments."""
|
|
75
|
+
# Create mutually exclusive group for input sources
|
|
76
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
77
|
+
|
|
78
|
+
input_group.add_argument(
|
|
79
|
+
"--path",
|
|
80
|
+
"-p",
|
|
81
|
+
action="append",
|
|
82
|
+
dest="paths",
|
|
83
|
+
help="Path to IAM policy file or directory (can be specified multiple times)",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
input_group.add_argument(
|
|
87
|
+
"--stdin",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Read policy from stdin (JSON format)",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--format",
|
|
94
|
+
"-f",
|
|
95
|
+
choices=["console", "enhanced", "json", "markdown", "html", "csv", "sarif"],
|
|
96
|
+
default="console",
|
|
97
|
+
help="Output format (default: console). Use 'enhanced' for modern visual output with Rich library",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--output",
|
|
102
|
+
"-o",
|
|
103
|
+
help="Output file path (for json/markdown/html/csv/sarif formats)",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--no-recursive",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help="Don't recursively search directories",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--fail-on-warnings",
|
|
114
|
+
action="store_true",
|
|
115
|
+
help="Fail validation if warnings are found (default: only fail on errors)",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--policy-type",
|
|
120
|
+
"-t",
|
|
121
|
+
choices=[
|
|
122
|
+
"IDENTITY_POLICY",
|
|
123
|
+
"RESOURCE_POLICY",
|
|
124
|
+
"TRUST_POLICY",
|
|
125
|
+
"SERVICE_CONTROL_POLICY",
|
|
126
|
+
"RESOURCE_CONTROL_POLICY",
|
|
127
|
+
],
|
|
128
|
+
default="IDENTITY_POLICY",
|
|
129
|
+
help="Type of IAM policy being validated (default: IDENTITY_POLICY). "
|
|
130
|
+
"IDENTITY_POLICY: Attached to users/groups/roles | "
|
|
131
|
+
"RESOURCE_POLICY: S3/SNS/SQS policies | "
|
|
132
|
+
"TRUST_POLICY: Role assumption policies | "
|
|
133
|
+
"SERVICE_CONTROL_POLICY: AWS Orgs SCPs | "
|
|
134
|
+
"RESOURCE_CONTROL_POLICY: AWS Orgs RCPs",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--github-comment",
|
|
139
|
+
action="store_true",
|
|
140
|
+
help="Post summary comment to PR conversation",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--github-review",
|
|
145
|
+
action="store_true",
|
|
146
|
+
help="Create line-specific review comments on PR files",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--github-summary",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help="Write summary to GitHub Actions job summary (visible in Actions tab)",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--verbose",
|
|
157
|
+
"-v",
|
|
158
|
+
action="store_true",
|
|
159
|
+
help="Enable verbose logging",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--config",
|
|
164
|
+
"-c",
|
|
165
|
+
help="Path to configuration file (default: auto-discover iam-validator.yaml)",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--custom-checks-dir",
|
|
170
|
+
help="Path to directory containing custom checks for auto-discovery",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--aws-services-dir",
|
|
175
|
+
help="Path to directory containing pre-downloaded AWS service definitions "
|
|
176
|
+
"(enables offline mode, avoids API rate limiting). "
|
|
177
|
+
"Use 'iam-validator download-services' to create this directory.",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--stream",
|
|
182
|
+
action="store_true",
|
|
183
|
+
help="Process files one-by-one (memory efficient, progressive feedback)",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--batch-size",
|
|
188
|
+
type=int,
|
|
189
|
+
default=10,
|
|
190
|
+
help="Number of policies to process per batch (default: 10, only with --stream)",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"--summary",
|
|
195
|
+
action="store_true",
|
|
196
|
+
help="Show Executive Summary section in enhanced format output",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--severity-breakdown",
|
|
201
|
+
action="store_true",
|
|
202
|
+
help="Show Issue Severity Breakdown section in enhanced format output",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--allow-owner-ignore",
|
|
207
|
+
action="store_true",
|
|
208
|
+
default=True,
|
|
209
|
+
help="Allow CODEOWNERS to ignore findings by replying 'ignore' to review comments (default: enabled)",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--no-owner-ignore",
|
|
214
|
+
action="store_true",
|
|
215
|
+
help="Disable CODEOWNERS ignore feature",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--ci",
|
|
220
|
+
action="store_true",
|
|
221
|
+
help="CI mode: print enhanced console output for visibility in job logs, "
|
|
222
|
+
"and write JSON report to file (use --ci-output to specify filename, "
|
|
223
|
+
"defaults to 'validation-report.json').",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"--ci-output",
|
|
228
|
+
default="validation-report.json",
|
|
229
|
+
help="Output file for JSON report in CI mode (default: validation-report.json)",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
233
|
+
"""Execute the validate command."""
|
|
234
|
+
# Check if streaming mode is enabled
|
|
235
|
+
use_stream = getattr(args, "stream", False)
|
|
236
|
+
|
|
237
|
+
# Auto-enable streaming for CI environments or large policy sets
|
|
238
|
+
# to provide progressive feedback
|
|
239
|
+
if not use_stream and os.getenv("CI"):
|
|
240
|
+
logging.info(
|
|
241
|
+
"CI environment detected, enabling streaming mode for progressive feedback"
|
|
242
|
+
)
|
|
243
|
+
use_stream = True
|
|
244
|
+
|
|
245
|
+
if use_stream:
|
|
246
|
+
return await self._execute_streaming(args)
|
|
247
|
+
else:
|
|
248
|
+
return await self._execute_batch(args)
|
|
249
|
+
|
|
250
|
+
async def _execute_batch(self, args: argparse.Namespace) -> int:
|
|
251
|
+
"""Execute validation by loading all policies at once (original behavior)."""
|
|
252
|
+
# Load policies from all specified paths or stdin
|
|
253
|
+
loader = PolicyLoader()
|
|
254
|
+
|
|
255
|
+
if args.stdin:
|
|
256
|
+
# Read from stdin
|
|
257
|
+
import json
|
|
258
|
+
import sys
|
|
259
|
+
|
|
260
|
+
stdin_content = sys.stdin.read()
|
|
261
|
+
if not stdin_content.strip():
|
|
262
|
+
logging.error("No policy data provided on stdin")
|
|
263
|
+
return 1
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
policy_data = json.loads(stdin_content)
|
|
267
|
+
# Create a synthetic policy entry
|
|
268
|
+
policies = [("stdin", policy_data)]
|
|
269
|
+
logging.info("Loaded policy from stdin")
|
|
270
|
+
except json.JSONDecodeError as e:
|
|
271
|
+
logging.error(f"Invalid JSON from stdin: {e}")
|
|
272
|
+
return 1
|
|
273
|
+
else:
|
|
274
|
+
# Load from paths
|
|
275
|
+
policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
|
|
276
|
+
|
|
277
|
+
if not policies:
|
|
278
|
+
logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
|
|
279
|
+
return 1
|
|
280
|
+
|
|
281
|
+
logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
|
|
282
|
+
|
|
283
|
+
# Validate policies
|
|
284
|
+
config_path = getattr(args, "config", None)
|
|
285
|
+
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
286
|
+
aws_services_dir = getattr(args, "aws_services_dir", None)
|
|
287
|
+
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
288
|
+
results = await validate_policies(
|
|
289
|
+
policies,
|
|
290
|
+
config_path=config_path,
|
|
291
|
+
custom_checks_dir=custom_checks_dir,
|
|
292
|
+
policy_type=policy_type,
|
|
293
|
+
aws_services_dir=aws_services_dir,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Generate report (include parsing errors if any)
|
|
297
|
+
generator = ReportGenerator()
|
|
298
|
+
report = generator.generate_report(results, parsing_errors=loader.parsing_errors)
|
|
299
|
+
|
|
300
|
+
# Handle --ci flag: show enhanced output in console, write JSON to file
|
|
301
|
+
ci_mode = getattr(args, "ci", False)
|
|
302
|
+
if ci_mode:
|
|
303
|
+
# CI mode: enhanced output to console, JSON to file
|
|
304
|
+
self._print_ci_console_output(report, generator)
|
|
305
|
+
ci_output_file = getattr(args, "ci_output", "validation-report.json")
|
|
306
|
+
generator.save_json_report(report, ci_output_file)
|
|
307
|
+
logging.info(f"Saved JSON report to {ci_output_file}")
|
|
308
|
+
elif args.format is None:
|
|
309
|
+
# Default: use classic console output (direct Rich printing)
|
|
310
|
+
generator.print_console_report(report)
|
|
311
|
+
elif args.format == "json":
|
|
312
|
+
if args.output:
|
|
313
|
+
generator.save_json_report(report, args.output)
|
|
314
|
+
else:
|
|
315
|
+
print(generator.generate_json_report(report))
|
|
316
|
+
elif args.format == "markdown":
|
|
317
|
+
if args.output:
|
|
318
|
+
generator.save_markdown_report(report, args.output)
|
|
319
|
+
else:
|
|
320
|
+
print(generator.generate_github_comment(report))
|
|
321
|
+
else:
|
|
322
|
+
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
323
|
+
# Pass options for enhanced format
|
|
324
|
+
format_options = {}
|
|
325
|
+
if args.format == "enhanced":
|
|
326
|
+
format_options["show_summary"] = getattr(args, "summary", False)
|
|
327
|
+
format_options["show_severity_breakdown"] = getattr(
|
|
328
|
+
args, "severity_breakdown", False
|
|
329
|
+
)
|
|
330
|
+
output_content = generator.format_report(report, args.format, **format_options)
|
|
331
|
+
if args.output:
|
|
332
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
333
|
+
f.write(output_content)
|
|
334
|
+
logging.info(f"Saved {args.format.upper()} report to {args.output}")
|
|
335
|
+
else:
|
|
336
|
+
print(output_content)
|
|
337
|
+
|
|
338
|
+
# Post to GitHub if configured
|
|
339
|
+
if args.github_comment or getattr(args, "github_review", False):
|
|
340
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
341
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
342
|
+
|
|
343
|
+
# Load config to get fail_on_severity, severity_labels, and ignore settings
|
|
344
|
+
config = ConfigLoader.load_config(config_path)
|
|
345
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
346
|
+
severity_labels = config.get_setting("severity_labels", {})
|
|
347
|
+
|
|
348
|
+
# Get ignore settings from config, but CLI flag can override
|
|
349
|
+
ignore_settings = config.get_setting("ignore_settings", {})
|
|
350
|
+
enable_ignore = ignore_settings.get("enabled", True)
|
|
351
|
+
# CLI --no-owner-ignore takes precedence
|
|
352
|
+
if getattr(args, "no_owner_ignore", False):
|
|
353
|
+
enable_ignore = False
|
|
354
|
+
allowed_users = ignore_settings.get("allowed_users", [])
|
|
355
|
+
|
|
356
|
+
async with GitHubIntegration() as github:
|
|
357
|
+
commenter = PRCommenter(
|
|
358
|
+
github,
|
|
359
|
+
fail_on_severities=fail_on_severities,
|
|
360
|
+
severity_labels=severity_labels,
|
|
361
|
+
enable_codeowners_ignore=enable_ignore,
|
|
362
|
+
allowed_ignore_users=allowed_users,
|
|
363
|
+
)
|
|
364
|
+
success = await commenter.post_findings_to_pr(
|
|
365
|
+
report,
|
|
366
|
+
create_review=getattr(args, "github_review", False),
|
|
367
|
+
add_summary_comment=args.github_comment,
|
|
368
|
+
)
|
|
369
|
+
if not success:
|
|
370
|
+
logging.error("Failed to post to GitHub PR")
|
|
371
|
+
|
|
372
|
+
# Write to GitHub Actions job summary if configured
|
|
373
|
+
if getattr(args, "github_summary", False):
|
|
374
|
+
self._write_github_actions_summary(report)
|
|
375
|
+
|
|
376
|
+
# Return exit code based on validation results
|
|
377
|
+
if args.fail_on_warnings:
|
|
378
|
+
return 0 if report.total_issues == 0 else 1
|
|
379
|
+
else:
|
|
380
|
+
return 0 if report.invalid_policies == 0 else 1
|
|
381
|
+
|
|
382
|
+
async def _execute_streaming(self, args: argparse.Namespace) -> int:
|
|
383
|
+
"""Execute validation by streaming policies one-by-one.
|
|
384
|
+
|
|
385
|
+
This provides:
|
|
386
|
+
- Lower memory usage
|
|
387
|
+
- Progressive feedback (see results as they come)
|
|
388
|
+
- Partial results if errors occur
|
|
389
|
+
- Better for CI/CD pipelines
|
|
390
|
+
"""
|
|
391
|
+
loader = PolicyLoader()
|
|
392
|
+
generator = ReportGenerator()
|
|
393
|
+
config_path = getattr(args, "config", None)
|
|
394
|
+
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
395
|
+
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
396
|
+
|
|
397
|
+
all_results = []
|
|
398
|
+
total_processed = 0
|
|
399
|
+
# Track all validated files across the streaming session for final cleanup
|
|
400
|
+
all_validated_files: set[str] = set()
|
|
401
|
+
|
|
402
|
+
logging.info(f"Starting streaming validation from {len(args.paths)} path(s)")
|
|
403
|
+
|
|
404
|
+
# Process policies one at a time
|
|
405
|
+
for file_path, policy in loader.stream_from_paths(
|
|
406
|
+
args.paths, recursive=not args.no_recursive
|
|
407
|
+
):
|
|
408
|
+
total_processed += 1
|
|
409
|
+
logging.info(f"[{total_processed}] Processing: {file_path}")
|
|
410
|
+
|
|
411
|
+
# Validate single policy
|
|
412
|
+
results = await validate_policies(
|
|
413
|
+
[(file_path, policy)],
|
|
414
|
+
config_path=config_path,
|
|
415
|
+
custom_checks_dir=custom_checks_dir,
|
|
416
|
+
policy_type=policy_type,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if results:
|
|
420
|
+
result = results[0]
|
|
421
|
+
all_results.append(result)
|
|
422
|
+
|
|
423
|
+
# Track validated file (convert to relative path for cleanup)
|
|
424
|
+
relative_path = self._make_relative_path(file_path)
|
|
425
|
+
if relative_path:
|
|
426
|
+
all_validated_files.add(relative_path)
|
|
427
|
+
|
|
428
|
+
# Print immediate feedback for this file
|
|
429
|
+
if args.format == "console":
|
|
430
|
+
if result.is_valid:
|
|
431
|
+
logging.info(f" â {file_path}: Valid")
|
|
432
|
+
else:
|
|
433
|
+
logging.warning(f" â {file_path}: {len(result.issues)} issue(s) found")
|
|
434
|
+
# Note: validation_success tracks overall status
|
|
435
|
+
|
|
436
|
+
# Post to GitHub immediately for this file (progressive PR comments)
|
|
437
|
+
# skip_cleanup=True because we process files one at a time and don't want
|
|
438
|
+
# to delete comments from files processed earlier. Cleanup runs at the end.
|
|
439
|
+
if getattr(args, "github_review", False):
|
|
440
|
+
await self._post_file_review(result, args)
|
|
441
|
+
|
|
442
|
+
if total_processed == 0:
|
|
443
|
+
logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
|
|
444
|
+
return 1
|
|
445
|
+
|
|
446
|
+
logging.info(f"\nCompleted validation of {total_processed} policies")
|
|
447
|
+
|
|
448
|
+
# Run final cleanup after all files are processed
|
|
449
|
+
# This uses the full report to know all current findings and deletes stale comments
|
|
450
|
+
if getattr(args, "github_review", False):
|
|
451
|
+
await self._run_final_review_cleanup(args, all_results, all_validated_files)
|
|
452
|
+
|
|
453
|
+
# Generate final summary report
|
|
454
|
+
report = generator.generate_report(all_results)
|
|
455
|
+
|
|
456
|
+
# Handle --ci flag: show enhanced output in console, write JSON to file
|
|
457
|
+
ci_mode = getattr(args, "ci", False)
|
|
458
|
+
if ci_mode:
|
|
459
|
+
# CI mode: enhanced output to console, JSON to file
|
|
460
|
+
self._print_ci_console_output(report, generator)
|
|
461
|
+
ci_output_file = getattr(args, "ci_output", "validation-report.json")
|
|
462
|
+
generator.save_json_report(report, ci_output_file)
|
|
463
|
+
logging.info(f"Saved JSON report to {ci_output_file}")
|
|
464
|
+
elif args.format == "console":
|
|
465
|
+
# Classic console output (direct Rich printing from report.py)
|
|
466
|
+
generator.print_console_report(report)
|
|
467
|
+
elif args.format == "json":
|
|
468
|
+
if args.output:
|
|
469
|
+
generator.save_json_report(report, args.output)
|
|
470
|
+
else:
|
|
471
|
+
print(generator.generate_json_report(report))
|
|
472
|
+
elif args.format == "markdown":
|
|
473
|
+
if args.output:
|
|
474
|
+
generator.save_markdown_report(report, args.output)
|
|
475
|
+
else:
|
|
476
|
+
print(generator.generate_github_comment(report))
|
|
477
|
+
else:
|
|
478
|
+
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
479
|
+
# Pass options for enhanced format
|
|
480
|
+
format_options = {}
|
|
481
|
+
if args.format == "enhanced":
|
|
482
|
+
format_options["show_summary"] = getattr(args, "summary", False)
|
|
483
|
+
format_options["show_severity_breakdown"] = getattr(
|
|
484
|
+
args, "severity_breakdown", False
|
|
485
|
+
)
|
|
486
|
+
output_content = generator.format_report(report, args.format, **format_options)
|
|
487
|
+
if args.output:
|
|
488
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
489
|
+
f.write(output_content)
|
|
490
|
+
logging.info(f"Saved {args.format.upper()} report to {args.output}")
|
|
491
|
+
else:
|
|
492
|
+
print(output_content)
|
|
493
|
+
|
|
494
|
+
# Post summary comment to GitHub (if requested and not already posted per-file reviews)
|
|
495
|
+
if args.github_comment:
|
|
496
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
497
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
498
|
+
|
|
499
|
+
# Load config to get fail_on_severity, severity_labels, and ignore settings
|
|
500
|
+
config = ConfigLoader.load_config(config_path)
|
|
501
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
502
|
+
severity_labels = config.get_setting("severity_labels", {})
|
|
503
|
+
|
|
504
|
+
# Get ignore settings from config, but CLI flag can override
|
|
505
|
+
ignore_settings = config.get_setting("ignore_settings", {})
|
|
506
|
+
enable_ignore = ignore_settings.get("enabled", True)
|
|
507
|
+
# CLI --no-owner-ignore takes precedence
|
|
508
|
+
if getattr(args, "no_owner_ignore", False):
|
|
509
|
+
enable_ignore = False
|
|
510
|
+
allowed_users = ignore_settings.get("allowed_users", [])
|
|
511
|
+
|
|
512
|
+
async with GitHubIntegration() as github:
|
|
513
|
+
commenter = PRCommenter(
|
|
514
|
+
github,
|
|
515
|
+
fail_on_severities=fail_on_severities,
|
|
516
|
+
severity_labels=severity_labels,
|
|
517
|
+
enable_codeowners_ignore=enable_ignore,
|
|
518
|
+
allowed_ignore_users=allowed_users,
|
|
519
|
+
)
|
|
520
|
+
success = await commenter.post_findings_to_pr(
|
|
521
|
+
report,
|
|
522
|
+
create_review=False, # Already posted per-file reviews in streaming mode
|
|
523
|
+
add_summary_comment=True,
|
|
524
|
+
)
|
|
525
|
+
if not success:
|
|
526
|
+
logging.error("Failed to post summary to GitHub PR")
|
|
527
|
+
|
|
528
|
+
# Write to GitHub Actions job summary if configured
|
|
529
|
+
if getattr(args, "github_summary", False):
|
|
530
|
+
self._write_github_actions_summary(report)
|
|
531
|
+
|
|
532
|
+
# Return exit code based on validation results
|
|
533
|
+
if args.fail_on_warnings:
|
|
534
|
+
return 0 if report.total_issues == 0 else 1
|
|
535
|
+
else:
|
|
536
|
+
return 0 if report.invalid_policies == 0 else 1
|
|
537
|
+
|
|
538
|
+
async def _cleanup_old_comments(self, args: argparse.Namespace) -> None:
|
|
539
|
+
"""Clean up old bot review comments from previous validation runs.
|
|
540
|
+
|
|
541
|
+
Note: This method is kept for backward compatibility but cleanup is now handled
|
|
542
|
+
automatically by update_or_create_review_comments(). It will update existing
|
|
543
|
+
comments, create new ones, and delete resolved ones smartly.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
args: Command-line arguments (kept for compatibility)
|
|
547
|
+
"""
|
|
548
|
+
# Cleanup is now handled automatically by update_or_create_review_comments()
|
|
549
|
+
# No action needed here
|
|
550
|
+
logging.debug("Comment cleanup will be handled automatically during review posting")
|
|
551
|
+
|
|
552
|
+
async def _post_file_review(self, result, args: argparse.Namespace) -> None:
|
|
553
|
+
"""Post review comments for a single file immediately.
|
|
554
|
+
|
|
555
|
+
This provides progressive feedback in PRs as files are processed.
|
|
556
|
+
"""
|
|
557
|
+
try:
|
|
558
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
559
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
560
|
+
|
|
561
|
+
async with GitHubIntegration() as github:
|
|
562
|
+
if not github.is_configured():
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
# Load config to get fail_on_severity and ignore settings
|
|
566
|
+
config_path = getattr(args, "config", None)
|
|
567
|
+
config = ConfigLoader.load_config(config_path)
|
|
568
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
569
|
+
|
|
570
|
+
# Get ignore settings from config, but CLI flag can override
|
|
571
|
+
ignore_settings = config.get_setting("ignore_settings", {})
|
|
572
|
+
enable_ignore = ignore_settings.get("enabled", True)
|
|
573
|
+
# CLI --no-owner-ignore takes precedence
|
|
574
|
+
if getattr(args, "no_owner_ignore", False):
|
|
575
|
+
enable_ignore = False
|
|
576
|
+
allowed_users = ignore_settings.get("allowed_users", [])
|
|
577
|
+
|
|
578
|
+
# In streaming mode, don't cleanup comments (we want to keep earlier files)
|
|
579
|
+
# Cleanup will happen once at the end
|
|
580
|
+
commenter = PRCommenter(
|
|
581
|
+
github,
|
|
582
|
+
cleanup_old_comments=False,
|
|
583
|
+
fail_on_severities=fail_on_severities,
|
|
584
|
+
enable_codeowners_ignore=enable_ignore,
|
|
585
|
+
allowed_ignore_users=allowed_users,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Create a mini-report for just this file
|
|
589
|
+
generator = ReportGenerator()
|
|
590
|
+
mini_report = generator.generate_report([result])
|
|
591
|
+
|
|
592
|
+
# Post line-specific comments (skip cleanup - runs at end of streaming)
|
|
593
|
+
await commenter.post_findings_to_pr(
|
|
594
|
+
mini_report,
|
|
595
|
+
create_review=True,
|
|
596
|
+
add_summary_comment=False, # Summary comes later
|
|
597
|
+
)
|
|
598
|
+
except Exception as e:
|
|
599
|
+
logging.warning(f"Failed to post review for {result.policy_file}: {e}")
|
|
600
|
+
|
|
601
|
+
def _make_relative_path(self, file_path: str) -> str | None:
|
|
602
|
+
"""Convert absolute path to relative path for GitHub.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
file_path: Absolute or relative path to file
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Relative path from repository root, or None if cannot be determined
|
|
609
|
+
"""
|
|
610
|
+
from pathlib import Path
|
|
611
|
+
|
|
612
|
+
# If already relative, use as-is
|
|
613
|
+
if not os.path.isabs(file_path):
|
|
614
|
+
return file_path
|
|
615
|
+
|
|
616
|
+
# Try to get workspace path from environment
|
|
617
|
+
workspace = os.getenv("GITHUB_WORKSPACE")
|
|
618
|
+
if workspace:
|
|
619
|
+
try:
|
|
620
|
+
abs_file_path = Path(file_path).resolve()
|
|
621
|
+
workspace_path = Path(workspace).resolve()
|
|
622
|
+
|
|
623
|
+
if abs_file_path.is_relative_to(workspace_path):
|
|
624
|
+
relative = abs_file_path.relative_to(workspace_path)
|
|
625
|
+
return str(relative).replace("\\", "/")
|
|
626
|
+
except (ValueError, OSError) as exc:
|
|
627
|
+
logging.debug(f"Could not make path relative to GitHub workspace: {exc}")
|
|
628
|
+
|
|
629
|
+
# Fallback: try current working directory
|
|
630
|
+
try:
|
|
631
|
+
cwd = Path.cwd()
|
|
632
|
+
abs_file_path = Path(file_path).resolve()
|
|
633
|
+
if abs_file_path.is_relative_to(cwd):
|
|
634
|
+
relative = abs_file_path.relative_to(cwd)
|
|
635
|
+
return str(relative).replace("\\", "/")
|
|
636
|
+
except (ValueError, OSError) as exc:
|
|
637
|
+
logging.debug(f"Could not make path relative to cwd: {exc}")
|
|
638
|
+
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
async def _run_final_review_cleanup(
|
|
642
|
+
self,
|
|
643
|
+
args: argparse.Namespace,
|
|
644
|
+
all_results: list,
|
|
645
|
+
all_validated_files: set[str],
|
|
646
|
+
) -> None:
|
|
647
|
+
"""Run final cleanup after all files are processed in streaming mode.
|
|
648
|
+
|
|
649
|
+
This deletes stale comments for findings that are no longer present,
|
|
650
|
+
using the complete set of validated files and current findings.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
args: Command-line arguments
|
|
654
|
+
all_results: All validation results from the streaming session
|
|
655
|
+
all_validated_files: Set of all validated file paths (relative)
|
|
656
|
+
"""
|
|
657
|
+
try:
|
|
658
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
659
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
660
|
+
|
|
661
|
+
async with GitHubIntegration() as github:
|
|
662
|
+
if not github.is_configured():
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
# Load config
|
|
666
|
+
config_path = getattr(args, "config", None)
|
|
667
|
+
config = ConfigLoader.load_config(config_path)
|
|
668
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
669
|
+
|
|
670
|
+
# Get ignore settings
|
|
671
|
+
ignore_settings = config.get_setting("ignore_settings", {})
|
|
672
|
+
enable_ignore = ignore_settings.get("enabled", True)
|
|
673
|
+
if getattr(args, "no_owner_ignore", False):
|
|
674
|
+
enable_ignore = False
|
|
675
|
+
allowed_users = ignore_settings.get("allowed_users", [])
|
|
676
|
+
|
|
677
|
+
# Create commenter WITH cleanup enabled for the final pass
|
|
678
|
+
commenter = PRCommenter(
|
|
679
|
+
github,
|
|
680
|
+
cleanup_old_comments=True, # Enable cleanup for final pass
|
|
681
|
+
fail_on_severities=fail_on_severities,
|
|
682
|
+
enable_codeowners_ignore=enable_ignore,
|
|
683
|
+
allowed_ignore_users=allowed_users,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Create a full report with all results
|
|
687
|
+
generator = ReportGenerator()
|
|
688
|
+
full_report = generator.generate_report(all_results)
|
|
689
|
+
|
|
690
|
+
# Post with create_review=True to run the full update/create/delete logic
|
|
691
|
+
# but pass all_validated_files so cleanup knows the full scope
|
|
692
|
+
logging.info("Running final comment cleanup...")
|
|
693
|
+
await commenter.post_findings_to_pr(
|
|
694
|
+
full_report,
|
|
695
|
+
create_review=True,
|
|
696
|
+
add_summary_comment=False,
|
|
697
|
+
manage_labels=False, # Labels are managed separately
|
|
698
|
+
process_ignores=False, # Already processed per-file
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
except Exception as e:
|
|
702
|
+
logging.warning(f"Failed to run final review cleanup: {e}")
|
|
703
|
+
|
|
704
|
+
def _write_github_actions_summary(self, report: ValidationReport) -> None:
|
|
705
|
+
"""Write a high-level summary to GitHub Actions job summary.
|
|
706
|
+
|
|
707
|
+
This appears in the Actions tab and provides a quick overview without all details.
|
|
708
|
+
Uses GITHUB_STEP_SUMMARY environment variable.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
report: Validation report to summarize
|
|
712
|
+
"""
|
|
713
|
+
summary_file = os.getenv("GITHUB_STEP_SUMMARY")
|
|
714
|
+
if not summary_file:
|
|
715
|
+
logging.warning(
|
|
716
|
+
"--github-summary specified but GITHUB_STEP_SUMMARY env var not found. "
|
|
717
|
+
"This feature only works in GitHub Actions."
|
|
718
|
+
)
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
# Generate high-level summary (no detailed issue list)
|
|
723
|
+
summary_parts = []
|
|
724
|
+
|
|
725
|
+
# Header with status
|
|
726
|
+
if report.total_issues == 0:
|
|
727
|
+
summary_parts.append("# â
IAM Policy Validation - Passed")
|
|
728
|
+
elif report.invalid_policies > 0:
|
|
729
|
+
summary_parts.append("# â IAM Policy Validation - Failed")
|
|
730
|
+
else:
|
|
731
|
+
summary_parts.append("# â ī¸ IAM Policy Validation - Security Issues Found")
|
|
732
|
+
|
|
733
|
+
summary_parts.append("")
|
|
734
|
+
|
|
735
|
+
# Summary table
|
|
736
|
+
summary_parts.append("## Summary")
|
|
737
|
+
summary_parts.append("")
|
|
738
|
+
summary_parts.append("| Metric | Count |")
|
|
739
|
+
summary_parts.append("|--------|-------|")
|
|
740
|
+
summary_parts.append(f"| Total Policies | {report.total_policies} |")
|
|
741
|
+
summary_parts.append(f"| Valid Policies | {report.valid_policies} |")
|
|
742
|
+
summary_parts.append(f"| Invalid Policies | {report.invalid_policies} |")
|
|
743
|
+
summary_parts.append(
|
|
744
|
+
f"| Policies with Security Issues | {report.policies_with_security_issues} |"
|
|
745
|
+
)
|
|
746
|
+
summary_parts.append(f"| **Total Issues** | **{report.total_issues}** |")
|
|
747
|
+
|
|
748
|
+
# Issue breakdown by severity if there are issues
|
|
749
|
+
if report.total_issues > 0:
|
|
750
|
+
summary_parts.append("")
|
|
751
|
+
summary_parts.append("## đ Issues by Severity")
|
|
752
|
+
summary_parts.append("")
|
|
753
|
+
|
|
754
|
+
# Count issues by severity
|
|
755
|
+
severity_counts: dict[str, int] = {}
|
|
756
|
+
for result in report.results:
|
|
757
|
+
for issue in result.issues:
|
|
758
|
+
severity_counts[issue.severity] = severity_counts.get(issue.severity, 0) + 1
|
|
759
|
+
|
|
760
|
+
# Sort by severity rank (highest first)
|
|
761
|
+
from iam_validator.core.models import ValidationIssue
|
|
762
|
+
|
|
763
|
+
sorted_severities = sorted(
|
|
764
|
+
severity_counts.items(),
|
|
765
|
+
key=lambda x: ValidationIssue.SEVERITY_RANK.get(x[0], 0),
|
|
766
|
+
reverse=True,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
summary_parts.append("| Severity | Count |")
|
|
770
|
+
summary_parts.append("|----------|-------|")
|
|
771
|
+
for severity, count in sorted_severities:
|
|
772
|
+
emoji = {
|
|
773
|
+
"error": "â",
|
|
774
|
+
"critical": "đ´",
|
|
775
|
+
"high": "đ ",
|
|
776
|
+
"warning": "â ī¸",
|
|
777
|
+
"medium": "đĄ",
|
|
778
|
+
"low": "đĩ",
|
|
779
|
+
"info": "âšī¸",
|
|
780
|
+
}.get(severity, "âĸ")
|
|
781
|
+
summary_parts.append(f"| {emoji} {severity.upper()} | {count} |")
|
|
782
|
+
|
|
783
|
+
# Add footer with links
|
|
784
|
+
summary_parts.append("")
|
|
785
|
+
summary_parts.append("---")
|
|
786
|
+
summary_parts.append("")
|
|
787
|
+
summary_parts.append(
|
|
788
|
+
"đ For detailed findings, check the PR comments or review the workflow logs."
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Write to summary file (append mode)
|
|
792
|
+
with open(summary_file, "a", encoding="utf-8") as f:
|
|
793
|
+
f.write("\n".join(summary_parts))
|
|
794
|
+
f.write("\n")
|
|
795
|
+
|
|
796
|
+
logging.info("Wrote summary to GitHub Actions job summary")
|
|
797
|
+
|
|
798
|
+
except Exception as e:
|
|
799
|
+
logging.warning(f"Failed to write GitHub Actions summary: {e}")
|
|
800
|
+
|
|
801
|
+
def _print_ci_console_output(
|
|
802
|
+
self, report: ValidationReport, generator: ReportGenerator
|
|
803
|
+
) -> None:
|
|
804
|
+
"""Print enhanced console output for CI visibility.
|
|
805
|
+
|
|
806
|
+
This shows validation results in the CI job logs in a human-readable format.
|
|
807
|
+
JSON output is written to a separate file (specified by --ci-output).
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
report: Validation report to print
|
|
811
|
+
generator: ReportGenerator instance
|
|
812
|
+
"""
|
|
813
|
+
# Generate enhanced format output with summary and severity breakdown
|
|
814
|
+
try:
|
|
815
|
+
enhanced_output = generator.format_report(
|
|
816
|
+
report,
|
|
817
|
+
"enhanced",
|
|
818
|
+
show_summary=True,
|
|
819
|
+
show_severity_breakdown=True,
|
|
820
|
+
)
|
|
821
|
+
print(enhanced_output)
|
|
822
|
+
|
|
823
|
+
except Exception as e:
|
|
824
|
+
# Fallback to basic summary if enhanced format fails
|
|
825
|
+
logging.warning(f"Failed to generate enhanced output: {e}")
|
|
826
|
+
print("\nValidation Summary:")
|
|
827
|
+
print(f" Total policies: {report.total_policies}")
|
|
828
|
+
print(f" Valid: {report.valid_policies}")
|
|
829
|
+
print(f" Invalid: {report.invalid_policies}")
|
|
830
|
+
print(f" Total issues: {report.total_issues}\n")
|