iris-security-cli 0.1.7__tar.gz → 0.2.0__tar.gz

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.
Files changed (60) hide show
  1. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/PKG-INFO +5 -5
  2. iris_security_cli-0.2.0/iris_cli/certify.py +154 -0
  3. iris_security_cli-0.2.0/iris_cli/compliance_check_cmd.py +193 -0
  4. iris_security_cli-0.2.0/iris_cli/compliance_fix.py +202 -0
  5. iris_security_cli-0.2.0/iris_cli/declare.py +177 -0
  6. iris_security_cli-0.2.0/iris_cli/delegation.py +172 -0
  7. iris_security_cli-0.2.0/iris_cli/enforce.py +148 -0
  8. iris_security_cli-0.2.0/iris_cli/hitl.py +303 -0
  9. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/license_cmd.py +1 -1
  10. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/main.py +122 -93
  11. iris_security_cli-0.2.0/iris_cli/models_cmd.py +138 -0
  12. iris_security_cli-0.2.0/iris_cli/preview.py +128 -0
  13. iris_security_cli-0.2.0/iris_cli/quickstart.py +423 -0
  14. iris_security_cli-0.2.0/iris_cli/sentinel.py +124 -0
  15. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/status_cmd.py +10 -0
  16. iris_security_cli-0.2.0/iris_cli/witness.py +123 -0
  17. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_security_cli.egg-info/PKG-INFO +5 -5
  18. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_security_cli.egg-info/SOURCES.txt +16 -1
  19. iris_security_cli-0.2.0/iris_security_cli.egg-info/requires.txt +8 -0
  20. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/pyproject.toml +6 -5
  21. iris_security_cli-0.2.0/tests/test_hitl_cli.py +111 -0
  22. iris_security_cli-0.2.0/tests/test_mcp_model_governance.py +35 -0
  23. iris_security_cli-0.2.0/tests/test_quickstart.py +95 -0
  24. iris_security_cli-0.2.0/tests/test_vocabulary.py +353 -0
  25. iris_security_cli-0.1.7/iris_cli/compliance_check_cmd.py +0 -127
  26. iris_security_cli-0.1.7/iris_security_cli.egg-info/requires.txt +0 -8
  27. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/README.md +0 -0
  28. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/__init__.py +0 -0
  29. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/action_plan.py +0 -0
  30. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/assess.py +0 -0
  31. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/cedar_parser.py +0 -0
  32. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/compiler_config.py +0 -0
  33. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/cost.py +0 -0
  34. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/dlp_cmd.py +0 -0
  35. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/drift.py +0 -0
  36. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/entitlements_cmd.py +0 -0
  37. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/evidence.py +0 -0
  38. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/explain.py +0 -0
  39. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/framework_suggest.py +0 -0
  40. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/framework_test.py +0 -0
  41. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/mcp_server.py +0 -0
  42. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/policy_cache.py +0 -0
  43. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/policy_diff.py +0 -0
  44. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/redteam.py +0 -0
  45. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/regulatory.py +0 -0
  46. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/scan_govern.py +0 -0
  47. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/scan_report.py +0 -0
  48. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/scm.py +0 -0
  49. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/users.py +0 -0
  50. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/vault.py +0 -0
  51. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_cli/watch.py +0 -0
  52. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_security_cli.egg-info/dependency_links.txt +0 -0
  53. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_security_cli.egg-info/entry_points.txt +0 -0
  54. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/iris_security_cli.egg-info/top_level.txt +0 -0
  55. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/setup.cfg +0 -0
  56. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/tests/test_entitlements_display.py +0 -0
  57. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/tests/test_evidence.py +0 -0
  58. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/tests/test_framework_suggest.py +0 -0
  59. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/tests/test_framework_test.py +0 -0
  60. {iris_security_cli-0.1.7 → iris_security_cli-0.2.0}/tests/test_policy_diff.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-security-cli
3
- Version: 0.1.7
4
- Summary: IRIS CLI — iris scan, iris register, iris policy, iris compliance
3
+ Version: 0.2.0
4
+ Summary: IRIS CLI — declare, compile, preview, enforce, witness, certify, sentinel
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
7
7
  Project-URL: Repository, https://github.com/gimartinb/iris-sdk
