iris-security-cli 0.1.4__tar.gz → 0.1.9__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 (57) hide show
  1. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/PKG-INFO +5 -5
  2. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/assess.py +2 -2
  3. iris_security_cli-0.1.9/iris_cli/certify.py +105 -0
  4. iris_security_cli-0.1.9/iris_cli/compliance_check_cmd.py +180 -0
  5. iris_security_cli-0.1.9/iris_cli/compliance_fix.py +202 -0
  6. iris_security_cli-0.1.9/iris_cli/declare.py +177 -0
  7. iris_security_cli-0.1.9/iris_cli/delegation.py +172 -0
  8. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/drift.py +11 -12
  9. iris_security_cli-0.1.9/iris_cli/enforce.py +148 -0
  10. iris_security_cli-0.1.9/iris_cli/entitlements_cmd.py +16 -0
  11. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/framework_test.py +151 -25
  12. iris_security_cli-0.1.9/iris_cli/license_cmd.py +153 -0
  13. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/main.py +126 -236
  14. iris_security_cli-0.1.9/iris_cli/models_cmd.py +138 -0
  15. iris_security_cli-0.1.9/iris_cli/preview.py +128 -0
  16. iris_security_cli-0.1.9/iris_cli/quickstart.py +423 -0
  17. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/redteam.py +2 -2
  18. iris_security_cli-0.1.9/iris_cli/regulatory.py +365 -0
  19. iris_security_cli-0.1.9/iris_cli/sentinel.py +124 -0
  20. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/status_cmd.py +13 -0
  21. iris_security_cli-0.1.9/iris_cli/vault.py +219 -0
  22. iris_security_cli-0.1.9/iris_cli/witness.py +123 -0
  23. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/PKG-INFO +5 -5
  24. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/SOURCES.txt +20 -1
  25. iris_security_cli-0.1.9/iris_security_cli.egg-info/requires.txt +8 -0
  26. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/pyproject.toml +6 -5
  27. iris_security_cli-0.1.9/tests/test_entitlements_display.py +135 -0
  28. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/tests/test_framework_test.py +1 -1
  29. iris_security_cli-0.1.9/tests/test_mcp_model_governance.py +35 -0
  30. iris_security_cli-0.1.9/tests/test_quickstart.py +95 -0
  31. iris_security_cli-0.1.9/tests/test_vocabulary.py +353 -0
  32. iris_security_cli-0.1.4/iris_security_cli.egg-info/requires.txt +0 -8
  33. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/README.md +0 -0
  34. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/__init__.py +0 -0
  35. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/action_plan.py +0 -0
  36. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/cedar_parser.py +0 -0
  37. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/compiler_config.py +0 -0
  38. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/cost.py +0 -0
  39. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/dlp_cmd.py +0 -0
  40. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/evidence.py +0 -0
  41. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/explain.py +0 -0
  42. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/framework_suggest.py +0 -0
  43. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/mcp_server.py +0 -0
  44. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/policy_cache.py +0 -0
  45. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/policy_diff.py +0 -0
  46. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/scan_govern.py +0 -0
  47. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/scan_report.py +0 -0
  48. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/scm.py +0 -0
  49. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/users.py +0 -0
  50. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/watch.py +0 -0
  51. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/dependency_links.txt +0 -0
  52. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/entry_points.txt +0 -0
  53. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/top_level.txt +0 -0
  54. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/setup.cfg +0 -0
  55. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/tests/test_evidence.py +0 -0
  56. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/tests/test_framework_suggest.py +0 -0
  57. {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/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.4
4
- Summary: IRIS CLI — iris scan, iris register, iris policy, iris compliance
3
+ Version: 0.1.9
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.3
19
- Requires-Dist: iris-security-sdk>=0.1.4
18
+ Requires-Dist: iris-security-core>=0.1.7
19
+ Requires-Dist: iris-security-sdk>=0.1.9
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
 
@@ -21,7 +21,7 @@ from rich.console import Console
21
21
  from rich.panel import Panel
22
22
  from rich.prompt import Prompt, Confirm
23
23
  from rich.table import Table
24
- from rich import print as rprint
24
+ from iris_core import __version__ as IRIS_PLATFORM_VERSION
25
25
 
26
26
  console = Console()
27
27
 
@@ -342,7 +342,7 @@ def generate_assessment_markdown(
342
342
  > **Date**: {now}
343
343
  > **Framework**: Colorado AI Act (SB 26-189), effective January 1, 2027
344
344
  > **Note**: Replaces SB 24-205, signed May 14, 2026
345
- > **Generated by**: IRIS Compliance Platform v0.1.0
345
+ > **Generated by**: IRIS Compliance Platform v{IRIS_PLATFORM_VERSION}
346
346
 
347
347
  ---
348
348
 
@@ -0,0 +1,105 @@
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
+
103
+
104
+ # Legacy alias: iris test → iris certify
105
+ test = certify_cmd
@@ -0,0 +1,180 @@
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
+
12
+ from iris import AgentPassport
13
+ from iris_core.compliance.framework_check import run_framework_check
14
+ from iris_core.compliance.results import ComplianceCheckStatus
15
+ from iris_core.entitlements.display import build_pro_preview_box
16
+
17
+ from iris_cli.compliance_fix import offer_remediation, recheck_after_fixes
18
+ from iris_core.compliance.remediation import collect_fixable_issues
19
+
20
+ console = Console()
21
+
22
+
23
+ def _discover_passports(
24
+ gov_dir: Path,
25
+ agent: Optional[str],
26
+ ) -> list[AgentPassport]:
27
+ passports: list[AgentPassport] = []
28
+ if agent:
29
+ passport_file = gov_dir / agent / "passport.yaml"
30
+ if passport_file.exists():
31
+ passports.append(AgentPassport.from_yaml(passport_file.read_text()))
32
+ return passports
33
+
34
+ for passport_file in gov_dir.rglob("passport.yaml"):
35
+ try:
36
+ passports.append(AgentPassport.from_yaml(passport_file.read_text()))
37
+ except Exception:
38
+ continue
39
+ return passports
40
+
41
+
42
+ def _render_result(
43
+ passport: AgentPassport,
44
+ framework: str,
45
+ result,
46
+ ) -> bool:
47
+ """Print one agent's check result. Returns False if failures were found."""
48
+ if result.preview_only:
49
+ preview_lines = [
50
+ f"{rule.rule_id} {rule.severity} {rule.name} [{rule.status}]"
51
+ for rule in result.rule_results
52
+ ]
53
+ console.print(
54
+ build_pro_preview_box(
55
+ framework,
56
+ result.framework_name,
57
+ result.total_controls,
58
+ preview_lines,
59
+ )
60
+ )
61
+ return False
62
+
63
+ has_failures = any(rule.status == "FAIL" for rule in result.rule_results) or bool(
64
+ result.violations
65
+ )
66
+ status = "[bold green]PASS[/bold green]" if not has_failures else "[bold red]FAIL[/bold red]"
67
+ console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
68
+
69
+ if result.violations:
70
+ for violation in result.violations:
71
+ console.print(f" [red]✗[/red] [{violation.rule_id}] {violation.message}")
72
+ if violation.remediation:
73
+ console.print(f" [yellow]→[/yellow] {violation.remediation}")
74
+
75
+ shown_ids = {v.rule_id for v in result.violations}
76
+ for rule in result.rule_results:
77
+ if rule.rule_id in shown_ids:
78
+ continue
79
+ if rule.status == "PASS":
80
+ console.print(f" [green]✓[/green] [{rule.rule_id}] {rule.name}")
81
+ elif rule.status == "FAIL":
82
+ console.print(f" [red]✗[/red] [{rule.rule_id}] {rule.name}")
83
+ if rule.message:
84
+ console.print(f" {rule.message}")
85
+ if rule.remediation:
86
+ console.print(f" [yellow]→[/yellow] {rule.remediation}")
87
+
88
+ if not result.violations and not any(r.status == "FAIL" for r in result.rule_results):
89
+ console.print(f" [green]✓[/green] All {framework} rules satisfied")
90
+
91
+ for guidance in result.guidance:
92
+ if guidance.status == ComplianceCheckStatus.GUIDANCE:
93
+ console.print(f" [blue]ℹ[/blue] [{guidance.rule_id}] {guidance.message}")
94
+ if guidance.remediation:
95
+ console.print(f" [yellow]→[/yellow] {guidance.remediation}")
96
+ elif guidance.status == ComplianceCheckStatus.PRO_REQUIRED:
97
+ console.print(f" [yellow]⚠[/yellow] [{guidance.rule_id}] {guidance.message}")
98
+ if guidance.remediation:
99
+ console.print(f" [yellow]→[/yellow] {guidance.remediation}")
100
+
101
+ for note in result.check_notes:
102
+ console.print(f" [dim]ℹ {note}[/dim]")
103
+
104
+ return not has_failures
105
+
106
+
107
+ @click.command("check")
108
+ @click.option("--agent", default=None, help="Specific agent to check (or all)")
109
+ @click.option("--framework", "-f", default="colorado-ai-act")
110
+ @click.option("--dir", "governance_dir", type=click.Path(path_type=Path), default=None)
111
+ @click.option(
112
+ "--fix",
113
+ is_flag=True,
114
+ help="Apply all auto-fixable remediations without per-issue prompts",
115
+ )
116
+ @click.option(
117
+ "--yes",
118
+ "-y",
119
+ is_flag=True,
120
+ help="Apply auto-fixes without confirmation (use with --fix)",
121
+ )
122
+ @click.option(
123
+ "--no-fix",
124
+ is_flag=True,
125
+ help="Show remediations only — never offer to apply fixes",
126
+ )
127
+ def compliance_check_cmd(
128
+ agent: Optional[str],
129
+ framework: str,
130
+ governance_dir: Optional[Path],
131
+ fix: bool,
132
+ yes: bool,
133
+ no_fix: bool,
134
+ ) -> None:
135
+ """
136
+ Check an agent against a compliance framework.
137
+
138
+ Shows a detailed breakdown of which rules pass and fail,
139
+ with plain-English remediation guidance for each failure.
140
+ When run interactively, IRIS offers to apply safe fixes automatically.
141
+
142
+ Example:
143
+ iris compliance check --framework colorado-ai-act
144
+ iris compliance check --agent payment-agent --framework hipaa
145
+ iris compliance check --agent payment-agent --framework hipaa --fix -y
146
+ """
147
+ gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
148
+ passports = _discover_passports(gov_dir, agent)
149
+ if not passports:
150
+ console.print("[yellow]No agent passports found.[/yellow]")
151
+ sys.exit(0)
152
+
153
+ all_pass = True
154
+ offer_fixes = bool(agent) or len(passports) == 1
155
+ for passport in passports:
156
+ result = run_framework_check(passport, framework)
157
+ agent_passed = _render_result(passport, framework, result)
158
+ if not agent_passed:
159
+ all_pass = False
160
+
161
+ if offer_fixes:
162
+ applied = offer_remediation(
163
+ passport,
164
+ framework,
165
+ result,
166
+ gov_dir,
167
+ auto_fix=fix,
168
+ yes=yes,
169
+ no_fix=no_fix,
170
+ )
171
+ recheck_after_fixes(passport, framework, gov_dir, applied)
172
+ elif not no_fix and not agent_passed and collect_fixable_issues(
173
+ passport, framework, result
174
+ ):
175
+ console.print(
176
+ f"[dim]Fixable issues for {passport.name}. "
177
+ f"Re-run with --agent {passport.name} to let IRIS apply fixes.[/dim]"
178
+ )
179
+
180
+ 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