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.
tests/test_evidence.py ADDED
@@ -0,0 +1,296 @@
1
+ """Tests for Evidence Vault read path and iris evidence CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+ from click.testing import CliRunner
11
+
12
+ from iris_cli.evidence import (
13
+ aggregate_stats,
14
+ build_report_data,
15
+ format_report_json,
16
+ format_report_markdown,
17
+ )
18
+ from iris_cli.main import cli
19
+ from iris_core.evidence.vault import EvidenceVault
20
+
21
+
22
+ PASSPORT_YAML = """
23
+ apiVersion: iris.io/v1alpha1
24
+ kind: AgentPassport
25
+ metadata:
26
+ name: payment-agent
27
+ agent_id: 6684638e-582c-4be5-ad94-f6029738305f
28
+ spec:
29
+ version: 0.1.0
30
+ owner: gilbert.martin@gmail.com
31
+ team: iris-platform
32
+ data_classification: pii
33
+ compliance_tags:
34
+ - colorado-ai-act
35
+ environments:
36
+ - dev
37
+ - staging
38
+ intent_ref: governance/agents/payment-agent/policy-intent.md
39
+ is_high_risk_ai: true
40
+ evidence_vault_id: IA-payment-agent-A3F2B1C4
41
+ last_reviewed_at: '2026-05-20T07:28:24.684454'
42
+ """
43
+
44
+
45
+ def _write_vault(vault_root: Path, agent: str, events: list, assessments: list | None = None):
46
+ agent_dir = vault_root / agent
47
+ agent_dir.mkdir(parents=True)
48
+ with open(agent_dir / "events.jsonl", "w") as f:
49
+ for event in events:
50
+ f.write(json.dumps(event) + "\n")
51
+ if assessments is not None:
52
+ with open(agent_dir / "assessments.jsonl", "w") as f:
53
+ for assessment in assessments:
54
+ f.write(json.dumps(assessment) + "\n")
55
+
56
+
57
+ def _sample_events(agent: str) -> list:
58
+ return [
59
+ {
60
+ "event_id": "e1",
61
+ "timestamp": "2026-05-28T10:00:00",
62
+ "agent_id": agent,
63
+ "action": "call",
64
+ "resource": "payments-api",
65
+ "environment": "dev",
66
+ "decision": "PERMIT",
67
+ "violations": [],
68
+ },
69
+ {
70
+ "event_id": "e2",
71
+ "timestamp": "2026-05-28T11:00:00",
72
+ "agent_id": agent,
73
+ "action": "call",
74
+ "resource": "unknown-tool",
75
+ "environment": "dev",
76
+ "decision": "DENY",
77
+ "violations": [
78
+ {
79
+ "rule_id": "IRIS-TOOL-001",
80
+ "severity": "HIGH",
81
+ "message": "Tool not in declared permissions",
82
+ "compliance_refs": [],
83
+ }
84
+ ],
85
+ },
86
+ {
87
+ "event_id": "e3",
88
+ "timestamp": "2026-05-28T12:00:00",
89
+ "agent_id": agent,
90
+ "action": "write",
91
+ "resource": "storage-api",
92
+ "environment": "staging",
93
+ "decision": "DENY",
94
+ "violations": [
95
+ {
96
+ "rule_id": "IRIS-XR-001",
97
+ "severity": "CRITICAL",
98
+ "message": "Cross-region transfer attempted",
99
+ "compliance_refs": ["china-pipl"],
100
+ }
101
+ ],
102
+ },
103
+ {
104
+ "event_id": "e4",
105
+ "timestamp": "2026-05-28T13:00:00",
106
+ "agent_id": agent,
107
+ "action": "approve",
108
+ "resource": "loan-decision",
109
+ "environment": "staging",
110
+ "decision": "HITL",
111
+ "violations": [],
112
+ },
113
+ ]
114
+
115
+
116
+ @pytest.fixture
117
+ def gov_dir(tmp_path):
118
+ agent_dir = tmp_path / "governance" / "agents" / "payment-agent"
119
+ agent_dir.mkdir(parents=True)
120
+ (agent_dir / "passport.yaml").write_text(PASSPORT_YAML)
121
+ return tmp_path / "governance" / "agents"
122
+
123
+
124
+ @pytest.fixture
125
+ def vault_root(tmp_path):
126
+ return tmp_path / "evidence"
127
+
128
+
129
+ @pytest.fixture
130
+ def populated_vault(vault_root):
131
+ assessments = [
132
+ {
133
+ "assessment_id": "IA-payment-agent-A3F2B1C4",
134
+ "agent": "payment-agent",
135
+ "risk_level": "MEDIUM",
136
+ "assessed_by": "iris-platform",
137
+ "timestamp": "2026-05-20T07:28:24.684454",
138
+ "findings_count": 2,
139
+ "framework": "colorado-ai-act",
140
+ }
141
+ ]
142
+ _write_vault(
143
+ vault_root,
144
+ "payment-agent",
145
+ _sample_events("payment-agent"),
146
+ assessments=assessments,
147
+ )
148
+ return vault_root
149
+
150
+
151
+ def test_vault_summary_calculates_correctly(populated_vault):
152
+ vault = EvidenceVault(agent_id="payment-agent", vault_dir=populated_vault)
153
+ summary = vault.get_summary(last_reviewed_at="2026-05-20T07:28:24.684454")
154
+
155
+ assert summary.agent_id == "payment-agent"
156
+ assert summary.total_evaluations == 4
157
+ assert summary.total_violations == 2
158
+ assert summary.violations_by_severity["HIGH"] == 1
159
+ assert summary.violations_by_severity["CRITICAL"] == 1
160
+ assert summary.violations_by_rule["IRIS-TOOL-001"] == 1
161
+ assert summary.violations_by_rule["IRIS-XR-001"] == 1
162
+ assert summary.most_violated_rule in ("IRIS-TOOL-001", "IRIS-XR-001")
163
+ assert summary.compliance_pass_rate == pytest.approx(0.5)
164
+ assert set(summary.environments_active) == {"dev", "staging"}
165
+ assert summary.cross_region_blocks == 1
166
+ assert summary.hitl_gates_triggered == 1
167
+ assert summary.last_assessment_date.startswith("2026-05-20")
168
+
169
+
170
+ def test_report_includes_all_sections(gov_dir, populated_vault):
171
+ from iris_core.models.passport import AgentPassport
172
+
173
+ passport = AgentPassport.from_yaml((gov_dir / "payment-agent" / "passport.yaml").read_text())
174
+ vault = EvidenceVault(agent_id="payment-agent", vault_dir=populated_vault)
175
+ data = build_report_data("payment-agent", passport, vault)
176
+
177
+ markdown = format_report_markdown(data)
178
+ required_sections = [
179
+ "## Agent Identity",
180
+ "## Compliance Status",
181
+ "## Evaluation Statistics",
182
+ "## Top Violations",
183
+ "## Impact Assessment History",
184
+ "## Cross-Region Detection",
185
+ "## HITL Gate Events",
186
+ "## Annual Review",
187
+ "## Evidence Vault Integrity",
188
+ ]
189
+ for section in required_sections:
190
+ assert section in markdown
191
+
192
+ json_report = json.loads(format_report_json(data))
193
+ assert json_report["schema_version"] == "1.0"
194
+ assert json_report["integrity"]["valid"] is True
195
+
196
+
197
+ def test_export_json_is_valid(gov_dir, populated_vault, tmp_path):
198
+ runner = CliRunner()
199
+ output = tmp_path / "audit.json"
200
+ result = runner.invoke(
201
+ cli,
202
+ [
203
+ "evidence",
204
+ "export",
205
+ "--agent",
206
+ "payment-agent",
207
+ "--output",
208
+ str(output),
209
+ "--dir",
210
+ str(gov_dir),
211
+ "--vault-dir",
212
+ str(populated_vault),
213
+ ],
214
+ )
215
+ assert result.exit_code == 0, result.output
216
+
217
+ exported = json.loads(output.read_text())
218
+ assert exported["agent_id"] == "payment-agent"
219
+ assert len(exported["events"]) == 4
220
+ assert len(exported["assessments"]) == 1
221
+ assert exported["integrity"]["valid"] is True
222
+
223
+
224
+ def test_stats_aggregates_across_agents(gov_dir, vault_root):
225
+ loan_dir = gov_dir / "loan-agent"
226
+ loan_dir.mkdir()
227
+ (loan_dir / "passport.yaml").write_text(
228
+ PASSPORT_YAML.replace("payment-agent", "loan-agent").replace(
229
+ "IA-payment-agent-A3F2B1C4", "IA-loan-agent-B1C2D3E4"
230
+ )
231
+ )
232
+
233
+ _write_vault(
234
+ vault_root,
235
+ "payment-agent",
236
+ _sample_events("payment-agent"),
237
+ assessments=[
238
+ {
239
+ "assessment_id": "IA-payment-agent-A3F2B1C4",
240
+ "timestamp": "2026-05-20T07:28:24.684454",
241
+ }
242
+ ],
243
+ )
244
+ _write_vault(
245
+ vault_root,
246
+ "loan-agent",
247
+ [
248
+ {
249
+ "event_id": "l1",
250
+ "timestamp": datetime.utcnow().isoformat(),
251
+ "agent_id": "loan-agent",
252
+ "action": "read",
253
+ "resource": "credit-score",
254
+ "environment": "dev",
255
+ "decision": "DENY",
256
+ "violations": [
257
+ {
258
+ "rule_id": "IRIS-TOOL-001",
259
+ "severity": "CRITICAL",
260
+ "message": "blocked",
261
+ "compliance_refs": [],
262
+ }
263
+ ],
264
+ }
265
+ ],
266
+ )
267
+
268
+ stats = aggregate_stats(gov_dir, vault_root=vault_root)
269
+ assert stats["total_agents"] == 2
270
+ assert stats["total_evaluations_this_week"] >= 1
271
+ assert any(r["rule_id"] == "IRIS-TOOL-001" for r in stats["top_violated_rules"])
272
+ assert any(a["agent"] == "loan-agent" for a in stats["agents_with_critical_violations"])
273
+
274
+
275
+ def test_annual_review_deadline_calculated(gov_dir, populated_vault):
276
+ from iris_core.models.passport import AgentPassport
277
+
278
+ passport = AgentPassport.from_yaml((gov_dir / "payment-agent" / "passport.yaml").read_text())
279
+ vault = EvidenceVault(agent_id="payment-agent", vault_dir=populated_vault)
280
+
281
+ reviewed = datetime.utcnow() - timedelta(days=10)
282
+ passport.last_reviewed_at = reviewed
283
+ summary = vault.get_summary(last_reviewed_at=reviewed.isoformat())
284
+
285
+ assert summary.days_until_annual_review == 355
286
+
287
+ data = build_report_data("payment-agent", passport, vault)
288
+ assert data["annual_review"]["status"] == "CURRENT"
289
+ assert data["annual_review"]["days_until"] == 355
290
+
291
+ passport.last_reviewed_at = None
292
+ summary_no_review = vault.get_summary(last_reviewed_at=None)
293
+ assert summary_no_review.days_until_annual_review is None
294
+
295
+ data_overdue = build_report_data("payment-agent", passport, vault)
296
+ assert data_overdue["annual_review"]["status"] == "OVERDUE"
@@ -0,0 +1,250 @@
1
+ """Tests for iris policy diff and Cedar parser."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from iris_cli.cedar_parser import CedarRule, diff_cedar, parse_cedar, summarize_diffs
8
+ from iris_cli.policy_cache import check_draft_cache, save_policy_draft
9
+ from iris_cli.policy_diff import format_diff_json, run_policy_diff
10
+
11
+
12
+ PERMIT_READ_PAYMENTS = """
13
+ // Satisfies: CO-004
14
+ permit (
15
+ principal == iris::AgentPassport::"payment-agent",
16
+ action == iris::Action::"read",
17
+ resource == iris::API::"payments"
18
+ )
19
+ when {
20
+ context.environment in ["dev", "test", "staging", "production"] &&
21
+ context.user_consent_logged == true
22
+ };
23
+ """
24
+
25
+ PERMIT_CALL_SENDGRID = """
26
+ // Satisfies: CO-004 (consent gate)
27
+ permit (
28
+ principal == iris::AgentPassport::"payment-agent",
29
+ action == iris::Action::"call",
30
+ resource == iris::API::"sendgrid-email-api"
31
+ )
32
+ when {
33
+ context.environment in ["dev", "test", "staging", "production"] &&
34
+ context.user_consent_logged == true
35
+ };
36
+ """
37
+
38
+ FORBID_PII = """
39
+ // Satisfies: CO-004, GDPR
40
+ forbid (
41
+ principal == iris::AgentPassport::"payment-agent",
42
+ action == iris::Action::"write",
43
+ resource == iris::DataClass::"pii"
44
+ )
45
+ unless {
46
+ context.user_consent_logged == true
47
+ };
48
+ """
49
+
50
+ PERMIT_READ_PAYMENTS_PROD_ONLY = """
51
+ // Satisfies: CO-004
52
+ permit (
53
+ principal == iris::AgentPassport::"payment-agent",
54
+ action == iris::Action::"read",
55
+ resource == iris::API::"payments"
56
+ )
57
+ when {
58
+ context.environment == "production" &&
59
+ context.user_consent_logged == true
60
+ };
61
+ """
62
+
63
+
64
+ @pytest.fixture
65
+ def gov_dir(tmp_path):
66
+ agent_dir = tmp_path / "governance" / "agents" / "payment-agent"
67
+ agent_dir.mkdir(parents=True)
68
+ (agent_dir / "passport.yaml").write_text(
69
+ """
70
+ name: payment-agent
71
+ owner: test@example.com
72
+ team: platform
73
+ data_classification: pii
74
+ compliance_tags:
75
+ - colorado-ai-act
76
+ environments:
77
+ - dev
78
+ is_high_risk_ai: true
79
+ """
80
+ )
81
+ (agent_dir / "policy-intent.md").write_text("# Intent\nAllow payments access.")
82
+ return agent_dir
83
+
84
+
85
+ @pytest.fixture
86
+ def sample_old_cedar():
87
+ return PERMIT_READ_PAYMENTS + FORBID_PII
88
+
89
+
90
+ @pytest.fixture
91
+ def sample_new_cedar():
92
+ return PERMIT_READ_PAYMENTS + PERMIT_CALL_SENDGRID + FORBID_PII
93
+
94
+
95
+ def test_diff_detects_added_rule(sample_old_cedar, sample_new_cedar):
96
+ old_rules = parse_cedar(sample_old_cedar)
97
+ new_rules = parse_cedar(sample_new_cedar)
98
+ diffs = diff_cedar(old_rules, new_rules)
99
+
100
+ added = [d for d in diffs if d.status == "ADDED"]
101
+ assert len(added) == 1
102
+ assert added[0].new_rule.action == 'iris::Action::"call"'
103
+ assert "sendgrid" in added[0].new_rule.resource.lower()
104
+ assert added[0].risk_delta == "NEUTRAL"
105
+ assert "CO-004" in added[0].compliance_affected
106
+
107
+
108
+ def test_diff_detects_removed_permit_as_increased_risk():
109
+ old = parse_cedar(PERMIT_READ_PAYMENTS + PERMIT_CALL_SENDGRID)
110
+ new = parse_cedar(PERMIT_READ_PAYMENTS)
111
+ diffs = diff_cedar(old, new)
112
+
113
+ removed = [d for d in diffs if d.status == "REMOVED"]
114
+ assert len(removed) == 1
115
+ assert removed[0].old_rule.type == "permit"
116
+ assert removed[0].risk_delta == "INCREASED"
117
+ assert "capability removed" in removed[0].risk_reason.lower()
118
+
119
+
120
+ def test_diff_detects_added_forbid_as_decreased_risk(sample_old_cedar):
121
+ old = parse_cedar(PERMIT_READ_PAYMENTS)
122
+ new = parse_cedar(PERMIT_READ_PAYMENTS + FORBID_PII)
123
+ diffs = diff_cedar(old, new)
124
+
125
+ added = [d for d in diffs if d.status == "ADDED"]
126
+ assert len(added) == 1
127
+ assert added[0].new_rule.type == "forbid"
128
+ assert added[0].risk_delta == "DECREASED"
129
+ assert "restriction" in added[0].risk_reason.lower()
130
+
131
+
132
+ def test_diff_unchanged_not_shown_by_default(sample_old_cedar, sample_new_cedar):
133
+ old_rules = parse_cedar(sample_old_cedar)
134
+ new_rules = parse_cedar(sample_new_cedar)
135
+ all_diffs = diff_cedar(old_rules, new_rules)
136
+ visible = [d for d in all_diffs if d.status != "UNCHANGED"]
137
+
138
+ unchanged = [d for d in all_diffs if d.status == "UNCHANGED"]
139
+ assert len(unchanged) >= 1
140
+ assert all(d.status != "UNCHANGED" for d in visible)
141
+
142
+
143
+ def test_diff_compliance_impact_shown():
144
+ old = parse_cedar(PERMIT_READ_PAYMENTS)
145
+ new = parse_cedar(PERMIT_READ_PAYMENTS_PROD_ONLY)
146
+ diffs = diff_cedar(old, new)
147
+ summary = summarize_diffs(diffs)
148
+
149
+ modified = [d for d in diffs if d.status == "MODIFIED"]
150
+ assert len(modified) == 1
151
+ assert "CO-004" in modified[0].compliance_affected
152
+ assert modified[0].risk_delta == "DECREASED"
153
+ assert summary["violations_opened"] == 0
154
+ assert summary["coverage_strengthened"].get("CO-004", 0) >= 1
155
+
156
+
157
+ def test_diff_json_output(sample_old_cedar, sample_new_cedar, gov_dir):
158
+ (gov_dir / "policy.cedar").write_text(sample_old_cedar)
159
+ intent = (gov_dir / "policy-intent.md").read_text()
160
+ save_policy_draft(gov_dir, intent, sample_new_cedar, "anthropic", "claude-sonnet-4-6")
161
+
162
+ result = run_policy_diff(
163
+ agent="payment-agent",
164
+ governance_dir=gov_dir,
165
+ )
166
+ payload = json.loads(format_diff_json(result))
167
+
168
+ assert payload["agent"] == "payment-agent"
169
+ assert payload["draft_stale"] is False
170
+ assert "summary" in payload
171
+ assert payload["summary"]["counts"]["ADDED"] == 1
172
+ assert len(payload["diffs"]) == 1
173
+ assert payload["diffs"][0]["status"] == "ADDED"
174
+ assert payload["diffs"][0]["risk_delta"] == "NEUTRAL"
175
+
176
+
177
+ def test_diff_uses_cached_draft_offline(sample_old_cedar, sample_new_cedar, gov_dir):
178
+ (gov_dir / "policy.cedar").write_text(sample_old_cedar)
179
+ intent = (gov_dir / "policy-intent.md").read_text()
180
+ save_policy_draft(gov_dir, intent, sample_new_cedar, "openai", "gpt-4o")
181
+
182
+ result = run_policy_diff(agent="payment-agent", governance_dir=gov_dir)
183
+ assert result.summary["counts"]["ADDED"] == 1
184
+ assert result.draft_stale is False
185
+ assert result.draft_status.meta.compiler_backend == "openai"
186
+
187
+
188
+ def test_diff_detects_stale_draft(sample_old_cedar, sample_new_cedar, gov_dir):
189
+ (gov_dir / "policy.cedar").write_text(sample_old_cedar)
190
+ save_policy_draft(
191
+ gov_dir,
192
+ "old intent text",
193
+ sample_new_cedar,
194
+ "anthropic",
195
+ "claude-sonnet-4-6",
196
+ )
197
+
198
+ result = run_policy_diff(agent="payment-agent", governance_dir=gov_dir)
199
+ assert result.draft_stale is True
200
+
201
+
202
+ def test_diff_missing_draft_raises_helpful_error(gov_dir):
203
+ (gov_dir / "policy.cedar").write_text(PERMIT_READ_PAYMENTS)
204
+
205
+ with pytest.raises(FileNotFoundError, match="No cached policy draft"):
206
+ run_policy_diff(agent="payment-agent", governance_dir=gov_dir)
207
+
208
+
209
+ def test_save_and_check_draft_cache(gov_dir):
210
+ intent = "# Intent\nAllow payments."
211
+ save_policy_draft(gov_dir, intent, PERMIT_READ_PAYMENTS, "anthropic", "test-model")
212
+ status = check_draft_cache(gov_dir, intent)
213
+ assert status.draft_exists
214
+ assert status.is_stale is False
215
+
216
+ status_after_edit = check_draft_cache(gov_dir, intent + "\nMore text.")
217
+ assert status_after_edit.is_stale is True
218
+
219
+
220
+ def test_cedar_parser_extracts_permit():
221
+ rules = parse_cedar(PERMIT_READ_PAYMENTS)
222
+ assert len(rules) == 1
223
+ assert rules[0].type == "permit"
224
+ assert "read" in rules[0].plain_english.lower()
225
+ assert "payments" in rules[0].plain_english.lower()
226
+ assert rules[0].compliance_refs == ["CO-004"]
227
+
228
+
229
+ def test_cedar_parser_extracts_forbid_with_unless():
230
+ rules = parse_cedar(FORBID_PII)
231
+ assert len(rules) == 1
232
+ assert "forbidden" in rules[0].plain_english.lower()
233
+ assert "PII" in rules[0].plain_english
234
+ assert "CO-004" in rules[0].compliance_refs
235
+ assert "GDPR" in rules[0].compliance_refs
236
+
237
+
238
+ def test_plain_english_generation():
239
+ permit = parse_cedar(PERMIT_READ_PAYMENTS)[0]
240
+ assert permit.plain_english == "Agent may read from payments API with consent"
241
+
242
+ forbid = parse_cedar(FORBID_PII)[0]
243
+ assert forbid.plain_english == (
244
+ "Agent is forbidden from write to PII data unless conditions are met"
245
+ )
246
+
247
+ sendgrid = parse_cedar(PERMIT_CALL_SENDGRID)[0]
248
+ assert "call" in sendgrid.plain_english.lower()
249
+ assert "sendgrid" in sendgrid.plain_english.lower()
250
+ assert "consent" in sendgrid.plain_english.lower()