iris-security-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,467 @@
1
+ """
2
+ iris policy diff — show Cedar rule changes before compiling.
3
+
4
+ Fully offline by default: compares policy.cedar on disk against a cached
5
+ policy-draft.cedar produced by the developer's last compile/dry-run.
6
+
7
+ To refresh the draft (uses the developer's own LLM key):
8
+ iris policy compile --agent <name> --dry-run
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ import click
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import List, Optional
19
+
20
+ from iris_cli.cedar_parser import (
21
+ CedarDiff,
22
+ CedarRule,
23
+ _environment_scope,
24
+ diff_cedar,
25
+ parse_cedar,
26
+ summarize_diffs,
27
+ )
28
+ from iris_cli.policy_cache import DraftCacheStatus, check_draft_cache, load_cached_draft
29
+
30
+
31
+ @dataclass
32
+ class PolicyDiffResult:
33
+ agent: str
34
+ diffs: List[CedarDiff]
35
+ summary: dict
36
+ compare_label: str = "policy.cedar → policy-draft.cedar"
37
+ old_source: str = "policy.cedar"
38
+ new_source: str = "policy-draft.cedar"
39
+ draft_stale: bool = False
40
+ draft_status: Optional[DraftCacheStatus] = None
41
+
42
+
43
+ def run_policy_diff(
44
+ agent: str,
45
+ governance_dir: Optional[Path] = None,
46
+ from_branch: Optional[str] = None,
47
+ verbose: bool = False,
48
+ draft_cedar: Optional[str] = None,
49
+ draft_path: Optional[Path] = None,
50
+ compile: bool = False,
51
+ ) -> PolicyDiffResult:
52
+ """
53
+ Compare policy.cedar against a cached or explicit Cedar draft.
54
+
55
+ Offline by default — reads policy-draft.cedar from the last compile/dry-run.
56
+ Pass compile=True to regenerate the draft via the developer's LLM (opt-in).
57
+ """
58
+ gov_dir = governance_dir or Path.cwd() / "governance" / "agents" / agent
59
+ passport_file = gov_dir / "passport.yaml"
60
+ intent_file = gov_dir / "policy-intent.md"
61
+ cedar_file = gov_dir / "policy.cedar"
62
+
63
+ if not passport_file.exists():
64
+ raise FileNotFoundError(f"Passport not found: {passport_file}")
65
+ if not intent_file.exists():
66
+ raise FileNotFoundError(f"Intent file not found: {intent_file}")
67
+
68
+ intent_text = intent_file.read_text()
69
+ draft_status = check_draft_cache(gov_dir, intent_text)
70
+
71
+ if from_branch:
72
+ old_cedar = _load_cedar_from_git(gov_dir, from_branch, agent)
73
+ compare_label = f"Current: policy.cedar@{from_branch} → Draft: compiled from intent"
74
+ old_source = f"policy.cedar@{from_branch}"
75
+ elif cedar_file.exists():
76
+ old_cedar = cedar_file.read_text()
77
+ compare_label = "Current: policy.cedar → Draft: compiled from intent"
78
+ old_source = "policy.cedar"
79
+ else:
80
+ old_cedar = ""
81
+ compare_label = "Current: (none) → Draft: compiled from intent"
82
+ old_source = "(none)"
83
+
84
+ if draft_cedar is not None:
85
+ new_cedar = draft_cedar
86
+ new_source = "injected draft"
87
+ elif draft_path is not None:
88
+ if not draft_path.exists():
89
+ raise FileNotFoundError(f"Draft file not found: {draft_path}")
90
+ new_cedar = draft_path.read_text()
91
+ new_source = str(draft_path)
92
+ elif compile:
93
+ new_cedar = _compile_draft(gov_dir, intent_text)
94
+ draft_status = check_draft_cache(gov_dir, intent_text)
95
+ new_source = "policy-draft.cedar (just compiled)"
96
+ else:
97
+ new_cedar, draft_status = load_cached_draft(gov_dir, intent_text)
98
+ new_source = "policy-draft.cedar"
99
+
100
+ old_rules = parse_cedar(old_cedar)
101
+ new_rules = parse_cedar(new_cedar)
102
+ diffs = diff_cedar(old_rules, new_rules)
103
+
104
+ if not verbose:
105
+ diffs = [d for d in diffs if d.status != "UNCHANGED"]
106
+
107
+ summary = summarize_diffs(diff_cedar(old_rules, new_rules))
108
+
109
+ return PolicyDiffResult(
110
+ agent=agent,
111
+ diffs=diffs,
112
+ summary=summary,
113
+ compare_label=compare_label,
114
+ old_source=old_source,
115
+ new_source=new_source,
116
+ draft_stale=draft_status.is_stale if draft_status else False,
117
+ draft_status=draft_status,
118
+ )
119
+
120
+
121
+ def _compile_draft(gov_dir: Path, intent_text: str) -> str:
122
+ from iris_core.models.passport import AgentPassport
123
+
124
+ from iris_cli.compiler_config import compiler_info, create_policy_compiler
125
+ from iris_cli.policy_cache import save_policy_draft
126
+
127
+ passport = AgentPassport.from_yaml((gov_dir / "passport.yaml").read_text())
128
+ compiler = create_policy_compiler()
129
+ result = compiler.compile(intent_text, passport, dry_run=True)
130
+ if not result.cedar_policy:
131
+ raise RuntimeError(
132
+ "Policy compiler returned empty Cedar.\n"
133
+ "Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or configure ~/.iris/config.yaml"
134
+ )
135
+ backend, model = compiler_info(compiler)
136
+ save_policy_draft(gov_dir, intent_text, result.cedar_policy, backend, model)
137
+ return result.cedar_policy
138
+
139
+
140
+ def _load_cedar_from_git(gov_dir: Path, branch: str, agent: str) -> str:
141
+ rel = f"governance/agents/{agent}/policy.cedar"
142
+ cwd = gov_dir
143
+ while cwd != cwd.parent:
144
+ if (cwd / ".git").exists():
145
+ break
146
+ cwd = cwd.parent
147
+ else:
148
+ cwd = Path.cwd()
149
+
150
+ try:
151
+ result = subprocess.run(
152
+ ["git", "show", f"{branch}:{rel}"],
153
+ capture_output=True,
154
+ text=True,
155
+ check=True,
156
+ cwd=cwd,
157
+ )
158
+ return result.stdout
159
+ except (subprocess.CalledProcessError, FileNotFoundError):
160
+ cedar_file = gov_dir / "policy.cedar"
161
+ if cedar_file.exists():
162
+ return cedar_file.read_text()
163
+ return ""
164
+
165
+
166
+ def _print_diff_entry(console, diff: CedarDiff) -> None:
167
+ refs = ", ".join(f"[{r}]" for r in diff.compliance_affected) or "[—]"
168
+ rule = diff.new_rule or diff.old_rule
169
+ english = rule.plain_english if rule else ""
170
+
171
+ if diff.status == "ADDED":
172
+ console.print(f"\nADDED {refs} {english}")
173
+ elif diff.status == "REMOVED":
174
+ console.print(f"\nREMOVED {refs} {english}")
175
+ elif diff.status == "MODIFIED":
176
+ console.print(f"\n~ MODIFIED {refs} {_modified_summary(diff)}")
177
+ if diff.old_rule and diff.new_rule:
178
+ console.print(
179
+ f"Was: {_condition_summary(diff.old_rule)} → "
180
+ f"Now: {_condition_summary(diff.new_rule)}"
181
+ )
182
+ else:
183
+ console.print(f"\n[dim]UNCHANGED[/dim] {refs} {english}")
184
+
185
+ risk_color = {
186
+ "INCREASED": "red",
187
+ "DECREASED": "green",
188
+ "NEUTRAL": "yellow",
189
+ }.get(diff.risk_delta, "white")
190
+ console.print(
191
+ f"Risk: [{risk_color}]{diff.risk_delta}[/{risk_color}] — {diff.risk_reason}"
192
+ )
193
+
194
+
195
+ def _modified_summary(diff: CedarDiff) -> str:
196
+ if not diff.old_rule or not diff.new_rule:
197
+ return diff.new_rule.plain_english if diff.new_rule else ""
198
+ old_env = _environment_scope(diff.old_rule)
199
+ new_env = _environment_scope(diff.new_rule)
200
+ resource = _resource_label(diff.new_rule)
201
+ if old_env != new_env:
202
+ return f"{resource} now restricted to {new_env} only"
203
+ return diff.new_rule.plain_english
204
+
205
+
206
+ def _resource_label(rule: CedarRule) -> str:
207
+ quoted = __import__("re").search(r'"([^"]+)"', rule.resource)
208
+ if quoted:
209
+ name = quoted.group(1).replace("-", " ")
210
+ if "API" in rule.resource:
211
+ return f"{name.title()} API"
212
+ return name.title()
213
+ return "Resource"
214
+
215
+
216
+ def _condition_summary(rule: CedarRule) -> str:
217
+ scope = _environment_scope(rule)
218
+ if scope != "unspecified scope":
219
+ return scope
220
+ if rule.conditions:
221
+ return f"{len(rule.conditions)} condition(s)"
222
+ return "no conditions"
223
+
224
+
225
+ def _print_compliance_impact(console, result: PolicyDiffResult) -> None:
226
+ summary = result.summary
227
+ if not any(d.status != "UNCHANGED" for d in result.diffs):
228
+ return
229
+
230
+ console.print("\n[bold]Compliance impact:[/bold]")
231
+ if summary["violations_opened"] == 0:
232
+ console.print("[green]✓ No new violations opened[/green]")
233
+ else:
234
+ console.print(
235
+ f"[red]✗ {summary['violations_opened']} change(s) may open compliance gaps[/red]"
236
+ )
237
+
238
+ if summary["violations_closed"] > 0:
239
+ console.print(
240
+ f"[green]✓ {summary['violations_closed']} change(s) reduced compliance risk[/green]"
241
+ )
242
+
243
+ for ref, count in sorted(result.summary["coverage_strengthened"].items()):
244
+ if count == 1:
245
+ console.print(f"[green]✓ {ref} coverage strengthened[/green]")
246
+ else:
247
+ console.print(
248
+ f"[green]✓ {ref} coverage strengthened in {count} rule(s)[/green]"
249
+ )
250
+
251
+
252
+ def _print_draft_notice(console, result: PolicyDiffResult) -> None:
253
+ if result.draft_stale:
254
+ console.print(
255
+ "\n[yellow]⚠ policy-intent.md changed since the cached draft was compiled.[/yellow]"
256
+ )
257
+ console.print(
258
+ f'[yellow]Refresh the draft:[/yellow] '
259
+ f'[bold]iris policy compile --agent {result.agent} --dry-run[/bold]'
260
+ )
261
+ if result.draft_status and result.draft_status.meta:
262
+ meta = result.draft_status.meta
263
+ console.print(
264
+ f"[dim]Cached draft: {meta.compiled_at} "
265
+ f"via {meta.compiler_backend}/{meta.compiler_model}[/dim]"
266
+ )
267
+ elif result.draft_status and result.draft_status.meta:
268
+ meta = result.draft_status.meta
269
+ console.print(
270
+ f"\n[dim]Draft compiled {meta.compiled_at} "
271
+ f"via {meta.compiler_backend}/{meta.compiler_model}[/dim]"
272
+ )
273
+
274
+
275
+ def _print_footer(console, result: PolicyDiffResult) -> None:
276
+ _print_draft_notice(console, result)
277
+ if result.draft_stale:
278
+ console.print(
279
+ f'\nRun "[bold]iris policy compile --agent {result.agent} --dry-run[/bold]" '
280
+ f"to refresh the draft, then diff again."
281
+ )
282
+ else:
283
+ console.print(
284
+ f'\nRun "[bold]iris policy compile --agent {result.agent}[/bold]" '
285
+ f"to apply these changes."
286
+ )
287
+
288
+
289
+ def format_diff_json(result: PolicyDiffResult) -> str:
290
+ """Machine-readable diff for CI integration."""
291
+ payload = {
292
+ "agent": result.agent,
293
+ "compare": result.compare_label,
294
+ "draft_stale": result.draft_stale,
295
+ "summary": result.summary,
296
+ "diffs": [
297
+ {
298
+ "status": d.status,
299
+ "risk_delta": d.risk_delta,
300
+ "risk_reason": d.risk_reason,
301
+ "compliance_affected": d.compliance_affected,
302
+ "plain_english": (
303
+ (d.new_rule or d.old_rule).plain_english
304
+ if (d.new_rule or d.old_rule)
305
+ else ""
306
+ ),
307
+ "old_rule": _rule_to_dict(d.old_rule),
308
+ "new_rule": _rule_to_dict(d.new_rule),
309
+ }
310
+ for d in result.diffs
311
+ ],
312
+ }
313
+ if result.draft_status and result.draft_status.meta:
314
+ payload["draft_meta"] = {
315
+ "compiled_at": result.draft_status.meta.compiled_at,
316
+ "compiler_backend": result.draft_status.meta.compiler_backend,
317
+ "compiler_model": result.draft_status.meta.compiler_model,
318
+ }
319
+ return json.dumps(payload, indent=2)
320
+
321
+
322
+ def _rule_to_dict(rule: Optional[CedarRule]) -> Optional[dict]:
323
+ if rule is None:
324
+ return None
325
+ return {
326
+ "type": rule.type,
327
+ "principal": rule.principal,
328
+ "action": rule.action,
329
+ "resource": rule.resource,
330
+ "conditions": rule.conditions,
331
+ "compliance_refs": rule.compliance_refs,
332
+ "plain_english": rule.plain_english,
333
+ }
334
+
335
+
336
+ def format_diff_markdown(result: PolicyDiffResult) -> str:
337
+ """Render diff as markdown."""
338
+ counts = result.summary["counts"]
339
+ lines = [
340
+ f"# Policy diff: {result.agent}",
341
+ "",
342
+ f"**Comparing:** {result.compare_label}",
343
+ "",
344
+ (
345
+ f"**Rules:** {counts['ADDED']} added, {counts['REMOVED']} removed, "
346
+ f"{counts['MODIFIED']} modified, {counts['UNCHANGED']} unchanged"
347
+ ),
348
+ "",
349
+ ]
350
+
351
+ if result.draft_stale:
352
+ lines.append(
353
+ "> ⚠ policy-intent.md changed since the cached draft. "
354
+ f"Run `iris policy compile --agent {result.agent} --dry-run` to refresh."
355
+ )
356
+ lines.append("")
357
+
358
+ for diff in result.diffs:
359
+ refs = ", ".join(diff.compliance_affected) or "—"
360
+ rule = diff.new_rule or diff.old_rule
361
+ english = rule.plain_english if rule else ""
362
+ lines.append(f"## {diff.status} [{refs}]")
363
+ lines.append("")
364
+ lines.append(english)
365
+ lines.append("")
366
+ lines.append(f"**Risk:** {diff.risk_delta} — {diff.risk_reason}")
367
+ lines.append("")
368
+
369
+ lines.append("## Compliance impact")
370
+ lines.append("")
371
+ if result.summary["violations_opened"] == 0:
372
+ lines.append("- ✓ No new violations opened")
373
+ for ref, count in sorted(result.summary["coverage_strengthened"].items()):
374
+ lines.append(f"- ✓ {ref} coverage strengthened in {count} rule(s)")
375
+
376
+ lines.append("")
377
+ if result.draft_stale:
378
+ lines.append(
379
+ f"Run `iris policy compile --agent {result.agent} --dry-run` to refresh the draft."
380
+ )
381
+ else:
382
+ lines.append(f"Run `iris policy compile --agent {result.agent}` to apply these changes.")
383
+ return "\n".join(lines)
384
+
385
+
386
+ @click.command("diff")
387
+ @click.option("--agent", required=True, help="Agent name")
388
+ @click.option("--from", "from_branch", default=None, help="Git branch for baseline policy.cedar")
389
+ @click.option("--dir", "governance_dir", type=Path, default=None)
390
+ @click.option("--draft", "draft_path", type=Path, default=None, help="Compare against this Cedar file")
391
+ @click.option(
392
+ "--compile",
393
+ "compile_draft",
394
+ is_flag=True,
395
+ help="Recompile draft via your LLM before diffing (uses your API key)",
396
+ )
397
+ @click.option(
398
+ "--format",
399
+ "output_format",
400
+ default="table",
401
+ type=click.Choice(["table", "json", "markdown"]),
402
+ )
403
+ @click.option("--verbose", is_flag=True, help="Include unchanged rules in output")
404
+ def policy_diff(
405
+ agent,
406
+ from_branch,
407
+ governance_dir,
408
+ draft_path,
409
+ compile_draft,
410
+ output_format,
411
+ verbose,
412
+ ):
413
+ """
414
+ Preview Cedar policy changes before compiling.
415
+
416
+ Fully offline by default — compares policy.cedar against a cached
417
+ policy-draft.cedar from your last compile/dry-run. No API calls.
418
+
419
+ Workflow:
420
+ 1. Edit policy-intent.md
421
+ 2. iris policy compile --agent <name> --dry-run (your LLM key)
422
+ 3. iris policy diff --agent <name> (offline, free)
423
+
424
+ Example:
425
+ iris policy diff --agent payment-agent
426
+ iris policy diff --agent payment-agent --format json
427
+ iris policy diff --agent payment-agent --compile
428
+ """
429
+ from rich.console import Console
430
+ from rich.panel import Panel
431
+
432
+ console = Console()
433
+
434
+ try:
435
+ result = run_policy_diff(
436
+ agent=agent,
437
+ governance_dir=governance_dir,
438
+ from_branch=from_branch,
439
+ verbose=verbose,
440
+ draft_path=draft_path,
441
+ compile=compile_draft,
442
+ )
443
+ except FileNotFoundError as exc:
444
+ console.print(f"[red]{exc}[/red]")
445
+ raise SystemExit(1)
446
+ except RuntimeError as exc:
447
+ console.print(f"[red]{exc}[/red]")
448
+ raise SystemExit(1)
449
+
450
+ if output_format == "json":
451
+ click.echo(format_diff_json(result))
452
+ elif output_format == "markdown":
453
+ click.echo(format_diff_markdown(result))
454
+ else:
455
+ counts = result.summary["counts"]
456
+ header = (
457
+ f"Current: {result.old_source} → Draft: compiled from intent\n"
458
+ f"Changes: {counts['ADDED']} added, {counts['REMOVED']} removed, "
459
+ f"{counts['MODIFIED']} modified, {counts['UNCHANGED']} unchanged"
460
+ )
461
+ console.print(Panel(header, title=f"Policy diff: {agent}", style="blue"))
462
+
463
+ for diff in result.diffs:
464
+ _print_diff_entry(console, diff)
465
+
466
+ _print_compliance_impact(console, result)
467
+ _print_footer(console, result)
@@ -0,0 +1,146 @@
1
+ """Rich terminal rendering for enhanced iris scan results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from iris_core.compliance.registry import ComplianceRegistry
13
+ from iris_core.discovery.scanner import ScanResult, UngovernedFinding
14
+ from iris_core.models.passport import AgentPassport
15
+
16
+
17
+ def _compliance_label(passport: AgentPassport) -> str:
18
+ if passport.compliance_tags:
19
+ return passport.compliance_tags[0].value
20
+ return "colorado-ai-act"
21
+
22
+
23
+ def _agent_status(
24
+ passport: AgentPassport,
25
+ registry: ComplianceRegistry,
26
+ framework: Optional[str],
27
+ ) -> str:
28
+ violations = registry.check_passport(passport, framework)
29
+ return "PASS" if not violations else "FAIL"
30
+
31
+
32
+ def render_discover_scan(
33
+ console: Console,
34
+ result: ScanResult,
35
+ scan_dir: Path,
36
+ framework: Optional[str] = None,
37
+ auto_register: bool = False,
38
+ drafts_written: Optional[List[Path]] = None,
39
+ ) -> None:
40
+ """Render governed agents, ungoverned findings, and scan summary."""
41
+ registry = ComplianceRegistry()
42
+ drafts_written = drafts_written or []
43
+
44
+ console.print(
45
+ Panel(
46
+ f"[bold]IRIS Governance Scan[/bold]\n"
47
+ f"Directory: {scan_dir}\n"
48
+ f"Framework: {framework or 'all active bundles'}\n"
49
+ f"Files scanned: {result.files_scanned:,} | "
50
+ f"Lines scanned: {result.lines_scanned:,}",
51
+ style="blue",
52
+ )
53
+ )
54
+
55
+ governed = result.governed_agents
56
+ console.print(f"\n[bold]GOVERNED AGENTS ({len(governed)})[/bold]")
57
+ if governed:
58
+ for passport in governed:
59
+ compliance = _compliance_label(passport)
60
+ status = _agent_status(passport, registry, framework)
61
+ status_style = "green" if status == "PASS" else "red"
62
+ console.print(
63
+ f"[green]✓[/green] {passport.name:<20} "
64
+ f"{compliance:<18} [{status_style}]{status}[/{status_style}]"
65
+ )
66
+ else:
67
+ console.print("[dim]No passport.yaml files found.[/dim]")
68
+
69
+ findings = result.ungoverned_findings
70
+ console.print(f"\n[bold]UNGOVERNED AGENTS DETECTED ({len(findings)})[/bold]")
71
+ if findings:
72
+ for finding in findings:
73
+ _print_finding(console, finding)
74
+ if not auto_register:
75
+ console.print(
76
+ '\n[dim]Run "iris scan --discover --auto-register" to generate '
77
+ "passport drafts for all findings.[/dim]"
78
+ )
79
+ else:
80
+ console.print("[green]No ungoverned AI patterns detected in source files.[/green]")
81
+
82
+ if result.shadow_agents:
83
+ console.print(f"\n[bold]SHADOW AGENTS ({len(result.shadow_agents)})[/bold]")
84
+ for shadow in result.shadow_agents:
85
+ console.print(
86
+ f"[yellow]⚠[/yellow] {shadow.resource_path} "
87
+ f"({shadow.resource_name} @ {shadow.namespace})\n"
88
+ f" {shadow.reason}"
89
+ )
90
+
91
+ if drafts_written:
92
+ console.print(f"\n[bold green]Passport drafts written ({len(drafts_written)})[/bold green]")
93
+ for draft in drafts_written:
94
+ console.print(f" [cyan]{draft}[/cyan]")
95
+
96
+
97
+ def _print_finding(console: Console, finding: UngovernedFinding) -> None:
98
+ risk_style = {
99
+ "HIGH": "red",
100
+ "MEDIUM": "yellow",
101
+ "LOW": "green",
102
+ }.get(finding.risk_level, "white")
103
+ console.print(
104
+ f"\n[yellow]⚠[/yellow] {finding.file_path}:{finding.line_number}\n"
105
+ f"Pattern: {finding.pattern_matched}\n"
106
+ f"Framework: {_display_framework(finding.framework_detected)}\n"
107
+ f"Risk: [{risk_style}]{finding.risk_level}[/{risk_style}] — {finding.risk_reason}\n"
108
+ f"Fix: [bold cyan]{finding.suggested_command}[/bold cyan]"
109
+ )
110
+
111
+
112
+ def _display_framework(framework: str) -> str:
113
+ labels: Dict[str, str] = {
114
+ "langchain": "LangChain",
115
+ "crewai": "CrewAI",
116
+ "openai": "OpenAI",
117
+ "anthropic": "Anthropic SDK",
118
+ "generic": "Generic",
119
+ }
120
+ return labels.get(framework, framework)
121
+
122
+
123
+ def render_violations_table(console: Console, violations: list) -> None:
124
+ """Render compliance violations table (existing scan behavior)."""
125
+ table = Table(title=f"Violations ({len(violations)} found)")
126
+ table.add_column("Rule", style="cyan", no_wrap=True)
127
+ table.add_column("Severity", style="red")
128
+ table.add_column("Message")
129
+ table.add_column("Remediation", style="yellow")
130
+ for v in violations:
131
+ severity_color = {
132
+ "CRITICAL": "red",
133
+ "HIGH": "orange3",
134
+ "MEDIUM": "yellow",
135
+ "LOW": "green",
136
+ }.get(v.severity.value, "white")
137
+ remediation = v.remediation
138
+ if len(remediation) > 80:
139
+ remediation = remediation[:80] + "..."
140
+ table.add_row(
141
+ v.rule_id,
142
+ f"[{severity_color}]{v.severity.value}[/{severity_color}]",
143
+ v.message,
144
+ remediation,
145
+ )
146
+ console.print(table)
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: iris-security-cli
3
+ Version: 0.1.0
4
+ Summary: IRIS CLI — iris scan, iris register, iris policy, iris compliance
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
7
+ Project-URL: Repository, https://github.com/gimartinb/iris-sdk
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Security
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: iris-security-core>=0.1.0
19
+ Requires-Dist: iris-security-sdk>=0.1.0
20
+ Requires-Dist: click>=8.1
21
+ Requires-Dist: rich>=13.0
22
+ Requires-Dist: pyyaml>=6.0
23
+
24
+ # iris-security-cli
25
+
26
+ IRIS CLI — `iris scan`, `iris register`, `iris policy`, `iris compliance`.
27
+
28
+ Command-line tools for AI agent governance and Colorado AI Act compliance.
29
+
30
+ Part of the [IRIS SDK](https://github.com/gimartinb/iris-sdk).
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install iris-security-cli
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ iris --version
42
+ iris register --name my-agent --owner you@company.com --team my-team --compliance colorado-ai-act
43
+ iris scan
44
+ iris compliance check --framework colorado-ai-act
45
+ ```
@@ -0,0 +1,17 @@
1
+ iris_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ iris_cli/assess.py,sha256=-18cQZBlbqGnk2Q-3i1SNrfeV98LUt2Go8w3WMsXU54,16515
3
+ iris_cli/cedar_parser.py,sha256=3V1t-L3cSBCAMDmCkzcoBDA6S4ulg5LKpWDN2fU_jbM,15161
4
+ iris_cli/compiler_config.py,sha256=DbKxqe3D4hpkDMyPT2QOfUg7RJNTneMMC0hLgTkCh7k,1610
5
+ iris_cli/evidence.py,sha256=ZEHUUOK6TNTPgeUGycFy4f9CfAXJAHASyThi3gW3mOE,29631
6
+ iris_cli/main.py,sha256=xpLZbGLePysH_dF7ex9snsg5toPHIuHNogz2gmBV-dI,19834
7
+ iris_cli/mcp_server.py,sha256=FcIhBFxEfFy5RYsS9jchYuE2d57fmdZja58EFiwab0o,20782
8
+ iris_cli/policy_cache.py,sha256=cSqbbvSwFfQU3JnD8EWxQ0dW8sOcHYfTEXVpMkre87U,3456
9
+ iris_cli/policy_diff.py,sha256=CymsDBeWJ30WonIzhTtWcA03mcW6v9UA-6VFh1fpByc,15945
10
+ iris_cli/scan_report.py,sha256=myYuvF6rfUxJFDHWi-iErB17-QbHbfwzUleXt6MU0uo,5097
11
+ tests/test_evidence.py,sha256=uPN_XKWqktrQvepC55HXQ0SNOy3KoxKDOByzLavLWFk,9395
12
+ tests/test_policy_diff.py,sha256=338L3eb6BKiUAixan_TGSCSGFkUEw74F12RxH2rHxLo,8052
13
+ iris_security_cli-0.1.0.dist-info/METADATA,sha256=6Eul4xdCFyzy_Aqd8XhcqjiiijHCkfeOQ08yLxMxhNw,1403
14
+ iris_security_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ iris_security_cli-0.1.0.dist-info/entry_points.txt,sha256=MGi5_jQ_DETbkKiZLmgKYNRt6vIXtMQvmwCSU0T7e64,43
16
+ iris_security_cli-0.1.0.dist-info/top_level.txt,sha256=OanTp8Sdq_suSs9gfugtQibn3SJ71i-JCSsOfA9CSsg,15
17
+ iris_security_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ iris = iris_cli.main:cli
@@ -0,0 +1,2 @@
1
+ iris_cli
2
+ tests