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.
iris_cli/main.py ADDED
@@ -0,0 +1,542 @@
1
+ """
2
+ IRIS CLI — the developer's command-line interface for agent governance.
3
+
4
+ Commands:
5
+ iris scan — scan governance directory for policy violations
6
+ iris policy generate — generate Cedar policy from intent file
7
+ iris policy compile — compile natural language to Cedar
8
+ iris policy diff — preview Cedar changes offline (no API cost)
9
+ iris policy check — validate policy against compliance framework
10
+ iris register — register a new agent (creates passport.yaml)
11
+ iris compliance check — run compliance check against a framework
12
+ iris compliance report — generate a compliance report (PDF/markdown)
13
+
14
+ This CLI is the traction instrument. Every 'iris scan' or 'iris policy check'
15
+ is a weekly active developer event.
16
+ """
17
+
18
+ import click
19
+ import sys
20
+ from pathlib import Path
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+
24
+ console = Console()
25
+
26
+
27
+ @click.group()
28
+ @click.version_option(version="0.1.0", prog_name="iris")
29
+ def cli():
30
+ """
31
+ IRIS — AI Agent Governance Platform
32
+
33
+ Discover, register, and govern AI agents locally.
34
+ Colorado AI Act compliant out of the box.
35
+
36
+ Docs: https://docs.iris.ai
37
+ """
38
+ pass
39
+
40
+
41
+ # ── iris scan ─────────────────────────────────────────────────────────────────
42
+
43
+ @cli.command()
44
+ @click.option("--dir", "scan_dir", type=Path, default=Path.cwd(), help="Directory to scan")
45
+ @click.option("--framework", "-f", default=None, help="Compliance framework (e.g. colorado-ai-act)")
46
+ @click.option("--format", "output_format", default="table", type=click.Choice(["table", "json", "markdown"]))
47
+ @click.option("--fail-on", default="critical", type=click.Choice(["critical", "high", "any"]))
48
+ @click.option(
49
+ "--discover",
50
+ is_flag=True,
51
+ help="Scan Python/TypeScript source files for ungoverned AI agent patterns",
52
+ )
53
+ @click.option(
54
+ "--auto-register",
55
+ is_flag=True,
56
+ help="Write passport.yaml drafts for each ungoverned finding (does not register)",
57
+ )
58
+ def scan(scan_dir: Path, framework: str, output_format: str, fail_on: str, discover: bool, auto_register: bool):
59
+ """
60
+ Scan governance directory for policy violations.
61
+
62
+ Finds all passport.yaml files and checks them against the
63
+ configured compliance frameworks. Exits with code 1 if violations
64
+ above the --fail-on threshold are found (for CI integration).
65
+
66
+ With --discover, also crawls Python and TypeScript files for ungoverned
67
+ LLM/agent patterns (LangChain, OpenAI, Anthropic, CrewAI, etc.).
68
+
69
+ Examples:
70
+ iris scan
71
+ iris scan --framework colorado-ai-act
72
+ iris scan --discover
73
+ iris scan --discover --auto-register
74
+ iris scan --format json | jq '.violations[]'
75
+ """
76
+ from iris import iris_scan
77
+ from iris_core.models.policy import Severity
78
+
79
+ if discover:
80
+ from iris_core.discovery.scanner import CodebaseScanner
81
+ from iris_core.models.passport import AgentPassport, ComplianceTag, DataClassification
82
+ from iris_cli.scan_report import render_discover_scan
83
+
84
+ scanner = CodebaseScanner()
85
+ discover_result = scanner.scan_directory(scan_dir)
86
+ drafts_written = []
87
+
88
+ if auto_register:
89
+ gov_root = scan_dir / "governance" / "agents"
90
+ for finding in discover_result.ungoverned_findings:
91
+ agent_dir = gov_root / finding.agent_name_hint
92
+ passport_path = agent_dir / "passport.yaml"
93
+ if passport_path.exists():
94
+ continue
95
+ agent_dir.mkdir(parents=True, exist_ok=True)
96
+ draft = AgentPassport(
97
+ name=finding.agent_name_hint,
98
+ owner="CHANGE_ME@company.com",
99
+ team="CHANGE_ME",
100
+ compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
101
+ data_classification=DataClassification.INTERNAL,
102
+ is_high_risk_ai="--high-risk" in finding.suggested_command,
103
+ description=(
104
+ f"Auto-drafted from ungoverned scan finding in "
105
+ f"{finding.file_path}:{finding.line_number}"
106
+ ),
107
+ )
108
+ passport_path.write_text(draft.to_yaml())
109
+ drafts_written.append(passport_path)
110
+
111
+ if output_format == "json":
112
+ import json
113
+
114
+ payload = {
115
+ "scan_timestamp": discover_result.scan_timestamp,
116
+ "files_scanned": discover_result.files_scanned,
117
+ "lines_scanned": discover_result.lines_scanned,
118
+ "governed_agents": [p.name for p in discover_result.governed_agents],
119
+ "ungoverned_findings": [
120
+ {
121
+ "file_path": f.file_path,
122
+ "line_number": f.line_number,
123
+ "pattern_matched": f.pattern_matched,
124
+ "framework_detected": f.framework_detected,
125
+ "agent_name_hint": f.agent_name_hint,
126
+ "suggested_command": f.suggested_command,
127
+ "risk_level": f.risk_level,
128
+ "risk_reason": f.risk_reason,
129
+ }
130
+ for f in discover_result.ungoverned_findings
131
+ ],
132
+ "shadow_agents": [
133
+ {
134
+ "resource_path": s.resource_path,
135
+ "resource_name": s.resource_name,
136
+ "namespace": s.namespace,
137
+ "reason": s.reason,
138
+ }
139
+ for s in discover_result.shadow_agents
140
+ ],
141
+ }
142
+ click.echo(json.dumps(payload, indent=2))
143
+ else:
144
+ render_discover_scan(
145
+ console,
146
+ discover_result,
147
+ scan_dir,
148
+ framework=framework,
149
+ auto_register=auto_register,
150
+ drafts_written=drafts_written,
151
+ )
152
+
153
+ if discover_result.ungoverned_findings:
154
+ sys.exit(1)
155
+ sys.exit(0)
156
+
157
+ console.print(Panel(
158
+ f"[bold]IRIS Governance Scan[/bold]\n"
159
+ f"Directory: {scan_dir}\n"
160
+ f"Framework: {framework or 'all active bundles'}",
161
+ style="blue"
162
+ ))
163
+
164
+ violations = iris_scan(directory=scan_dir, framework=framework)
165
+
166
+ if not violations:
167
+ console.print("\n[bold green]✓ All agents passed compliance check[/bold green]")
168
+ sys.exit(0)
169
+
170
+ if output_format == "table":
171
+ from iris_cli.scan_report import render_violations_table
172
+
173
+ render_violations_table(console, violations)
174
+
175
+ elif output_format == "json":
176
+ import json
177
+ output = [{"rule_id": v.rule_id, "severity": v.severity.value, "message": v.message, "remediation": v.remediation, "compliance_refs": v.compliance_refs} for v in violations]
178
+ click.echo(json.dumps(output, indent=2))
179
+
180
+ threshold_map = {"critical": Severity.CRITICAL, "high": Severity.HIGH, "any": Severity.LOW}
181
+ threshold = threshold_map[fail_on]
182
+ blocking = [v for v in violations if v.severity.value >= threshold.value]
183
+ if blocking:
184
+ sys.exit(1)
185
+
186
+
187
+ # ── iris register ──────────────────────────────────────────────────────────────
188
+
189
+ @cli.command()
190
+ @click.option("--name", prompt="Agent name", help="Agent identifier (kebab-case)")
191
+ @click.option("--owner", prompt="Owner email", help="Owner email address")
192
+ @click.option("--team", prompt="Team name", help="Team or squad name")
193
+ @click.option("--env", multiple=True, default=["dev"], help="Environments (repeatable)")
194
+ @click.option("--high-risk", is_flag=True, default=False, help="Flag as high-risk AI (Colorado AI Act)")
195
+ @click.option("--compliance", "-c", multiple=True, help="Compliance frameworks to tag")
196
+ @click.option("--dir", "output_dir", type=Path, default=None)
197
+ def register(name, owner, team, env, high_risk, compliance, output_dir):
198
+ """
199
+ Register a new agent — creates passport.yaml in the governance directory.
200
+
201
+ Creates the full GitOps governance structure for a new agent:
202
+ governance/agents/<name>/passport.yaml
203
+ governance/agents/<name>/policy-intent.md (template)
204
+
205
+ Example:
206
+ iris register --name payment-agent --owner alice@co.com --team platform
207
+ """
208
+ from iris import IrisAgent, DataClassification, ComplianceTag
209
+
210
+ compliance_list = list(compliance) or ["colorado-ai-act"]
211
+ output = output_dir or (Path.cwd() / "governance" / "agents" / name)
212
+ output.mkdir(parents=True, exist_ok=True)
213
+
214
+ agent = IrisAgent(
215
+ name=name,
216
+ owner=owner,
217
+ team=team,
218
+ compliance=compliance_list,
219
+ environments=list(env),
220
+ is_high_risk_ai=high_risk,
221
+ policy_dir=output,
222
+ )
223
+
224
+ # Write passport
225
+ (output / "passport.yaml").write_text(agent.passport.to_yaml())
226
+
227
+ # Write intent template
228
+ intent_template = f"""# Policy Intent — {name}
229
+
230
+ > Edit this file to describe what your agent is allowed to do.
231
+ > Then run: iris policy compile --agent {name}
232
+
233
+ ## What this agent does
234
+ [Describe the agent's purpose here]
235
+
236
+ ## What it is allowed to access
237
+ [List the tools, APIs, and data sources this agent needs]
238
+
239
+ ## What it must never do
240
+ [List explicit prohibitions]
241
+
242
+ ## Compliance notes
243
+ [Any compliance-specific context]
244
+ """
245
+ (output / "policy-intent.md").write_text(intent_template)
246
+
247
+ console.print(Panel(
248
+ f"[bold green]✓ Agent registered[/bold green]\n\n"
249
+ f"Name: [cyan]{name}[/cyan]\n"
250
+ f"Passport: {output / 'passport.yaml'}\n"
251
+ f"Intent template: {output / 'policy-intent.md'}\n\n"
252
+ f"Next step: Edit [bold]{output / 'policy-intent.md'}[/bold]\n"
253
+ f"Then run: [bold]iris policy compile --agent {name}[/bold]",
254
+ style="green"
255
+ ))
256
+
257
+
258
+ # ── iris policy ────────────────────────────────────────────────────────────────
259
+
260
+ @cli.group()
261
+ def policy():
262
+ """Policy management commands."""
263
+ pass
264
+
265
+
266
+ @policy.command("compile")
267
+ @click.option("--agent", required=True, help="Agent name")
268
+ @click.option("--intent", type=Path, default=None, help="Path to policy-intent.md")
269
+ @click.option("--dir", "governance_dir", type=Path, default=None)
270
+ @click.option("--dry-run", is_flag=True, help="Show generated Cedar without writing to disk")
271
+ def policy_compile(agent, intent, governance_dir, dry_run):
272
+ """
273
+ Compile a natural language policy-intent.md to Cedar.
274
+
275
+ Reads the policy-intent.md for the agent, sends it to the IRIS
276
+ policy compiler (your LLM — see ~/.iris/config.yaml), validates
277
+ against compliance bundles, and writes the generated Cedar.
278
+
279
+ Always caches policy-draft.cedar for offline `iris policy diff`.
280
+
281
+ Requires your API key: ANTHROPIC_API_KEY or OPENAI_API_KEY.
282
+
283
+ Example:
284
+ iris policy compile --agent payment-agent
285
+ iris policy compile --agent payment-agent --dry-run
286
+ """
287
+ from pathlib import Path
288
+
289
+ from iris_core.models.passport import AgentPassport
290
+
291
+ from iris_cli.compiler_config import compiler_info, create_policy_compiler
292
+ from iris_cli.policy_cache import save_policy_draft
293
+
294
+ gov_dir = governance_dir or Path.cwd() / "governance" / "agents" / agent
295
+ passport_file = gov_dir / "passport.yaml"
296
+ intent_file = intent or gov_dir / "policy-intent.md"
297
+
298
+ if not passport_file.exists():
299
+ console.print(f"[red]Passport not found: {passport_file}[/red]")
300
+ console.print(f"Run: iris register --name {agent}")
301
+ sys.exit(1)
302
+
303
+ if not intent_file.exists():
304
+ console.print(f"[red]Intent file not found: {intent_file}[/red]")
305
+ sys.exit(1)
306
+
307
+ passport = AgentPassport.from_yaml(passport_file.read_text())
308
+ intent_text = intent_file.read_text()
309
+
310
+ with console.status(f"[bold blue]Compiling policy for {agent}...[/bold blue]"):
311
+ compiler = create_policy_compiler()
312
+ result = compiler.compile(intent_text, passport)
313
+
314
+ if result.has_blocking_violations():
315
+ console.print("\n[bold red]Policy compilation blocked[/bold red]")
316
+ for v in result.violations:
317
+ console.print(f"\n[red]✗ {v.rule_id}[/red]: {v.message}")
318
+ console.print(f" [yellow]Remediation:[/yellow] {v.remediation}")
319
+ console.print("\n[yellow]Contact your security engineer to resolve these violations.[/yellow]")
320
+ sys.exit(1)
321
+
322
+ backend, model = compiler_info(compiler)
323
+ draft_path = save_policy_draft(
324
+ gov_dir, intent_text, result.cedar_policy, backend, model
325
+ )
326
+
327
+ if dry_run:
328
+ console.print("\n[bold]Generated Cedar policy (dry run):[/bold]")
329
+ console.print(result.cedar_policy)
330
+ console.print(f"\n[dim]Draft cached: {draft_path}[/dim]")
331
+ console.print(f"[dim]Run: iris policy diff --agent {agent}[/dim]")
332
+ else:
333
+ (gov_dir / "policy.cedar").write_text(result.cedar_policy)
334
+ console.print(f"[bold green]✓ Policy compiled: {gov_dir / 'policy.cedar'}[/bold green]")
335
+ console.print(f"[dim]Draft cached: {draft_path}[/dim]")
336
+
337
+ if result.warnings:
338
+ for w in result.warnings:
339
+ console.print(f"[yellow]Warning:[/yellow] {w}")
340
+
341
+
342
+ from iris_cli.policy_diff import policy_diff
343
+ policy.add_command(policy_diff)
344
+
345
+
346
+ # ── iris license ───────────────────────────────────────────────────────────────
347
+
348
+ @cli.group()
349
+ def license():
350
+ """IRIS Pro license management."""
351
+ pass
352
+
353
+
354
+ @license.command("activate")
355
+ @click.argument("key")
356
+ def license_activate(key: str):
357
+ """
358
+ Activate an IRIS Pro license key.
359
+
360
+ Validates the key format and saves it to ~/.iris/license.key.
361
+ You can also set IRIS_LICENSE_KEY in your environment.
362
+
363
+ Example:
364
+ iris license activate IRIS-XXXX-XXXX-XXXX-XXXX
365
+ """
366
+ from iris_core.compliance.license import (
367
+ IrisLicense,
368
+ validate_license_key_format,
369
+ write_license_key,
370
+ )
371
+
372
+ key = key.strip()
373
+ if not validate_license_key_format(key):
374
+ console.print(
375
+ "[red]Invalid license key format.[/red]\n"
376
+ "Expected: IRIS-XXXX-XXXX-XXXX-XXXX (uppercase letters and digits)\n"
377
+ "Get a license: https://iris.ai/pricing"
378
+ )
379
+ sys.exit(1)
380
+
381
+ path = write_license_key(key)
382
+ result = IrisLicense().check("gdpr")
383
+ expiry = result.expires_at or "—"
384
+ console.print(
385
+ Panel(
386
+ f"[bold green]✓ License activated[/bold green]\n\n"
387
+ f"Tier: [cyan]{result.tier}[/cyan]\n"
388
+ f"Expires: {expiry}\n"
389
+ f"Saved to: {path}\n\n"
390
+ f"Pro bundles unlocked: gdpr, hipaa, soc2",
391
+ style="green",
392
+ )
393
+ )
394
+
395
+
396
+ @license.command("status")
397
+ def license_status():
398
+ """
399
+ Show current license status and available compliance bundles.
400
+
401
+ Example:
402
+ iris license status
403
+ """
404
+ from iris_core.compliance.license import (
405
+ FREE_BUNDLES,
406
+ PAID_BUNDLES,
407
+ IrisLicense,
408
+ _read_license_key,
409
+ )
410
+
411
+ key = _read_license_key()
412
+ result = IrisLicense().check("gdpr")
413
+ if result.valid:
414
+ status_line = f"[bold green]Active[/bold green] — tier [cyan]{result.tier}[/cyan]"
415
+ if result.expires_at:
416
+ status_line += f", expires {result.expires_at}"
417
+ key_display = f"{key[:9]}…{key[-4:]}" if key and len(key) > 16 else (key or "—")
418
+ elif result.reason == "no_key":
419
+ status_line = "[yellow]No license key[/yellow] — free bundles only"
420
+ key_display = "—"
421
+ else:
422
+ status_line = f"[red]Invalid license[/red] ({result.reason})"
423
+ key_display = "—"
424
+
425
+ free_list = ", ".join(sorted(FREE_BUNDLES))
426
+ paid_list = ", ".join(sorted(PAID_BUNDLES))
427
+ paid_status = (
428
+ "[green]available[/green]"
429
+ if result.valid
430
+ else "[dim]requires Pro license[/dim]"
431
+ )
432
+
433
+ console.print(
434
+ Panel(
435
+ f"{status_line}\n"
436
+ f"Key: {key_display}\n\n"
437
+ f"[bold]Free bundles:[/bold] {free_list}\n"
438
+ f"[bold]Pro bundles:[/bold] {paid_list} ({paid_status})",
439
+ title="IRIS License",
440
+ style="blue",
441
+ )
442
+ )
443
+
444
+
445
+ # ── iris compliance ────────────────────────────────────────────────────────────
446
+
447
+ @cli.group()
448
+ def compliance():
449
+ """Compliance checking and reporting commands."""
450
+ pass
451
+
452
+ # Wire in the assess command
453
+ from iris_cli.assess import compliance_assess
454
+ compliance.add_command(compliance_assess)
455
+
456
+ # Wire in evidence commands
457
+ from iris_cli.evidence import evidence
458
+ cli.add_command(evidence)
459
+
460
+
461
+ @compliance.command("check")
462
+ @click.option("--agent", default=None, help="Specific agent to check (or all)")
463
+ @click.option("--framework", "-f", default="colorado-ai-act")
464
+ @click.option("--dir", "governance_dir", type=Path, default=None)
465
+ def compliance_check(agent, framework, governance_dir):
466
+ """
467
+ Check an agent against a compliance framework.
468
+
469
+ Shows a detailed breakdown of which rules pass and fail,
470
+ with plain-English remediation guidance for each failure.
471
+
472
+ Example:
473
+ iris compliance check --framework colorado-ai-act
474
+ iris compliance check --agent payment-agent --framework hipaa
475
+ """
476
+ from iris_core.compliance.registry import ComplianceRegistry
477
+ from iris import AgentPassport
478
+
479
+ gov_dir = governance_dir or Path.cwd() / "governance" / "agents"
480
+ registry = ComplianceRegistry()
481
+
482
+ passports_to_check = []
483
+ if agent:
484
+ passport_file = gov_dir / agent / "passport.yaml"
485
+ if passport_file.exists():
486
+ passports_to_check.append(AgentPassport.from_yaml(passport_file.read_text()))
487
+ else:
488
+ for f in gov_dir.rglob("passport.yaml"):
489
+ try:
490
+ passports_to_check.append(AgentPassport.from_yaml(f.read_text()))
491
+ except Exception:
492
+ pass
493
+
494
+ if not passports_to_check:
495
+ console.print("[yellow]No agent passports found.[/yellow]")
496
+ sys.exit(0)
497
+
498
+ all_pass = True
499
+ for passport in passports_to_check:
500
+ violations = registry.check_passport(passport, framework)
501
+ status = "[bold green]PASS[/bold green]" if not violations else "[bold red]FAIL[/bold red]"
502
+ console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
503
+
504
+ if violations:
505
+ all_pass = False
506
+ for v in violations:
507
+ console.print(f" [red]✗[/red] [{v.rule_id}] {v.message}")
508
+ console.print(f" [yellow]→[/yellow] {v.remediation}")
509
+ else:
510
+ console.print(f" [green]✓[/green] All {framework} rules satisfied")
511
+
512
+ for note in registry.last_check_notes:
513
+ console.print(f" [dim]ℹ {note}[/dim]")
514
+
515
+ sys.exit(0 if all_pass else 1)
516
+
517
+
518
+ if __name__ == "__main__":
519
+ cli()
520
+
521
+
522
+ @cli.group()
523
+ def mcp():
524
+ """MCP server commands for Cursor IDE integration."""
525
+ pass
526
+
527
+
528
+ @mcp.command("start")
529
+ def mcp_start():
530
+ """
531
+ Start the IRIS MCP server for Cursor IDE.
532
+
533
+ Cursor connects to this server to get real-time compliance
534
+ feedback as you write agent code.
535
+
536
+ Example:
537
+ iris mcp start
538
+ """
539
+ from iris_cli.mcp_server import start
540
+ console.print("[bold blue]IRIS MCP server starting on stdio...[/bold blue]")
541
+ console.print("[dim]Cursor is now connected to IRIS governance.[/dim]")
542
+ start()