iris-security-cli 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/PKG-INFO +4 -2
  2. iris_security_cli-0.1.2/iris_cli/action_plan.py +305 -0
  3. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/assess.py +24 -6
  4. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/compiler_config.py +18 -5
  5. iris_security_cli-0.1.2/iris_cli/dlp_cmd.py +160 -0
  6. iris_security_cli-0.1.2/iris_cli/drift.py +272 -0
  7. iris_security_cli-0.1.2/iris_cli/explain.py +70 -0
  8. iris_security_cli-0.1.2/iris_cli/framework_suggest.py +423 -0
  9. iris_security_cli-0.1.2/iris_cli/framework_test.py +465 -0
  10. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/main.py +113 -15
  11. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/mcp_server.py +1 -1
  12. iris_security_cli-0.1.2/iris_cli/redteam.py +179 -0
  13. iris_security_cli-0.1.2/iris_cli/scan_govern.py +330 -0
  14. iris_security_cli-0.1.2/iris_cli/scm.py +354 -0
  15. iris_security_cli-0.1.2/iris_cli/status_cmd.py +109 -0
  16. iris_security_cli-0.1.2/iris_cli/users.py +99 -0
  17. iris_security_cli-0.1.2/iris_cli/watch.py +102 -0
  18. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_security_cli.egg-info/PKG-INFO +4 -2
  19. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_security_cli.egg-info/SOURCES.txt +14 -0
  20. iris_security_cli-0.1.2/iris_security_cli.egg-info/requires.txt +8 -0
  21. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/pyproject.toml +5 -2
  22. iris_security_cli-0.1.2/tests/test_framework_suggest.py +111 -0
  23. iris_security_cli-0.1.2/tests/test_framework_test.py +183 -0
  24. iris_security_cli-0.1.0/iris_security_cli.egg-info/requires.txt +0 -5
  25. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/README.md +0 -0
  26. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/__init__.py +0 -0
  27. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/cedar_parser.py +0 -0
  28. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/evidence.py +0 -0
  29. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/policy_cache.py +0 -0
  30. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/policy_diff.py +0 -0
  31. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_cli/scan_report.py +0 -0
  32. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_security_cli.egg-info/dependency_links.txt +0 -0
  33. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_security_cli.egg-info/entry_points.txt +0 -0
  34. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/iris_security_cli.egg-info/top_level.txt +0 -0
  35. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/setup.cfg +0 -0
  36. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/tests/test_evidence.py +0 -0
  37. {iris_security_cli-0.1.0 → iris_security_cli-0.1.2}/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.0
3
+ Version: 0.1.2
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
@@ -16,10 +16,12 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  Requires-Dist: iris-security-core>=0.1.0
19
- Requires-Dist: iris-security-sdk>=0.1.0
19
+ Requires-Dist: iris-security-sdk>=0.1.2
20
20
  Requires-Dist: click>=8.1
21
21
  Requires-Dist: rich>=13.0
22
22
  Requires-Dist: pyyaml>=6.0
23
+ Provides-Extra: scm
24
+ Requires-Dist: iris-security-scm[all]>=0.1.0; extra == "scm"
23
25
 
24
26
  # iris-security-cli
25
27
 
