iris-security-cli 0.1.4__tar.gz → 0.1.7__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.7}/PKG-INFO +3 -3
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/assess.py +2 -2
- iris_security_cli-0.1.7/iris_cli/compliance_check_cmd.py +127 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/drift.py +11 -12
- iris_security_cli-0.1.7/iris_cli/entitlements_cmd.py +16 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/framework_test.py +151 -25
- iris_security_cli-0.1.7/iris_cli/license_cmd.py +153 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/main.py +14 -150
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/redteam.py +2 -2
- iris_security_cli-0.1.7/iris_cli/regulatory.py +365 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/status_cmd.py +3 -0
- iris_security_cli-0.1.7/iris_cli/vault.py +219 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/PKG-INFO +3 -3
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/SOURCES.txt +6 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/requires.txt +2 -2
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/pyproject.toml +3 -3
- iris_security_cli-0.1.7/tests/test_entitlements_display.py +135 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/tests/test_framework_test.py +1 -1
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/README.md +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/action_plan.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/compiler_config.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/cost.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/dlp_cmd.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/evidence.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/explain.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/framework_suggest.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/mcp_server.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/policy_cache.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/scan_govern.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/scm.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/users.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_cli/watch.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/setup.cfg +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/tests/test_framework_suggest.py +0 -0
- {iris_security_cli-0.1.4 → iris_security_cli-0.1.7}/tests/test_policy_diff.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iris-security-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: IRIS CLI — iris scan, iris register, iris policy, iris compliance
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
@@ -15,8 +15,8 @@ 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.6
|
|
19
|
+
Requires-Dist: iris-security-sdk>=0.1.7
|
|
20
20
|
Requires-Dist: click>=8.1
|
|
21
21
|
Requires-Dist: rich>=13.0
|
|
22
22
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -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,127 @@
|
|
|
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
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _discover_passports(
|
|
21
|
+
gov_dir: Path,
|
|
22
|
+
agent: Optional[str],
|
|
23
|
+
) -> list[AgentPassport]:
|
|
24
|
+
passports: list[AgentPassport] = []
|
|
25
|
+
if agent:
|
|
26
|
+
passport_file = gov_dir / agent / "passport.yaml"
|
|
27
|
+
if passport_file.exists():
|
|
28
|
+
passports.append(AgentPassport.from_yaml(passport_file.read_text()))
|
|
29
|
+
return passports
|
|
30
|
+
|
|
31
|
+
for passport_file in gov_dir.rglob("passport.yaml"):
|
|
32
|
+
try:
|
|
33
|
+
passports.append(AgentPassport.from_yaml(passport_file.read_text()))
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
return passports
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.command("check")
|
|
40
|
+
@click.option("--agent", default=None, help="Specific agent to check (or all)")
|
|
41
|
+
@click.option("--framework", "-f", default="colorado-ai-act")
|
|
42
|
+
@click.option("--dir", "governance_dir", type=click.Path(path_type=Path), default=None)
|
|
43
|
+
def compliance_check_cmd(
|
|
44
|
+
agent: Optional[str],
|
|
45
|
+
framework: str,
|
|
46
|
+
governance_dir: Optional[Path],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Check an agent against a compliance framework.
|
|
50
|
+
|
|
51
|
+
Shows a detailed breakdown of which rules pass and fail,
|
|
52
|
+
with plain-English remediation guidance for each failure.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
iris compliance check --framework colorado-ai-act
|
|
56
|
+
iris compliance check --agent payment-agent --framework hipaa
|
|
57
|
+
"""
|
|
58
|
+
gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
|
|
59
|
+
passports = _discover_passports(gov_dir, agent)
|
|
60
|
+
if not passports:
|
|
61
|
+
console.print("[yellow]No agent passports found.[/yellow]")
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
all_pass = True
|
|
65
|
+
for passport in passports:
|
|
66
|
+
result = run_framework_check(passport, framework)
|
|
67
|
+
|
|
68
|
+
if result.preview_only:
|
|
69
|
+
preview_lines = [
|
|
70
|
+
f"{rule.rule_id} {rule.severity} {rule.name} [{rule.status}]"
|
|
71
|
+
for rule in result.rule_results
|
|
72
|
+
]
|
|
73
|
+
console.print(
|
|
74
|
+
build_pro_preview_box(
|
|
75
|
+
framework,
|
|
76
|
+
result.framework_name,
|
|
77
|
+
result.total_controls,
|
|
78
|
+
preview_lines,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
all_pass = False
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
has_failures = any(rule.status == "FAIL" for rule in result.rule_results) or bool(
|
|
85
|
+
result.violations
|
|
86
|
+
)
|
|
87
|
+
status = "[bold green]PASS[/bold green]" if not has_failures else "[bold red]FAIL[/bold red]"
|
|
88
|
+
console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
|
|
89
|
+
|
|
90
|
+
if result.violations:
|
|
91
|
+
all_pass = False
|
|
92
|
+
for violation in result.violations:
|
|
93
|
+
console.print(f" [red]✗[/red] [{violation.rule_id}] {violation.message}")
|
|
94
|
+
if violation.remediation:
|
|
95
|
+
console.print(f" [yellow]→[/yellow] {violation.remediation}")
|
|
96
|
+
|
|
97
|
+
shown_ids = {v.rule_id for v in result.violations}
|
|
98
|
+
for rule in result.rule_results:
|
|
99
|
+
if rule.rule_id in shown_ids:
|
|
100
|
+
continue
|
|
101
|
+
if rule.status == "PASS":
|
|
102
|
+
console.print(f" [green]✓[/green] [{rule.rule_id}] {rule.name}")
|
|
103
|
+
elif rule.status == "FAIL":
|
|
104
|
+
all_pass = False
|
|
105
|
+
console.print(f" [red]✗[/red] [{rule.rule_id}] {rule.name}")
|
|
106
|
+
if rule.message:
|
|
107
|
+
console.print(f" {rule.message}")
|
|
108
|
+
if rule.remediation:
|
|
109
|
+
console.print(f" [yellow]→[/yellow] {rule.remediation}")
|
|
110
|
+
|
|
111
|
+
if not result.violations and not any(r.status == "FAIL" for r in result.rule_results):
|
|
112
|
+
console.print(f" [green]✓[/green] All {framework} rules satisfied")
|
|
113
|
+
|
|
114
|
+
for guidance in result.guidance:
|
|
115
|
+
if guidance.status == ComplianceCheckStatus.GUIDANCE:
|
|
116
|
+
console.print(f" [blue]ℹ[/blue] [{guidance.rule_id}] {guidance.message}")
|
|
117
|
+
if guidance.remediation:
|
|
118
|
+
console.print(f" [yellow]→[/yellow] {guidance.remediation}")
|
|
119
|
+
elif guidance.status == ComplianceCheckStatus.PRO_REQUIRED:
|
|
120
|
+
console.print(f" [yellow]⚠[/yellow] [{guidance.rule_id}] {guidance.message}")
|
|
121
|
+
if guidance.remediation:
|
|
122
|
+
console.print(f" [yellow]→[/yellow] {guidance.remediation}")
|
|
123
|
+
|
|
124
|
+
for note in result.check_notes:
|
|
125
|
+
console.print(f" [dim]ℹ {note}[/dim]")
|
|
126
|
+
|
|
127
|
+
sys.exit(0 if all_pass else 1)
|
|
@@ -13,7 +13,7 @@ import click
|
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
from rich.table import Table
|
|
15
15
|
|
|
16
|
-
from iris_core.
|
|
16
|
+
from iris_core.entitlements import Entitlements, Feature
|
|
17
17
|
from iris_core.drift.detector import DriftDetector, DriftReport
|
|
18
18
|
from iris_core.drift.notifier import DriftNotifier, load_alert_config, save_alert_config
|
|
19
19
|
|
|
@@ -24,15 +24,14 @@ def _gov_dir(governance_dir: Path | None) -> Path:
|
|
|
24
24
|
return governance_dir or Path.cwd() / "governance" / "agents"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def _require_pro_license(feature:
|
|
28
|
-
|
|
29
|
-
if not
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
sys.exit(1)
|
|
27
|
+
def _require_pro_license(feature_name: str, feature: Feature) -> None:
|
|
28
|
+
ents = Entitlements()
|
|
29
|
+
if not ents.has(feature):
|
|
30
|
+
try:
|
|
31
|
+
ents.require(feature, context=feature_name)
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
console.print(str(exc))
|
|
34
|
+
sys.exit(1)
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def _render_check_table(report: DriftReport) -> None:
|
|
@@ -249,10 +248,10 @@ def drift_alert_config(slack_webhook: Optional[str], email: Optional[str], webho
|
|
|
249
248
|
config = load_alert_config()
|
|
250
249
|
|
|
251
250
|
if slack_webhook:
|
|
252
|
-
_require_pro_license("Slack drift alerts")
|
|
251
|
+
_require_pro_license("Slack drift alerts", Feature.DRIFT_SLACK_ALERT)
|
|
253
252
|
config["slack_webhook"] = slack_webhook
|
|
254
253
|
if email:
|
|
255
|
-
_require_pro_license("Email drift alerts")
|
|
254
|
+
_require_pro_license("Email drift alerts", Feature.DRIFT_EMAIL_ALERT)
|
|
256
255
|
config["email"] = email
|
|
257
256
|
if webhook:
|
|
258
257
|
config["webhook"] = webhook
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""iris entitlements — transparent feature map by tier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from iris_core.entitlements.display import build_entitlements_panel
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("entitlements")
|
|
14
|
+
def entitlements_cmd() -> None:
|
|
15
|
+
"""Show the complete IRIS feature map — what is available at each tier."""
|
|
16
|
+
console.print(build_entitlements_panel())
|
|
@@ -12,9 +12,9 @@ import click
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.panel import Panel
|
|
14
14
|
|
|
15
|
-
from iris_core.
|
|
15
|
+
from iris_core.entitlements import Entitlements, Feature
|
|
16
16
|
from iris_core.evidence.vault import EvidenceVault
|
|
17
|
-
from iris_core.models.passport import AgentPassport
|
|
17
|
+
from iris_core.models.passport import AgentPassport, DataClassification
|
|
18
18
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
@@ -27,6 +27,17 @@ PRO_BUNDLES = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
|
|
30
|
+
MS25_PARTIAL_MISSING = (
|
|
31
|
+
"Free tier provides 30-day local retention. "
|
|
32
|
+
"NIST AI RMF MS-2.5 recommends ongoing risk tracking. "
|
|
33
|
+
"30 days of history partially satisfies this control."
|
|
34
|
+
)
|
|
35
|
+
MS25_PARTIAL_FIX = (
|
|
36
|
+
"For full satisfaction:\n"
|
|
37
|
+
"iris license activate <key>\n"
|
|
38
|
+
"This enables unlimited Evidence Vault retention so your "
|
|
39
|
+
"complete governance history satisfies MS-2.5."
|
|
40
|
+
)
|
|
30
41
|
EFFORT_BY_REASON = {
|
|
31
42
|
"infrastructure": "1 week",
|
|
32
43
|
"document": "1 hour",
|
|
@@ -190,10 +201,79 @@ def _framework_bundle(framework: str) -> Dict[str, Any]:
|
|
|
190
201
|
),
|
|
191
202
|
],
|
|
192
203
|
},
|
|
204
|
+
"hipaa": {
|
|
205
|
+
"bundle_id": "hipaa",
|
|
206
|
+
"full_name": _framework_name("hipaa"),
|
|
207
|
+
"rules": [
|
|
208
|
+
_rule(
|
|
209
|
+
"HIPAA-001",
|
|
210
|
+
"PHI data classification",
|
|
211
|
+
"CRITICAL",
|
|
212
|
+
"passport.data_classification == DataClassification.PHI",
|
|
213
|
+
"PHI data classification missing",
|
|
214
|
+
"Set data_classification=PHI in passport",
|
|
215
|
+
"passport",
|
|
216
|
+
),
|
|
217
|
+
_rule(
|
|
218
|
+
"HIPAA-002",
|
|
219
|
+
"Evidence Vault PHI audit trail",
|
|
220
|
+
"HIGH",
|
|
221
|
+
"vault.total_evaluations > 0",
|
|
222
|
+
"Evidence Vault not recording PHI",
|
|
223
|
+
"Ensure IRIS client is wrapping LLM calls",
|
|
224
|
+
"process",
|
|
225
|
+
),
|
|
226
|
+
_rule(
|
|
227
|
+
"HIPAA-003",
|
|
228
|
+
"Region restriction for PHI",
|
|
229
|
+
"HIGH",
|
|
230
|
+
"len(passport.allowed_regions) > 0",
|
|
231
|
+
"No region restriction declared",
|
|
232
|
+
"Add allowed_regions to passport",
|
|
233
|
+
"passport",
|
|
234
|
+
),
|
|
235
|
+
_rule(
|
|
236
|
+
"HIPAA-004",
|
|
237
|
+
"Minimum necessary tool permissions",
|
|
238
|
+
"HIGH",
|
|
239
|
+
"len(passport.tool_permissions) > 0",
|
|
240
|
+
"No minimum-necessary tool permissions declared",
|
|
241
|
+
"Declare tool_permissions in passport.yaml",
|
|
242
|
+
"passport",
|
|
243
|
+
),
|
|
244
|
+
_rule(
|
|
245
|
+
"HIPAA-005",
|
|
246
|
+
"Business Associate Agreement evidence",
|
|
247
|
+
"CRITICAL",
|
|
248
|
+
"passport.evidence_vault_id is not None",
|
|
249
|
+
"BAA evidence not linked in Evidence Vault",
|
|
250
|
+
f"Run: iris evidence init --agent <agent>",
|
|
251
|
+
"document",
|
|
252
|
+
),
|
|
253
|
+
_rule(
|
|
254
|
+
"HIPAA-006",
|
|
255
|
+
"Breach notification readiness",
|
|
256
|
+
"CRITICAL",
|
|
257
|
+
"files['policy_cedar_exists']",
|
|
258
|
+
"Runtime policy enforcement not configured",
|
|
259
|
+
"Run: iris policy compile --agent <agent>",
|
|
260
|
+
"document",
|
|
261
|
+
),
|
|
262
|
+
_rule(
|
|
263
|
+
"HIPAA-007",
|
|
264
|
+
"De-identification policy",
|
|
265
|
+
"HIGH",
|
|
266
|
+
"passport.intent_ref is not None",
|
|
267
|
+
"De-identification policy not declared",
|
|
268
|
+
"Add policy-intent.md and run iris policy compile",
|
|
269
|
+
"document",
|
|
270
|
+
),
|
|
271
|
+
],
|
|
272
|
+
},
|
|
193
273
|
}
|
|
194
274
|
|
|
195
275
|
# Reuse NIST-style checks for paid frameworks until dedicated bundles are added.
|
|
196
|
-
for paid_alias in ("fedramp-moderate", "
|
|
276
|
+
for paid_alias in ("fedramp-moderate", "soc2", "gdpr"):
|
|
197
277
|
alias_bundle = dict(bundles["nist-ai-rmf"])
|
|
198
278
|
alias_bundle["bundle_id"] = paid_alias
|
|
199
279
|
alias_bundle["full_name"] = _framework_name(paid_alias)
|
|
@@ -210,6 +290,7 @@ def _evaluate_check(check_expr: str, passport: AgentPassport, vault_summary: Any
|
|
|
210
290
|
"passport": passport,
|
|
211
291
|
"vault": vault_summary,
|
|
212
292
|
"files": files,
|
|
293
|
+
"DataClassification": DataClassification,
|
|
213
294
|
"bool": bool,
|
|
214
295
|
"len": len,
|
|
215
296
|
"all": all,
|
|
@@ -284,6 +365,13 @@ def _build_result(framework: str, agent: str, passport: AgentPassport) -> Framew
|
|
|
284
365
|
|
|
285
366
|
for rule in bundle["rules"]:
|
|
286
367
|
status = _evaluate_check(rule["check"], passport, summary, files)
|
|
368
|
+
if (
|
|
369
|
+
rule["rule_id"] == "MS-2.5"
|
|
370
|
+
and status == "FAIL"
|
|
371
|
+
and not Entitlements().has(Feature.VAULT_UNLIMITED_RETENTION)
|
|
372
|
+
):
|
|
373
|
+
status = "PARTIAL"
|
|
374
|
+
|
|
287
375
|
if status == "PASS":
|
|
288
376
|
pass_count += 1
|
|
289
377
|
continue
|
|
@@ -296,14 +384,20 @@ def _build_result(framework: str, agent: str, passport: AgentPassport) -> Framew
|
|
|
296
384
|
fail_count += 1
|
|
297
385
|
status = "FAIL"
|
|
298
386
|
|
|
387
|
+
what_is_missing = rule["what_is_missing"]
|
|
388
|
+
how_to_fix = rule["how_to_fix"]
|
|
389
|
+
if rule["rule_id"] == "MS-2.5" and status == "PARTIAL":
|
|
390
|
+
what_is_missing = MS25_PARTIAL_MISSING
|
|
391
|
+
how_to_fix = MS25_PARTIAL_FIX
|
|
392
|
+
|
|
299
393
|
gaps.append(
|
|
300
394
|
ControlGap(
|
|
301
395
|
rule_id=rule["rule_id"],
|
|
302
396
|
name=rule["name"],
|
|
303
397
|
severity=rule["severity"],
|
|
304
398
|
status=status,
|
|
305
|
-
what_is_missing=
|
|
306
|
-
how_to_fix=
|
|
399
|
+
what_is_missing=what_is_missing,
|
|
400
|
+
how_to_fix=how_to_fix.replace("<agent>", agent),
|
|
307
401
|
estimated_effort=rule["estimated_effort"],
|
|
308
402
|
)
|
|
309
403
|
)
|
|
@@ -329,12 +423,51 @@ def _build_result(framework: str, agent: str, passport: AgentPassport) -> Framew
|
|
|
329
423
|
)
|
|
330
424
|
|
|
331
425
|
|
|
426
|
+
def _render_free_tier_panel(result: FrameworkTestResult) -> str:
|
|
427
|
+
framework_label = _framework_name(result.framework)
|
|
428
|
+
evaluated = result.passed_controls + result.failed_controls + result.partial_controls
|
|
429
|
+
filled = int((evaluated / max(1, result.total_controls)) * 10)
|
|
430
|
+
bar = f"{'█' * filled}{'░' * (10 - filled)}"
|
|
431
|
+
title = f"{framework_label} Test: {result.agent_name}"
|
|
432
|
+
pad = max(0, 60 - len(title) - 4)
|
|
433
|
+
lines = [
|
|
434
|
+
f"┌─ {title} {'─' * pad}┐",
|
|
435
|
+
"│ Certification Readiness │",
|
|
436
|
+
f"│ {bar} {evaluated} / {result.total_controls} controls evaluated{' ' * max(0, 22 - len(str(evaluated)) - len(str(result.total_controls)))}│",
|
|
437
|
+
"│ Score: requires Pro for full evaluation │",
|
|
438
|
+
"│ │",
|
|
439
|
+
"│ TOP 3 GAPS (free preview) │",
|
|
440
|
+
]
|
|
441
|
+
for index, gap in enumerate(result.gaps[:3], start=1):
|
|
442
|
+
lines.append(
|
|
443
|
+
f"│ {index}. [{gap.severity}] {gap.rule_id} — {gap.name[:35]:<35} │"
|
|
444
|
+
)
|
|
445
|
+
fix = gap.how_to_fix.split("\n")[0][:48]
|
|
446
|
+
lines.append(f"│ Fix: {fix:<51}│")
|
|
447
|
+
lines.append(f"│ Time: {gap.estimated_effort:<49}│")
|
|
448
|
+
if index < min(3, len(result.gaps)):
|
|
449
|
+
lines.append("│ │")
|
|
450
|
+
total = result.total_controls
|
|
451
|
+
lines.extend(
|
|
452
|
+
[
|
|
453
|
+
"│ │",
|
|
454
|
+
f"│ See all {total} controls + evidence package:{' ' * max(0, 24 - len(str(total)))}│",
|
|
455
|
+
"│ iris license activate <key> │",
|
|
456
|
+
"└─────────────────────────────────────────────────────────────┘",
|
|
457
|
+
]
|
|
458
|
+
)
|
|
459
|
+
return "\n".join(lines)
|
|
460
|
+
|
|
461
|
+
|
|
332
462
|
def _render_table(result: FrameworkTestResult, has_pro: bool) -> None:
|
|
463
|
+
if not has_pro:
|
|
464
|
+
console.print(_render_free_tier_panel(result))
|
|
465
|
+
return
|
|
466
|
+
|
|
333
467
|
framework_label = _framework_name(result.framework)
|
|
334
|
-
subtitle = "IRIS Pro active" if has_pro else "IRIS Pro required for full report"
|
|
335
468
|
console.print(
|
|
336
469
|
Panel(
|
|
337
|
-
f"Framework: {framework_label}\nGenerated: {datetime.utcnow().strftime('%Y-%m-%d')} |
|
|
470
|
+
f"Framework: {framework_label}\nGenerated: {datetime.utcnow().strftime('%Y-%m-%d')} | IRIS Pro active",
|
|
338
471
|
title=f"{framework_label} Test — {result.agent_name}",
|
|
339
472
|
style="blue",
|
|
340
473
|
)
|
|
@@ -351,29 +484,20 @@ def _render_table(result: FrameworkTestResult, has_pro: bool) -> None:
|
|
|
351
484
|
f"PASSED ({result.passed_controls}) FAILED ({result.failed_controls}) "
|
|
352
485
|
f"PARTIAL ({result.partial_controls}) N/A ({result.not_applicable})"
|
|
353
486
|
)
|
|
354
|
-
if
|
|
487
|
+
if result.progress_delta_percent is not None:
|
|
355
488
|
delta = result.progress_delta_percent
|
|
356
489
|
sign = "+" if delta >= 0 else ""
|
|
357
490
|
console.print(f"Progress since last run: [bold]{sign}{delta}%[/bold]")
|
|
358
491
|
|
|
359
|
-
|
|
360
|
-
hidden = max(0, len(result.gaps) - len(gaps))
|
|
361
|
-
title = "ALL GAPS" if has_pro else f"TOP 3 GAPS (upgrade to IRIS Pro to see all {len(result.gaps)})"
|
|
362
|
-
console.print(f"\n[bold]{title}[/bold]")
|
|
492
|
+
console.print("\n[bold]ALL GAPS[/bold]")
|
|
363
493
|
console.print("─" * 65)
|
|
364
|
-
for gap in gaps:
|
|
494
|
+
for gap in result.gaps:
|
|
365
495
|
console.print(f"[{gap.severity}] {gap.rule_id} — {gap.name}")
|
|
496
|
+
console.print(f"Status: {gap.status}")
|
|
366
497
|
console.print(f"Missing: {gap.what_is_missing}")
|
|
367
498
|
console.print(f"Fix: {gap.how_to_fix}")
|
|
368
499
|
console.print(f"Effort: {gap.estimated_effort}\n")
|
|
369
500
|
|
|
370
|
-
if hidden and not has_pro:
|
|
371
|
-
console.print("─" * 65)
|
|
372
|
-
console.print("See all gaps and generate your evidence package:")
|
|
373
|
-
console.print("iris license activate <your-key>")
|
|
374
|
-
console.print("https://iris.ai/pricing")
|
|
375
|
-
console.print("─" * 65)
|
|
376
|
-
|
|
377
501
|
|
|
378
502
|
def _render_markdown(result: FrameworkTestResult, has_pro: bool) -> str:
|
|
379
503
|
lines = [
|
|
@@ -440,9 +564,8 @@ def test(framework: str, agent_name: str, output_format: str, export_format: Opt
|
|
|
440
564
|
raise click.ClickException(f"Passport not found: {passport_path}")
|
|
441
565
|
passport = AgentPassport.from_yaml(passport_path.read_text())
|
|
442
566
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
has_pro = IrisLicense().check("gdpr").valid
|
|
567
|
+
ents = Entitlements()
|
|
568
|
+
has_pro = ents.has(Feature.CLI_TEST_FULL_REPORT)
|
|
446
569
|
|
|
447
570
|
result = _build_result(framework, agent_name, passport)
|
|
448
571
|
previous = _load_previous_score(agent_name, framework)
|
|
@@ -452,8 +575,11 @@ def test(framework: str, agent_name: str, output_format: str, export_format: Opt
|
|
|
452
575
|
saved_path = _save_result(result)
|
|
453
576
|
|
|
454
577
|
if export_format == "pdf":
|
|
455
|
-
if not
|
|
456
|
-
|
|
578
|
+
if not ents.has(Feature.CLI_TEST_PDF_EXPORT):
|
|
579
|
+
try:
|
|
580
|
+
ents.require(Feature.CLI_TEST_PDF_EXPORT, context="PDF export")
|
|
581
|
+
except Exception as exc:
|
|
582
|
+
raise click.ClickException(str(exc)) from exc
|
|
457
583
|
pdf_path = saved_path.with_suffix(".pdf")
|
|
458
584
|
pdf_path.write_text("PDF export placeholder. Use IRIS Pro renderer in hosted pipeline.")
|
|
459
585
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""iris license — IRIS Pro license management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from iris_core.entitlements import Entitlements, Feature, LicenseKey, Tier
|
|
13
|
+
from iris_core.entitlements.features import FEATURE_TIERS, TIER_RANK
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
_STATUS_BOX = """┌─ IRIS License Status ──────────────────────────────────────┐
|
|
18
|
+
│ Tier: {tier:<52}│
|
|
19
|
+
│ Key: {key:<52}│
|
|
20
|
+
│ │
|
|
21
|
+
│ Available now (free forever): │
|
|
22
|
+
│ Colorado AI Act bundle, all LLM integrations, │
|
|
23
|
+
│ Cursor MCP, local Evidence Vault (30 days), │
|
|
24
|
+
│ iris scan, iris status, iris framework suggest │
|
|
25
|
+
│ │
|
|
26
|
+
│ Unlock with Pro: │
|
|
27
|
+
│ NIST AI RMF, FedRAMP, HIPAA, SOC 2, GDPR, EU AI Act │
|
|
28
|
+
│ Unlimited Evidence Vault + cloud sync + PDF export │
|
|
29
|
+
│ K8s sidecar, HITL gate, SCM org scanner │
|
|
30
|
+
│ Team RBAC, SSO, drift alerts, cost anomaly alerts │
|
|
31
|
+
│ │
|
|
32
|
+
│ Activate: iris license activate <your-key> │
|
|
33
|
+
│ Get a key: https://iris.ai/pricing │
|
|
34
|
+
└────────────────────────────────────────────────────────────┘"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mask_key(key: str | None) -> str:
|
|
38
|
+
if not key:
|
|
39
|
+
return "Not activated"
|
|
40
|
+
if len(key) <= 16:
|
|
41
|
+
return key
|
|
42
|
+
return f"{key[:9]}…{key[-4:]}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_test_key(key: str) -> bool:
|
|
46
|
+
return key.startswith("IRIS-TEST-") or key.startswith("IRIS-DEMO-")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _features_for_tier(tier: Tier) -> list[Feature]:
|
|
50
|
+
return [
|
|
51
|
+
feature
|
|
52
|
+
for feature, required in FEATURE_TIERS.items()
|
|
53
|
+
if TIER_RANK[tier] >= TIER_RANK[required]
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _features_exactly_tier(tier: Tier) -> list[Feature]:
|
|
58
|
+
return [feature for feature, required in FEATURE_TIERS.items() if required == tier]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.group()
|
|
62
|
+
def license():
|
|
63
|
+
"""IRIS Pro license management."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@license.command("status")
|
|
68
|
+
def license_status():
|
|
69
|
+
"""Show current license status, tier, and what is unlocked."""
|
|
70
|
+
ents = Entitlements()
|
|
71
|
+
key = ents.license_key
|
|
72
|
+
tier_label = ents.tier_name()
|
|
73
|
+
key_label = _mask_key(key)
|
|
74
|
+
if key and not ents.license_valid:
|
|
75
|
+
tier_label = f"{tier_label} (invalid key: {ents.license_reason})"
|
|
76
|
+
key_label = "Invalid"
|
|
77
|
+
|
|
78
|
+
console.print(_STATUS_BOX.format(tier=tier_label, key=key_label))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@license.command("activate")
|
|
82
|
+
@click.argument("key")
|
|
83
|
+
def license_activate(key: str):
|
|
84
|
+
"""Validate and save an IRIS license key."""
|
|
85
|
+
key = key.strip()
|
|
86
|
+
valid, tier, reason = LicenseKey.validate(key)
|
|
87
|
+
if not valid:
|
|
88
|
+
console.print(
|
|
89
|
+
"[red]Invalid license key format.[/red]\n"
|
|
90
|
+
"Expected: IRIS-XXXX-XXXX-XXXX-XXXX (uppercase letters and digits)\n"
|
|
91
|
+
"Example: IRIS-P123-ABCD-EFGH-5678\n"
|
|
92
|
+
"Get a key: https://iris.ai/pricing"
|
|
93
|
+
)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
LicenseKey.save(key)
|
|
97
|
+
ents = Entitlements()
|
|
98
|
+
test_note = " (test key)" if _is_test_key(key) else ""
|
|
99
|
+
unlocked = _features_for_tier(tier)
|
|
100
|
+
newly = [f.value for f in unlocked if FEATURE_TIERS[f] == tier]
|
|
101
|
+
feature_list = ", ".join(newly[:12])
|
|
102
|
+
if len(newly) > 12:
|
|
103
|
+
feature_list += f", … (+{len(newly) - 12} more)"
|
|
104
|
+
|
|
105
|
+
console.print(
|
|
106
|
+
Panel(
|
|
107
|
+
f"[bold green]✓ License activated{test_note}[/bold green]\n\n"
|
|
108
|
+
f"Tier unlocked: [cyan]{ents.tier_name()}[/cyan]\n"
|
|
109
|
+
f"Saved to: {LicenseKey.license_file_path()}\n\n"
|
|
110
|
+
f"Features unlocked at this tier:\n {feature_list}",
|
|
111
|
+
style="green",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@license.command("deactivate")
|
|
117
|
+
@click.confirmation_option(prompt="Remove your IRIS license key?")
|
|
118
|
+
def license_deactivate():
|
|
119
|
+
"""Remove the license key and revert to the free tier."""
|
|
120
|
+
removed = LicenseKey.clear()
|
|
121
|
+
if removed:
|
|
122
|
+
console.print("[yellow]License deactivated.[/yellow] You are now on the Free tier.")
|
|
123
|
+
else:
|
|
124
|
+
console.print("[dim]No license key was stored.[/dim]")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@license.command("features")
|
|
128
|
+
@click.option("--tier", type=click.Choice(["free", "pro", "enterprise"]), default=None)
|
|
129
|
+
def license_features(tier: str | None):
|
|
130
|
+
"""List features by tier (comparison table by default)."""
|
|
131
|
+
if tier:
|
|
132
|
+
selected = Tier(tier)
|
|
133
|
+
features = _features_exactly_tier(selected)
|
|
134
|
+
console.print(f"\n[bold]{selected.value.title()} tier features[/bold] ({len(features)})\n")
|
|
135
|
+
for feature in sorted(features, key=lambda f: f.value):
|
|
136
|
+
console.print(f" • {feature.value}")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
table = Table(title="IRIS Feature Comparison")
|
|
140
|
+
table.add_column("Feature", style="bold")
|
|
141
|
+
table.add_column("Free")
|
|
142
|
+
table.add_column("Pro")
|
|
143
|
+
table.add_column("Enterprise")
|
|
144
|
+
|
|
145
|
+
for feature in sorted(FEATURE_TIERS, key=lambda f: f.value):
|
|
146
|
+
required = FEATURE_TIERS[feature]
|
|
147
|
+
table.add_row(
|
|
148
|
+
feature.value,
|
|
149
|
+
"✓" if TIER_RANK[Tier.FREE] >= TIER_RANK[required] else "—",
|
|
150
|
+
"✓" if TIER_RANK[Tier.PRO] >= TIER_RANK[required] else "—",
|
|
151
|
+
"✓" if TIER_RANK[Tier.ENTERPRISE] >= TIER_RANK[required] else "—",
|
|
152
|
+
)
|
|
153
|
+
console.print(table)
|