iris-security-cli 0.2.12__tar.gz → 0.2.16__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.2.12 → iris_security_cli-0.2.16}/PKG-INFO +2 -2
- iris_security_cli-0.2.16/iris_cli/audit_log.py +614 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/certify.py +89 -4
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/compliance_check_cmd.py +38 -1
- iris_security_cli-0.2.16/iris_cli/compliance_scan_cmd.py +216 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/evidence.py +75 -10
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/main.py +36 -2
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/scm.py +88 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_security_cli.egg-info/PKG-INFO +2 -2
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_security_cli.egg-info/SOURCES.txt +5 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_security_cli.egg-info/requires.txt +1 -1
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/pyproject.toml +2 -2
- iris_security_cli-0.2.16/tests/test_audit_log.py +96 -0
- iris_security_cli-0.2.16/tests/test_docs_pages_check.py +45 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_entitlements_display.py +2 -1
- iris_security_cli-0.2.16/tests/test_evidence_export.py +81 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_framework_test.py +2 -1
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/README.md +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/action_plan.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/assess.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/compiler_config.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/compliance_fix.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/cost.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/declare.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/delegation.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/discover.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/dlp_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/drift.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/enforce.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/entitlements_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/explain.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/framework_suggest.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/framework_test.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/hitl.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/license_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/list_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/mcp_server.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/models_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/onboarding_report.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/org_policy.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/policy_cache.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/policy_commit.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/policy_status.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/preview.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/quickstart.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/redteam.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/regulatory.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/scan_govern.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/sentinel.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/status_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/users.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/vault.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/watch.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_cli/witness.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/setup.cfg +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_evidence_query.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_framework_suggest.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_hitl_cli.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_list_cmd.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_mcp_model_governance.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_policy_diff.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_quickstart.py +0 -0
- {iris_security_cli-0.2.12 → iris_security_cli-0.2.16}/tests/test_vocabulary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iris-security-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
4
4
|
Summary: IRIS CLI — declare, compile, preview, enforce, witness, certify, sentinel
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
@@ -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.
|
|
19
|
+
Requires-Dist: iris-security-sdk>=0.2.16
|
|
20
20
|
Requires-Dist: click>=8.1
|
|
21
21
|
Requires-Dist: rich>=13.0
|
|
22
22
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -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]")
|