iris-security-cli 0.2.12__tar.gz → 0.2.14__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 (70) hide show
  1. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/PKG-INFO +5 -5
  2. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/README.md +1 -1
  3. iris_security_cli-0.2.14/iris_cli/audit_log.py +614 -0
  4. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/certify.py +89 -4
  5. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/compliance_check_cmd.py +38 -1
  6. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/evidence.py +75 -10
  7. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/explain.py +1 -1
  8. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/main.py +9 -1
  9. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/quickstart.py +2 -2
  10. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/scm.py +89 -1
  11. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_security_cli.egg-info/PKG-INFO +5 -5
  12. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_security_cli.egg-info/SOURCES.txt +4 -0
  13. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_security_cli.egg-info/requires.txt +1 -1
  14. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/pyproject.toml +4 -4
  15. iris_security_cli-0.2.14/tests/test_audit_log.py +96 -0
  16. iris_security_cli-0.2.14/tests/test_docs_pages_check.py +45 -0
  17. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_entitlements_display.py +2 -1
  18. iris_security_cli-0.2.14/tests/test_evidence_export.py +81 -0
  19. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_framework_test.py +2 -1
  20. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/__init__.py +0 -0
  21. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/action_plan.py +0 -0
  22. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/assess.py +0 -0
  23. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/cedar_parser.py +0 -0
  24. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/compiler_config.py +0 -0
  25. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/compliance_fix.py +0 -0
  26. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/cost.py +0 -0
  27. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/declare.py +0 -0
  28. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/delegation.py +0 -0
  29. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/discover.py +0 -0
  30. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/dlp_cmd.py +0 -0
  31. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/drift.py +0 -0
  32. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/enforce.py +0 -0
  33. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/entitlements_cmd.py +0 -0
  34. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/framework_suggest.py +0 -0
  35. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/framework_test.py +0 -0
  36. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/hitl.py +0 -0
  37. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/license_cmd.py +0 -0
  38. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/list_cmd.py +0 -0
  39. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/mcp_server.py +0 -0
  40. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/models_cmd.py +0 -0
  41. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/onboarding_report.py +0 -0
  42. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/org_policy.py +0 -0
  43. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/policy_cache.py +0 -0
  44. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/policy_commit.py +0 -0
  45. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/policy_diff.py +0 -0
  46. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/policy_status.py +0 -0
  47. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/preview.py +0 -0
  48. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/redteam.py +0 -0
  49. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/regulatory.py +0 -0
  50. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/scan_govern.py +0 -0
  51. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/scan_report.py +0 -0
  52. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/sentinel.py +0 -0
  53. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/status_cmd.py +0 -0
  54. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/users.py +0 -0
  55. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/vault.py +0 -0
  56. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/watch.py +0 -0
  57. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_cli/witness.py +0 -0
  58. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_security_cli.egg-info/dependency_links.txt +0 -0
  59. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_security_cli.egg-info/entry_points.txt +0 -0
  60. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/iris_security_cli.egg-info/top_level.txt +0 -0
  61. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/setup.cfg +0 -0
  62. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_evidence.py +0 -0
  63. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_evidence_query.py +0 -0
  64. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_framework_suggest.py +0 -0
  65. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_hitl_cli.py +0 -0
  66. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_list_cmd.py +0 -0
  67. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_mcp_model_governance.py +0 -0
  68. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_policy_diff.py +0 -0
  69. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_quickstart.py +0 -0
  70. {iris_security_cli-0.2.12 → iris_security_cli-0.2.14}/tests/test_vocabulary.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris-security-cli
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: IRIS CLI — declare, compile, preview, enforce, witness, certify, sentinel
5
5
  License: Apache-2.0
6
- Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
7
- Project-URL: Repository, https://github.com/gimartinb/iris-sdk
6
+ Project-URL: Homepage, https://iris-security.github.io/iris-sdk/
7
+ Project-URL: Repository, https://github.com/IRIS-Security/iris-sdk
8
8
  Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Intended Audience :: Developers
10
10
  Classifier: Topic :: Security
@@ -16,7 +16,7 @@ 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.16
19
- Requires-Dist: iris-security-sdk>=0.2.12
19
+ Requires-Dist: iris-security-sdk>=0.2.14
20
20
  Requires-Dist: click>=8.1
