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.
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/PKG-INFO +5 -5
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/assess.py +2 -2
- iris_security_cli-0.1.9/iris_cli/certify.py +105 -0
- iris_security_cli-0.1.9/iris_cli/compliance_check_cmd.py +180 -0
- iris_security_cli-0.1.9/iris_cli/compliance_fix.py +202 -0
- iris_security_cli-0.1.9/iris_cli/declare.py +177 -0
- iris_security_cli-0.1.9/iris_cli/delegation.py +172 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/drift.py +11 -12
- iris_security_cli-0.1.9/iris_cli/enforce.py +148 -0
- iris_security_cli-0.1.9/iris_cli/entitlements_cmd.py +16 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/framework_test.py +151 -25
- iris_security_cli-0.1.9/iris_cli/license_cmd.py +153 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/main.py +126 -236
- iris_security_cli-0.1.9/iris_cli/models_cmd.py +138 -0
- iris_security_cli-0.1.9/iris_cli/preview.py +128 -0
- iris_security_cli-0.1.9/iris_cli/quickstart.py +423 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/redteam.py +2 -2
- iris_security_cli-0.1.9/iris_cli/regulatory.py +365 -0
- iris_security_cli-0.1.9/iris_cli/sentinel.py +124 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/status_cmd.py +13 -0
- iris_security_cli-0.1.9/iris_cli/vault.py +219 -0
- iris_security_cli-0.1.9/iris_cli/witness.py +123 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/PKG-INFO +5 -5
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/SOURCES.txt +20 -1
- iris_security_cli-0.1.9/iris_security_cli.egg-info/requires.txt +8 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/pyproject.toml +6 -5
- iris_security_cli-0.1.9/tests/test_entitlements_display.py +135 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/tests/test_framework_test.py +1 -1
- iris_security_cli-0.1.9/tests/test_mcp_model_governance.py +35 -0
- iris_security_cli-0.1.9/tests/test_quickstart.py +95 -0
- iris_security_cli-0.1.9/tests/test_vocabulary.py +353 -0
- iris_security_cli-0.1.4/iris_security_cli.egg-info/requires.txt +0 -8
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/README.md +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/action_plan.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/compiler_config.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/cost.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/dlp_cmd.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/evidence.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/explain.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/framework_suggest.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/mcp_server.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/policy_cache.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/scan_govern.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/scm.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/users.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_cli/watch.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/setup.cfg +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.9}/tests/test_framework_suggest.py +0 -0
- {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
|
-
Summary: IRIS CLI —
|
|
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.
|
|
19
|
-
Requires-Dist: iris-security-sdk>=0.1.
|
|
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.
|
|
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
|
|
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
|
|
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
|