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,171 @@
|
|
|
1
|
+
"""CSV formatter for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from iam_validator.core import constants
|
|
8
|
+
from iam_validator.core.formatters.base import OutputFormatter
|
|
9
|
+
from iam_validator.core.models import ValidationReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CSVFormatter(OutputFormatter):
|
|
13
|
+
"""Formats validation results as CSV for spreadsheet analysis."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def format_id(self) -> str:
|
|
17
|
+
return "csv"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def description(self) -> str:
|
|
21
|
+
return "CSV format for spreadsheet import and analysis"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def file_extension(self) -> str:
|
|
25
|
+
return "csv"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def content_type(self) -> str:
|
|
29
|
+
return "text/csv"
|
|
30
|
+
|
|
31
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
32
|
+
"""Format report as CSV.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
report: The validation report
|
|
36
|
+
**kwargs: Additional options like 'include_summary'
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
CSV string
|
|
40
|
+
"""
|
|
41
|
+
include_summary = kwargs.get("include_summary", True)
|
|
42
|
+
include_header = kwargs.get("include_header", True)
|
|
43
|
+
|
|
44
|
+
output = io.StringIO()
|
|
45
|
+
writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
|
|
46
|
+
|
|
47
|
+
if include_summary:
|
|
48
|
+
self._write_summary_section(writer, report)
|
|
49
|
+
writer.writerow([]) # Empty row separator
|
|
50
|
+
|
|
51
|
+
self._write_issues_section(writer, report, include_header)
|
|
52
|
+
|
|
53
|
+
return output.getvalue()
|
|
54
|
+
|
|
55
|
+
def _write_summary_section(self, writer: Any, report: ValidationReport) -> None:
|
|
56
|
+
"""Write summary statistics to CSV."""
|
|
57
|
+
# Count issues by severity - support both IAM validity and security severities
|
|
58
|
+
errors = sum(
|
|
59
|
+
1
|
|
60
|
+
for r in report.results
|
|
61
|
+
for i in r.issues
|
|
62
|
+
if i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
63
|
+
)
|
|
64
|
+
warnings = sum(
|
|
65
|
+
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
66
|
+
)
|
|
67
|
+
infos = sum(1 for r in report.results for i in r.issues if i.severity in ("info", "low"))
|
|
68
|
+
|
|
69
|
+
writer.writerow(["Summary Statistics"])
|
|
70
|
+
writer.writerow(["Metric", "Value"])
|
|
71
|
+
writer.writerow(["Total Policies", report.total_policies])
|
|
72
|
+
writer.writerow(["Valid Policies (IAM)", report.valid_policies])
|
|
73
|
+
writer.writerow(["Invalid Policies (IAM)", report.invalid_policies])
|
|
74
|
+
writer.writerow(["Policies with Security Findings", report.policies_with_security_issues])
|
|
75
|
+
writer.writerow(["Total Issues", report.total_issues])
|
|
76
|
+
writer.writerow(["Validity Issues", report.validity_issues])
|
|
77
|
+
writer.writerow(["Security Issues", report.security_issues])
|
|
78
|
+
writer.writerow([""])
|
|
79
|
+
writer.writerow(["Legacy Severity Breakdown"])
|
|
80
|
+
writer.writerow(["Errors", errors])
|
|
81
|
+
writer.writerow(["Warnings", warnings])
|
|
82
|
+
writer.writerow(["Info", infos])
|
|
83
|
+
|
|
84
|
+
def _write_issues_section(
|
|
85
|
+
self, writer: Any, report: ValidationReport, include_header: bool
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Write detailed issues to CSV."""
|
|
88
|
+
if include_header:
|
|
89
|
+
writer.writerow(
|
|
90
|
+
[
|
|
91
|
+
"Policy File",
|
|
92
|
+
"Statement Index",
|
|
93
|
+
"Statement SID",
|
|
94
|
+
"Line Number",
|
|
95
|
+
"Severity",
|
|
96
|
+
"Issue Type",
|
|
97
|
+
"Action",
|
|
98
|
+
"Resource",
|
|
99
|
+
"Condition Key",
|
|
100
|
+
"Message",
|
|
101
|
+
"Suggestion",
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
for policy_result in report.results:
|
|
106
|
+
for issue in policy_result.issues:
|
|
107
|
+
writer.writerow(
|
|
108
|
+
[
|
|
109
|
+
policy_result.policy_file,
|
|
110
|
+
(issue.statement_index + 1 if issue.statement_index is not None else ""),
|
|
111
|
+
issue.statement_sid or "",
|
|
112
|
+
issue.line_number or "",
|
|
113
|
+
issue.severity,
|
|
114
|
+
issue.issue_type or "",
|
|
115
|
+
issue.action or "",
|
|
116
|
+
issue.resource or "",
|
|
117
|
+
issue.condition_key or "",
|
|
118
|
+
issue.message,
|
|
119
|
+
issue.suggestion or "",
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def format_pivot_table(self, report: ValidationReport) -> str:
|
|
124
|
+
"""Format report as a pivot table CSV for analysis.
|
|
125
|
+
|
|
126
|
+
Groups issues by check_id and severity for easy analysis.
|
|
127
|
+
"""
|
|
128
|
+
output = io.StringIO()
|
|
129
|
+
writer = csv.writer(output)
|
|
130
|
+
|
|
131
|
+
# Create pivot data
|
|
132
|
+
pivot_data = self._create_pivot_data(report)
|
|
133
|
+
|
|
134
|
+
# Write header
|
|
135
|
+
writer.writerow(["Issue Type", "Severity", "Count", "Policy Files"])
|
|
136
|
+
|
|
137
|
+
# Write pivot rows
|
|
138
|
+
for (issue_type, severity), data in sorted(pivot_data.items()):
|
|
139
|
+
writer.writerow(
|
|
140
|
+
[
|
|
141
|
+
issue_type or "unknown",
|
|
142
|
+
severity,
|
|
143
|
+
data["count"],
|
|
144
|
+
"; ".join(data["files"][:5]) + ("..." if len(data["files"]) > 5 else ""),
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return output.getvalue()
|
|
149
|
+
|
|
150
|
+
def _create_pivot_data(self, report: ValidationReport) -> dict[tuple, dict[str, Any]]:
|
|
151
|
+
"""Create pivot table data structure."""
|
|
152
|
+
pivot_data = {}
|
|
153
|
+
|
|
154
|
+
for policy_result in report.results:
|
|
155
|
+
for issue in policy_result.issues:
|
|
156
|
+
key = (issue.issue_type or "unknown", issue.severity)
|
|
157
|
+
|
|
158
|
+
if key not in pivot_data:
|
|
159
|
+
pivot_data[key] = {
|
|
160
|
+
"count": 0,
|
|
161
|
+
"files": set(),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pivot_data[key]["count"] += 1
|
|
165
|
+
pivot_data[key]["files"].add(policy_result.policy_file)
|
|
166
|
+
|
|
167
|
+
# Convert sets to lists
|
|
168
|
+
for key in pivot_data:
|
|
169
|
+
pivot_data[key]["files"] = sorted(list(pivot_data[key]["files"]))
|
|
170
|
+
|
|
171
|
+
return pivot_data
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""Enhanced formatter - Rich-based console output with modern design."""
|
|
2
|
+
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.rule import Rule
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.tree import Tree
|
|
11
|
+
|
|
12
|
+
from iam_validator.__version__ import __version__
|
|
13
|
+
from iam_validator.core import constants
|
|
14
|
+
from iam_validator.core.formatters.base import OutputFormatter
|
|
15
|
+
from iam_validator.core.models import PolicyValidationResult, ValidationReport
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EnhancedFormatter(OutputFormatter):
|
|
19
|
+
"""Enhanced console formatter with modern, visually rich output using Rich library."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def format_id(self) -> str:
|
|
23
|
+
return "enhanced"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def description(self) -> str:
|
|
27
|
+
return "Enhanced console output with progress bars, tree structure, and rich visuals"
|
|
28
|
+
|
|
29
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
30
|
+
"""Format validation report as modern Rich console output.
|
|
31
|
+
|
|
32
|
+
This creates a visually enhanced string representation with:
|
|
33
|
+
- Gradient-styled headers
|
|
34
|
+
- Progress bars for validation metrics
|
|
35
|
+
- Tree structure for issues
|
|
36
|
+
- Bordered panels with icons
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
report: Validation report to format
|
|
40
|
+
**kwargs: Additional options:
|
|
41
|
+
- color (bool): Enable color output (default: True)
|
|
42
|
+
- show_summary (bool): Show Executive Summary panel (default: True)
|
|
43
|
+
- show_severity_breakdown (bool): Show Issue Severity Breakdown panel (default: True)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Formatted string with ANSI codes for console display
|
|
47
|
+
"""
|
|
48
|
+
# Allow disabling color for plain text output
|
|
49
|
+
color = kwargs.get("color", True)
|
|
50
|
+
show_summary = kwargs.get("show_summary", True)
|
|
51
|
+
show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
|
|
52
|
+
|
|
53
|
+
# Use StringIO to capture Rich console output
|
|
54
|
+
from iam_validator.utils import get_terminal_width
|
|
55
|
+
|
|
56
|
+
string_buffer = StringIO()
|
|
57
|
+
# Get terminal width for proper text wrapping
|
|
58
|
+
terminal_width = get_terminal_width()
|
|
59
|
+
console = Console(
|
|
60
|
+
file=string_buffer, force_terminal=color, width=terminal_width, legacy_windows=False
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Header with title
|
|
64
|
+
title = Text(
|
|
65
|
+
f"IAM Policy Validation Report (v{__version__})",
|
|
66
|
+
style="bold cyan",
|
|
67
|
+
justify="center",
|
|
68
|
+
)
|
|
69
|
+
console.print(
|
|
70
|
+
Panel(
|
|
71
|
+
title,
|
|
72
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
73
|
+
padding=(1, 0),
|
|
74
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Executive Summary with progress bars (optional)
|
|
79
|
+
if show_summary:
|
|
80
|
+
self._print_summary_panel(console, report)
|
|
81
|
+
|
|
82
|
+
# Severity breakdown if there are issues (optional)
|
|
83
|
+
if show_severity_breakdown and report.total_issues > 0:
|
|
84
|
+
self._print_severity_breakdown(console, report)
|
|
85
|
+
|
|
86
|
+
console.print(
|
|
87
|
+
Rule(
|
|
88
|
+
title="[bold]Detailed Results",
|
|
89
|
+
style=constants.CONSOLE_HEADER_COLOR,
|
|
90
|
+
),
|
|
91
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Detailed results using tree structure
|
|
95
|
+
for idx, result in enumerate(report.results, 1):
|
|
96
|
+
self._format_policy_result_modern(console, result, idx, len(report.results))
|
|
97
|
+
|
|
98
|
+
# Final status with styled box
|
|
99
|
+
console.print()
|
|
100
|
+
self._print_final_status(console, report)
|
|
101
|
+
|
|
102
|
+
# Get the formatted output
|
|
103
|
+
output = string_buffer.getvalue().rstrip("\n")
|
|
104
|
+
string_buffer.close()
|
|
105
|
+
|
|
106
|
+
return output
|
|
107
|
+
|
|
108
|
+
def _print_summary_panel(self, console: Console, report: ValidationReport) -> None:
|
|
109
|
+
"""Print summary panel with clean metrics display."""
|
|
110
|
+
# Create a simple table for metrics without progress bars
|
|
111
|
+
metrics_table = Table.grid(padding=(0, 2))
|
|
112
|
+
metrics_table.add_column(style="bold", justify="left", width=30)
|
|
113
|
+
metrics_table.add_column(style="bold", justify="left", width=20)
|
|
114
|
+
|
|
115
|
+
# Total policies
|
|
116
|
+
metrics_table.add_row(
|
|
117
|
+
"📋 Total Policies",
|
|
118
|
+
str(report.total_policies),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Valid policies
|
|
122
|
+
if report.total_policies > 0:
|
|
123
|
+
valid_pct = report.valid_policies * 100 // report.total_policies
|
|
124
|
+
metrics_table.add_row(
|
|
125
|
+
"✅ Valid Policies",
|
|
126
|
+
f"[green]{report.valid_policies} ({valid_pct}%)[/green]",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Invalid policies
|
|
130
|
+
invalid_pct = report.invalid_policies * 100 // report.total_policies
|
|
131
|
+
if report.invalid_policies > 0:
|
|
132
|
+
metrics_table.add_row(
|
|
133
|
+
"❌ Invalid Policies",
|
|
134
|
+
f"[red]{report.invalid_policies} ({invalid_pct}%)[/red]",
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
metrics_table.add_row(
|
|
138
|
+
"❌ Invalid Policies",
|
|
139
|
+
f"[dim]{report.invalid_policies} ({invalid_pct}%)[/dim]",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Total issues
|
|
143
|
+
if report.total_issues > 0:
|
|
144
|
+
metrics_table.add_row(
|
|
145
|
+
"⚠️ Total Issues Found",
|
|
146
|
+
f"[red]{report.total_issues}[/red]",
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
metrics_table.add_row(
|
|
150
|
+
"⚠️ Total Issues Found",
|
|
151
|
+
f"[green]{report.total_issues}[/green]",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
console.print(
|
|
155
|
+
Panel(
|
|
156
|
+
metrics_table,
|
|
157
|
+
title="📊 Executive Summary",
|
|
158
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
159
|
+
padding=(1, 2),
|
|
160
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _create_progress_bar(self, value: int, total: int, color: str) -> str:
|
|
165
|
+
"""Create a simple text-based progress bar."""
|
|
166
|
+
if total == 0:
|
|
167
|
+
return "[dim]───────────────────────[/dim]"
|
|
168
|
+
|
|
169
|
+
percentage = min(value * 100 // total, 100)
|
|
170
|
+
filled = int(percentage / 5) # 20 bars total (100/5)
|
|
171
|
+
empty = 20 - filled
|
|
172
|
+
|
|
173
|
+
bar = f"[{color}]{'█' * filled}[/{color}][dim]{'░' * empty}[/dim]"
|
|
174
|
+
return bar
|
|
175
|
+
|
|
176
|
+
def _print_severity_breakdown(self, console: Console, report: ValidationReport) -> None:
|
|
177
|
+
"""Print a clean breakdown of issues by severity."""
|
|
178
|
+
# Count issues by severity
|
|
179
|
+
severity_counts = {
|
|
180
|
+
"critical": 0,
|
|
181
|
+
"high": 0,
|
|
182
|
+
"error": 0,
|
|
183
|
+
"medium": 0,
|
|
184
|
+
"warning": 0,
|
|
185
|
+
"low": 0,
|
|
186
|
+
"info": 0,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for result in report.results:
|
|
190
|
+
for issue in result.issues:
|
|
191
|
+
severity = issue.severity.lower()
|
|
192
|
+
if severity in severity_counts:
|
|
193
|
+
severity_counts[severity] += 1
|
|
194
|
+
|
|
195
|
+
# Create clean severity table
|
|
196
|
+
severity_table = Table.grid(padding=(0, 2))
|
|
197
|
+
severity_table.add_column(style="bold", justify="left", width=25)
|
|
198
|
+
severity_table.add_column(style="bold", justify="left", width=15)
|
|
199
|
+
|
|
200
|
+
# Show individual severity counts
|
|
201
|
+
if severity_counts["critical"] > 0:
|
|
202
|
+
severity_table.add_row(
|
|
203
|
+
"🔴 Critical",
|
|
204
|
+
f"[red]{severity_counts['critical']}[/red]",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if severity_counts["high"] > 0:
|
|
208
|
+
severity_table.add_row(
|
|
209
|
+
"🔴 High",
|
|
210
|
+
f"[red]{severity_counts['high']}[/red]",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if severity_counts["error"] > 0:
|
|
214
|
+
severity_table.add_row(
|
|
215
|
+
"🔴 Error",
|
|
216
|
+
f"[red]{severity_counts['error']}[/red]",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if severity_counts["medium"] > 0:
|
|
220
|
+
severity_table.add_row(
|
|
221
|
+
"🟡 Medium",
|
|
222
|
+
f"[yellow]{severity_counts['medium']}[/yellow]",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if severity_counts["warning"] > 0:
|
|
226
|
+
severity_table.add_row(
|
|
227
|
+
"🟡 Warning",
|
|
228
|
+
f"[yellow]{severity_counts['warning']}[/yellow]",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if severity_counts["low"] > 0:
|
|
232
|
+
severity_table.add_row(
|
|
233
|
+
"🔵 Low",
|
|
234
|
+
f"[blue]{severity_counts['low']}[/blue]",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if severity_counts["info"] > 0:
|
|
238
|
+
severity_table.add_row(
|
|
239
|
+
"🔵 Info",
|
|
240
|
+
f"[blue]{severity_counts['info']}[/blue]",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
console.print(
|
|
244
|
+
Panel(
|
|
245
|
+
severity_table,
|
|
246
|
+
title="🎯 Issue Severity Breakdown",
|
|
247
|
+
border_style=constants.CONSOLE_HEADER_COLOR,
|
|
248
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def _format_policy_result_modern(
|
|
253
|
+
self, console: Console, result: PolicyValidationResult, idx: int, total: int
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Format policy results with modern tree structure.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
console: Rich console instance
|
|
259
|
+
result: Policy validation result
|
|
260
|
+
idx: Index of this policy (1-based)
|
|
261
|
+
total: Total number of policies
|
|
262
|
+
"""
|
|
263
|
+
# Status icon and color
|
|
264
|
+
if result.is_valid and not result.issues:
|
|
265
|
+
icon = "✅"
|
|
266
|
+
color = "green"
|
|
267
|
+
status_text = "VALID"
|
|
268
|
+
elif result.is_valid and result.issues:
|
|
269
|
+
# Valid IAM policy but has security findings
|
|
270
|
+
# Check severity to determine the appropriate status
|
|
271
|
+
has_critical = any(i.severity in constants.HIGH_SEVERITY_LEVELS for i in result.issues)
|
|
272
|
+
if has_critical:
|
|
273
|
+
icon = "⚠️"
|
|
274
|
+
color = "red"
|
|
275
|
+
status_text = "VALID (with security issues)"
|
|
276
|
+
else:
|
|
277
|
+
icon = "⚠️"
|
|
278
|
+
color = "yellow"
|
|
279
|
+
status_text = "VALID (with warnings)"
|
|
280
|
+
else:
|
|
281
|
+
# Policy failed validation (is_valid=false)
|
|
282
|
+
# Check if it's due to IAM errors or security issues
|
|
283
|
+
has_iam_errors = any(i.severity == "error" for i in result.issues)
|
|
284
|
+
has_security_critical = any(i.severity in ("critical", "high") for i in result.issues)
|
|
285
|
+
|
|
286
|
+
if has_iam_errors and has_security_critical:
|
|
287
|
+
# Both IAM errors and security issues
|
|
288
|
+
status_text = "INVALID (IAM errors + security issues)"
|
|
289
|
+
elif has_iam_errors:
|
|
290
|
+
# Only IAM validation errors
|
|
291
|
+
status_text = "INVALID (IAM errors)"
|
|
292
|
+
else:
|
|
293
|
+
# Only security issues (failed due to fail_on_severity config)
|
|
294
|
+
status_text = "FAILED (critical security issues)"
|
|
295
|
+
|
|
296
|
+
icon = "❌"
|
|
297
|
+
color = "red"
|
|
298
|
+
|
|
299
|
+
# Policy header
|
|
300
|
+
header = Text()
|
|
301
|
+
header.append(f"{icon} ", style=color)
|
|
302
|
+
header.append(f"[{idx}/{total}] ", style="dim")
|
|
303
|
+
header.append(result.policy_file, style=f"bold {color}")
|
|
304
|
+
header.append(f" • {status_text}", style=f"{color}")
|
|
305
|
+
|
|
306
|
+
if not result.issues:
|
|
307
|
+
console.print(header)
|
|
308
|
+
console.print(" [dim italic]No issues detected[/dim italic]")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
console.print(header)
|
|
312
|
+
console.print(f" [dim]{len(result.issues)} issue(s) found[/dim]")
|
|
313
|
+
# Create tree structure for issues
|
|
314
|
+
tree = Tree(f"[bold]Issues ({len(result.issues)})[/bold]", guide_style="bright_black")
|
|
315
|
+
|
|
316
|
+
# Group issues by severity with proper categorization
|
|
317
|
+
critical_issues = [i for i in result.issues if i.severity == "critical"]
|
|
318
|
+
high_issues = [i for i in result.issues if i.severity == "high"]
|
|
319
|
+
error_issues = [i for i in result.issues if i.severity == "error"]
|
|
320
|
+
medium_issues = [i for i in result.issues if i.severity == "medium"]
|
|
321
|
+
warning_issues = [i for i in result.issues if i.severity == "warning"]
|
|
322
|
+
low_issues = [i for i in result.issues if i.severity == "low"]
|
|
323
|
+
info_issues = [i for i in result.issues if i.severity == "info"]
|
|
324
|
+
|
|
325
|
+
# Add critical issues (security checks)
|
|
326
|
+
if critical_issues:
|
|
327
|
+
critical_branch = tree.add("🔴 [bold red]Critical[/bold red]")
|
|
328
|
+
for issue in critical_issues:
|
|
329
|
+
self._add_issue_to_tree(critical_branch, issue, "red")
|
|
330
|
+
|
|
331
|
+
# Add high severity issues (security checks)
|
|
332
|
+
if high_issues:
|
|
333
|
+
high_branch = tree.add("🔴 [bold red]High[/bold red]")
|
|
334
|
+
for issue in high_issues:
|
|
335
|
+
self._add_issue_to_tree(high_branch, issue, "red")
|
|
336
|
+
|
|
337
|
+
# Add errors (IAM validation)
|
|
338
|
+
if error_issues:
|
|
339
|
+
error_branch = tree.add("🔴 [bold red]Error[/bold red]")
|
|
340
|
+
for issue in error_issues:
|
|
341
|
+
self._add_issue_to_tree(error_branch, issue, "red")
|
|
342
|
+
|
|
343
|
+
# Add medium severity issues (security checks)
|
|
344
|
+
if medium_issues:
|
|
345
|
+
medium_branch = tree.add("🟡 [bold yellow]Medium[/bold yellow]")
|
|
346
|
+
for issue in medium_issues:
|
|
347
|
+
self._add_issue_to_tree(medium_branch, issue, "yellow")
|
|
348
|
+
|
|
349
|
+
# Add warnings (IAM validation)
|
|
350
|
+
if warning_issues:
|
|
351
|
+
warning_branch = tree.add("🟡 [bold yellow]Warning[/bold yellow]")
|
|
352
|
+
for issue in warning_issues:
|
|
353
|
+
self._add_issue_to_tree(warning_branch, issue, "yellow")
|
|
354
|
+
|
|
355
|
+
# Add low severity issues (security checks)
|
|
356
|
+
if low_issues:
|
|
357
|
+
low_branch = tree.add("🔵 [bold blue]Low[/bold blue]")
|
|
358
|
+
for issue in low_issues:
|
|
359
|
+
self._add_issue_to_tree(low_branch, issue, "blue")
|
|
360
|
+
|
|
361
|
+
# Add info (IAM validation)
|
|
362
|
+
if info_issues:
|
|
363
|
+
info_branch = tree.add("🔵 [bold blue]Info[/bold blue]")
|
|
364
|
+
for issue in info_issues:
|
|
365
|
+
self._add_issue_to_tree(info_branch, issue, "blue")
|
|
366
|
+
|
|
367
|
+
console.print(" ", tree)
|
|
368
|
+
console.print()
|
|
369
|
+
|
|
370
|
+
def _add_issue_to_tree(self, branch: Tree, issue, color: str) -> None:
|
|
371
|
+
"""Add an issue to a tree branch."""
|
|
372
|
+
# Build location string (use 1-indexed statement numbers for user-facing output)
|
|
373
|
+
# Handle policy-level issues (statement_index = -1)
|
|
374
|
+
if issue.statement_index == -1:
|
|
375
|
+
location = "Policy-level"
|
|
376
|
+
else:
|
|
377
|
+
statement_num = issue.statement_index + 1
|
|
378
|
+
location = f"Statement {statement_num}"
|
|
379
|
+
if issue.statement_sid:
|
|
380
|
+
location = f"{issue.statement_sid} (#{statement_num})"
|
|
381
|
+
if issue.line_number is not None:
|
|
382
|
+
location += f" @L{issue.line_number}"
|
|
383
|
+
|
|
384
|
+
# Issue summary
|
|
385
|
+
issue_text = Text()
|
|
386
|
+
issue_text.append(f"[{location}] ", style="dim")
|
|
387
|
+
issue_text.append(issue.issue_type, style=f"bold {color}")
|
|
388
|
+
issue_node = branch.add(issue_text)
|
|
389
|
+
|
|
390
|
+
# Message
|
|
391
|
+
msg_node = issue_node.add(Text(issue.message, style="white"))
|
|
392
|
+
|
|
393
|
+
# Details
|
|
394
|
+
if issue.action or issue.resource or issue.condition_key:
|
|
395
|
+
details = []
|
|
396
|
+
if issue.action:
|
|
397
|
+
details.append(f"Action: {issue.action}")
|
|
398
|
+
if issue.resource:
|
|
399
|
+
details.append(f"Resource: {issue.resource}")
|
|
400
|
+
if issue.condition_key:
|
|
401
|
+
details.append(f"Condition: {issue.condition_key}")
|
|
402
|
+
msg_node.add(Text(" • ".join(details), style="dim cyan"))
|
|
403
|
+
|
|
404
|
+
# Suggestion and Example - combine into single node to reduce spacing
|
|
405
|
+
if issue.suggestion or issue.example:
|
|
406
|
+
combined_text = Text()
|
|
407
|
+
|
|
408
|
+
# Add suggestion
|
|
409
|
+
if issue.suggestion:
|
|
410
|
+
combined_text.append("💡 ", style="yellow")
|
|
411
|
+
combined_text.append(issue.suggestion, style="italic yellow")
|
|
412
|
+
|
|
413
|
+
# Add example on same node (reduces vertical spacing)
|
|
414
|
+
if issue.example:
|
|
415
|
+
if issue.suggestion:
|
|
416
|
+
combined_text.append("\n", style="yellow") # Single newline separator
|
|
417
|
+
combined_text.append("Example:", style="bold cyan")
|
|
418
|
+
combined_text.append("\n")
|
|
419
|
+
combined_text.append(issue.example, style="dim")
|
|
420
|
+
|
|
421
|
+
msg_node.add(combined_text)
|
|
422
|
+
|
|
423
|
+
def _print_final_status(self, console: Console, report: ValidationReport) -> None:
|
|
424
|
+
"""Print final status panel."""
|
|
425
|
+
if report.invalid_policies == 0 and report.total_issues == 0:
|
|
426
|
+
# Perfect success
|
|
427
|
+
status = Text("🎉 ALL POLICIES VALIDATED SUCCESSFULLY! 🎉", style="bold green")
|
|
428
|
+
message = Text(
|
|
429
|
+
f"All {report.valid_policies} policies passed validation with no issues.",
|
|
430
|
+
style="green",
|
|
431
|
+
)
|
|
432
|
+
border_color = "green"
|
|
433
|
+
elif report.invalid_policies == 0:
|
|
434
|
+
# Valid IAM policies but may have security findings
|
|
435
|
+
# Check if there are critical/high security issues
|
|
436
|
+
has_critical = any(
|
|
437
|
+
i.severity in constants.HIGH_SEVERITY_LEVELS
|
|
438
|
+
for r in report.results
|
|
439
|
+
for i in r.issues
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if has_critical:
|
|
443
|
+
status = Text("⚠️ All Policies Valid (with security issues)", style="bold red")
|
|
444
|
+
message = Text(
|
|
445
|
+
f"{report.valid_policies} policies are valid, but {report.total_issues} "
|
|
446
|
+
f"security issue(s) were found that must be addressed.",
|
|
447
|
+
style="red",
|
|
448
|
+
)
|
|
449
|
+
border_color = "red"
|
|
450
|
+
else:
|
|
451
|
+
status = Text("✅ All Policies Valid (with warnings)", style="bold yellow")
|
|
452
|
+
message = Text(
|
|
453
|
+
f"{report.valid_policies} policies are valid, but {report.total_issues} "
|
|
454
|
+
f"warning(s) were found that should be reviewed.",
|
|
455
|
+
style="yellow",
|
|
456
|
+
)
|
|
457
|
+
border_color = "yellow"
|
|
458
|
+
else:
|
|
459
|
+
# Has invalid policies
|
|
460
|
+
status = Text("❌ VALIDATION FAILED", style="bold red")
|
|
461
|
+
message = Text(
|
|
462
|
+
f"{report.invalid_policies} of {report.total_policies} policies have critical "
|
|
463
|
+
f"issues that must be resolved.",
|
|
464
|
+
style="red",
|
|
465
|
+
)
|
|
466
|
+
border_color = "red"
|
|
467
|
+
|
|
468
|
+
# Combine status and message
|
|
469
|
+
final_text = Text()
|
|
470
|
+
final_text.append(status)
|
|
471
|
+
final_text.append("\n") # Reduced from \n\n to single newline
|
|
472
|
+
final_text.append(message)
|
|
473
|
+
|
|
474
|
+
console.print(
|
|
475
|
+
Panel(
|
|
476
|
+
final_text,
|
|
477
|
+
border_style=border_color,
|
|
478
|
+
padding=(1, 2),
|
|
479
|
+
width=constants.CONSOLE_PANEL_WIDTH,
|
|
480
|
+
)
|
|
481
|
+
)
|