@@ -0,0 +1,305 @@
1
+ """Opinionated compliance action plan and score calculation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from iris_core.models.passport import AgentPassport
10
+
11
+
12
+ @dataclass
13
+ class Action:
14
+ priority: int
15
+ command: str
16
+ why: str
17
+ time_estimate: str
18
+ rule_id: str
19
+ urgency: str # "before deployment" | "this month"
20
+
21
+
22
+ @dataclass
23
+ class ActionPlan:
24
+ immediate_actions: list[Action] = field(default_factory=list)
25
+ next_30_days: list[Action] = field(default_factory=list)
26
+ current_scores: dict[str, float] = field(default_factory=dict)
27
+ estimated_time_to_compliant: str = "about 2 weeks"
28
+ one_liner: str = ""
29
+
30
+
31
+ def progress_bar(score: float, width: int = 10) -> str:
32
+ filled = int(round(score * width))
33
+ filled = max(0, min(width, filled))
34
+ return "█" * filled + "░" * (width - filled)
35
+
36
+
37
+ def _gov_dir(agent_name: str, base: Path | None = None) -> Path:
38
+ root = base or Path.cwd() / "governance" / "agents"
39
+ return root / agent_name
40
+
41
+
42
+ def colorado_controls_passed(passport: AgentPassport, agent_dir: Path) -> tuple[int, int]:
43
+ """Return (satisfied, total) for Colorado AI Act controls."""
44
+ checks = [
45
+ bool(passport.is_high_risk_ai and passport.agent_id),
46
+ bool(passport.evidence_vault_id),
47
+ bool(passport.intent_ref),
48
+ (agent_dir / "policy.cedar").exists(),
49
+ bool(passport.last_reviewed_at),
50
+ (agent_dir / "impact-assessment.md").exists(),
51
+ ]
52
+ return sum(1 for c in checks if c), 6
53
+
54
+
55
+ def nist_controls_passed(passport: AgentPassport, agent_dir: Path) -> tuple[int, int]:
56
+ """NIST AI RMF — 0 until iris test is run; stub for advisor display."""
57
+ test_file = agent_dir / "nist-ai-rmf-results.json"
58
+ if test_file.exists():
59
+ return 24, 24
60
+ return 0, 24
61
+
62
+
63
+ def compliance_score(passport: AgentPassport, agent_dir: Path, framework: str) -> float:
64
+ if framework == "colorado-ai-act":
65
+ satisfied, total = colorado_controls_passed(passport, agent_dir)
66
+ return satisfied / total if total else 0.0
67
+ if framework == "nist-ai-rmf":
68
+ satisfied, total = nist_controls_passed(passport, agent_dir)
69
+ return satisfied / total if total else 0.0
70
+ return 0.0
71
+
72
+
73
+ def detect_one_liner(answers: dict[str, Any]) -> str:
74
+ q4 = answers.get("q4", "")
75
+ if "health" in str(q4).lower() or "medical" in str(q4).lower():
76
+ return (
77
+ "from iris_anthropic import IrisAnthropic\n"
78
+ "client = IrisAnthropic(passport=your_passport)"
79
+ )
80
+ return (
81
+ "from iris_openai import IrisOpenAI\n"
82
+ "client = IrisOpenAI(passport=your_passport)"
83
+ )
84
+
85
+
86
+ def build_action_plan(
87
+ agent_name: str | None,
88
+ answers: dict[str, Any],
89
+ recommendations: list[Any],
90
+ passport: dict[str, Any] | None = None,
91
+ governance_base: Path | None = None,
92
+ ) -> ActionPlan:
93
+ plan = ActionPlan()
94
+ name = agent_name or "my-agent"
95
+ spec = (passport or {}).get("spec", {})
96
+ agent_dir = _gov_dir(name, governance_base)
97
+ passport_exists = (agent_dir / "passport.yaml").exists()
98
+ has_assessment = bool(spec.get("evidence_vault_id")) or (agent_dir / "impact-assessment.md").exists()
99
+ has_policy = (agent_dir / "policy.cedar").exists()
100
+ is_high_risk = spec.get("is_high_risk_ai", False)
101
+ q2 = answers.get("q2", "")
102
+ q4 = answers.get("q4", "")
103
+ q6 = bool(answers.get("q6"))
104
+ q7 = set(answers.get("q7", []))
105
+
106
+ plan.one_liner = detect_one_liner(answers)
107
+ priority = 1
108
+
109
+ if not passport_exists:
110
+ risk_flag = " --high-risk" if answers.get("q5") or is_high_risk else ""
111
+ plan.immediate_actions.append(
112
+ Action(
113
+ priority=priority,
114
+ command=f"iris register --name {name} --compliance colorado-ai-act{risk_flag}",
115
+ why=(
116
+ "Your agent needs an identity contract before production. "
117
+ "The Colorado AI Act requires high-risk systems to be inventoried."
118
+ ),
119
+ time_estimate="2 minutes",
120
+ rule_id="CO-001",
121
+ urgency="before deployment",
122
+ )
123
+ )
124
+ priority += 1
125
+
126
+ if is_high_risk or answers.get("q5"):
127
+ if not has_assessment:
128
+ plan.immediate_actions.append(
129
+ Action(
130
+ priority=priority,
131
+ command=f"iris compliance assess --agent {name}",
132
+ why=(
133
+ "This generates the CO-002 impact assessment document. "
134
+ "Without it, production deployment is blocked."
135
+ ),
136
+ time_estimate="5 minutes",
137
+ rule_id="CO-002",
138
+ urgency="before deployment",
139
+ )
140
+ )
141
+ priority += 1
142
+
143
+ plan.immediate_actions.append(
144
+ Action(
145
+ priority=priority,
146
+ command=plan.one_liner.split("\n")[0] + "\n " + plan.one_liner.split("\n")[1],
147
+ why=(
148
+ "Runtime enforcement. IRIS blocks unapproved tool calls "
149
+ "and logs every decision to your local Evidence Vault."
150
+ ),
151
+ time_estimate="1 minute",
152
+ rule_id="CO-004",
153
+ urgency="before deployment",
154
+ )
155
+ )
156
+ priority += 1
157
+
158
+ if not has_policy:
159
+ plan.immediate_actions.append(
160
+ Action(
161
+ priority=priority,
162
+ command=f"iris policy compile --agent {name}",
163
+ why=(
164
+ "Compiles your policy-intent.md to Cedar. "
165
+ "Required for transparency (CO-003) and runtime gates."
166
+ ),
167
+ time_estimate="3 minutes",
168
+ rule_id="CO-003",
169
+ urgency="before deployment",
170
+ )
171
+ )
172
+
173
+ if q6 or "FedRAMP" in q7:
174
+ plan.next_30_days.append(
175
+ Action(
176
+ priority=1,
177
+ command="iris test --framework nist-ai-rmf",
178
+ why=(
179
+ "Your federal deployment requires NIST AI RMF alignment. "
180
+ "You are currently at 0 of 24 controls."
181
+ ),
182
+ time_estimate="ongoing — ~3 controls/week",
183
+ rule_id="NIST-001",
184
+ urgency="this month",
185
+ )
186
+ )
187
+
188
+ if "health" in str(q4).lower() or "HIPAA" in q7:
189
+ plan.next_30_days.append(
190
+ Action(
191
+ priority=2,
192
+ command="iris test --framework hipaa",
193
+ why="Health data handling requires HIPAA safeguards and audit controls.",
194
+ time_estimate="1-2 weeks",
195
+ rule_id="HIPAA-001",
196
+ urgency="this month",
197
+ )
198
+ )
199
+
200
+ if "financial" in str(q2).lower() or "financial" in str(q4).lower():
201
+ plan.next_30_days.append(
202
+ Action(
203
+ priority=3,
204
+ command="# Review CFPB fair lending controls with your compliance team",
205
+ why=(
206
+ "Financial agents must document fair lending practices "
207
+ "and non-discrimination review (CO-005)."
208
+ ),
209
+ time_estimate="this month",
210
+ rule_id="CO-005",
211
+ urgency="this month",
212
+ )
213
+ )
214
+
215
+ if "Outside the US" in set(answers.get("q8", [])):
216
+ plan.next_30_days.append(
217
+ Action(
218
+ priority=4,
219
+ command="iris test --framework gdpr",
220
+ why="EU users require GDPR data processing documentation and controls.",
221
+ time_estimate="2-3 weeks",
222
+ rule_id="GDPR-001",
223
+ urgency="this month",
224
+ )
225
+ )
226
+
227
+ if passport_exists:
228
+ from iris import AgentPassport
229
+
230
+ p = AgentPassport.from_yaml((agent_dir / "passport.yaml").read_text())
231
+ plan.current_scores["colorado-ai-act"] = compliance_score(p, agent_dir, "colorado-ai-act")
232
+ else:
233
+ plan.current_scores["colorado-ai-act"] = 0.0
234
+
235
+ plan.current_scores["nist-ai-rmf"] = 0.0
236
+ if agent_dir.exists() and (agent_dir / "passport.yaml").exists():
237
+ from iris import AgentPassport
238
+
239
+ p = AgentPassport.from_yaml((agent_dir / "passport.yaml").read_text())
240
+ plan.current_scores["nist-ai-rmf"] = compliance_score(p, agent_dir, "nist-ai-rmf")
241
+
242
+ remaining = sum(1 for a in plan.immediate_actions if a.command.startswith("iris"))
243
+ if remaining <= 1:
244
+ plan.estimated_time_to_compliant = "under 1 day"
245
+ elif remaining <= 3:
246
+ plan.estimated_time_to_compliant = "about 1 week"
247
+ else:
248
+ plan.estimated_time_to_compliant = "about 2 weeks"
249
+
250
+ return plan
251
+
252
+
253
+ def render_action_plan(plan: ActionPlan, agent_name: str | None, console) -> None:
254
+ from rich.panel import Panel
255
+
256
+ lines = [
257
+ "Based on your answers, here is what to do, in order.",
258
+ "",
259
+ "[bold]THIS WEEK (before you deploy this agent):[/bold]",
260
+ "",
261
+ ]
262
+ for action in plan.immediate_actions:
263
+ lines.append(f" [cyan]{action.priority}.[/cyan] Run: [bold]{action.command}[/bold]")
264
+ lines.append(f" Why: {action.why}")
265
+ lines.append(f" Time: {action.time_estimate}")
266
+ lines.append("")
267
+
268
+ if plan.next_30_days:
269
+ lines.append("[bold]NEXT 30 DAYS:[/bold]")
270
+ lines.append("")
271
+ for action in plan.next_30_days:
272
+ lines.append(f" [cyan]{action.priority}.[/cyan] Run: [bold]{action.command}[/bold]")
273
+ lines.append(f" Why: {action.why}")
274
+ lines.append(f" Time: {action.time_estimate}")
275
+ lines.append("")
276
+
277
+ lines.append("[bold]CURRENT SCORE:[/bold]")
278
+ agent_dir = _gov_dir(agent_name) if agent_name else Path.cwd() / "governance" / "agents" / "my-agent"
279
+ passport_obj = None
280
+ if agent_name and (agent_dir / "passport.yaml").exists():
281
+ from iris import AgentPassport
282
+
283
+ passport_obj = AgentPassport.from_yaml((agent_dir / "passport.yaml").read_text())
284
+
285
+ for framework, score in plan.current_scores.items():
286
+ pct = int(score * 100)
287
+ bar = progress_bar(score)
288
+ if passport_obj is not None:
289
+ if framework == "colorado-ai-act":
290
+ satisfied, total = colorado_controls_passed(passport_obj, agent_dir)
291
+ else:
292
+ satisfied, total = nist_controls_passed(passport_obj, agent_dir)
293
+ else:
294
+ satisfied, total = (0, 6 if framework == "colorado-ai-act" else 24)
295
+ label = "Colorado AI Act" if framework == "colorado-ai-act" else "NIST AI RMF"
296
+ lines.append(f" {label}: {satisfied} of {total} controls {bar} {pct}%")
297
+ lines.append(" [dim](Scores update automatically as you complete each step)[/dim]")
298
+ lines.append("")
299
+ lines.append(
300
+ f"Estimated time to compliant: [bold]{plan.estimated_time_to_compliant}[/bold]"
301
+ )
302
+ lines.append("")
303
+ lines.append("Run these commands now and your score changes in minutes.")
304
+
305
+ console.print(Panel("\n".join(lines), title="Your Compliance Action Plan", style="green"))
@@ -12,6 +12,7 @@ Outputs:
12
12
  """
