Home / API reference / Risk and protection

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. 0 returns an empty list.
  • host (string, optional): one machine’s own index, incidents and history, by the name in GET /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:

  1. its SSH key fingerprint is a known administrator key (ADMIN_SSH_KEYS, taught or learned);
  2. 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);
  3. it is a console sign-in by a known administrator account (ADMIN_ACCOUNTS or 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 forget or kind 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, Admin or Auditor; case is ignored.
  • address (string or null, default null): 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: on is false and reason is 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. In host scope that means the confirmed block_ip and lock_user actions taken for that machine’s incidents. In fleet scope 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