21
21
  Requires-Dist: rich>=13.0
22
22
  Requires-Dist: pyyaml>=6.0
@@ -29,7 +29,7 @@ IRIS CLI — `iris scan`, `iris register`, `iris policy`, `iris compliance`.
29
29
 
30
30
  Command-line tools for AI agent governance and Colorado AI Act compliance.
31
31
 
32
- Part of the [IRIS SDK](https://github.com/gimartinb/iris-sdk).
32
+ Part of the [IRIS SDK](https://github.com/IRIS-Security/iris-sdk).
33
33
 
34
34
  ## Install
35
35
 
@@ -4,7 +4,7 @@ IRIS CLI — `iris scan`, `iris register`, `iris policy`, `iris compliance`.
4
4
 
5
5
  Command-line tools for AI agent governance and Colorado AI Act compliance.
6
6
 
7
- Part of the [IRIS SDK](https://github.com/gimartinb/iris-sdk).
7
+ Part of the [IRIS SDK](https://github.com/IRIS-Security/iris-sdk).
8
8
 
9
9
  ## Install
10
10
 
@@ -0,0 +1,614 @@
1
+ # Copyright 2024-2025 Gilbert Martin / IRIS Security, Inc.
2
+ # All Rights Reserved.
3
+ # Author: Gilbert Martin <gilbert@iris-security.io>
4
+ # IRIS CLI — Policy as Code for AI Agents — https://iris-security.io
5
+
6
+ """
7
+ iris audit-log — SIEM-ready audit log export.
8
+ Track A3 capability. CLI-only, fully offline.
9
+ Answers the CISO question: "plug this into what I already have."
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import csv
15
+ import hashlib
16
+ import io
17
+ import json
18
+ import signal
19
+ import sys
20
+ import time
21
+ from datetime import datetime, timedelta
22
+ from pathlib import Path
23
+ from typing import Iterable, List, Optional
24
+
25
+ import click
26
+ from rich.console import Console
27
+
28
+ from iris_cli.evidence import (
29
+ _event_regulation,
30
+ _event_risk_score,
31
+ _load_query_events,
32
+ _open_vault,
33
+ filter_query_events,
34
+ )
35
+ from iris_core.evidence.vault import EvidenceVault
36
+
37
+ err_console = Console(file=sys.stderr)
38
+
39
+ IRIS_VERSION = "0.2.12"
40
+ DEFAULT_RETENTION_DAYS = EvidenceVault.FREE_RETENTION_DAYS
41
+
42
+
43
+ def _agent_name(event: dict) -> str:
44
+ return event.get("_agent") or event.get("agent_name") or event.get("agent_id") or "unknown"
45
+
46
+
47
+ def _event_policy(event: dict) -> str:
48
+ if event.get("policy_name"):
49
+ return str(event["policy_name"])
50
+ violations = event.get("violations") or []
51
+ if violations:
52
+ return str(violations[0].get("rule_id", ""))
53
+ if event.get("triggered_by"):
54
+ return str(event["triggered_by"])
55
+ return "—"
56
+
57
+
58
+ def _event_hash(event: dict) -> str:
59
+ if event.get("hash"):
60
+ return str(event["hash"])
61
+ if event.get("signature"):
62
+ return str(event["signature"])
63
+ raw = f"{event.get('event_id', '')}:{_agent_name(event)}"
64
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
65
+
66
+
67
+ def _normalize_decision(event: dict) -> str:
68
+ decision = str(event.get("decision", "PERMIT")).upper()
69
+ if event.get("event_type") == "hitl_requested":
70
+ return "HITL"
71
+ return decision
72
+
73
+
74
+ def _aiuc1_control(event: dict) -> str:
75
+ regulation = _event_regulation(event)
76
+ return regulation if regulation != "—" else ""
77
+
78
+
79
+ def _filter_decisions(events: List[dict], decisions: str) -> List[dict]:
80
+ if decisions == "all":
81
+ return events
82
+ if decisions == "deny":
83
+ return [e for e in events if _normalize_decision(e) == "DENY"]
84
+ if decisions == "hitl":
85
+ return [
86
+ e
87
+ for e in events
88
+ if _normalize_decision(e) == "HITL"
89
+ or e.get("event_type") in ("hitl_requested", "hitl_resolved")
90
+ ]
91
+ if decisions == "violations":
92
+ return [e for e in events if len(e.get("violations", [])) > 0]
93
+ return events
94
+
95
+
96
+ def _load_audit_events(
97
+ agent: Optional[str],
98
+ governance_dir: Optional[Path],
99
+ vault_dir: Optional[Path],
100
+ ) -> List[dict]:
101
+ vault_root = vault_dir or Path.home() / ".iris" / "evidence"
102
+ if agent:
103
+ return _load_query_events(agent, governance_dir, vault_dir)
104
+
105
+ if vault_root.exists():
106
+ events: List[dict] = []
107
+ for agent_dir in sorted(vault_root.iterdir()):
108
+ if not agent_dir.is_dir():
109
+ continue
110
+ events_file = agent_dir / "events.jsonl"
111
+ if not events_file.exists():
112
+ continue
113
+ vault = _open_vault(agent_dir.name, vault_dir)
114
+ for event in vault.get_events(limit=10_000):
115
+ tagged = dict(event)
116
+ tagged["_agent"] = agent_dir.name
117
+ events.append(tagged)
118
+ if events:
119
+ return events
120
+
121
+ return _load_query_events(None, governance_dir, vault_dir)
122
+
123
+
124
+ def _parse_timestamp(event: dict) -> datetime:
125
+ ts = str(event.get("timestamp", ""))
126
+ if not ts:
127
+ return datetime.utcnow()
128
+ try:
129
+ return datetime.fromisoformat(ts.replace("Z", "+00:00").replace("+00:00", ""))
130
+ except ValueError:
131
+ return datetime.fromisoformat(ts[:19])
132
+
133
+
134
+ def _format_splunk(event: dict) -> dict:
135
+ ts = _parse_timestamp(event)
136
+ agent = _agent_name(event)
137
+ decision = _normalize_decision(event)
138
+ return {
139
+ "time": ts.timestamp(),
140
+ "host": "iris-governance",
141
+ "source": f"iris:agent:{agent}",
142
+ "sourcetype": "iris:evidence",
143
+ "index": "security",
144
+ "event": {
145
+ "iris_agent": agent,
146
+ "iris_action": event.get("action", ""),
147
+ "iris_decision": decision,
148
+ "iris_policy": _event_policy(event),
149
+ "iris_risk_score": _event_risk_score(event),
150
+ "iris_regulation": _event_regulation(event),
151
+ "iris_event_id": event.get("event_id", ""),
152
+ "iris_hash": _event_hash(event),
153
+ "aarm_r5": True,
154
+ "aiuc1_control": _aiuc1_control(event),
155
+ },
156
+ }
157
+
158
+
159
+ def _format_datadog(event: dict) -> dict:
160
+ ts = _parse_timestamp(event)
161
+ agent = _agent_name(event)
162
+ decision = _normalize_decision(event)
163
+ resource = event.get("resource") or event.get("tool") or "unknown"
164
+ action = event.get("action", "call")
165
+ return {
166
+ "ddsource": "iris",
167
+ "ddtags": f"env:prod,iris_decision:{decision.lower()}",
168
+ "hostname": "iris-governance",
169
+ "message": f"{agent} {decision}: {action} on {resource}",
170
+ "service": "iris-agent-governance",
171
+ "date": int(ts.timestamp() * 1000),
172
+ "iris.agent": agent,
173
+ "iris.decision": decision,
174
+ "iris.risk_score": _event_risk_score(event),
175
+ "iris.policy": _event_policy(event),
176
+ }
177
+
178
+
179
+ def _format_elastic(event: dict) -> dict:
180
+ ts = _parse_timestamp(event)
181
+ agent = _agent_name(event)
182
+ decision = _normalize_decision(event)
183
+ outcome = "success" if decision in ("PERMIT", "ALLOW") else "failure"
184
+ return {
185
+ "@timestamp": ts.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
186
+ "event.kind": "event",
187
+ "event.category": ["authentication", "network"],
188
+ "event.type": ["access"],
189
+ "event.action": decision.lower(),
190
+ "event.outcome": outcome,
191
+ "agent.name": agent,
192
+ "agent.type": "iris-governance",
193
+ "rule.name": _event_policy(event),
194
+ "rule.description": event.get("action", ""),
195
+ "risk.score": _event_risk_score(event),
196
+ "labels": {
197
+ "iris_regulation": _event_regulation(event),
198
+ "aarm_r5": "true",
199
+ },
200
+ }
201
+
202
+
203
+ def _otel_attribute(key: str, value: str | int) -> dict:
204
+ if isinstance(value, int):
205
+ return {"key": key, "value": {"intValue": value}}
206
+ return {"key": key, "value": {"stringValue": str(value)}}
207
+
208
+
209
+ def _format_otel_envelope(events: List[dict]) -> dict:
210
+ spans = []
211
+ for event in events:
212
+ decision = _normalize_decision(event)
213
+ event_id = str(event.get("event_id", ""))
214
+ agent = _agent_name(event)
215
+ trace_id = hashlib.sha256(f"{event_id}{agent}".encode()).hexdigest()[:32]
216
+ span_id = event_id.replace("ev_", "").ljust(16, "0")[:16]
217
+ start_ms = int(_parse_timestamp(event).timestamp() * 1000)
218
+ spans.append(
219
+ {
220
+ "traceId": trace_id,
221
+ "spanId": span_id,
222
+ "parentSpanId": event.get("session_id"),
223
+ "name": f"iris.agent.action.{decision.lower()}",
224
+ "kind": 3,
225
+ "startTimeUnixNano": start_ms * 1_000_000,
226
+ "endTimeUnixNano": (start_ms + 2) * 1_000_000,
227
+ "attributes": [
228
+ _otel_attribute("iris.agent.id", agent),
229
+ _otel_attribute("iris.action", event.get("action", "")),
230
+ _otel_attribute("iris.decision", decision),
231
+ _otel_attribute("iris.policy.name", _event_policy(event)),
232
+ _otel_attribute("iris.risk_score", _event_risk_score(event)),
233
+ _otel_attribute("iris.regulation", _event_regulation(event)),
234
+ _otel_attribute("iris.evidence.hash", _event_hash(event)),
235
+ _otel_attribute("aarm.requirement", "R5"),
236
+ _otel_attribute("aiuc1.control", _aiuc1_control(event)),
237
+ ],
238
+ "status": {"code": 2 if decision == "DENY" else 1},
239
+ }
240
+ )
241
+
242
+ return {
243
+ "resourceSpans": [
244
+ {
245
+ "resource": {
246
+ "attributes": [
247
+ _otel_attribute("service.name", "iris-cli"),
248
+ _otel_attribute("service.version", IRIS_VERSION),
249
+ _otel_attribute("iris.vendor", "IRIS Security, Inc."),
250
+ _otel_attribute("aarm.conformance", "Core"),
251
+ ],
252
+ },
253
+ "scopeSpans": [
254
+ {
255
+ "scope": {"name": "iris.evidence", "version": "1.0"},
256
+ "spans": spans,
257
+ }
258
+ ],
259
+ }
260
+ ],
261
+ }
262
+
263
+
264
+ def _cef_severity(risk: int) -> int:
265
+ if risk >= 90:
266
+ return 10
267
+ if risk >= 80:
268
+ return 8
269
+ if risk >= 60:
270
+ return 6
271
+ if risk >= 40:
272
+ return 4
273
+ return 2
274
+
275
+
276
+ def _format_cef(event: dict) -> str:
277
+ agent = _agent_name(event)
278
+ decision = _normalize_decision(event)
279
+ resource = event.get("resource") or event.get("tool") or "unknown"
280
+ policy = _event_policy(event)
281
+ risk = _event_risk_score(event)
282
+ rule_id = policy.replace("|", "\\|")
283
+ action = str(event.get("action", "")).replace("|", "\\|")
284
+ regulation = _event_regulation(event).replace("=", "\\=")
285
+ return (
286
+ f"CEF:0|IRIS Security|iris-governance|{IRIS_VERSION}|{rule_id}|"
287
+ f"{action}|{_cef_severity(risk)}|"
288
+ f"src={agent} dst={resource} act={decision} "
289
+ f"cs1={policy} cs1Label=PolicyName "
290
+ f"cn1={risk} cn1Label=RiskScore "
291
+ f"flexString1={regulation} flexString1Label=Regulation"
292
+ )
293
+
294
+
295
+ def _format_leef(event: dict) -> str:
296
+ agent = _agent_name(event)
297
+ decision = _normalize_decision(event)
298
+ resource = event.get("resource") or event.get("tool") or "unknown"
299
+ policy = _event_policy(event)
300
+ risk = _event_risk_score(event)
301
+ event_id = str(event.get("event_id", "unknown"))
302
+ ts = _parse_timestamp(event).strftime("%Y-%m-%dT%H:%M:%SZ")
303
+ regulation = _event_regulation(event)
304
+ return (
305
+ f"LEEF:2.0|IRIS Security|iris-governance|{IRIS_VERSION}|{event_id}|"
306
+ f"devTime={ts} devTimeFormat=ISO 8601 "
307
+ f"src={agent} dst={resource} usrName={agent} "
308
+ f"proto=HTTPS sev={risk} cat={decision} "
309
+ f"policy={policy} regulation={regulation}"
310
+ )
311
+
312
+
313
+ def _format_json_events(events: List[dict]) -> str:
314
+ payload = []
315
+ for event in events:
316
+ row = dict(event)
317
+ row.pop("_agent", None)
318
+ row["agent"] = _agent_name(event)
319
+ row["risk_score"] = _event_risk_score(event)
320
+ row["regulation"] = _event_regulation(event)
321
+ payload.append(row)
322
+ return json.dumps(payload, indent=2)
323
+
324
+
325
+ def _format_csv_events(events: List[dict]) -> str:
326
+ fieldnames = [
327
+ "event_id",
328
+ "timestamp",
329
+ "agent",
330
+ "action",
331
+ "resource",
332
+ "environment",
333
+ "decision",
334
+ "risk_score",
335
+ "policy",
336
+ "regulation",
337
+ "hash",
338
+ ]
339
+ buf = io.StringIO()
340
+ writer = csv.DictWriter(buf, fieldnames=fieldnames)
341
+ writer.writeheader()
342
+ for event in events:
343
+ writer.writerow(
344
+ {
345
+ "event_id": event.get("event_id", ""),
346
+ "timestamp": event.get("timestamp", ""),
347
+ "agent": _agent_name(event),
348
+ "action": event.get("action", ""),
349
+ "resource": event.get("resource") or event.get("tool", ""),
350
+ "environment": event.get("environment", ""),
351
+ "decision": _normalize_decision(event),
352
+ "risk_score": _event_risk_score(event),
353
+ "policy": _event_policy(event),
354
+ "regulation": _event_regulation(event),
355
+ "hash": _event_hash(event),
356
+ }
357
+ )
358
+ return buf.getvalue()
359
+
360
+
361
+ def _serialize_events(events: List[dict], output_format: str) -> str:
362
+ if output_format == "json":
363
+ return _format_json_events(events)
364
+ if output_format == "csv":
365
+ return _format_csv_events(events)
366
+ if output_format == "otel":
367
+ return json.dumps(_format_otel_envelope(events), indent=2)
368
+
369
+ lines: List[str] = []
370
+ for event in events:
371
+ if output_format == "splunk":
372
+ lines.append(json.dumps(_format_splunk(event)))
373
+ elif output_format == "datadog":
374
+ lines.append(json.dumps(_format_datadog(event)))
375
+ elif output_format == "elastic":
376
+ lines.append(json.dumps(_format_elastic(event)))
377
+ elif output_format == "cef":
378
+ lines.append(_format_cef(event))
379
+ elif output_format == "leef":
380
+ lines.append(_format_leef(event))
381
+ return "\n".join(lines) + ("\n" if lines else "")
382
+
383
+
384
+ def _format_label(output_format: str) -> str:
385
+ labels = {
386
+ "splunk": "Splunk HEC",
387
+ "datadog": "Datadog",
388
+ "elastic": "Elastic ECS",
389
+ "otel": "OTel OTLP",
390
+ "cef": "CEF",
391
+ "leef": "LEEF",
392
+ "json": "JSON",
393
+ "csv": "CSV",
394
+ }
395
+ return labels.get(output_format, output_format)
396
+
397
+
398
+ def _print_export_summary(
399
+ count: int,
400
+ output_format: str,
401
+ since: str,
402
+ until: str,
403
+ ) -> None:
404
+ err_console.print(
405
+ f"Exported {count} events · {_format_label(output_format)} format · "
406
+ f"{since}–{until}"
407
+ )
408
+ err_console.print(
409
+ "Formats: Splunk HEC · Datadog · Elastic ECS · OTel OTLP"
410
+ )
411
+ err_console.print(
412
+ "AARM R8 conformant · https://iris-security.io"
413
+ )
414
+
415
+
416
+ def _resolve_date_range(
417
+ since: Optional[str],
418
+ until: Optional[str],
419
+ ) -> tuple[str, str]:
420
+ resolved_since = since or (
421
+ datetime.utcnow() - timedelta(days=DEFAULT_RETENTION_DAYS)
422
+ ).strftime("%Y-%m-%d")
423
+ resolved_until = until or datetime.utcnow().strftime("%Y-%m-%d")
424
+ return resolved_since, resolved_until
425
+
426
+
427
+ def _collect_events(
428
+ agent: Optional[str],
429
+ since: str,
430
+ until: str,
431
+ decisions: str,
432
+ governance_dir: Optional[Path],
433
+ vault_dir: Optional[Path],
434
+ ) -> List[dict]:
435
+ events = _load_audit_events(agent, governance_dir, vault_dir)
436
+ events = filter_query_events(events, since=since, until=until)
437
+ events = _filter_decisions(events, decisions)
438
+ events.sort(key=lambda e: e.get("timestamp", ""))
439
+ return events
440
+
441
+
442
+ def _format_single_event(event: dict, output_format: str) -> str:
443
+ if output_format == "json":
444
+ return json.dumps(
445
+ {
446
+ **{k: v for k, v in event.items() if k != "_agent"},
447
+ "agent": _agent_name(event),
448
+ "risk_score": _event_risk_score(event),
449
+ "regulation": _event_regulation(event),
450
+ }
451
+ )
452
+ if output_format == "splunk":
453
+ return json.dumps(_format_splunk(event))
454
+ if output_format == "elastic":
455
+ return json.dumps(_format_elastic(event))
456
+ if output_format == "cef":
457
+ return _format_cef(event)
458
+ return json.dumps(event)
459
+
460
+
461
+ def _vault_agent_dirs(vault_dir: Optional[Path], agent: Optional[str]) -> Iterable[Path]:
462
+ vault_root = vault_dir or Path.home() / ".iris" / "evidence"
463
+ if agent:
464
+ yield vault_root / agent
465
+ return
466
+ if not vault_root.exists():
467
+ return
468
+ for path in sorted(vault_root.iterdir()):
469
+ if path.is_dir() and (path / "events.jsonl").exists():
470
+ yield path
471
+
472
+
473
+ @click.group("audit-log")
474
+ def audit_log():
475
+ """Audit log export in SIEM-compatible formats."""
476
+ pass
477
+
478
+
479
+ @audit_log.command("export")
480
+ @click.option("--agent", default=None, help="Agent name. Omit to export all agents.")
481
+ @click.option(
482
+ "--since",
483
+ default=None,
484
+ help=f"Start date YYYY-MM-DD. Default: last {DEFAULT_RETENTION_DAYS} days.",
485
+ )
486
+ @click.option("--until", default=None, help="End date YYYY-MM-DD.")
487
+ @click.option(
488
+ "--format",
489
+ "output_format",
490
+ required=True,
491
+ type=click.Choice(
492
+ ["splunk", "datadog", "elastic", "otel", "cef", "leef", "json", "csv"]
493
+ ),
494
+ help="Output format for SIEM ingestion.",
495
+ )
496
+ @click.option(
497
+ "--output",
498
+ "output_path",
499
+ type=click.Path(path_type=Path),
500
+ default=None,
501
+ help="Write to file. Omit to print to stdout.",
502
+ )
503
+ @click.option("--dir", "governance_dir", type=Path, default=None)
504
+ @click.option("--vault-dir", type=Path, default=None)
505
+ @click.option(
506
+ "--decisions",
507
+ default="all",
508
+ type=click.Choice(["all", "deny", "hitl", "violations"]),
509
+ help="Filter which events to export.",
510
+ )
511
+ def audit_log_export(
512
+ agent,
513
+ since,
514
+ until,
515
+ output_format,
516
+ output_path,
517
+ governance_dir,
518
+ vault_dir,
519
+ decisions,
520
+ ):
521
+ """
522
+ Export audit log in SIEM-compatible format.
523
+
524
+ Plug IRIS evidence directly into your existing security stack.
525
+ No IRIS account required — reads from local Evidence Vault.
526
+
527
+ AARM R8 conformant: standard telemetry export.
528
+
529
+ Examples:
530
+ iris audit-log export --format splunk --output events.json
531
+ iris audit-log export --format elastic --agent billing-agent
532
+ iris audit-log export --format cef | nc splunk-hec 9997
533
+ iris audit-log export --format csv --since 2025-06-01 > audit.csv
534
+ iris audit-log export --format otel --decisions deny
535
+ """
536
+ since, until = _resolve_date_range(since, until)
537
+ events = _collect_events(agent, since, until, decisions, governance_dir, vault_dir)
538
+ body = _serialize_events(events, output_format)
539
+
540
+ if output_path:
541
+ output_path.parent.mkdir(parents=True, exist_ok=True)
542
+ output_path.write_text(body, encoding="utf-8")
543
+ else:
544
+ click.echo(body, nl=False)
545
+
546
+ _print_export_summary(len(events), output_format, since, until)
547
+
548
+
549
+ @audit_log.command("stream")
550
+ @click.option(
551
+ "--format",
552
+ "output_format",
553
+ default="json",
554
+ type=click.Choice(["json", "splunk", "elastic", "cef"]),
555
+ )
556
+ @click.option("--agent", default=None)
557
+ @click.option(
558
+ "--decisions",
559
+ default="deny",
560
+ type=click.Choice(["all", "deny", "hitl", "violations"]),
561
+ )
562
+ @click.option("--vault-dir", type=Path, default=None)
563
+ def audit_log_stream(output_format, agent, decisions, vault_dir):
564
+ """
565
+ Stream live audit events to stdout (tail -f style).
566
+
567
+ Pipe directly into Splunk/Datadog/Elastic forwarders.
568
+
569
+ Example:
570
+ iris audit-log stream --format splunk | splunk-forwarder
571
+ iris audit-log stream --decisions deny --format elastic
572
+ """
573
+ err_console.print(
574
+ "[dim]Streaming IRIS audit events... Ctrl+C to stop[/dim]"
575
+ )
576
+
577
+ seen: set[str] = set()
578
+ running = True
579
+
580
+ def _stop(*_args):
581
+ nonlocal running
582
+ running = False
583
+
584
+ signal.signal(signal.SIGINT, _stop)
585
+
586
+ poll_interval = 2.0
587
+ while running:
588
+ for agent_dir in _vault_agent_dirs(vault_dir, agent):
589
+ events_file = agent_dir / "events.jsonl"
590
+ if not events_file.exists():
591
+ continue
592
+ try:
593
+ lines = events_file.read_text(encoding="utf-8").strip().splitlines()
594
+ except OSError:
595
+ continue
596
+ for raw in lines:
597
+ try:
598
+ event = json.loads(raw)
599
+ except json.JSONDecodeError:
600
+ continue
601
+ event_id = event.get("event_id") or raw
602
+ if event_id in seen:
603
+ continue
604
+ tagged = dict(event)
605
+ tagged["_agent"] = agent_dir.name
606
+ if decisions != "all":
607
+ filtered = _filter_decisions([tagged], decisions)
608
+ if not filtered:
609
+ continue
610
+ seen.add(event_id)
611
+ click.echo(_format_single_event(tagged, output_format))
612
+ time.sleep(poll_interval)
613
+
614
+ err_console.print("[dim]Audit log stream stopped.[/dim]")