13
13
 
14
14
  import click
15
+ import json
15
16
  import uuid
16
17
  import yaml
17
18
  from datetime import datetime
@@ -379,12 +380,29 @@ the agent's capabilities, data access, or decision scope changes.*
379
380
  """
380
381
 
381
382
 
383
+ def _load_answers(answers_path: Path | None) -> dict | None:
384
+ if answers_path is None:
385
+ return None
386
+ if not answers_path.exists():
387
+ raise click.ClickException(f"Answers file not found: {answers_path}")
388
+ data = json.loads(answers_path.read_text())
389
+ if not isinstance(data, dict):
390
+ raise click.ClickException(f"Answers file must be a JSON object: {answers_path}")
391
+ return data
392
+
393
+
382
394
  @click.command("assess")
383
395
  @click.option("--agent", required=True, help="Agent name to assess")
384
396
  @click.option("--assessor", default=None, help="Your name or email (recorded in audit trail)")
385
397
  @click.option("--dir", "governance_dir", type=Path, default=None)
386
398
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
387
- def compliance_assess(agent, assessor, governance_dir, yes):
399
+ @click.option(
400
+ "--answers",
401
+ type=click.Path(path_type=Path, exists=True),
402
+ default=None,
403
+ help="JSON file with pre-filled questionnaire answers (for automated demos)",
404
+ )
405
+ def compliance_assess(agent, assessor, governance_dir, yes, answers):
388
406
  """
