A host intrusion sentinel you run yourself. A small agent on each machine watches running programs and their network connections, scores what it sees in code, and reports the cases that look like an attack to a service you deploy in your own Cloudflare account. By default it also ends a hostile program, and everything that program started, on the spot.
It was built in response to FBI FLASH-20261113-001 on agentic intrusion frameworks, and it is aimed at the things those tools do on a machine: web shells, s, remote-access implants, movement between Windows machines, Active Directory attacks, and the pace and repetition of an automated operator.
Status: draft, not yet released. Read "What has and has not been tested" before relying on it.
each machine your Cloudflare account
ββββββββββββββββββββββββββββββββ HTTPS out βββββββββββββββββββββββββββββββββββ
β agent/sentinel_agent.py β βββββββββββββΊ β server/src/worker.mjs (Worker) β
β sensors β scoring β cases β βββββββββββββ β cases, decisions, learning β
β / stop (if approved) β decisions, β D1 database, optional AI β
ββββββββββββββββββββββββββββββββ weights βββββββββββββββββββββββββββββββββββ
- The agent reads the operating system's own process and connection tables. It captures no packets and reads no file contents. It opens no port. Scoring happens on the machine.
- Only cases leave the machine : a program that reached the SUSPECT or HOSTILE level, with the signals that fired, the programs that started it, and the addresses it talked to. A command line never leaves the machine; the agent reports which pattern matched, not the text.
- The service checks every field, scores the signals again (the level is never taken from the device), stores the case encrypted, raises an alert, and hands back two things: the operator's decision on a program that agent d, and the learned signal weights.
- It acts, within limits. It never touches operating-system processes, security software, container
runtimes, remote access (
sshd), the web server itself, or the browser, mail or document program a person is working in: it ends the harmful thing those programs started, not the programs. It never signals a process that has already exited.
| Mode | What the agent does about a hostile program | Choose it when |
|---|---|---|
kill |
ends it and everything it started, at once, then reports what it did | an intruder's minutes matter more than a wrongly stopped job |
decide |
freezes it at once, then the service's decision model says stop, release or hold | you have a model and want a false alarm to cost seconds, not a program |
| `` | freezes it (reversible) and waits for an operator to stop or release it | a wrong stop would be an outage |
report |
nothing; it only reports | you are evaluating, or another tool responds |
Set the ceiling for the whole deployment with the service variable RESPONSE_MODE, and a machine's own mode
with --mode at enrolment. A machine gets the lower of the two.
The default. With RESPONSE_MODE left empty, a deployment with a model configured runs decide, and one
without runs kill. Agents ask for kill unless told otherwise.
How decide works. The agent freezes the program the moment it is judged hostile, so the attack is already
stopped while the decision is made. The service asks the model once, with an eight-second limit, and answers in
the same exchange:
- stop when the model recommends it with high confidence: the agent ends the program.
- release only if you set
DECISION_RELEASEtohigh-confidence, the model is highly confident, and no locked signal is involved (a published indicator, credential theft, a directory attack). The agent lets it run and leaves it alone afterwards. A model's release does not mark the program as yours and teaches nothing. - hold in every other case: it stays frozen until an operator decides.
If the model fails or times out, DECISION_FALLBACK decides: kill (the default) or hold. An agent that
hears nothing from the service at all for twenty seconds applies the same fallback by itself.
Kill is not reversible. In kill mode a false alarm ends a legitimate program; decide exists to avoid
exactly that. The agent acts only on
specific launch chains with almost no honest use, or on cases whose evidence spans three categories, and a
program an operator marks "ours" is never touched again. Even so: run `` or report first on machines
where a wrong stop is costly, and watch what it would have done.
If you run report or ``, run the AI analyst as well (see "The AI part"). In those modes nothing ends
an attack until a person reads the case, and an automated intruder works in minutes. A second, independent
model reading every case and recommending stop or release shortens that wait; keep it separate from whatever
AI your organisation already uses, so one compromised or confused system is not judging its own work.
| Area | Signals |
|---|---|
| Email and lures | scripts started by mail, document and browser programs; programs run out of attachments and archives; shortcut and paste-this-command launches |
| Web applications | a shell or interpreter started by a web server; download-and-run and reverse-shell commands; who-am-I commands |
| s and implants | system tools used as s (MSBuild, cmstp, regsvr32, rundll32, mshta); programs in temporary folders talking to the internet; regular call-home timing; fan-out scanning |
| Windows domains | credential dumping and Kerberoasting commands; domain enumeration; shells inside WinRM, WMI and PsExec sessions; and, when the agent runs with administrator rights, Security-log patterns for Kerberoasting, DCSync, failed sign-in bursts, cleared logs and privileged group changes |
| Automated operators | a stream of different commands at machine pace under one foothold; near-copy retries; the same command started twice at once |
| Web requests | read from the web server's own access log (nginx, Apache, IIS): directory fuzzing, injection-shaped requests and whether the application answered them, walking through record numbers, sign-in guessing, calls to a server-side script with a command parameter, and bursts of application failures on requests that carry a body |
| Web folders | a server-side script that appears in a web folder after the agent started, with no release around it |
| Tunnels | Chisel by name; SoftEther by its program names with its install folder or its ports; Tor by its client, by any program sending traffic through the local Tor proxy, and by inbound connections from the Tor Project's published exit nodes |
| Attempts from outside | bursts on SSH, Remote Desktop, WinRM, SMB, VNC and Telnet |
| Published indicators | the addresses and domain listed in FBI FLASH-20261113-001, as a scored signal with the FBI's caveat that addresses get reassigned; never an automatic block |
| Antivirus | new Windows Defender detections become cases |
| Cloud credentials | a web server process reaching for the cloud metadata service, where instance credentials are handed out |
| Cloud control planes | from audit records you forward: credentials added to applications and service principals, ownership, role and consent grants, weakened Conditional Access and federation settings (Microsoft Entra); permission mapping by dry-run, key pairs, public addresses, large volumes, role-bearing instances, access keys made for other users, administrator policies (AWS CloudTrail); validation dry-runs, publishing credentials and storage keys (Azure activity log); and any of it from an address in the FLASH |
Signals are weighted and grouped into categories. A case is called HOSTILE only when the evidence spans at least three categories, so no single rule decides it.
Needs Node.js 22.5 or newer and Python 3.9 or newer.
cd server
export SENTINEL_ENROLL_KEY=$(python3 -c "import secrets;print(secrets.token_hex(24))")
export SENTINEL_ADMIN_TOKEN=$(python3 -c "import secrets;print(secrets.token_hex(24))")
node local.mjs # in-memory database, for evaluation only
In another terminal, with the same two variables set:
python3 agent/sentinel_agent.py --state-dir ./state enroll --server http://127.0.0.1:8787
python3 agent/sentinel_agent.py --state-dir ./state run
curl -H "Authorization: Bearer $SENTINEL_ADMIN_TOKEN" http://127.0.0.1:8787/v1/devices
curl -H "Authorization: Bearer $SENTINEL_ADMIN_TOKEN" "http://127.0.0.1:8787/v1/cases?state=open"
Copy server/wrangler.example.jsonc to server/wrangler.jsonc, then follow the commands at the top of that
file: create the D1 database, load schema.sql, set the three secrets, deploy. Three secrets are required and
each must be at least 32 characters:
| Secret | What it is for |
|---|---|
DATA_KEY |
encrypts case evidence at rest (AES-256-GCM) |
ENROLL_KEY |
lets a machine enrol; share it only with the people who install agents |
ADMIN_TOKEN |
lets an operator read cases and make decisions |
Optional: ALERT_WEBHOOK_URL receives a JSON alert for each new case. The alert text is written by the
service from the signal ids; text from a device is never placed in an alert.
| Where | How |
|---|---|
| Linux (systemd) | sudo SENTINEL_ENROLL_KEY=<key> deploy/install-linux.sh https://your-service [kill||report] |
| Windows (as SYSTEM) | elevated PowerShell: deploy\install-windows.ps1 -Server https://your-service -EnrollKey <key> [-Mode kill||report] |
| Kubernetes | build deploy/Dockerfile , fill in the Secret indeploy/kubernetes-daemonset.yaml , apply it; every node enrols itself |
| Anything else | python3 agent/sentinel_agent.py enroll --server ... --key ... then... run under your own supervisor |
Run the agent with administrator or root rights where you can. Without them it sees only the current user's programs, and on Windows it cannot read the Security log.
Every installer enrols in kill mode unless you name another.
What an intruder does in Microsoft Entra, Azure or AWS leaves nothing on a machine; it is all in the audit logs. Forward those records to the service and it applies the cloud rules:
curl -X POST https://your-service/v1/cloud-events \
-H "Authorization: Bearer $INGEST_TOKEN" -H "Content-Type: application/json" \
-d '{"source": "aws-cloudtrail", "records": [ ...CloudTrail records... ]}'
source is entra-audit, entra-signin, aws-cloudtrail or azure-activity; send up to 1000 records at a
time, exactly as the cloud wrote them. Use whatever already moves your logs (an Event Hub or EventBridge
target, a scheduled export, your SIEM's forwarder). The service never calls a cloud API and holds no cloud
credential. Each identity whose records add up to a case gets one, under a stand-in device named after the
source; an operator can mark an identity as theirs so it is not raised again.
Each batch is judged on its own, so forward records in batches that cover a few minutes. The rules are written from the vendors' published log references and tested with records in those shapes; they have not been run against a live tenant.
Detection, scoring, stopping, pausing and learning are all plain code. They need no model and work with
AI_PROVIDER set to none, which is the default.
One optional model does two jobs, both in server/src/analyst.mjs:
- Analyst. It adds a second opinion to every case for the operator: a two-sentence summary, the likely technique, and a recommendation.
- Decision model. In
decidemode the same answer settles what happens to a frozen program, within the limits under "Howdecideworks".
The model never changes a score and never teaches the policy. It sees signal ids with the service's own sentences, program names and counts; it is never sent a command line, and it is told that everything in the case is data. Choose it with one variable:
AI_PROVIDER |
What it uses | Settings |
|---|---|---|
none |
no model | |
workers-ai |
Cloudflare Workers AI through the AI binding |
AI_MODEL |
openai |
any OpenAI-compatible chat-completions endpoint, hosted or your own | AI_BASE_URL ,AI_MODEL , secretAI_API_KEY |
custom |
your own analyst: a service binding named ANALYST , or a URL inAI_BASE_URL |
receives {instructions, case} as JSON, answers{summary, technique, recommendation, confidence} |
To plug in a different model, add a branch in that file.
What one decision model covers. Every case the agent can freeze passes through the same model: email and document launches, web shells, s, implants, credential theft and movement between Windows machines. It does not decide cases where there is nothing on the machine to stop: web-request attacks, connection bursts, directory-log patterns and antivirus detections are analysed for the operator, not acted on. And a model adds judgement, not sight: it covers only what the sensors report and what you forward.
The service learns from your operators' own verdicts and nothing else:
- Each case an operator marks "ours" or "an attack" is remembered as its shape: the signal ids, the platform and the verdict. No program name, path, address or command line is kept in that memory.
- A signal your operators keep dismissing becomes worth less on your machines; one they keep confirming becomes worth more. A weight moves one point at a time and at most two points in total.
- Published indicators, antivirus verdicts, attacks on a service and directory attacks never move.
- A change is adopted only if every attack your operators confirmed before, and every reference attack in
server/src/signals.mjs, would still be reported under it. Otherwise it is rejected. POST /v1/learning/resetforgets everything learned.
No model takes part in this. It is bounded tuning, not a system that rewrites itself.
Plugging in a memory engine. Recall of similar past cases works out of the box by comparing shapes. If
you have a semantic memory store, bind an object as SENTINEL_MEMORY with remember(text) and
recall(query, k); the service will write each verdict to it and show what it recalls beside a new case.
All operator calls need Authorization: Bearer <ADMIN_TOKEN>.
| Call | Purpose |
|---|---|
GET /v1/devices ,POST /v1/devices/:id/revoke |
list machines, remove one |
GET /v1/cases?state=open ,GET /v1/cases/:id |
list cases, read one with its evidence, chain and similar past verdicts |
POST /v1/cases/:id/action |
{"action": "trust" | "confirm" | "resolve" | "reopen" | "stop" | "release"} |
POST /v1/cases/:id/analyze |
ask the AI analyst again |
GET /v1/learning ,POST /v1/learning/reset |
see or forget what was learned |
POST /v1/cloud-events |
forward cloud audit records (accepts the separate INGEST_TOKEN as well) |
There is no web dashboard in this repository yet; the API is the interface.
Measured on the development machines; your numbers will differ.
| What | Measured |
|---|---|
Time from a staged document-launched script starting to it being ended (Windows, kill mode) |
within 1.4 seconds |
The same in decide mode with a stand-in model |
frozen after 0.1 seconds, ended on the model's "stop" within 1 second |
A stand-in false alarm in decide mode (model says release, release allowed) |
frozen briefly, then left running |
| Time to a staged web shell (Linux) | within one 5-second sample; later runs judge new programs as they start |
| Cost of one full sample | about 6 ms on Windows, about 15 ms on Linux |
| Cost of one look for new programs (five a second) | about 3 ms on Windows |
| Traffic when quiet | one small report per machine every five minutes |
| Model calls | none, unless the AI analyst is on: then one per new case |
cd agent && python3 -m unittest discover -s tests -p "test_*.py" # engine, /stop, 100 scripted events
cd agent && python3 tests/test_cases.py # the per-event table
cd server && npm test # the service
MIT. See LICENSE.