iris-security-cli 0.1.0__tar.gz → 0.1.1__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.0 → iris_security_cli-0.1.1}/PKG-INFO +4 -2
- iris_security_cli-0.1.1/iris_cli/action_plan.py +305 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/assess.py +24 -6
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/compiler_config.py +18 -5
- iris_security_cli-0.1.1/iris_cli/dlp_cmd.py +160 -0
- iris_security_cli-0.1.1/iris_cli/drift.py +272 -0
- iris_security_cli-0.1.1/iris_cli/explain.py +70 -0
- iris_security_cli-0.1.1/iris_cli/framework_suggest.py +423 -0
- iris_security_cli-0.1.1/iris_cli/framework_test.py +465 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/main.py +113 -15
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/mcp_server.py +1 -1
- iris_security_cli-0.1.1/iris_cli/redteam.py +179 -0
- iris_security_cli-0.1.1/iris_cli/scan_govern.py +330 -0
- iris_security_cli-0.1.1/iris_cli/scm.py +354 -0
- iris_security_cli-0.1.1/iris_cli/status_cmd.py +109 -0
- iris_security_cli-0.1.1/iris_cli/users.py +99 -0
- iris_security_cli-0.1.1/iris_cli/watch.py +102 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_security_cli.egg-info/PKG-INFO +4 -2
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_security_cli.egg-info/SOURCES.txt +14 -0
- iris_security_cli-0.1.1/iris_security_cli.egg-info/requires.txt +8 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/pyproject.toml +5 -2
- iris_security_cli-0.1.1/tests/test_framework_suggest.py +111 -0
- iris_security_cli-0.1.1/tests/test_framework_test.py +183 -0
- iris_security_cli-0.1.0/iris_security_cli.egg-info/requires.txt +0 -5
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/README.md +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/evidence.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/policy_cache.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/setup.cfg +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.1.0 → iris_security_cli-0.1.1}/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.1
|
|
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.
|
|
19
|
+
Requires-Dist: iris-security-sdk>=0.1.1
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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(
|
|
45
|
-
|
|
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"
|
|
49
|
-
model=cfg.get("model"
|
|
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)
|