Safety model¶
Status: shipped. The analyst-executed write actions, the audit logger and the reasoning-trace feature are all live. The triage pipeline shows reasoning only if the analyst model emits a
reasoning_contentfield, and that depends on the gateway config. The blocklist licenses are below.
Tool classification¶
- A read tool auto-executes. soc-ai registers it with
@tool(read_only=True). - soc-ai registers a write tool with
@tool(read_only=False). The agent never executes one. The report recommends it, and the analyst executes each one explicitly through the actions API atPOST /api/v1/investigations/{id}/actions/{index}/execute. That route is the single write path, and it runs through the auditedexecute_write_toolinsoc_ai/tools/write_exec.py.
Audit log hardening¶
soc-ai writes every LLM input, every LLM output and every tool invocation to a
date-stamped ES audit index, soc-ai-audit-YYYY.MM.dd. That includes the
read tools an MCP client calls through python -m soc_ai.mcp_server. Each call
lands as a tool_call / tool_result pair under a mcp-* session id with
user=mcp. A grid query or an IOC lookup over stdio is as visible to
soc-ai audit verify as one that the agent made. These hardening properties
apply:
-
Tamper-evidence through a hash chain. Each record carries a monotonic
seq, aprev_hashand ahash. Thehashis a SHA-256 over the canonicalised record content and the hash of the previous record. Any edit, reorder, insertion or deletion of a record breaks the recomputed linkage.soc_ai.audit.verify_chain(records)recomputes the chain and reports the first brokenseq.soc_ai.audit.verify_chain_detailalso reports what TYPE of break it is. A position that two writers claimed reads differently from a record whose content no longer matches its own hash. Read Audit chain breaks.The hash chain shows that a record changed. It cannot stop the change. A privileged ES user can still alter records, and you can detect it. - Atomic position claims. soc-ai writes each record with
op_type=createat a deterministic_idthat it derives from theseq. The first writer to reach a position takes it. Any other writer gets a version conflict and moves to the next position. Allocation and persistence are one atomic operation. That makes the chain safe in three cases. The first is a second process, such as asoc-aicommand that cron runs beside the server. The second is a second logger in the same process. The third is a write whose acknowledgement never arrived. The in-memory head stays as a cache. soc-ai recovers it from the most recent record on startup, so the chain continues across restarts. - Scheduled verification. soc-ai re-runs the chain check on a schedule, daily over the last 7 days.AUDIT_VERIFY_SCHEDULE_ENABLEDcontrols it, and it is on by default. It reports a break to a human in 3 ways: an audit record, the notification webhook, and a standing entry on the in-app bell. soc-ai logs a verification that could not run, and it raises no alarm for it. - Epoch-aware verification. A restart is a legitimate reason why the chain cannot link all the way back. Theprev_hashof a genesis record, atseq=0, is the all-zero hash by construction, so it never links to the epoch that came before it. From 2026-06-24 to 2026-08-16, a bug in chain-head recovery reset the head on every restart. The bug made that rare case happen 134 times. The fix landed on 2026-08-17.soc-ai audit verifyand the Diagnostics "Verify audit chain" control check each restart boundary as its own epoch. They do not report every boundary after the first as tamper. An all-clear that spans more than one epoch shows as a distinct state, amber and with no checkmark. Nobody can prove cross-epoch linkage, so soc-ai does not show such a result as one unbroken chain. - Fail-closed for mutating writes.AUDIT_FAIL_CLOSED=trueis the default. With it, soc-ai aborts an action that changes SO state if it cannot write the audit record for that action. Those actions are ack, escalate, comment and auto-ack. Every acknowledgement and every escalation has an audit trail. The audit writes for a read, a triage and an enrichment stay fail-open.
Deployment recommendation: a least-privilege credential. The audit index shares the read and write ES credential of soc-ai. It lives on a cluster that soc-ai itself can write to and delete from. A compromised soc-ai or a compromised credential could therefore rewrite the audit records despite the hash chain.
To strengthen the trail, provision a distinct, least-privilege ES credential for
the audit index. Give it the create, create_doc and index privileges
on soc-ai-audit-* only, with no delete and no manage. Point the audit
writer at that credential. Pair it with an append-only or read-only ILM policy
on soc-ai-audit-*. A data-stream policy also works. Then nobody can rewrite a
record in place. Put that policy on a separate monitoring cluster that the main
credential of soc-ai cannot reach. The hash chain then gives
tamper-evidence on top of an index that the application cannot edit.
Out of scope¶
- Detection mutation tools. The agent can suggest rule tuning, and the analyst applies it.
- VirusTotal / AlienVault OTX integrations.
- Auto-resolution of an alert. Auto-creation of a case.
External-intel egress¶
This egress is optional and off by default. The hunt agent and the chat agent can reach a small set of external reputation services. The services are Shodan InternetDB, Shodan host with a paid key, GreyNoise, and the CIRCL CVE database. They can also reach SearXNG web search and crawl4ai page-fetch. These calls go to a third party, so they are an explicit egress surface. An IP address, a domain or a CVE that the agent looks up leaves your network.
WEB_SEARCH_ENABLED and CRAWL4AI_ENABLED gate the web search and the crawl,
and both are off by default. The Shodan, GreyNoise and CVE lookups reach public
endpoints if the agent chooses to call them. None of them ever sends an alert
payload. Each one sends only the single indicator under enrichment. Leave them
unused on an air-gapped grid, because the local vendored blocklists and GeoIP
cover the offline path.
Egress redaction for a cloud model¶
This redaction is opt-in. By default soc-ai assumes that ANALYST_MODEL points
at a local model. It then sends that model the enriched alert context, the
prompts and the tool results verbatim. If you point the analyst model at a cloud
provider, set ANALYST_CLOUD_REDACTION=true. You can also change it live on
the Settings page, in the Agent section.
Each investigation turn, hunt turn and chat turn gets one EgressGuard from
soc_ai/agent/egress_guard.py. The guard holds a single reversible label map.
The Oracle path uses the same mechanism.
On the outbound side, soc-ai replaces internal IPs, hostnames, usernames, MACs
and internal-domain emails with stable opaque labels such as IP_01 and
HOST_02. The replacement covers everything that crosses the gateway: the
enriched context JSON, every composed prompt and every tool result. A composed
prompt can belong to an investigation, a hunt or a chat. A composed prompt includes the analyst's own
question text. soc-ai wraps each read tool at registration.
On the inbound side, soc-ai restores the tool arguments that the model sends to
their real values before they reach Elasticsearch. An example is an OQL query
that cites HOST_01. soc-ai also restores the labels in the model's outputs
before it stores or displays them. Those outputs are the verdicts, the
rationales, the reasoning traces, the hunt reports and the chat replies. The
identifier set is the same effective set that the Oracle uses. It is
ORACLE_INTERNAL_SUFFIXES and ORACLE_EXTRA_HOSTS, together with the
discovered identifiers in the database.
The guard learns a host name from free text. The sources are a UNC path, a logon
name, an account domain, a NetBIOS name and a configured host name. A learned name joins
the learned set only if it looks like a host name. The name has at least 3
characters. It has no path separator, no run of dots and no dot at an end. It
is not a word on a small list of common English words. soc-ai labels a name that
fails this rule in place. soc-ai does not write it into other fields,
and the residue sweep does not search for it elsewhere. Before this rule, the
Oracle refused cases on a 2-letter token from an ICMP payload dump. A name from
a structured field, such as host.name, always joins the learned set. If the
Oracle residue sweep refuses on a learned name, the refusal reason names the
rule and the field that the name came from. The reason never holds the name.
What redaction does NOT cover:
- It is best-effort. It does not fail closed. The Oracle path has an independent residue sweep that refuses to transmit, and this path has none. An internal FQDN on a public-looking suffix leaves verbatim if you did not list the suffix. The Oracle gate carries the same caveat. Configure your suffixes and hosts.
- Lower verdict quality. The model reasons over opaque labels, so
it cannot recognise
dc01as a domain controller. soc-ai preserves the cross-references between labels, so behavioural reasoning still works. - The Oracle second-opinion path keeps its own independent sanitization pipeline, and that pipeline fails closed. This setting does not change it.
Leave the setting off for a local analyst model. Off is the default. Redaction is pure overhead if nothing leaves your network.
Blocklist licenses¶
The BlocklistDB of soc-ai consumes public IOC blocklists by default:
| Source | License | Default |
|---|---|---|
| abuse.ch URLhaus / ThreatFox / Feodo Tracker | CC0 | ✅ ON |
| Tor Project exit-node list | Public | ✅ ON |
Operator-curated internal_seed.yaml |
n/a | ✅ ON |
| Spamhaus DROP / EDROP | Free for non-commercial use only; commercial use requires a paid license | ⛔ OFF, opt-in |
To enable Spamhaus in your deployment:
- Read the Spamhaus terms at https://www.spamhaus.org/legal/terms/. Confirm that your deployment qualifies for non-commercial use, or obtain a commercial license.
- Set in
.env:
BLOCKLIST_SOURCES=urlhaus,threatfox,feodo,tor,internal_seed,spamhaus_drop
SPAMHAUS_LICENSE_ACKNOWLEDGED=true
- Run
soc-ai blocklists refresh.
Without SPAMHAUS_LICENSE_ACKNOWLEDGED=true, the loader logs a WARNING and skips
the source. This path fails open.