Risk and protection
These endpoints answer “how bad is it right now, and is SentrAI allowed to act?”. The dashboard’s overview reads the risk index, each machine and the protection switch from here; the lab website, a proxy or an agent on another machine reads the block list to refuse blocked addresses and accounts. They also answer who is signed in on each machine and which sign-ins SentrAI knows as its administrators’, so an administrator’s own root work is never mistaken for an intruder’s.
Every endpoint here needs the API token (see Signing in) unless it says otherwise.
Check that the core is up
GET /health
Answers as soon as the core is running. The launchers and your own monitoring use it as a readiness check. Open: no token needed (see Open endpoints).
Returns
{
"ok": true
}
curl http://127.0.0.1:8000/health
Read the risk index
The risk index runs from 0 to 100. SentrAI adds up the points of every incident that is still
open or acknowledged (see the contribution field of the incident record)
into a raw score, and turns it into the index with 100 × (1 − e^(−raw / 60)). The index then falls
in a band:
| Band | Index |
|---|---|
green |
0 to 29 |
amber |
30 to 59 |
red |
60 to 79 |
critical |
80 to 100 |
When the index reaches the threshold (RISK_THRESHOLD, 80 by default), SentrAI may contain open
incidents on its own, within the limits.
SENTRAI_RISK_SCOPE decides how machines count. In fleet scope (the default, as on a single
server) every machine’s incidents add up into one score. In host scope, which a central panel
uses, each machine is scored on its own and the headline index is the worst machine’s, so an attack
on one machine never blocks traffic on another.
GET /risk
The headline risk index with its recent history, or one machine’s own with ?host=. The dashboard
polls it for the gauge and the chart.
Query
history(integer, default 600, 0 to 3600): how many of the newest history points to return. The core adds a point every second (TICK_S) and after each batch of events, and keeps the last 3,600.0returns an empty list.host(string, optional): one machine’s own index, incidents and history, by the name inGET /machines. A name the core has never heard of answers with an index of 0 and an empty history.
Returns
{
"risk_index": 83,
"band": "critical",
"raw_score": 106.2,
"threshold": 80,
"risk_scope": "fleet",
"open_incidents": ["RSK-2026-082"],
"needs_review": [],
"contained_incidents": ["RSK-2026-081"],
"monitor_only": false,
"demo_speed": 1.0,
"demo_hours_elapsed": 0.0,
"timestamp": "2026-10-27T02:45:11+00:00",
"history": [
{"t": "2026-10-27T02:45:11+00:00", "risk_index": 83, "raw": 106.2, "band": "critical"}
]
}
| Field | Meaning |
|---|---|
raw_score |
the sum of the incidents’ contributions before it is turned into the index |
threshold |
the index at which SentrAI may contain on its own (RISK_THRESHOLD) |
risk_scope |
fleet or host, see above |
open_incidents |
incidents still counting: open or acknowledged |
needs_review |
those of them the classifier was unsure about, which SentrAI never contains on its own |
contained_incidents |
incidents whose actions were put in force |
monitor_only |
true while protection is off (see GET /protection) |
demo_speed, demo_hours_elapsed |
the timer speed (DEMO_SPEED, 1.0 is real time; higher speeds up the timers for testing) and the timer hours since the core started |
history |
oldest first: t, risk_index, raw and band at each point |
host |
only with ?host=: the machine asked about |
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8000/risk?host=web-01&history=60"
GET /machines
Every machine SentrAI has heard from or opened an incident for, each with its own score, worst first (ties by name). The dashboard’s machine list and a central panel’s overview read it.
Returns
[
{
"host": "web-01",
"risk_index": 53,
"band": "amber",
"raw_score": 45.0,
"open_incidents": 1,
"contained_incidents": 1,
"blocked": 2,
"last_seen": "2026-10-27T02:45:11+00:00",
"silent": false,
"sessions": 1,
"services": [
{"name": "nginx", "port": 443, "proto": "tcp", "process": "nginx", "open_incidents": 1}
],
"services_at": "2026-10-27T02:45:00+00:00",
"hardware": {"cpu": {"percent": 12.5, "cores": 4}, "memory": {"percent": 41.0}, "at": "2026-10-27T02:45:20+00:00",
"warnings": []}
}
]
| Field | Meaning |
|---|---|
risk_index, band, raw_score |
the machine’s own score, counted as in host scope whatever SENTRAI_RISK_SCOPE says |
open_incidents, contained_incidents |
how many of its incidents are counting, and how many are contained |
blocked |
how many addresses its block list holds |
last_seen |
when its last event or heartbeat arrived since the core started, null when only an incident names it |
silent |
true when its collector sent no heartbeat for WATCHDOG_SILENCE_S seconds (30 by default) |
sessions |
how many people are signed in on it right now, as GET /admins lists them; 0 when its collector sends no sign-in lines |
services |
what runs on it, as its agent reported through POST /fleet/services, each with the number of open incidents about it. A service that only an incident names is listed with null port, protocol and process |
services_at |
when the agent last reported its services, null if it never did |
hardware |
its agent’s latest hardware reading, as in GET /fleet/hardware latest; null until one arrives |
Who is signed in, and the administrators
The rules read every log line the same way whoever typed it, so an administrator’s own
sudo systemctl stop sentrai-core reads like an intruder covering their tracks. The core therefore
keeps track of who is signed in on each machine, from the sshd, su, sudo and sign-in lines
the collector sends and its poll of who is signed in, and whether it knows that person as an
administrator. A sign-in is trusted when, in this order:
- its SSH key fingerprint is a known administrator key (
ADMIN_SSH_KEYS, taught or learned); - its address is a known administrator address:
ADMIN_IPS, taught with This was me, learned, or vouched for by a dashboard sign-in as Owner or Admin (POST /admins/sign-in); - it is a console sign-in by a known administrator account (
ADMIN_ACCOUNTSor taught).
Root work inside a trusted sign-in is recorded as admin activity (audit record admin_action) and
raises nothing. Root work from a sign-in the core does not recognise opens one Root access from an
unrecognised sign-in incident per address (see the incident record),
and a sign-in that follows password guessing from the same address is suspect and treated the
same. With ADMIN_LEARN on, a trusted sign-in teaches the other half: a known key from a new address
makes the address known, a new key from a known address makes the key known, and a sudo command in
a trusted sign-in makes its account a known administrator account on that machine. A command is tied
to its sign-in by the terminal it ran on, or failing that by the account’s latest open sign-in on
that host; a command with no sign-in behind it is trusted only when its account is a known
administrator account. Everything taught or learned is kept in people/admins.json next to the
core database (SENTRAI_ADMINS moves it), on this server only. A sign-in counts as open for
ADMIN_SESSION_HOURS (12) after its last line. ADMIN_SESSIONS=off turns all of this off.
GET /admins
The administrators the core knows, who is signed in right now and their recent root work. The dashboard’s Admins tab on each machine’s page reads it.
Query: host (string, optional): keep only that machine’s sessions, activity and accounts
(accounts known on every machine, host: "*", stay). Addresses, keys and dashboard sign-ins are not
tied to a machine, so they are always all listed.
Returns
{
"on": true,
"accounts": [
{"account": "erick", "host": "web-01", "how": "taught", "by": "erick", "at": "2026-10-28T10:21:00+08:00", "last_seen": "2026-10-28T10:30:00+08:00"},
{"account": "deploy", "host": "*", "how": "setting", "by": "ADMIN_ACCOUNTS", "at": null}
],
"addresses": [
{"address": "203.0.113.5", "how": "taught", "by": "erick", "at": "2026-10-28T10:21:00+08:00", "label": "home", "last_seen": "2026-10-28T10:30:00+08:00"},
{"address": "10.0.0.0/8", "how": "setting", "by": "ADMIN_IPS", "at": null, "label": "", "last_seen": null}
],
"keys": [
{"fingerprint": "SHA256:Q7w3cZk2u5Yd9bJm1LxkfA0Hn6Rr8PtEoSvNqGiCz4U", "type": "ED25519", "users": ["erick"], "how": "learned", "by": "address 203.0.113.5", "at": "2026-10-28T10:21:00+08:00", "last_seen": "2026-10-28T10:30:00+08:00"}
],
"sign_ins": [
{"address": "203.0.113.8", "email": "erick@example.com", "role": "Owner", "at": "2026-10-28T09:00:00+08:00", "until": "2026-10-28T21:00:00+08:00"}
],
"sessions": [
{"session": "s3", "host": "web-01", "user": "erick", "via": "ssh", "address": "203.0.113.5", "tty": "pts/0", "key": "SHA256:Q7w3cZk2u5Yd9bJm1LxkfA0Hn6Rr8PtEoSvNqGiCz4U", "key_type": "ED25519", "method": "publickey", "trust": "trusted", "why": "known administrator address 203.0.113.5 (taught by erick)", "since": "2026-10-28T10:21:00+08:00", "last_seen": "2026-10-28T10:30:00+08:00", "ended": null, "preexisting": false, "actions": 2, "incident": null}
],
"activity": [
{"at": "2026-10-28T10:30:00+08:00", "host": "web-01", "user": "erick", "via": "ssh", "address": "203.0.113.5", "session": "s3", "what": "sudo: /usr/bin/systemctl stop sentrai-core", "flagged_as": "log_tampering", "reason": "Log tampering: a SentrAI service was stopped (...)"}
],
"suspect": [
{"address": "198.51.100.7", "why": "password guessing", "until": "2026-10-28T22:30:00+08:00"}
],
"settings": {"session_hours": 12.0, "signin_hours": 12.0, "learn": true}
}
| Field | Meaning |
|---|---|
on |
whether the core keeps track of the administrators (ADMIN_SESSIONS) |
accounts |
known administrator accounts: host is a machine’s name, or * for every machine. how is setting (from ADMIN_ACCOUNTS), taught (This was me, or added by hand) or learned (a sudo command in a trusted sign-in); by names the person, the setting or what it was learned from; at is when, null for a setting |
addresses |
known administrator addresses, each an address or a network, with the same how, by and at; label is a name given by hand, such as home; last_seen the last sign-in from it |
keys |
known SSH key fingerprints, with the key’s type and the users seen with it |
sign_ins |
dashboard sign-ins as Owner or Admin that vouch for their address until until |
sessions |
who is signed in right now, latest activity first: via (ssh, console, windows, logind or who), address and tty, the SSH key, key_type and method, trust (trusted, unknown or suspect) with why in plain words, since and last_seen, preexisting (already signed in when SentrAI started watching the machine), actions (root commands seen in it) and incident, the root-access incident its work opened, or null |
activity |
newest first: the root work of known administrators, with what was run and, when the rules alone would have raised it, flagged_as and reason. Up to 300 entries; the audit trail keeps them all as admin_action |
suspect |
addresses whose sign-ins are not trusted until until: password guessing came from them, or a person said a sign-in from them was not theirs |
settings |
ADMIN_SESSION_HOURS, ADMIN_SIGNIN_HOURS and ADMIN_LEARN as in force |
Entries with how: setting come from ADMIN_IPS, ADMIN_SSH_KEYS and ADMIN_ACCOUNTS and cannot be
forgotten with POST /admins: change the setting instead.
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8000/admins?host=web-01"
POST /admins
Add or forget a known administrator address, SSH key fingerprint or account by hand, as the Admins
tab’s form and its Forget buttons do. What is added counts from the next line on; what is
forgotten stops counting at once (forgetting an address also drops a dashboard sign-in that vouched
for it). Written to the audit trail as admin_changed, with a key shortened.
Body
{"operator": "erick", "action": "add", "kind": "address", "value": "203.0.113.12", "label": "home"}
| Field | Type | Required | Meaning |
|---|---|---|---|
operator |
string | yes | who made the change, recorded as by |
action |
string | yes | add or forget |
kind |
string | yes | address, key or account |
value |
string | yes | an address or a network (203.0.113.12, 198.51.100.0/24); a key fingerprint as ssh-keygen -lf ~/.ssh/id_ed25519.pub prints it (SHA256:...); an account name, or user@machine for one machine only |
label |
string | default "" |
a name for it, such as home or office |
host |
string or null | default null |
for an account: the one machine it is an administrator on. Without it, and with no @ in value, every machine |
Returns the same object as GET /admins, for every machine.
Errors
- 400
'x' is not an address or a network,a key fingerprint looks like SHA256:... (ssh-keygen -lf id_ed25519.pub shows it),an account is a user name, or user@machine,action must be add or forgetorkind must be address, key or account.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"operator": "erick", "action": "add", "kind": "key", "value": "SHA256:Q7w3cZk2u5Yd9bJm1LxkfA0Hn6Rr8PtEoSvNqGiCz4U", "label": "laptop"}' \
http://127.0.0.1:8000/admins
POST /admins/sign-in
The dashboard calls it right after a successful sign-in (password and authenticator code) with the
browser’s address; you do not normally call it yourself. An Owner’s or Admin’s sign-in vouches for
that address for ADMIN_SIGNIN_HOURS (12 by default): root work from it on any machine is the
administrator’s own meanwhile. An Auditor’s sign-in vouches for nothing. Written to the audit trail
as admin_signed_in when it vouched.
Body
{"email": "erick@example.com", "role": "Owner", "address": "203.0.113.8"}
email(string, required): who signed in.role(string, required):Owner,AdminorAuditor; case is ignored.address(string or null, defaultnull): the browser’s address. Nothing is vouched for without one.
Returns
{"ok": true, "vouched": true, "address": "203.0.113.8", "until": "2026-10-28T21:00:00+08:00"}
vouched is false, with until null, for an Auditor, without a usable address, or while
ADMIN_SESSIONS is off.
Switch protection on and off
Protection is on by default. Turned off, SentrAI is in monitor-only mode: it keeps collecting, classifying and scoring, but applies no containment, autonomous or approved. Actions already in force stay until they expire or are rolled back.
GET /protection
Whether protection is on, who last changed it and why.
Returns
{
"protection": "off",
"monitor_only": true,
"changed_at": "2026-10-27T02:45:12+00:00",
"changed_by": "erick",
"reason": "Load test tonight",
"active_actions": ["act-0001", "act-0002"]
}
| Field | Meaning |
|---|---|
protection, monitor_only |
on and false, or off and true |
changed_at, changed_by, reason |
the last switch; all null when it was never switched. When SENTRAI_MONITOR_ONLY set it at start, changed_by is SENTRAI_MONITOR_ONLY and reason is set in the environment |
active_actions |
the ids of the protective actions in force now (see the actions table in the incident record) |
POST /protection
Turn protection on or off. Turning it off needs a reason. The setting is saved in protection.json
next to the audit database and kept across restarts; SENTRAI_MONITOR_ONLY=1 or 0 overrides it
when the core starts.
A switch is written to the audit trail as protection_changed, with the actions left in force, and
added to the timeline of every incident that is still counting. It queues an alert: protection_off
to the on-duty person, the IT manager and the CXO, or protection_on to the on-duty person and the
IT manager. Asking for the state it is already in changes nothing and records nothing.
While protection is off, each containment SentrAI would have applied is written as
containment_skipped, and approving an incident answers 409 (see
POST /incidents/{iid}/decision).
Body
{
"operator": "erick",
"on": false,
"reason": "Load test tonight"
}
| Field | Type | Required | Meaning |
|---|---|---|---|
operator |
string | yes | the name recorded in the audit trail and in the alert |
on |
boolean | yes | true turns protection on, false turns it off |
reason |
string or null | needed to turn it off, default null |
why; spaces at either end are dropped |
Returns the new state, as GET /protection.
Errors
- 400
turning protection off requires a reason:onisfalseandreasonis missing or blank.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"operator": "erick", "on": false, "reason": "Load test tonight"}' \
http://127.0.0.1:8000/protection
GET /limits
The business limits on protective actions as in force now: which types SentrAI may take on its own, how long each lasts and how many it may take in an hour. The dashboard’s settings page shows them. They are set in the environment; nothing here changes them.
Returns
{
"auto_actions": ["block_ip", "rate_limit", "waf_rule"],
"needs_a_person": ["kill_process", "lock_user", "revoke_public_acl"],
"ttl_hours": {
"block_ip": 2.0,
"lock_user": 2.0,
"rate_limit": 2.0
},
"max_ttl_hours": 24.0,
"max_auto_actions_per_hour": 10,
"auto_actions_last_hour": 4,
"permanent_needs_a_person": true,
"check_after_min": 10.0,
"health_url_set": false
}
| Field | Meaning |
|---|---|
auto_actions |
action types SentrAI may take on its own (AUTO_ACTIONS, default block_ip,rate_limit,waf_rule), among those a responder can carry out |
needs_a_person |
the other types: they wait for a person to approve them |
ttl_hours |
how long a temporary action of each type lasts: ACTION_TTL_HOURS per type (such as lock_user=1,block_ip=4), else HOTPATCH_TTL_HOURS (2), never more than max_ttl_hours |
max_ttl_hours |
the longest any temporary action may last (MAX_TTL_HOURS) |
max_auto_actions_per_hour, auto_actions_last_hour |
the budget of automatic actions in any rolling hour (MAX_AUTO_ACTIONS_PER_HOUR, 0 means none on its own) and how many were taken in the last hour; past it a person decides |
permanent_needs_a_person |
always true: only a person makes an action permanent |
check_after_min |
minutes after an action starts when SentrAI checks it worked and did no harm (CHECK_AFTER_MIN) |
health_url_set |
whether SENTRAI_HEALTH_URL is set, a page that should keep answering while SentrAI blocks things; the after-action check probes it |
Read the block list
GET /blocklist
The addresses and accounts blocked right now: every block_ip and lock_user action in force. A
website or proxy in front of your service polls it and refuses what it lists (the lab website polls
every 2 seconds and lets traffic through while the core is down); on a central panel each agent
polls it to put its own machine’s blocks in place.
Open without ?host=: no token needed, since it lists nothing else (see
Open endpoints). With ?host= it needs the token or that machine’s
key. A machine key may call it, for its own machine only (Machine keys):
the core then answers for the key’s own machine, whether or not ?host= is given.
Query
host(string, optional): only the blocks this machine must put in place. Inhostscope that means the confirmedblock_ipandlock_useractions taken for that machine’s incidents. Infleetscope every machine guards against every block, so the answer is the full list whatever the host.
Returns
{
"ips": ["203.0.113.45"],
"users": ["admin"]
}
ips and users are sorted; both are empty lists when nothing is blocked.
Errors
- 401
missing or wrong API token (python sentrai_config.py token shows it):?host=was given without the token or a machine key. - 401
unknown or revoked machine key: join the panel again: the machine key is not known. - 403
a machine key only sends its own events and reads its own block list: a machine key asked with?host=naming another machine.
curl "http://127.0.0.1:8000/blocklist"
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8000/blocklist?host=web-01"
See what each part of SentrAI is doing
GET /agents
The parts of SentrAI that read events and act on them, each with its role and how much it has done.
The dashboard’s Agents page shows it. The list is in a fixed order; the first entry is the
orchestrator (Cyanide, or Warden when SENTRAI_ENGINE=saguaro keeps the fixed playbooks), and
Planner appears only with Cyanide.
Returns, trimmed:
[
{
"name": "Gatehouse",
"role": "web layer: web/WAF logs, proposes WAF rules",
"can_execute": false,
"analyzed": 7
},
{
"name": "Watchdog",
"role": "collector heartbeats; flags silent collectors",
"can_execute": false,
"collectors": [
{"collector": "web-01", "silent": false, "last_seen_ts": 1793069111.9793544}
]
},
{
"name": "Jev",
"role": "TypeSafe System One classifier",
"status": "disabled: TYPESAFE_API_KEY not set (fallback classifier in use)",
"calls": 0,
"failures": 0,
"last_error": null,
"cache": {"size": 0, "max": 2048, "hits": 0, "misses": 0}
},
{
"name": "Planner",
"role": "Cyanide's AI model (system-aware containment plans)",
"status": "off (CYANIDE_ENABLED=0 turns the AI model off); playbooks only",
"calls": 0,
"failures": 0,
"last_error": null,
"profile": "none"
}
]
| Field | Meaning |
|---|---|
name, role |
which part it is: Cyanide (orchestrator), Gatehouse (web), Vault (databases), Watchtower (network), Garrison-Linux and Garrison-Win (machines), Countersign (the safety check that approves every action SentrAI takes on its own), Watchdog, Scribe (the audit trail), HelpDesk, Jev and Planner |
can_execute |
whether it may carry out actions itself; only the Garrison parts can |
analyzed |
the layer parts only: events it has read since the core started |
collectors |
Watchdog only: each collector, whether it has gone silent, and when it last sent anything, as Unix seconds |
status, calls, failures, last_error |
Jev and Planner: whether the AI model is in use, and how its calls went |
cache |
Jev only: the classification cache, its size, limit, hits and misses |
profile |
Planner only: the organisation in the system profile the plans are made for, none without one |