@@ -15,13 +15,13 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: iris-security-core>=0.1.6
19
- Requires-Dist: iris-security-sdk>=0.1.7
18
+ Requires-Dist: iris-security-core>=0.1.8
19
+ Requires-Dist: iris-security-sdk>=0.2.0
20
20
  Requires-Dist: click>=8.1
21
21
  Requires-Dist: rich>=13.0
22
22
  Requires-Dist: pyyaml>=6.0
23
23
  Provides-Extra: scm
24
- Requires-Dist: iris-security-scm[all]>=0.1.0; extra == "scm"
24
+ Requires-Dist: iris-security-scm[all]>=0.1.1; extra == "scm"
25
25
 
26
26
  # iris-security-cli
27
27
 
@@ -0,0 +1,154 @@
1
+ """iris certify — prove compliance readiness to any regulatory framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+
13
+ from iris_core.entitlements import Entitlements, Feature
14
+ from iris_core.models.passport import AgentPassport
15
+
16
+ from iris_cli.framework_test import (
17
+ _build_result,
18
+ _framework_name,
19
+ _load_previous_score,
20
+ _render_json,
21
+ _render_markdown,
22
+ _render_table,
23
+ _save_result,
24
+ )
25
+
26
+ console = Console()
27
+
28
+
29
+ def _render_certification_header(
30
+ framework: str,
31
+ agent_name: str,
32
+ result,
33
+ ) -> None:
34
+ framework_label = _framework_name(framework)
35
+ if framework == "colorado-ai-act":
36
+ framework_label = "Colorado AI Act (SB 26-189)"
37
+
38
+ console.print(Panel(
39
+ f"Agent: {agent_name}\n"
40
+ f"Framework: {framework_label}\n"
41
+ f"Generated: {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}\n"
42
+ f"Attested: IRIS · Commit: local",
43
+ title=f"IRIS Certification — {framework}",
44
+ style="blue",
45
+ ))
46
+
47
+ evaluated = result.passed_controls + result.failed_controls + result.partial_controls
48
+ pct = result.score_percent
49
+ filled = int((pct / 100) * 20)
50
+ bar = "█" * filled + "░" * (20 - filled)
51
+ readiness = "READY" if pct >= 95 else result.readiness_level
52
+
53
+ console.print(f"\n[bold]CERTIFICATION RESULT: {readiness}[/bold]")
54
+ console.print(f"{bar} {result.passed_controls} / {result.total_controls} controls {pct}%")
55
+
56
+ if framework == "colorado-ai-act" and pct >= 95:
57
+ console.print(
58
+ f"\nThis agent is certification-ready for Colorado AI Act (SB 26-189).\n"
59
+ f"Effective date: January 1, 2027\n\n"
60
+ f"Evidence package: iris evidence report --agent {agent_name}\n"
61
+ f"Auditor note: Present this output alongside the Evidence Vault report "
62
+ f"to demonstrate compliance readiness."
63
+ )
64
+
65
+
66
+ @click.command("certify")
67
+ @click.option(
68
+ "--framework",
69
+ required=True,
70
+ type=click.Choice(sorted(["colorado-ai-act", "nist-ai-rmf", "fedramp-moderate", "hipaa", "soc2", "gdpr"])),
71
+ )
72
+ @click.option("--agent", "agent_name", required=True, help="Agent name under governance/agents")
73
+ @click.option("--format", "output_format", default="table", type=click.Choice(["table", "json", "markdown"]))
74
+ def certify_cmd(framework: str, agent_name: str, output_format: str) -> None:
75
+ """
76
+ Certify compliance readiness against a regulatory framework.
77
+
78
+ Alias: iris test
79
+ """
80
+ passport_path = Path.cwd() / "governance" / "agents" / agent_name / "passport.yaml"
81
+ if not passport_path.exists():
82
+ raise click.ClickException(f"Passport not found: {passport_path}")
83
+ passport = AgentPassport.from_yaml(passport_path.read_text())
84
+
85
+ ents = Entitlements()
86
+ has_pro = ents.has(Feature.CLI_TEST_FULL_REPORT)
87
+
88
+ result = _build_result(framework, agent_name, passport)
89
+ previous = _load_previous_score(agent_name, framework)
90
+ if previous is not None and has_pro:
91
+ result.progress_delta_percent = result.score_percent - previous
92
+
93
+ _save_result(result)
94
+
95
+ if output_format == "json":
96
+ click.echo(_render_json(result, has_pro))
97
+ elif output_format == "markdown":
98
+ click.echo(_render_markdown(result, has_pro))
99
+ else:
100
+ _render_certification_header(framework, agent_name, result)
101
+ _render_table(result, has_pro)
102
+ _render_response_configuration(framework, passport)
103
+
104
+
105
+ def _render_response_configuration(framework: str, passport: AgentPassport) -> None:
106
+ from iris_core.compliance.framework_check import load_bundle_data
107
+ from iris_core.compliance.violation_response import (
108
+ get_effective_response,
109
+ rule_response_label,
110
+ ViolationResponse,
111
+ )
112
+
113
+ try:
114
+ bundle = load_bundle_data(framework)
115
+ except ValueError:
116
+ return
117
+ rules = bundle.get("rules", [])
118
+ if not rules:
119
+ return
120
+
121
+ console.print("\n[bold]RESPONSE CONFIGURATION[/bold]")
122
+ console.print("─" * 65)
123
+ overrides = passport.compliance_response_overrides or {}
124
+ for rule in rules:
125
+ rule_id = rule.get("rule_id", "")
126
+ default = rule.get("violation_response", "inform").upper()
127
+ effective = rule_response_label(rule, overrides)
128
+ line = f"{rule_id:<12} {effective:<8}"
129
+ if rule.get("violation_response"):
130
+ base = ViolationResponse(rule["violation_response"])
131
+ if base == ViolationResponse.BLOCK:
132
+ line += " (unconditional — cannot be overridden)"
133
+ elif effective != default:
134
+ line += f" (override active)"
135
+ else:
136
+ line += " (default)"
137
+ console.print(line)
138
+
139
+ if overrides:
140
+ console.print("\n[dim]Active overrides in passport.yaml:[/dim]")
141
+ for rule_id, cfg in overrides.items():
142
+ console.print(f" {rule_id}: {cfg.get('response', 'inform')} — {cfg.get('reason', '')}")
143
+ else:
144
+ console.print(
145
+ "\n[dim]Override example (add to passport.yaml if needed):[/dim]\n"
146
+ " compliance_response_overrides:\n"
147
+ " CO-003:\n"
148
+ " response: inform\n"
149
+ " reason: Pre-production — not serving CO consumers yet"
150
+ )
151
+
152
+
153
+ # Legacy alias: iris test → iris certify
154
+ test = certify_cmd
@@ -0,0 +1,193 @@
1
+ """iris compliance check — framework compliance with Pro preview."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from iris import AgentPassport
14
+ from iris_core.compliance.framework_check import run_framework_check
15
+ from iris_core.compliance.results import ComplianceCheckStatus
16
+ from iris_core.entitlements.display import build_pro_preview_box
17
+
18
+ from iris_cli.compliance_fix import offer_remediation, recheck_after_fixes
19
+ from iris_core.compliance.remediation import collect_fixable_issues
20
+
21
+ console = Console()
22
+
23
+
24
+ def _discover_passports(
25
+ gov_dir: Path,
26
+ agent: Optional[str],
27
+ ) -> list[AgentPassport]:
28
+ passports: list[AgentPassport] = []
29
+ if agent:
30
+ passport_file = gov_dir / agent / "passport.yaml"
31
+ if passport_file.exists():
32
+ passports.append(AgentPassport.from_yaml(passport_file.read_text()))
33
+ return passports
34
+
35
+ for passport_file in gov_dir.rglob("passport.yaml"):
36
+ try:
37
+ passports.append(AgentPassport.from_yaml(passport_file.read_text()))
38
+ except Exception:
39
+ continue
40
+ return passports
41
+
42
+
43
+ def _render_result(
44
+ passport: AgentPassport,
45
+ framework: str,
46
+ result,
47
+ ) -> bool:
48
+ """Print one agent's check result. Returns False if failures were found."""
49
+ if result.preview_only:
50
+ preview_lines = [
51
+ f"{rule.rule_id} {rule.severity} {rule.name} [{rule.status}]"
52
+ for rule in result.rule_results
53
+ ]
54
+ console.print(
55
+ build_pro_preview_box(
56
+ framework,
57
+ result.framework_name,
58
+ result.total_controls,
59
+ preview_lines,
60
+ )
61
+ )
62
+ return False
63
+
64
+ has_failures = any(rule.status == "FAIL" for rule in result.rule_results) or bool(
65
+ result.violations
66
+ )
67
+ status = "[bold green]PASS[/bold green]" if not has_failures else "[bold red]FAIL[/bold red]"
68
+ console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
69
+
70
+ if result.rule_results:
71
+ table = Table(title=f"{result.framework_name}", show_header=True, header_style="bold")
72
+ table.add_column("Rule", style="cyan")
73
+ table.add_column("Severity")
74
+ table.add_column("Status")
75
+ table.add_column("Response")
76
+ table.add_column("Notes")
77
+ for rule in result.rule_results:
78
+ status_icon = "[green]✓ PASS[/green]" if rule.status == "PASS" else "[red]✗ FAIL[/red]"
79
+ notes = rule.message if rule.status == "FAIL" else ""
80
+ table.add_row(
81
+ rule.rule_id,
82
+ rule.severity,
83
+ status_icon,
84
+ rule.response,
85
+ notes[:80] + ("..." if len(notes) > 80 else ""),
86
+ )
87
+ console.print(table)
88
+ console.print(
89
+ "\n[dim]RESPONSE KEY: INFORM — log and proceed | "
90
+ "HITL — human review required | BLOCK — unconditional block[/dim]"
91
+ )
92
+
93
+ if result.violations:
94
+ for violation in result.violations:
95
+ if any(r.rule_id == violation.rule_id for r in result.rule_results):
96
+ continue
97
+ console.print(f" [red]✗[/red] [{violation.rule_id}] {violation.message}")
98
+ if violation.remediation:
99
+ console.print(f" [yellow]→[/yellow] {violation.remediation}")
100
+
101
+ if not result.violations and not any(r.status == "FAIL" for r in result.rule_results):
102
+ console.print(f" [green]✓[/green] All {framework} rules satisfied")
103
+
104
+ for guidance in result.guidance:
105
+ if guidance.status == ComplianceCheckStatus.GUIDANCE:
106
+ console.print(f" [blue]ℹ[/blue] [{guidance.rule_id}] {guidance.message}")
107
+ if guidance.remediation:
108
+ console.print(f" [yellow]→[/yellow] {guidance.remediation}")
109
+ elif guidance.status == ComplianceCheckStatus.PRO_REQUIRED:
110
+ console.print(f" [yellow]⚠[/yellow] [{guidance.rule_id}] {guidance.message}")
111
+ if guidance.remediation:
112
+ console.print(f" [yellow]→[/yellow] {guidance.remediation}")
113
+
114
+ for note in result.check_notes:
115
+ console.print(f" [dim]ℹ {note}[/dim]")
116
+
117
+ return not has_failures
118
+
119
+
120
+ @click.command("check")
121
+ @click.option("--agent", default=None, help="Specific agent to check (or all)")
122
+ @click.option("--framework", "-f", default="colorado-ai-act")
123
+ @click.option("--dir", "governance_dir", type=click.Path(path_type=Path), default=None)
124
+ @click.option(
125
+ "--fix",
126
+ is_flag=True,
127
+ help="Apply all auto-fixable remediations without per-issue prompts",
128
+ )
129
+ @click.option(
130
+ "--yes",
131
+ "-y",
132
+ is_flag=True,
133
+ help="Apply auto-fixes without confirmation (use with --fix)",
134
+ )
135
+ @click.option(
136
+ "--no-fix",
137
+ is_flag=True,
138
+ help="Show remediations only — never offer to apply fixes",
139
+ )
140
+ def compliance_check_cmd(
141
+ agent: Optional[str],
142
+ framework: str,
143
+ governance_dir: Optional[Path],
144
+ fix: bool,
145
+ yes: bool,
146
+ no_fix: bool,
147
+ ) -> None:
148
+ """
149
+ Check an agent against a compliance framework.
150
+
151
+ Shows a detailed breakdown of which rules pass and fail,
152
+ with plain-English remediation guidance for each failure.
153
+ When run interactively, IRIS offers to apply safe fixes automatically.
154
+
155
+ Example:
156
+ iris compliance check --framework colorado-ai-act
157
+ iris compliance check --agent payment-agent --framework hipaa
158
+ iris compliance check --agent payment-agent --framework hipaa --fix -y
159
+ """
160
+ gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
161
+ passports = _discover_passports(gov_dir, agent)
162
+ if not passports:
163
+ console.print("[yellow]No agent passports found.[/yellow]")
164
+ sys.exit(0)
165
+
166
+ all_pass = True
167
+ offer_fixes = bool(agent) or len(passports) == 1
168
+ for passport in passports:
169
+ result = run_framework_check(passport, framework)
170
+ agent_passed = _render_result(passport, framework, result)
171
+ if not agent_passed:
172
+ all_pass = False
173
+
174
+ if offer_fixes:
175
+ applied = offer_remediation(
176
+ passport,
177
+ framework,
178
+ result,
179
+ gov_dir,
180
+ auto_fix=fix,
181
+ yes=yes,
182
+ no_fix=no_fix,
183
+ )
184
+ recheck_after_fixes(passport, framework, gov_dir, applied)
185
+ elif not no_fix and not agent_passed and collect_fixable_issues(
186
+ passport, framework, result
187
+ ):
188
+ console.print(
189
+ f"[dim]Fixable issues for {passport.name}. "
190
+ f"Re-run with --agent {passport.name} to let IRIS apply fixes.[/dim]"
191
+ )
192
+
193
+ sys.exit(0 if all_pass else 1)
@@ -0,0 +1,202 @@
1
+ """Interactive compliance remediation — IRIS governance assistant."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ from iris import AgentPassport
13
+ from iris_core.compliance.framework_check import FrameworkCheckResult, run_framework_check
14
+ from iris_core.compliance.remediation import (
15
+ FixResult,
16
+ FixableIssue,
17
+ apply_fix,
18
+ collect_fixable_issues,
19
+ )
20
+
21
+ console = Console()
22
+
23
+
24
+ def _is_interactive() -> bool:
25
+ return click.get_text_stream("stdin").isatty()
26
+
27
+
28
+ def _print_fixable_summary(agent_name: str, issues: List[FixableIssue]) -> None:
29
+ lines = [
30
+ f"[bold]IRIS found {len(issues)} fixable issue"
31
+ f"{'s' if len(issues) != 1 else ''} for [cyan]{agent_name}[/cyan][/bold]\n"
32
+ ]
33
+ for issue in issues:
34
+ lines.append(f" [yellow]•[/yellow] [{issue.rule_id}] {issue.preview}")
35
+ lines.append(
36
+ "\n[dim]IRIS can apply safe passport and policy updates automatically. "
37
+ "Review each change before applying.[/dim]"
38
+ )
39
+ console.print(Panel("\n".join(lines), title="IRIS Governance Assistant", style="blue"))
40
+
41
+
42
+ def offer_remediation(
43
+ passport: AgentPassport,
44
+ framework: str,
45
+ result: FrameworkCheckResult,
46
+ governance_dir: Path,
47
+ *,
48
+ auto_fix: bool = False,
49
+ yes: bool = False,
50
+ no_fix: bool = False,
51
+ ) -> List[FixResult]:
52
+ """
53
+ Offer to apply fixable remediations after a compliance check.
54
+
55
+ Returns applied fix results (may be empty).
56
+ """
57
+ if no_fix or result.preview_only:
58
+ return []
59
+
60
+ issues = collect_fixable_issues(passport, framework, result)
61
+ if not issues:
62
+ return []
63
+
64
+ agent_dir = governance_dir / passport.name
65
+ passport_file = agent_dir / "passport.yaml"
66
+
67
+ if auto_fix or yes:
68
+ return _apply_all(issues, passport_file, framework, passport.name)
69
+
70
+ if not _is_interactive():
71
+ console.print(
72
+ "[dim]Fixable issues detected. Re-run with --fix to apply automatically.[/dim]"
73
+ )
74
+ return []
75
+
76
+ _print_fixable_summary(passport.name, issues)
77
+ if not click.confirm(
78
+ f"\nWould you like IRIS to fix these {len(issues)} issue"
79
+ f"{'s' if len(issues) != 1 else ''} for {passport.name}?",
80
+ default=True,
81
+ ):
82
+ console.print("[dim]Skipped automatic fixes. Apply remediations manually when ready.[/dim]")
83
+ return []
84
+
85
+ return _apply_interactive(issues, passport_file, framework, passport.name)
86
+
87
+
88
+ def _apply_all(
89
+ issues: List[FixableIssue],
90
+ passport_file: Path,
91
+ framework: str,
92
+ agent_name: str,
93
+ ) -> List[FixResult]:
94
+ results: List[FixResult] = []
95
+ for issue in issues:
96
+ result = apply_fix(issue, passport_file, framework)
97
+ results.append(result)
98
+ _print_fix_result(agent_name, issue, result)
99
+ if results:
100
+ console.print(
101
+ Panel(
102
+ f"[bold green]✓ IRIS applied {sum(1 for r in results if r.applied)} fix"
103
+ f"{'es' if sum(1 for r in results if r.applied) != 1 else ''}[/bold green]\n\n"
104
+ f"Re-run: [bold]iris compliance check --agent {agent_name} "
105
+ f"--framework {framework}[/bold]",
106
+ style="green",
107
+ )
108
+ )
109
+ return results
110
+
111
+
112
+ def _apply_interactive(
113
+ issues: List[FixableIssue],
114
+ passport_file: Path,
115
+ framework: str,
116
+ agent_name: str,
117
+ ) -> List[FixResult]:
118
+ results: List[FixResult] = []
119
+ apply_all = False
120
+
121
+ for issue in issues:
122
+ if apply_all:
123
+ result = apply_fix(issue, passport_file, framework)
124
+ results.append(result)
125
+ _print_fix_result(agent_name, issue, result)
126
+ continue
127
+
128
+ console.print(
129
+ f"\n[bold]IRIS can fix[/bold] [{issue.rule_id}] {issue.preview}\n"
130
+ f"[dim]{issue.message}[/dim]"
131
+ )
132
+ console.print("Apply this fix? ([yes]/no/skip/all)")
133
+ choice = click.prompt("", default="no", show_default=False).strip().lower()
134
+
135
+ if choice in ("yes", "y"):
136
+ result = apply_fix(issue, passport_file, framework)
137
+ results.append(result)
138
+ _print_fix_result(agent_name, issue, result)
139
+ elif choice == "all":
140
+ apply_all = True
141
+ result = apply_fix(issue, passport_file, framework)
142
+ results.append(result)
143
+ _print_fix_result(agent_name, issue, result)
144
+ elif choice == "skip":
145
+ console.print("[dim]Skipped.[/dim]")
146
+ else:
147
+ console.print("[dim]Not applied — fix manually when ready.[/dim]")
148
+
149
+ applied_count = sum(1 for r in results if r.applied)
150
+ if applied_count:
151
+ console.print(
152
+ Panel(
153
+ f"[bold green]✓ IRIS applied {applied_count} fix"
154
+ f"{'es' if applied_count != 1 else ''}[/bold green]\n\n"
155
+ f"Re-run: [bold]iris compliance check --agent {agent_name} "
156
+ f"--framework {framework}[/bold]",
157
+ style="green",
158
+ )
159
+ )
160
+ return results
161
+
162
+
163
+ def _print_fix_result(agent_name: str, issue: FixableIssue, result: FixResult) -> None:
164
+ if result.applied:
165
+ change_text = "\n".join(f" [dim]• {change}[/dim]" for change in result.changes)
166
+ console.print(f"[green]✓ Applied[/green] [{issue.rule_id}]\n{change_text}")
167
+ return
168
+ if result.skipped_reason:
169
+ console.print(
170
+ f"[yellow]⚠ Skipped[/yellow] [{issue.rule_id}]: {result.skipped_reason}"
171
+ )
172
+
173
+
174
+ def recheck_after_fixes(
175
+ passport: AgentPassport,
176
+ framework: str,
177
+ governance_dir: Path,
178
+ applied: List[FixResult],
179
+ ) -> Optional[FrameworkCheckResult]:
180
+ """Re-run check and show delta when fixes were applied."""
181
+ if not any(result.applied for result in applied):
182
+ return None
183
+
184
+ passport_file = governance_dir / passport.name / "passport.yaml"
185
+ if not passport_file.exists():
186
+ return None
187
+
188
+ updated = AgentPassport.from_yaml(passport_file.read_text())
189
+ result = run_framework_check(updated, framework)
190
+ remaining = collect_fixable_issues(updated, framework, result)
191
+ fail_count = sum(1 for rule in result.rule_results if rule.status == "FAIL")
192
+ fail_count += len(result.violations)
193
+
194
+ console.print(
195
+ Panel(
196
+ f"[bold]Re-check: {updated.name}[/bold]\n"
197
+ f"Remaining failures: {fail_count}\n"
198
+ f"Auto-fixable remaining: {len(remaining)}",
199
+ style="cyan",
200
+ )
201
+ )
202
+ return result