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/__init__.py +0 -0
- iris_cli/assess.py +498 -0
- iris_cli/cedar_parser.py +454 -0
- iris_cli/compiler_config.py +54 -0
- iris_cli/evidence.py +822 -0
- iris_cli/main.py +542 -0
- iris_cli/mcp_server.py +567 -0
- iris_cli/policy_cache.py +116 -0
- iris_cli/policy_diff.py +467 -0
- iris_cli/scan_report.py +146 -0
- iris_security_cli-0.1.0.dist-info/METADATA +45 -0
- iris_security_cli-0.1.0.dist-info/RECORD +17 -0
- iris_security_cli-0.1.0.dist-info/WHEEL +5 -0
- iris_security_cli-0.1.0.dist-info/entry_points.txt +2 -0
- iris_security_cli-0.1.0.dist-info/top_level.txt +2 -0
- tests/test_evidence.py +296 -0
- tests/test_policy_diff.py +250 -0
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()
|