389
407
  Run a Colorado AI Act impact assessment for an agent.
390
408
 
@@ -422,11 +440,12 @@ def compliance_assess(agent, assessor, governance_dir, yes):
422
440
  ):
423
441
  raise SystemExit(0)
424
442
 
425
- # Run questionnaire
426
- answers = run_questionnaire()
443
+ # Run questionnaire (or load pre-filled answers for demos)
444
+ preset_answers = _load_answers(answers)
445
+ questionnaire_answers = preset_answers if preset_answers is not None else run_questionnaire()
427
446
 
428
447
  # Calculate risk
429
- risk_level, findings, recommendations = calculate_risk_level(answers)
448
+ risk_level, findings, recommendations = calculate_risk_level(questionnaire_answers)
430
449
 
431
450
  # Generate assessment ID and document
432
451
  assessment_id = f"IA-{agent}-{uuid.uuid4().hex[:8].upper()}"
@@ -439,7 +458,7 @@ def compliance_assess(agent, assessor, governance_dir, yes):
439
458
  assessment_md = generate_assessment_markdown(
440
459
  agent_name=agent,
441
460
  owner=owner,
442
- answers=answers,
461
+ answers=questionnaire_answers,
443
462
  risk_level=risk_level,
444
463
  findings=findings,
445
464
  recommendations=recommendations,
@@ -460,7 +479,6 @@ def compliance_assess(agent, assessor, governance_dir, yes):
460
479
  # Write to local evidence vault
461
480
  vault_dir = Path.home() / ".iris" / "evidence" / agent
462
481
  vault_dir.mkdir(parents=True, exist_ok=True)
463
- import json
464
482
  with open(vault_dir / "assessments.jsonl", "a") as f:
465
483
  f.write(json.dumps({
466
484
  "assessment_id": assessment_id,
@@ -20,12 +20,15 @@ EXAMPLE_CONFIG = """\
20
20
  # Copy to ~/.iris/config.yaml and add your API key via environment variable.
21
21
 
22
22
  compiler:
23
- backend: anthropic # anthropic | openai
23
+ backend: anthropic # anthropic | openai | google | mistral | groq | ollama | together
24
24
  model: claude-sonnet-4-6
25
25
 
26
26
  # API keys are read from environment variables (never stored here):
27
27
  # export ANTHROPIC_API_KEY=sk-ant-...
28
28
  # export OPENAI_API_KEY=sk-...
29
+ # export MISTRAL_API_KEY=...
30
+ # export GROQ_API_KEY=...
31
+ # Or use LiteLLM: iris policy compile --litellm-model ollama/llama3.2
29
32
  """
30
33
 
31
34
 
@@ -41,14 +44,24 @@ def load_iris_config(config_path: Path | None = None) -> dict[str, Any]:
41
44
  return {}
42
45
 
43
46
 
44
- def create_policy_compiler(config_path: Path | None = None) -> PolicyCompiler:
45
- """Build a PolicyCompiler using the developer's ~/.iris/config.yaml settings."""
47
+ def create_policy_compiler(
48
+ config_path: Path | None = None,
49
+ llm_backend: str | None = None,
50
+ model: str | None = None,
51
+ litellm_model: str | None = None,
52
+ ) -> PolicyCompiler:
53
+ """Build a PolicyCompiler using CLI flags and ~/.iris/config.yaml settings."""
46
54
  cfg = load_iris_config(config_path).get("compiler", {})
47
55
  return PolicyCompiler(
48
- llm_backend=cfg.get("backend", "anthropic"),
49
- model=cfg.get("model", "claude-sonnet-4-6"),
56
+ llm_backend=llm_backend or cfg.get("backend"),
57
+ model=model or cfg.get("model"),
58
+ litellm_model=litellm_model,
50
59
  )
51
60
 
52
61
 
53
62
  def compiler_info(compiler: PolicyCompiler) -> tuple[str, str]:
63
+ if compiler._mode == "litellm":
64
+ return "litellm", compiler._litellm_model
65
+ if compiler._mode == "custom":
66
+ return "custom", compiler._model
54
67
  return compiler._llm_backend, compiler._model
@@ -0,0 +1,160 @@
1
+ """iris dlp — scan files and test prompts against DLP rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import click
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ console = Console()
15
+
16
+
17
+ def _line_number_for_offset(text: str, offset: int) -> int:
18
+ return text.count("\n", 0, offset) + 1
19
+
20
+
21
+ def _load_passport_for_agent(agent_name: str, governance_dir: Path):
22
+ from iris_core.models.passport import AgentPassport
23
+
24
+ passport_path = governance_dir / agent_name / "passport.yaml"
25
+ if not passport_path.exists():
26
+ for candidate in governance_dir.rglob("passport.yaml"):
27
+ passport = AgentPassport.from_yaml(candidate.read_text())
28
+ if passport.name == agent_name or candidate.parent.name == agent_name:
29
+ return passport
30
+ console.print(f"[red]Agent passport not found for '{agent_name}'.[/red]")
31
+ console.print(f"Expected: {governance_dir / agent_name / 'passport.yaml'}")
32
+ sys.exit(1)
33
+ return AgentPassport.from_yaml(passport_path.read_text())
34
+
35
+
36
+ @click.group()
37
+ def dlp():
38
+ """Data Loss Prevention scanning commands."""
39
+ pass
40
+
41
+
42
+ @dlp.command("scan")
43
+ @click.option("--file", "file_path", type=click.Path(exists=True, dir_okay=False), required=True)
44
+ def dlp_scan(file_path: str):
45
+ """
46
+ Scan a file for sensitive data patterns.
47
+
48
+ Shows findings with line numbers and severity — useful for checking test fixtures.
49
+
50
+ Example:
51
+ iris dlp scan --file tests/fixtures/patient_record.txt
52
+ """
53
+ from iris_core.dlp import DLPScanner
54
+ from iris_core.models.passport import AgentPassport, DataClassification
55
+
56
+ text = Path(file_path).read_text()
57
+ passport = AgentPassport(
58
+ name="dlp-scan",
59
+ data_classification=DataClassification.PHI,
60
+ )
61
+ scanner = DLPScanner(passport)
62
+ result = scanner.scan(text, direction="prompt")
63
+
64
+ if not result.findings:
65
+ console.print(Panel("[bold green]✓ No sensitive data patterns detected[/bold green]", style="green"))
66
+ sys.exit(0)
67
+
68
+ table = Table(title=f"DLP findings — {file_path}")
69
+ table.add_column("Line", style="cyan")
70
+ table.add_column("Pattern", style="magenta")
71
+ table.add_column("Severity", style="yellow")
72
+ table.add_column("Rule", style="dim")
73
+ table.add_column("Message")
74
+
75
+ for finding in result.findings:
76
+ line = _line_number_for_offset(text, finding.match_start)
77
+ table.add_row(
78
+ str(line),
79
+ finding.pattern_id,
80
+ finding.severity.value,
81
+ finding.rule_id,
82
+ finding.message,
83
+ )
84
+
85
+ console.print(table)
86
+ console.print(
87
+ f"\n[dim]Scan completed in {result.scan_duration_ms:.2f}ms "
88
+ f"({len(result.findings)} finding(s))[/dim]"
89
+ )
90
+ sys.exit(1 if result.should_block else 0)
91
+
92
+
93
+ @dlp.command("test")
94
+ @click.option("--agent", required=True, help="Agent name from governance/agents/")
95
+ @click.option("--prompt", required=True, help="Prompt text to test against the agent's DLP rules")
96
+ @click.option("--dir", "governance_dir", type=click.Path(file_okay=False), default=None)
97
+ def dlp_test(agent: str, prompt: str, governance_dir: str | None):
98
+ """
99
+ Test a prompt against an agent's DLP rules.
100
+
101
+ Shows what IRIS would do with that prompt in the current environment.
102
+
103
+ Example:
104
+ iris dlp test --agent payment-agent --prompt "Process SSN 123-45-6789"
105
+ """
106
+ from iris_core.dlp import DLPScanner
107
+ from iris_core.dlp.enforcement import dlp_policy_result
108
+ from iris_core.models.passport import Environment
109
+
110
+ gov_dir = Path(governance_dir) if governance_dir else Path.cwd() / "governance" / "agents"
111
+ passport = _load_passport_for_agent(agent, gov_dir)
112
+ env = Environment(os.environ.get("IRIS_ENV", "dev"))
113
+ scanner = DLPScanner(passport)
114
+ result = scanner.scan_prompt(prompt)
115
+
116
+ console.print(
117
+ Panel(
118
+ f"[bold]Agent:[/bold] {passport.name}\n"
119
+ f"[bold]Classification:[/bold] {passport.data_classification.value}\n"
120
+ f"[bold]Environment:[/bold] {env.value}",
121
+ title="IRIS DLP Test",
122
+ style="blue",
123
+ )
124
+ )
125
+
126
+ if not result.findings:
127
+ console.print("\n[bold green]✓ No DLP findings — prompt would be allowed[/bold green]")
128
+ sys.exit(0)
129
+
130
+ for finding in result.findings:
131
+ console.print(
132
+ f"\n[yellow]• {finding.pattern_id}[/yellow] "
133
+ f"({finding.severity.value}) — {finding.message}"
134
+ )
135
+ console.print(f" Rule: {finding.rule_id}")
136
+ console.print(f" Position: {finding.match_start}-{finding.match_end}")
137
+
138
+ if result.should_block:
139
+ action = "BLOCK (raise IrisViolationError)"
140
+ elif result.has_high:
141
+ action = (
142
+ "BLOCK in production"
143
+ if env in (Environment.PRODUCTION, Environment.STAGING)
144
+ else "WARN and continue in dev/test"
145
+ )
146
+ else:
147
+ action = "ALLOW with informational findings"
148
+
149
+ console.print(f"\n[bold]IRIS action:[/bold] {action}")
150
+ if result.redacted_text and result.redacted_text != prompt:
151
+ console.print("\n[bold]Redacted preview:[/bold]")
152
+ console.print(result.redacted_text)
153
+
154
+ policy = dlp_policy_result(result, passport, env, direction="prompt")
155
+ if policy.violations:
156
+ console.print(f"\n[dim]Primary violation: {policy.violations[0].rule_id}[/dim]")
157
+
158
+ sys.exit(1 if result.should_block or (
159
+ result.has_high and env in (Environment.PRODUCTION, Environment.STAGING)
160
+ ) else 0)