EyalSec User Guide

What is EyalSec?

How EyalSec watches untrusted data flow from a source, through your code, to a risky sink, and forks into Report or Report and Raise.

EyalSec is a secure Python. You install it next to your normal Python and run your programs through it exactly as you do today; nothing in your code has to change. We call the EyalSec runtime es-python, and you run it in place of python.

While your program runs, es-python quietly watches for one specific danger: untrusted data (a.k.a. taint), meaning data that came from the outside world, reaching a risky action in your code. Where that data enters is called a source: a network socket, a file, standard input, the program's environment variables and command-line arguments, or foreign code (Python code on disk that another user can write to). The risky action it flows into is called a sink: things like running a system command, opening a file, or running a database query. Untrusted data reaching a sink is how most real-world hacks happen, and that is exactly the pattern es-python is built to catch.

When es-python sees untrusted data reach a sink, it can do one of two things, and you choose which:

You can read more about the difference, and when to use each, in Report mode vs Report and Raise mode.

You manage everything from one website: your dashboard. It shows which of your machines have es-python installed, what they have detected, and lets you change your settings, all in one place.

es-python is one of several products

The same idea is available for other languages and runtimes too, and your dashboard has a separate events list for each product your account has. The two this guide covers are:

Which products your account has is set for you, per account. es-python is enabled by default; es-chromium and the other products are enabled on request, and once one is, you add its machines yourself from the Machines page, the same way you add an es-python machine. To have a product turned on, contact support@eyalsec.com. Your Events page shows one list per product you have, so a list you do not see is a product that is not enabled rather than a missing feature. Everything else in this guide (events, filters, severity, suppression, quotas) applies to every product the same way.

The three pieces

EyalSec is made of three parts that work together:

Quick start: your first 10 minutes

This is the fastest path from zero to seeing your first event: create an account, add a machine, install es-python (the EyalSec runtime), switch on the sources you want watched, run a script through it, and watch what it finds appear on your dashboard. Plan on about ten minutes.

1. Create your account

  1. Open EyalSec and go to the Register page.
  2. Pick a username, enter your email address, and choose a password (typed twice to confirm). Your password must be 8 to 72 characters. Tick the box to accept the Terms of Service and Privacy Policy.
  3. Submit. EyalSec emails a verification link to that address; click it to activate your account. That signs you in and drops you on your dashboard.

Verifying your email activates your sign-in and nothing else. A new account carries no machines and no event allowance until a plan is set up for it, and the dashboard says so at the top of every page. Email sales@eyalsec.com to book a live demo and have your plan sized; your limits then apply immediately, with nothing to reinstall. See Plans & roles for what a plan sets.

The rest of this walkthrough assumes your plan is active.

2. Log in (next time)

Clicking the verification link signed you in, so you can skip this for now. Next time you visit, go to the Login page and enter your username and password. If you later turn on two-factor authentication, you'll also be asked for a 6-digit code. See Accounts & signing in.

3. Add a machine

A machine is any host where you'll run your code through es-python: your laptop, a server, a container.

The Machines page: each machine shows a status pill, an OS chip, a build badge, and an install panel with the one-line install command.

  1. Go to the Machines page. If your account has more than one product, pick es-python under Product.
  2. Give it a Name (anything memorable, 1 to 128 characters).
  3. Pick the Distro (for example Ubuntu 24.04, Debian 12, RHEL 9, Amazon Linux 2023, Fedora, or Arch) and the Arch (x86_64 or aarch64) that match the host.
  4. Pick the Python version for this machine (3.13 is the default; 3.9 to 3.14 are supported, and the list shows the ones available for the distribution and architecture you picked).
  5. Click Add machine. The machine appears with a created status pill, waiting for you to install es-python on it.

More on the full machine lifecycle is in Machines.

4. Copy the install command

Click Install on the machine's row (status created, OS set). The page shows a single install command. Select Copy to grab it. It looks like this:

printf '  preparing your build on the server (this can take up to a minute)...\n'; T="$(mktemp)" && chmod 600 "$T" && curl -sSL -d "secret=<token>" --data-urlencode "glibc=$(ldd --version 2>/dev/null | head -1)" --data-urlencode "arch=$(uname -m 2>/dev/null)" 'https://eyalsec.com/install.sh' -o "$T" && . "$T"; ES_RC=$?; rm -f "$T"; (exit "$ES_RC")

The <token> is a one-time secret that's valid for 10 minutes, so run it soon. If it expires, click Install again for a fresh command.

5. Run the installer on the host

Paste the command into a terminal on the machine itself and run it. It:

The machine's status moves created → pending → installed. Once it shows installed (a green, blinking pill) on the dashboard, you're ready to go: es-python works immediately, in that same terminal, with no extra step.

The installer retries and resumes an interrupted download on its own. If the install still fails, click Install again for a fresh command and run that.

Note: the command runs the installer inside your current shell (the . "$T" part), which is what makes es-python available right away. If you instead pipe the script to bash (curl ... | bash), the install still works, but you'll need to open a fresh terminal (or run source ~/.bashrc) afterwards.

Privacy notice: es-python may capture values from your program's memory (including secrets and personal data) when untrusted data reaches a monitored sink. Only run it on systems and data you are authorized to monitor.

6. Switch on detection and run your script

Every taint source is off until you switch it on, so a fresh machine reports nothing yet. On the Machines page, click Configure on the machine's row and set the sources you want watched (for example socket and file) to On, or set them once for every machine on the Filters page. What each source watches is explained in Machine configuration. Source changes apply to programs started after the change.

Then, anywhere you'd normally type python, type es-python instead:

es-python your_script.py

Your program runs exactly as before, but now es-python is watching for untrusted data reaching a risky action and reporting what it sees. You don't need to change a single line of code. For more ways to run it, see Running es-python.

7. Watch events appear

Back on the dashboard, open the Events page.

The Events page: a filterable table of detections, with one row expanded to show where, repr, origin, and trace.

As your program runs, detections show up here, newest first. Each row is one kind of event; the count rises each time the same thing happens again, and the severity column tells you at a glance how serious it looks. Click a row to expand it and see the details. Full coverage of the table, filters, and the detail pane is in Events.

If nothing shows up yet, give it a few seconds (events are sent in the background), make sure you ran your script with es-python and not python, check that at least one source is switched on for the machine, and confirm the machine reads installed on the Machines page.

8. (Optional) Turn on Report and Raise mode

So far es-python is in Report mode: it records events but never interferes with your program. When you're ready to actually block a risky action, switch that machine to Report and Raise mode; then es-python stops the action (your program gets a RuntimeError) instead of just recording it.

You set this up per machine in the Configure modal by adding a Raise rule. The how-to is in Machine configuration, and the difference between the two modes is explained in Report mode vs Report and Raise mode.

Note: Raise rules must be enabled on your account. If they aren't, adding one is rejected with "Raise rules are not enabled for your account. Ask an administrator to enable them." To have it turned on, contact sales@eyalsec.com.

That's the whole loop: install once, run with es-python, watch your events. From here, explore Settings to secure your account, Filters to cut noise, and the Dashboard overview for a high-level summary. If you get stuck, see Getting help.

Accounts & signing in

Register and login card with a 2FA code field

Everything in EyalSec lives behind your account. This page walks through creating an account, signing in, the two-factor (2FA) check at login, signing out, and what to do when you forget your password.

Register

New here? Create an account from the Register page.

  1. Go to /register/.
  2. Pick a username and enter your email address.
  3. Enter a password, then type it again to confirm (it must follow the Password rules below).
  4. Tick the box to accept the Terms of Service and Privacy Policy. Depending on how the site is configured, a background human check (CAPTCHA) may also run.
  5. Submit.

You're not signed in yet: EyalSec emails a verification link to the address you gave, and your account stays inactive until you click it. Following that link activates the account, signs you in, and drops you on your dashboard at /scanner/, so keep a working email handy at signup. See Quick start: your first 10 minutes.

Verifying your email activates your sign-in, not your access. A self-signup account starts with no machines and no event allowance, and the dashboard shows a banner saying so: you can sign in and look around, but adding a machine is refused until a plan is set up for the account. Email sales@eyalsec.com to book a live demo and have your plan sized. New limits apply immediately, with nothing to reinstall. See Plans & roles for what a plan sets.

Some accounts are created from an invite link instead. An invite link opens a special registration page that works the same way (username, password, and accepting the terms); it simply starts the account with the settings an admin chose for you, already provisioned, and the link expires 24 hours after it was created.

Log in

Already have an account? Sign in from the Login page.

  1. Go to /login/.
  2. Enter your username and password.
  3. Submit.

If two-factor authentication is turned on for your account, you'll be sent to a second screen to finish signing in (see The 2FA challenge below).

For your protection, repeated failed sign-in attempts from the same place are slowed down. If you've been locked out by too many wrong guesses, wait a little and try again, or use Forgot password to reset.

The 2FA challenge

If you've enabled two-factor authentication, logging in takes one extra step. After your username and password are accepted, you land on the 2FA page (/login/2fa). There you can enter any one of:

Enter one of those and you're in. Backup codes are your safety net for when you don't have your phone, so keep them somewhere safe. You can turn 2FA on, see how many backup codes you have left, and generate fresh ones from Settings: your account.

Log out

To sign out, use Sign out at the bottom of the dashboard sidebar. This clears your session and returns you to the login page. Signing out is a good idea on shared or public computers.

Forgot password

Can't remember your password? You can reset it by email.

  1. Go to /login/forgot.
  2. Enter your username or email address and submit. For privacy, EyalSec always shows the same neutral message here; it won't tell you whether an account exists, so nobody can use this page to fish for valid accounts.
  3. If the account exists and has an email on file, a 6-digit code is emailed to it. That code is valid for 15 minutes.
  4. On the verify page (/login/forgot/verify), enter your username or email, the 6-digit code, and your new password twice. The new password must meet the Password rules; that's checked before the code is, so fix any password problem first.
  5. On success, your old codes are used up and you're sent to the login page.

Note that resetting your password does not sign you in automatically. Once the reset succeeds, log in normally with your new password.

To set or update the recovery email used here, see the email card in Settings: your account. Without an email on file there's nowhere to send the code, so add one before you need it.

Password rules

The same rules apply everywhere you set a password: when you register, change it in Settings, or reset it after a forgot-password. Your password must:

If a password is turned down, you'll see a message telling you which rule it broke; adjust it and try again.

Still stuck signing in? Email support@eyalsec.com.

Settings: your account

Settings page with Account, Two-factor, and API access cards

The Settings page (/scanner/settings) is where you manage your own account: your recovery email, your display time zone, your password, two-factor authentication, your API key, billing, and deleting the account. Each area is a separate card, described below.

Change your email

Your email is the address EyalSec uses to send you a password-reset code if you ever forget your password (see Forgot password).

  1. Open the Account card.
  2. Type a valid email address, or leave the field blank to remove the one on file.
  3. Enter your current password (and, if 2FA is on, a 2FA code).
  4. Click Save email.

Setting or changing the address takes one extra step: EyalSec emails a confirmation link to the new address, and the change only takes effect when you click it. Until then the old address stays in place. This proves you control the inbox before it becomes your recovery address; if you had an old address on file, it also gets a heads-up notice about the requested change. The confirmation link expires after 24 hours. Only one change can be pending at a time: a new request replaces the pending one, and you have to wait a few minutes after one confirmation email before asking for another.

Removing the address (saving an empty field) happens immediately, and any pending confirmation link is cancelled.

We recommend keeping a working email here so the Forgot password flow can reach you. Without one, there's nowhere to send a reset code.

Time zone

The Time zone card sets the zone that event, activity and dashboard times are shown in. Auto (the default) follows your browser. Pick a zone and click Save time zone.

Change your password

To change your password from the Change Password card:

  1. Enter your current password.
  2. Enter your new password, then type it again to confirm.
  3. Click Update password.

The new password must follow the Password rules: 8 to 72 characters, and it can't be a common password.

Two-factor authentication

Two-factor authentication (2FA) adds a second step at login: after your password, you also enter a one-time 6-digit code from an authenticator app on your phone. It's the single best thing you can do to protect your account. The Two-Factor Authentication card shows your current status and the actions below.

Enable 2FA

  1. Click Enable 2FA. EyalSec shows a QR code (and a manual key you can type in by hand if you can't scan).
  2. Scan the QR code with an authenticator app (such as Google Authenticator, 1Password, Authy, or similar).
  3. Enter the current 6-digit code the app shows, to prove the setup worked, and your current password.
  4. Click Verify & enable.

The moment 2FA turns on, you're shown 10 one-time backup codes. These are shown only once; copy them and store them somewhere safe (a password manager is ideal). Each backup code works a single time and is your way back in if you ever lose your phone. At login you can use either a 6-digit code or a backup code; see The 2FA challenge.

Status card

When 2FA is on, the card shows Enabled along with Backup codes remaining: N so you can tell at a glance how many you have left. When it's off, the card prompts you to turn it on.

Regenerate backup codes

Running low on backup codes, or think they may have leaked? Regenerate them to get a fresh set of 10, again shown only once. Generating a new set replaces the old one, so your previous backup codes stop working.

Disable 2FA

You can turn 2FA off from the same card. Disabling it clears your saved authenticator secret and your backup codes, so your account goes back to password-only login.

Email sign-in codes

Besides the authenticator app, there is a separate Email sign-in codes card: a one-time 6-digit code sent to your recovery email at sign-in. If you also use an authenticator app, the emailed code is an extra fallback; if you don't, the emailed code becomes your second factor on its own. It needs a recovery email on file (set one in the Account card first), and you can turn it on or off there at any time by confirming your password.

API access

An API key lets your own scripts and tools talk to the EyalSec dashboard API directly, without logging in through the website. A full-access key gives the same access your account has, so treat it like a password. You can have one key at a time. For the endpoints you can call with it, see the API reference.

Generate a key

  1. Open the API access card.
  2. Optionally set an expiry date (a future date), or leave it blank for a key that doesn't expire.
  3. Optionally tick Read-only for a key that can only read (GET requests) and never change anything.
  4. Enter your current password (and, if 2FA is on, a 2FA code).
  5. Click Generate API key.

You're shown the full key once. It starts with es2_ followed by a long random string, like this:

es2_Qm3xV8rT1kLp9ZcW4nYb7HsJ2dFg6AeU0oRiXtNvKw5

Copy it now; the full key is shown only once. If you lose it, you'll have to make a new one.

View your key's details

After a key exists, the card shows its metadata, never the full secret again. You'll see:

You send the key in a request header named X-API-Key:

curl -H "X-API-Key: es2_Qm3xV8rT1kLp9ZcW4nYb7HsJ2dFg6AeU0oRiXtNvKw5" \
  https://eyalsec.com/api/get_all_machines/

See the API reference for the full list of endpoints, request bodies, and example responses.

Regenerate or revoke a key

Billing

The Billing card's Manage billing button opens the billing portal for your subscription, where you can update your payment method and download invoices. If your account has no subscription billed there, it takes you to the Pricing page instead.

Delete your account

The Danger Zone card permanently deletes your account and everything in it: machines, events, filters and rules. Enter your password, click Delete my account and confirm. This cannot be undone.

A note on re-entering your password

Some sensitive actions ask you to re-enter your current password even though you're already signed in. This is a deliberate extra check so that nobody who walks up to your open session can quietly weaken your account. You'll be asked for your password to:

When 2FA is on, changing your email and generating an API key also ask for a 2FA code.

Need a hand with any of this? Email support@eyalsec.com.

Machines

The Machines page: a table of machines with colored status pills, an OS chip and build badge per row, a kebab menu, and an install panel showing the curl command

A machine is one computer (a laptop, a server, a container) where you run your code through es-python, the EyalSec runtime. The Machines page is where you add a machine, install EyalSec on it, and keep it up to date. Everything a machine detects shows up under Events, attributed to that machine.

You manage machines from Machines in the left sidebar.

What you can do here

Add a machine

  1. Pick the Product this machine runs. The dropdown lists the products enabled on your account, and the fields after it change to match: es-python installs onto a host and needs to know which one, every other product needs only a name. If only one product is enabled for you there is nothing to choose and the dropdown is not shown.

  2. Give it a Name: anything from 1 to 128 characters that helps you recognize the host (for example prod-web-1 or my-laptop).

    The steps below are es-python's. For any other product, skip to step 6.

  3. Pick the Distro (the Linux distribution it runs):

    • Ubuntu 24.04 LTS
    • Ubuntu 22.04 LTS
    • Ubuntu 20.04 LTS
    • Debian 13 (trixie)
    • Debian 12 (bookworm)
    • Debian 11 (bullseye)
    • RHEL / Rocky / Alma 9
    • Amazon Linux 2023
    • Fedora (latest)
    • Arch Linux (rolling)
  4. Pick the Arch (the processor architecture): x86_64 (most servers and desktops) or aarch64 (ARM machines, such as AWS Graviton or a Raspberry Pi). Every distro is offered for both.

  5. Pick the Python version the machine should run. The default is 3.13. Versions 3.9 through 3.14 are supported, and the list is filtered to the ones actually available for the distribution and architecture you picked, so it can change when you change either. This is the version of the es-python runtime the machine installs.

  6. Click Add machine.

The new machine appears in the table with a created status, ready to install.

How many machines you can add is a per-account limit set by your plan, counted per product. If you hit the cap you will see a message like "Machine limit reached (N). Contact sales@eyalsec.com to upgrade your plan." An account with no plan yet cannot add machines at all. See Plans & roles for the limits, and email sales@eyalsec.com to raise yours.

A note on socket-only machines

You may still see a socket-only badge on some older machines. A socket-only machine runs a slimmer version of the runtime that watches network data only: it cannot watch the file, stdin, foreign, or env sources, so on such a machine only the socket source is live and the other four toggles do nothing. See Running es-python for what each source watches.

Socket-only is no longer a choice you make when adding a machine: new machines are always created with the full-instrumentation build. The badge and behavior remain only for machines created under the older option.

Status lifecycle

Each machine moves through three states, shown as a colored pill in its row:

created  →  pending install  →  installed

If the installer hits an error it cannot recover from on the host, it reports it and the pill reads failed. Click Install again for a fresh command once the cause is fixed.

Row badges

Once a machine is installed, its row can show a few badges next to the status pill:

Set the OS if it is missing

If a machine was added without an operating system, its OS chip reads not set (click to set) in amber. Click it, pick the Distro and Arch (the same lists as when adding a machine), and click Save. You need the OS set before you can install, because EyalSec ships a build matched to that exact distro and architecture.

Get the install command

For a created machine that has its OS set:

  1. Click Install on the machine's row.

  2. EyalSec shows a one-line install command that looks like this:

    printf '  preparing your build on the server (this can take up to a minute)...\n'; T="$(mktemp)" && chmod 600 "$T" && curl -sSL -d "secret=<token>" --data-urlencode "glibc=$(ldd --version 2>/dev/null | head -1)" --data-urlencode "arch=$(uname -m 2>/dev/null)" 'https://eyalsec.com/install.sh' -o "$T" && . "$T"; ES_RC=$?; rm -f "$T"; (exit "$ES_RC")
    
  3. Click Copy to copy it.

The <token> is a one-time secret that is valid for 10 minutes. It can only be used once. If it expires or you need a fresh one, click Install again to get a new command.

The panel also carries an authorization statement (you own or are authorized to monitor this machine), ticked by default. A command is only issued while it is ticked. The command on screen shows the token partly masked, so a screenshot does not leak it; Copy copies the real one.

Add a machine for any other product

For every product other than es-python and es-chromium, choose it under Product, type a Name, and click Add machine. The install panel then shows two things:

Clicking Install on the row again issues a fresh command without adding another machine.

Add an es-chromium PC

If es-chromium is enabled on your account, choose it under Product, type a Name, and click Add machine. If es-chromium is not in the dropdown it is not enabled for you; see Plans & roles.

There is no distro, architecture or Python version to pick: there is a single published build, and the EyalSec browser brings its own runtime. The row that appears is named by you, exactly like an es-python machine, and that name is what its events are attributed to on the es-chromium view. It is also shown inside the browser itself, so you can tell which machine a running browser reports as.

Click Install on the row for a copy-paste command to run on that PC. The secret in it is one-time, exactly like es-python's, and clicking Install again issues a fresh one.

An es-chromium row moves through the same three states as any other machine, with one difference worth knowing: it reaches installed when the browser is first opened, not when the installer finishes. A browser that is installed but has not been opened yet sits at pending install, and that is normal rather than a failure, however long it lasts.

Two things an es-chromium row does not have: a Set OS step and a build badge, because there is one build and no target to choose; and an Uninstall command in the row menu, because that command removes es-python specifically. To remove a browser, Delete the machine here and remove the application from the PC the way you would any other.

Your machine limit is counted per product, so es-chromium PCs use their own allowance and never eat into your es-python slots.

Run the installer

Run the copied command on the host itself (in a terminal on that machine). The installer:

As it runs, the machine's status moves from created to pending install and then to installed.

It installs into your home directory, so you do not need root or sudo.

The download is large, so the installer retries on its own and resumes an interrupted download from where it stopped. If the install still fails, the machine stays in pending install (or shows failed); click Install again for a fresh command and run that. The old command's one-time token is already spent, so running it a second time is refused.

Once the status turns green, run your programs with es-python instead of python, right away, in the same terminal. (Only if you ran the installer by piping it to bash do you need a fresh terminal or source ~/.bashrc first.) Remember to switch on the taint sources you want watched (see Machine configuration); every source is off until you do. See Running es-python for how to use it.

Rename

To rename a machine, open its kebab menu (the icon on the row) and choose Rename. The new name can be up to 128 characters. Renaming is purely cosmetic; all of the machine's existing events stay attributed to it.

Reinstall (taking a newer build)

Reinstalling is how you update a machine to a newer EyalSec build. When a machine's build badge shows newer build available, reinstall it to pick up the latest build.

There is no separate Reinstall button: click Install on the machine's row, exactly as for a first install. For an installed machine this resets it to created and mints a fresh install command. Run that command on the host to lay down the new build. Your old events are safe: the machine's name, configuration and internal identity are unchanged, so everything it reported before stays attributed to it. The copy already on the host keeps working until the new install starts.

Configure

To change what a machine watches for and how it reacts, click Configure on the machine's row. This opens a modal with Taint sources and Rules sections. This is also where you turn on Report and Raise mode for a machine. See Machine configuration for the full walkthrough.

Uninstall

For an es-python machine, the kebab menu also has Uninstall. Confirm, and the page shows a one-line command to run on the host. It removes es-python from the host (the runtime, the launchers and the PATH line), and once the host runs it the machine and all its events are removed from the dashboard too.

Delete

To remove a machine, open its kebab menu, choose Delete, and confirm.

Warning: deleting a machine removes the machine and all of its events. This is irreversible; there is no undo. If you only want to stop using a machine, you can leave it in place instead; deleting is permanent. Delete does not touch the host: to remove es-python from it as well, use Uninstall.

Report mode vs Report and Raise mode

Side by side: in Report mode an event is recorded and the app keeps running; in Report and Raise mode a RuntimeError is thrown and the risky action is blocked

When EyalSec spots something dangerous (untrusted data, that is, data that came from the outside world, also called taint, about to be used in a risky action) it can do one of two things, and you choose which. We call these Report and Report and Raise.

The difference is simple: Report watches; Report and Raise watches and blocks.

Report mode

In Report mode, EyalSec lets your program keep running, but records what it saw on your dashboard. Nothing about your program's behavior changes; it runs exactly as it would under normal Python, and you get a full account of the risky moment under Events: what data was involved, where it came from, and the line of code that triggered it.

Report mode is the default once a source is switched on (every taint source starts off; see Machine configuration). It is the right starting point because it can never break a working program. You get full visibility with zero risk, so you can see what EyalSec finds before deciding whether to block anything.

Use Report mode when you want to:

Report and Raise mode

In Report and Raise mode, EyalSec stops the risky action before it runs. Instead of letting the dangerous operation happen, it aborts it by raising a Python error, a RuntimeError, at the exact spot. The dangerous thing never happens, so an attack is blocked. The event is still recorded on your dashboard, exactly as in Report mode, so you see what was blocked. Report and Raise does both: it reports and raises.

Here's what that looks like when a program tries to run a system command built from untrusted data:

import os
user_input = get_data_from_the_internet()   # untrusted (tainted) data
os.system(user_input)                        # risky action: a "sink"

Under Report mode this runs and is recorded. Under Report and Raise mode it does not run. The RuntimeError your program gets is how you know EyalSec blocked a dangerous operation, and its message names where the data came from and which sink it reached:

RuntimeError: EyalSec: untrusted data from socket:203.0.113.7:443 reached sink os system

Because Report and Raise mode throws a real Python error, your program sees it like any other exception. If your code already catches errors around that operation, the attack is blocked and your app can recover gracefully. If it doesn't, the program stops there, which is usually exactly what you want when something genuinely malicious is in flight.

Use Report and Raise mode when you want EyalSec to actively defend an app, not just observe it: typically once you've watched it in Report mode and trust what it's detecting.

How you turn each one on

See Machine configuration for how to add a mode-3 rule and choose which events it covers.

Account note: Report and Raise mode (mode-3 rules) needs to be enabled on your account. If it isn't, adding a mode-3 rule is rejected with "Raise rules are not enabled for your account. Ask an administrator to enable them." Reporting works on every account. See Plans & roles, and email sales@eyalsec.com to have it turned on.

Machine configuration

The Configure modal with two sections: Taint sources (socket, file, stdin, foreign, env) and Rules (source, event pattern, mode, enabled)

The Configure modal is where you tune what a machine watches for and how it reacts. Open it with the Configure button on a machine's row on the Machines page. The modal has two sections:

Changes in the Configure modal auto-save as you make them; there is no Save button.

Global vs per-machine scope

Configuration lives at two levels:

A per-machine setting overrides the global default for that machine. So you can set a sensible baseline globally and then tighten or loosen it on individual machines.

The Taint sources section

Taint is untrusted data, meaning data that came from the outside world. A source is where that data enters your program. Each source has its own dropdown, and the modal lists only the sources that machine's product can use. Every source is off until you switch it on, so a new machine reports nothing until you do. For an es-python machine the sources are:

The es-python modal also shows two toggles that are not sources, both experimental and off unless you turn them on: identity, which checks your database queries against the schema and their bound parameters and reports defects that need no attacker data at all, and handoff, a mode that lets an EyalSec browser follow a flow from a server response into the page.

A source that a given engine does not understand is simply ignored by that engine, never quietly mapped onto a different one. es-chromium fetches no machine configuration at all, so it has no sources on this page.

The five core sources (socket, file, stdin, foreign, env) show a Read more link into The taint sources, the full reference for what each source watches and the event origins it produces. On a socket-only machine only socket is live (see Machines).

Two spelling traps. The name of a source here is not always the name you scope a rule to (below), and a rule using the wrong form is rejected:

Source on this page Name to use in a rule
weak_random (underscore) weak-random (hyphen)
db_rows db

Every other source uses the same word in both places.

Each source can be set to one of three states:

Because unset = inherit, you only flip a source to on or off when you want this machine to differ from your global setting. The per-machine choice always wins over the global one. A Set all sources dropdown at the top applies On / Off / Unset to every source in one step.

When it takes effect: taint changes apply on the machine's next process restart. A program that is already running keeps the taint settings it started with; start it again (or start your next program) to pick up the new ones.

The Rules section

A rule decides what happens with a particular kind of event. Each rule has:

There are four modes. The first two only change what you see; the last two change what the machine does. Every mode acts on the events its pattern matches:

A product that cannot act on a mode does not offer it: es-chromium, for example, offers only Show and Hide.

A few important notes:

Your account starts with two default global drop rules; see Filters.

Plan notes: Mode 3 (Raise) needs the Report-and-Raise capability enabled on your account; without it, creating or updating a mode-3 rule is rejected. Modes 1, 2, and 4 work on every account. See Plans & roles, and email sales@eyalsec.com to enable it.

Bundled libraries and which copy your app imports

es-python bundles its own copies of a number of third-party libraries. Most of them are instrumented copies that carry a detector (the SQL drivers, the parsers, uvloop), so es-python puts its copy ahead of your application's own packages. That precedence is not a detail: with the bundled uvloop the HTTP surface of an asyncio app is tracked, and with your own copy of uvloop it is not, on the same machine with the same sources armed.

To see which libraries win on a machine, run:

es-python -m _eyalsec_bridge

It prints every bundled library, whether it carries a detector, the version we ship, and whether our copy or yours is the one that imports.

Two environment variables change that, and they are not equivalent:

Variable Effect
ES2_PREFER_APP_LIBS=name[:name…] Your copy of the named libraries wins; every other bundled library is untouched. Give a pip name (pyyaml) or an import name (yaml). This is the one to use.
ES2_NO_BUNDLED_LIBS=1 Your copies of all bundled libraries win. Full isolation, and it turns off every detector that lives in one of them.

Reach for the per-library form. A single version conflict is a reason to prefer your copy of that library, not a reason to disarm the rest.

Either way, if the library you exclude is one that carries a detector, the machine reports it to EyalSec as a coverage gap, so a detector you switched off never looks like a detector that found nothing. Libraries with no detector (cryptography, for example, which we deliberately let your copy win by default so a security pin of yours is never downgraded) are excluded silently, because nothing is lost.

If your application's copy of an instrumented library wins for any other reason (a virtualenv, PYTHONPATH, pip install --target), the machine notices that too, at process exit, and reports it the same way.

When configuration reaches the machine

The machine's runtime checks for its latest configuration shortly after startup and then roughly every 30 seconds, so most changes reach a running machine within about half a minute:

So if you add a raise rule, a long-running program starts honoring it within about half a minute. If you change which sources are tracked, restart the program to apply it.

Events

The Events page is where you read what EyalSec has detected across all your machines. Every time your code uses untrusted data (data that came from the outside world, also called taint) in a risky way, an event lands here.

Events table with a filter toolbar and one expanded detail row showing where, repr, origin, and trace

One list per product you have

Events are kept in a separate list for each EyalSec product enabled on your account, so a browser finding never gets mixed in with a server-side one. For example:

List What produces it
es-python the EyalSec Python runtime on your machines
es-chromium the EyalSec browser, reporting DOM-XSS flows

Every other product enabled on your account gets its own list the same way.

You switch between them in the sidebar, not with tabs inside the page. Each link carries its own count, and that count is what the list would actually show you: it already has your monthly quota and the suppressed-events setting applied, so it never promises rows the list cannot display.

You only see the lists for the products enabled on your account. A missing list is not a missing feature and not an error: it means that product is not enabled for you. Most accounts have es-python; es-chromium and the other products are enabled per account on request. If a product you expect is not there, ask an administrator to enable it, or email support@eyalsec.com.

Turning a product off is reversible and never deletes anything. While it is off, its list disappears and its agents stop reporting; when it is turned back on, the list returns with the events already stored under it. If no product at all is enabled, the Events page says so rather than showing you an empty list.

Filters are per list. Switching lists gives you that list's own filter state rather than carrying your es-python filters over to es-chromium.

Your monthly event quota applies to each list separately, at its full value. A 5000-event quota means up to 5000 es-python events and up to 5000 es-chromium, not 5000 shared between them, so enabling another product adds a whole fresh allowance rather than dividing the one you have. See Plans & roles.

The events table

Events are listed newest first. The table loads 100 rows at a time and fetches more automatically as you scroll, so you can keep going back in time without clicking through pages.

EyalSec doesn't store one row per occurrence; that would flood the page. Instead it groups and counts identical events. Two events are treated as the same when they share the same combination of:

When a matching event happens again, EyalSec just bumps a counter instead of adding a new row. The count column shows how many times that exact event has fired. A high count is a strong signal: something is hitting that code path over and over.

Each row also carries a severity: one of critical, high, medium, low, or info, computed on the server from what the event shows (for example, how attacker-reachable the data source looks). It's shown as a colored word in its own column so the rows that deserve attention stand out at a glance.

Filtering the list

A toolbar above the table lets you narrow what you see. None of these filters change what's stored; they only change what's displayed.

Under the toolbar is an Advanced search panel. It holds the controls you reach for when the question stops being "what is happening" and becomes "what happened in this window, under this path": the time range, the scope, the condition builder, and your saved searches. The number beside the panel's title counts how many of them are currently narrowing the list, so a collapsed panel never hides a filter from you.

Your filter choices are saved in the page's URL. That means you can bookmark a filtered view, share the link, or reload the page and land right back where you were.

Filter by tag

Every event carries one or more impact tags: the vulnerability class of the flow, shown as coloured pills beside the sink. sqli, xss, command injection, path traversal and so on. They answer what an attacker gets out of a flow, which the sink name alone does not.

The Tag dropdown narrows the list to one class. It applies to every view.

Right-clicking an event offers the same thing for the classes on that row: Filter: sqli arms the dropdown, and Block this tag: sqli saves a rule that hides the class from the current view until you remove it on the Filters page.

A class is not stored on the event, it is worked out from the sink and the source when the page is built. Filtering on a rare class therefore searches backwards through your history rather than jumping straight to matches, so the list can show "searching" for a moment before the first row appears.

The dropdown lists every class, including any you have blocked. Choosing a class you have blocked gives an empty list: the two rules apply together, and one hides what the other asks for. Unblock it on the Filters page to see those events again.

Opening a row for detail

Click any row to expand it in place and see the full story behind the event. Only one row is open at a time; opening another closes the first. The detail pane can include:

Every event shows its label (where), a text representation, the stack trace, and the file origin. To turn noisy detections into standing rules, use the Filters page.

Suppressed events

EyalSec checks whether the untrusted data was actually made safe before it reached the risky operation. When it can prove that (for example the value was correctly escaped for the grammar the operation parses, or the payload contains nothing that could break out of it), the event is marked suppressed and kept out of your default view. Nothing is deleted; it is only hidden.

A Show control above the table picks what you see:

The badge next to the control tells you how many suppressed events exist, so a quiet list never leaves you wondering whether it is quiet because nothing happened or because everything was filtered away. Suppression applies to every list, not just es-python.

Suppression is deliberately conservative: when EyalSec is not sure, it leaves the event visible. Some findings only get their severity capped rather than being hidden, because the evidence limits the impact without eliminating it.

Labels, notes and saved searches

Once you start triaging, three things help you keep your place:

Each row also carries an actions menu, opened with the button on the row or by right-clicking it. Alongside labelling and commenting, it offers one-click rule shortcuts: Block this sink and, where the row names a source, Block this source. These turn the event you are looking at into a standing rule without retyping its details; you review and undo them on the Filters page.

Where this fits

Events are the raw detections. To turn them into standing rules, like always hiding a known-safe pattern, dropping noise at the machine, or blocking a dangerous action at runtime, use Filters for global rules and Machine configuration for per-machine rules and Raise behavior. For a high-level summary instead of the full list, see the Dashboard overview.

Filters

Filters are standing rules that shape what shows up across your whole account. Where Events lets you narrow a single view on the fly, the Filters page holds the rules that apply everywhere, every time.

Filters page showing the blocked-events list and the account-wide sources and rules for one product

There is one Filters page per product on your account. If you have more than one product, tabs at the top switch between them (es-python, es-chromium, and so on). Each page opens with a one-line description of the product and, where it applies, a note on what that product's configuration cannot do (es-chromium, for example, does not fetch machine configuration, so on its page everything only filters what the dashboard shows).

Blocked events

The first section lists what you have blocked for this product. You block an event from the Events page: right-click a row (or use its menu) and choose Block this sink, Block this source (offered when the row names a source), or Block this tag. Each block hides the event and everything like it from the dashboard. Each row in the list shows:

Blocks affect display only: they change which events you see on the dashboard, never which events get recorded. Nothing is ever lost; a blocked event is still there once you pause or remove the block.

Global taint and rules live here too

Below the blocked list, Sources & rules hosts your global taint settings and global rules: the same controls you'd set on a single machine, but applied to all your machines at once.

These settings belong to your whole account, not to one product: changing a source or rule on one product's page also changes it for your other products.

These are exactly the controls described in Machine configuration; the only difference is scope. A setting on an individual machine overrides the global one; leave a machine's control as unset to inherit whatever you've chosen globally here. For the full explanation of taint sources, rule modes, and when Report and Raise applies, read Machine configuration.

The default rules

The first time you open this page, your global rule list starts with two default rules instead of an empty table: two enabled "Don't send" drop rules on source any, one with the event pattern re and one with write. Together they tell every machine to drop, at the machine, the events whose name matches re or write, and to send everything else: a conservative starting point that keeps early noise down.

They are ordinary rules, and they are yours to change: edit their patterns, switch them off, or delete them to have machines send everything. Deleting them sticks; the defaults are seeded only once per account, so they never come back on their own. If you ever enable a drop rule with an empty pattern, the rules editor shows a warning banner, because that rule drops every event.

Dashboard overview

The dashboard is your landing page, the first thing you see after signing in. It gives you a read-only, at-a-glance summary of everything EyalSec has detected, so you can spot trends without digging through the full Events list.

Dashboard with stat cards for total hits, last 24h, and unique types, plus a 14-day timeline

What the cards show

The overview is built from a set of summary cards, a timeline and a few panels:

Until you have added a machine, the dashboard also shows an Add your first machine prompt with a link to the Machines page.

Read-only by design

The dashboard doesn't let you change anything; it's a window, not a control panel. When you spot something worth investigating, follow it up elsewhere:

If your dashboard is empty, your machines may not have detected anything yet. Check that EyalSec is installed and running on the Machines page, and that at least one taint source is switched on (every source is off until you turn it on; see Machine configuration).

Activity log

The activity log is your own audit trail. Every meaningful action on your account is recorded here: who did what, when, from where, and whether it worked. It's a read-only record that lets you review your history and spot anything you don't recognize.

Activity log table showing time, action, target, IP, auth, and outcome columns

You'll find it on the Activity page (/scanner/activity) in the dashboard.

What each row records

Every entry is one action you (or something using your credentials) took. The table has these columns:

Filtering the log

The log is shown reverse-chronologically, newest first, and you can narrow it down with two filters:

Loading more history

The log shows 50 entries per page. When there are older entries, a Load more button appears at the bottom; click it to pull in the next batch. It keeps your current filters as you go, so you can page back through just the actions you care about.

How long entries are kept

The activity log keeps the last 30 days of history. Older entries are removed automatically, so this is a recent-activity view, not a permanent archive. If you need a longer record for compliance or an investigation, export or note down what you need before it ages out.

Where this fits

The activity log tracks what you did to your account and machines. To see what EyalSec detected in your code, head to the Events page or the Dashboard overview. If something in the log looks wrong (a login or an API request you didn't make), change your password right away on the Settings page, and consider turning on two-factor authentication and revoking your API key there. For anything you can't explain, contact support@eyalsec.com.

Running es-python

es-python is EyalSec secure Python: the EyalSec runtime that watches your program as it runs and reports (or blocks) risky behavior. After you install EyalSec on a machine, this is the program you run your code with instead of your regular python.

Running your code

Once the installer has finished, just swap python for es-python:

es-python script.py

It behaves like the Python you already know (same language, same standard library), but it's watching for the moment your code uses untrusted data (data that came from the outside world, also called taint) in a risky way. When that happens, EyalSec records an event, which you'll see on the Events page.

If es-python isn't found, make sure you've opened a new shell since installing, or that ~/.local/bin is on your PATH. See Machines for the install steps.

Report vs Raise at runtime

EyalSec has two ways to respond when it catches something:

Most of the time you'll run in Report mode. You normally turn on Raise from the dashboard with a mode-3 rule, which is the recommended way and doesn't require touching the command line; see Machine configuration and Report mode vs Report and Raise mode. At the command line, the legacy --raise (and --raise-on-found) flags switch the whole run into Report and Raise mode, but only while the machine has no Raise rules of its own; once it does, the rules decide. Adding Raise rules must be enabled on your account; see Plans & roles.

The taint sources

A source is a place untrusted data can enter your program. By default EyalSec is conservative about what it treats as untrusted, so each source is off by default; you switch the ones you want on. There are three ways to enable a source, in increasing order of how you'll use them day to day:

  1. A CLI flag: --make-<source>-vuln, handy for a quick local run.
  2. An environment variable: the mirror ES2_MAKE_<SOURCE>_VULN=1, the same switch for when you can't change the command line.
  3. The dashboard: the per-source toggle on the Filters page (global) or the Machine configuration Configure modal (per machine). This is how you'll normally do it.
Source What it watches CLI flag Env mirror Default
socket data received over the network (incl. TLS) --make-socket-vuln ES2_MAKE_SOCKET_VULN=1 off
file data read from files on disk --make-file-vuln ES2_MAKE_FILE_VULN=1 off
stdin data on standard input --make-stdin-vuln ES2_MAKE_STDIN_VULN=1 off
foreign code loaded from another user's files --make-foreign-vuln ES2_MAKE_FOREIGN_VULN=1 off
env environment variables and sys.argv --make-env-vuln ES2_MAKE_ENV_VULN=1 off

One shortcut applies to the whole set:

Every source is off by default, so plain es-python app.py with no flags, env vars, or dashboard rules enabled runs with no taint tracking at all, just like your regular Python.

For example, to treat everything coming off a socket as untrusted for one run, and combine that with Raise to block it:

ES2_MAKE_SOCKET_VULN=1 es-python app.py
es-python --make-everything-vuln --raise app.py

When a source catches untrusted data reaching a risky operation, the recorded event carries an origin label naming where the data came from (for example socket:fd or stdin:input). The sections below list each source's origins so you can recognise them on the Events page.

Socket-only machines: some older machines run a socket-only es-python that can watch only the socket source. On such a machine the file, stdin, foreign, and env toggles do nothing. Socket-only is no longer selectable when adding a machine. See Machines.

The socket source

What it is. Data arriving from the network: anything your program receives from another computer over TCP, UDP, or a local socket, including the plaintext read out of a TLS connection.

What's instrumented. The socket object is marked when it is created or connected, and the data returned by socket.recv(), recvfrom(), recv_into(), and recvmsg() (and the ssl.SSLSocket read methods) comes out tainted.

Enable it. --make-socket-vuln / ES2_MAKE_SOCKET_VULN=1, or the socket toggle on the dashboard. Off by default.

Event origins. socket:fd, socket:connected, socket:unbound; TLS reads carry ssl:fd; a memory-mapped socket fd carries mmap-socket:fd.

import socket
s = socket.create_connection(("example.com", 80))
s.sendall(b"GET / HTTP/1.0\r\n\r\n")
data = s.recv(4096)        # tainted, origin socket:connected

Limitations. When socket is the only source enabled, and on an older socket-only machine, data read with a raw os.read(fd, n) on a socket fd is not tainted: socket taint then follows the socket API (recv*) only. Enabling any other source as well restores it, except on a socket-only machine.

The file source

What it is. Data read from a file on disk: the contents of any file your program opens and reads.

What's instrumented. The bytes returned by open(path).read(), os.read(), os.pread(), and mmap of a file descriptor. Each read checks what kind of file descriptor it came from, and reads from regular files are tainted.

Enable it. --make-file-vuln / ES2_MAKE_FILE_VULN=1, or the file toggle. Off by default.

Event origins. fd:posix_read, fd:posix_pread, fileio.read(candidate), fileio.readall(candidate); a memory-mapped file fd carries mmap-file:fd.

ES2_MAKE_FILE_VULN=1 es-python -c 'print(open("/etc/hostname").read())'
# the file contents are tainted, origin fileio.readall(candidate)

Limitations. Checking each read adds some cost on file-heavy workloads. Not available on an older socket-only machine.

The stdin source

What it is. Data fed to your program on standard input: text piped in or typed at the keyboard.

What's instrumented. The string returned by the input() builtin and reads from sys.stdin (file descriptor 0).

Enable it. --make-stdin-vuln / ES2_MAKE_STDIN_VULN=1, or the stdin toggle. Off by default.

Event origins. stdin:input (the input() builtin) and stdin (stream reads via sys.stdin).

name = input("name? ")           # tainted, origin stdin:input
blob = sys.stdin.buffer.read()   # tainted, origin stdin

Limitations. Not available on an older socket-only machine.

The foreign source

What it is. Foreign code: Python loaded from a file that another user on the machine can write to. Loading code you don't fully control is a classic way for an attacker to slip in their own logic. This is the one source that is a detection rather than a taint flow: it fires on the act of loading the file, independent of any data flow, even during interpreter startup.

What's instrumented. Importing a .py/.pyc, running the main script, or compile/exec/eval of file source, whenever the resolved path is writable by another user.

Enable it. --make-foreign-vuln / ES2_MAKE_FOREIGN_VULN=1, or the foreign toggle. Off by default. Note --make-everything-vuln does not include it; turn it on explicitly.

Event. Loading such a file emits foreign-code:<path>, naming the exact file; you get one event per resolved path per process, so a noisy import loop won't flood you. Under Raise, EyalSec aborts the load with a RuntimeError.

Exemptions. If some paths are trusted on purpose (say, a shared tools directory you maintain), exempt them by listing their absolute path prefixes in the ES2_FOREIGN_CODE_ALLOW environment variable, separated by colons. There are no automatic exemptions: even the standard library is checked unless you add it here.

ES2_MAKE_FOREIGN_VULN=1 ES2_FOREIGN_CODE_ALLOW=/opt/team-tools:/srv/shared es-python app.py
# importing /tmp/evil.py (writable by another user) -> event foreign-code:/tmp/evil.py

Limitations. Not available on an older socket-only machine.

The env source

What it is. Data describing how the program was launched: its environment variables and command-line arguments. Both are influenced by whoever started the process.

What's instrumented. The values read from os.environ (and os.getenv) and the elements of sys.argv. The environment value is tainted, not the key (a fixed variable name is not attacker data).

Enable it. --make-env-vuln / ES2_MAKE_ENV_VULN=1, or the env toggle. Off by default.

Event origins. env (environment values) and argv (argument-vector elements).

import os, sys
token  = os.environ["API_TOKEN"]   # tainted, origin env
target = sys.argv[1]               # tainted, origin argv

Limitations. Only the value is tainted, not the variable name. Not available on an older socket-only machine.

Installing packages

es-python shares your machine's regular Python packages. There is no separate package manager to learn and no second copy of every dependency.

Anything already installed for your regular python3 is importable in es-python, with nothing to do:

pip install requests          # your normal python3 pip
es-python -c "import requests"

es-python reads your regular pythonX.Y site-packages directly. Both interpreters have to be the same X.Y for this to apply.

Installing from es-python works too. es-python -m pip defaults to a --user install, so packages land in the version-shared ~/.local that your regular python reads as well:

es-python -m pip install requests

So it does not matter which of the two you install with. Both see the result, and your regular pip and python are untouched.

The bundled database drivers keep priority. The runtime ships its own copies of the common database drivers (psycopg2, psycopg 3, mysqlclient, mariadb, oracledb), each watched for SQL injection at the query string. On Python 3.9, mysqlclient and psycopg 3 are not included. The bundled drivers sit ahead of your user packages on the import path, so a later pip install psycopg2 cannot silently replace the watched one. You keep the sink without pinning anything, and they behave identically to the regular drivers otherwise.

One consequence worth knowing: pip show psycopg2 can report the plain copy in ~/.local while import psycopg2 loads the bundled one. The import is what carries the sink.

Turning the sharing off. Set ES2_NO_SYSTEM_SITE=1 to run es-python fully isolated from the machine's packages:

ES2_NO_SYSTEM_SITE=1 es-python app.py

Use this if a foreign C extension misbehaves under es-python. Inside a virtualenv the sharing is already off, because a venv is self-contained by design.

How the machine picks up your settings

You don't have to restart everything every time you change a setting in the dashboard. When es-python starts, it fetches that machine's configuration from EyalSec, then refreshes it in the background roughly every 30 seconds, so new settings reach the running program on their own.

Two kinds of change behave a little differently:

A note on fuzz mode

es-python also carries some --fuzz-* flags. These are experimental and are not needed for normal use; you can safely ignore them.

Where this fits

The runtime is the part of EyalSec that lives on your host and produces the events you read in the dashboard. To control its behavior from the web instead of the command line, see Machine configuration; to understand the two response modes, see Report mode vs Report and Raise mode; and to read what it detects, see Events. If something isn't behaving as expected, reach out to support@eyalsec.com.

On-machine docs and AI assistants

Every install drops a small docs home at ~/.eyalsec/:

es-python -h also prints these paths at the end of its help.

Uninstalling removes ~/.eyalsec/. Any AGENTS.md symlink or @-include you added to a project with eyalsec agents-init stays in that project and becomes a dangling reference; remove it by hand if you no longer want it.

Plans & roles

Your EyalSec account has a set of limits and capabilities that we set for you. Most things (installing machines, viewing Events, running es-python, Report mode) work on every account. A few limits and capabilities vary per account; the table below shows the defaults.

What your account includes

Your account
Products es-python (others, such as es-chromium, only if enabled)
Machines Limited (5 by default), counted per product
Events visible Monthly quota, per view (5,000/month each by default)
Report and Raise Only if enabled

A few things to note:

What you'll experience as a regular user

Your account mostly stays out of your way. You'll only notice its limits at a few specific moments, each of which gives you a clear message.

The machine cap (409)

Every regular user has a machine limit (5 by default). Try to add one past your cap and EyalSec stops you with an error:

Machine limit reached (5). Contact sales@eyalsec.com to upgrade your plan.

The number in parentheses is your current limit. The limit applies to each product separately, so a limit of 5 means up to 5 es-python machines and up to 5 es-chromium machines, not 5 in total: running out of es-python slots never stops you adding a browser. To get more machines, ask us to raise your machine limit (see below). Deleting a machine you no longer need also frees a slot; see Machines.

An account with no plan yet has a limit of zero, and gets a different message naming that instead of a number:

Your account has no active plan yet, so it cannot add machines.
Book a live demo or email sales@eyalsec.com to get set up.

The event quota

Every regular user also has a monthly event quota (5,000/month by default) capping how many event rows are visible in Events. It resets every calendar month: the first N events that arrive each month are shown, and anything past that is hidden. Nothing is deleted; each new month starts a fresh quota, and an event that fires again in a later month counts against (and can appear in) that month's quota.

The quota applies to each view separately, at its full value. Events has one list per product enabled on your account. Each list gets its own 5,000/month window, so a busy es-python fleet can never eat the allowance for your browser events, and enabling another product adds a whole fresh window rather than dividing the one you have. In total a default account can see up to 5,000 events a month in each list.

When you scroll to the end of your events list, the footer reminds you of the quota for the list you are looking at. To see more events per month, ask us to raise it (see below); a raise lifts the cap on every list at once.

The Raise gate

Report and Raise mode (a "mode-3" rule that tells the runtime to block an action) needs to be enabled on your account. If you try to add a Raise rule before it's enabled, the request is rejected with:

Raise rules are not enabled for your account. Ask an administrator to enable them.

Email sales@eyalsec.com to turn it on. Reporting works for every account either way; only blocking (Raise) needs the capability enabled. See Machine configuration for how Raise rules work.

How to get more

There's no self-service upgrade button. To raise your machine limit or event quota, to enable a product such as es-chromium, or to enable Report and Raise, email sales@eyalsec.com and tell us what you need; we set it for you.

Once your grants change, the new limits apply immediately in the dashboard: a higher machine cap unlocks, a newly enabled product's list appears, and Raise becomes available the moment it's granted. You don't need to reinstall anything on your machines. The one place a change is not instant is the agents themselves: if a product is turned off, its agents can keep reporting for up to a minute before the server starts refusing them.

If you're not sure what you need, or you have a question about your account, reach out to support@eyalsec.com. See also Getting help.

API reference

Everything you can do in the dashboard, you can also do over HTTP. The EyalSec JSON API lets you read your data (events, stats, machines, activity) and automate every action the web UI performs: adding machines, generating install commands, creating filters and rules, configuring taint, and more. It's the same backend the dashboard itself talks to.

A curl request carrying an X-API-Key: es2_… header returning a JSON response

Base URL. All endpoints live under your EyalSec origin; for the hosted service that's https://eyalsec.com. If you run on your own domain, use that origin instead. Every path below is relative to it (e.g. the full URL for GET /api/get_all_machines/ is https://eyalsec.com/api/get_all_machines/).

Authentication

Every /api/* endpoint except the public GET /api/supported_os_types/ and GET /api/supported_python_versions/ needs you to prove who you are. There are two ways:

A few rules that apply to API-key requests specifically:

A worked example

Fetch your event statistics with nothing but the API key:

curl -s https://eyalsec.com/api/get_event_stats/ \
  -H "X-API-Key: es2_3f9a…"

A typical response:

{
  "total_hits": 1842,
  "unique_types": 27,
  "last_1h": 12,
  "last_24h": 305,
  "last_7d": 1402,
  "top_events": [],
  "recent": [],
  "timeline_14d": [],
  "first_detection": "2026-05-20T09:14:02Z",
  "latest_detection": "2026-06-05T11:48:31Z"
}

Add the same -H "X-API-Key: …" header to any request below.

Events & stats

Read your detected events, the aggregate statistics, and the list of known event names.

Get events (with counts)

POST /api/get_all_events_with_count/

The request body is JSON; every field is optional:

The response is a list of events plus a paging cursor:

{
  "events": [
    [
      "2026-06-05T11:48:31Z", "socket.recv -> eval", "<str 'id'>",
      14, "web-prod-01", "critical", "python", 90210,
      "", "", false, "This value was not sanitized.",
      [], [], false, ""
    ]
  ],
  "next_cursor": "eyJ0IjoiMjAyNi0wNi0wNVQxMTo0ODozMVoiLCJpZCI6OTl9",
  "scanned": 100,
  "exhausted": false,
  "oldest_examined": "2026-06-05T11:48:31Z",
  "event_limit": 5000,
  "event_limit_monthly": true
}

This response used to carry a counts object with your account-wide live and suppressed totals. It does not any more: those two numbers depend on none of this call's parameters, and counting a whole account inside a page fetch was most of the request on a large one. They now have their own cached call, /api/event_visibility_counts/.

Each event is a 16-element array. The positions are fixed, and new fields are only ever appended, so an older client that reads indices 0-11 keeps working:

Index Field Meaning
0 created_at When the event was first seen
1 where Display label for what happened
2 str_repr Text representation of the data
3 count How many identical occurrences
4 machine_name Which machine reported it
5 severity Computed severity (critical/high/medium/low/info)
6 agent_kind Which agent reported it (one of the view values above)
7 id Event id, for GET /api/event/{id}
8 source_label Short name for where the data came in (empty for es-python and for agents that do not report one)
9 context The page the flow happened on (es-chromium), or the file written to (an es-python write event); empty otherwise
10 suppressed Boolean: the sanitizer decided this value cannot reach its sink dangerously
11 sanitize_reason One sentence explaining that decision
12 impact List of {label, sev} impact classes for the flow (for example xss)
13 labels Your triage labels on this event, as {id, name, color}
14 has_comment Boolean: you have written a note on this event
15 extension Which browser extension produced this event, by name (es-chromium only; empty for a page script and for every other agent)

The large per-event fields (trace, origin, repr_created and detail) are not in this array. A stack trace alone averages about 6 KB, so shipping them on every row made one 100-event page roughly 1 MB even though the dashboard renders them only for the row you expand. Fetch them one event at a time with GET /api/event/{id}, described below.

Paging. The API returns up to 100 events per page, newest first. To fetch the next page, send the next_cursor value from the previous response as cursor in your next request. When next_cursor is empty there are no more pages.

A short page is not the last page. Every request examines a bounded slice of your history so that no single call can run for seconds, and a selective filter can match few rows or none inside that slice. So a response may carry fewer than 100 events, or zero, and still hand you a next_cursor. The only signal that a result set is complete is an empty next_cursor: keep following the cursor until you get one, even after an empty page. Three fields report the progress: scanned, how many rows this request examined, exhausted, true when the scan reached the end of the range, and oldest_examined, the timestamp of the oldest row it looked at. The dashboard uses them to show "Searching, reached 3 Mar" while it works back through a long history.

This applies to every filter, including whitelist_repr, blacklist_repr, machine and sources. A client that stops at the first empty page will report "no matches" for a search that had matches further back.

Your event quota. The API sees exactly what the dashboard sees, so your event quota applies (event_limit in the response; -1 means unlimited, and event_limit_monthly tells you whether it resets each calendar month). Limits are set per user, not by a plan tier.

The quota applies per view, at its full value. Each agent kind gets its own window, so an event_limit of 5000 means up to 5000 visible python events AND up to 5000 browser AND up to 5000 of every other view your account has, not 5000 across the account. Each window keeps that kind's earliest-arriving events for the period and hides the rest until it resets. This is why a busy es-python fleet can no longer hide your es-chromium events: one kind's volume cannot consume another kind's allowance, and enabling another product adds a whole fresh window rather than dividing the one you have.

This endpoint is a read, so it still works on a read-only account. Errors: 400 (bad JSON, bad cursor, invalid regex, unparseable timestamp, or an unknown view, bucket or tag), 403 (a view your account does not have), 500 (server error).

curl -s https://eyalsec.com/api/get_all_events_with_count/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"from":"2026-06-01","severity":["critical","high"],"blacklist_repr":"^healthcheck$"}'

Get one event's detail

GET /api/event/{id}

Returns the large fields the events list deliberately leaves out, for the single event whose id you pass (index 7 of an events-list row):

{
  "trace": "Traceback (most recent call last)…",
  "source": "",
  "origin": "{...origin...}",
  "repr_created": "<str 'id'>",
  "detail": "/var/www/app/handler.py",
  "comment": "",
  "extension": {"id": "", "name": "", "version": "", "label": ""}
}

source is a reserved field and is always blank on your account. comment is your own note on the event (see Write your note on an event), empty when you have not written one. Everything else is returned in full.

extension names the browser extension an es-chromium event came from. All four fields are empty when no extension was involved, which is the case for every es-python event and for anything an ordinary page script did. id is the 32-character Chrome extension id, which is the only part the browser itself reports; name and version come from that extension's own manifest, which es-chromium sends once per browser session. label is what you should display: the name and version when they are known, and the id when they are not.

The event must belong to you. Ownership is part of the lookup, so an id that is not yours is indistinguishable from one that does not exist: both return 404. Errors: 400 (id is not a number), 404 (no such event on your account), 500 (server error).

curl -s https://eyalsec.com/api/event/90210 -H "X-API-Key: es2_3f9a…"

Get event statistics

GET /api/get_event_stats/

Returns the dashboard summary:

{
  "total_hits": 1842, "unique_types": 27,
  "last_1h": 12, "last_24h": 305, "last_7d": 1402,
  "top_events": [], "recent": [], "timeline_14d": [],
  "first_detection": "2026-05-20T09:14:02Z",
  "latest_detection": "2026-06-05T11:48:31Z"
}

first_detection and latest_detection may be absent if you have no events yet.

Get your event count

GET /api/event_count/?view=python

How many stored events count toward your current quota period, plus your limit:

{ "count": 1234, "view": "python", "event_limit": 5000, "event_limit_monthly": true }

The period is the current calendar month when event_limit_monthly is true, and all time otherwise. event_limit is -1 when you have no limit.

view is optional and takes the same values as the events list's view. Because the quota applies per view, so does this tally: with a view the count is that kind's alone and lines up with the event_limit the same list is capped by. Omit it for the total across every view you have; if that is exactly one view, the unscoped tally is that view's, and if it is more than one it is a number no single list is capped by. A view your account does not have is a 403, and an unrecognised view is a 400, not a silent whole-account count. The response echoes back the view it counted, so a caller firing one request per list can tell the answers apart.

Counting every row you own is expensive on a large account, so the value is cached per user and view and refreshed in the background: repeat calls answer instantly and may be a few minutes behind.

Get your event count per view

GET /api/event_view_counts/

How many of your stored events came from each agent kind. The events page uses it for the count beside each list in the sidebar:

{ "python": 1204, "browser": 38 }

The keys are the views your account has, and every one of them is always present, at zero when you have none, so a missing key never has to be read as either zero or an error. Which views those are is set per account: every product is one an administrator enables for you individually. An account with no products enabled gets an empty object.

Each number is what the events list for that view would actually show you, not a raw row count. Two things narrow it, both the same ones that narrow the list, so that the tab and the rows under it can never disagree: your monthly event quota (admins have none) and the bucket parameter, which is the events page's "Show" control. bucket=live is the default and counts only unsuppressed events; bucket=suppressed counts only suppressed ones; bucket=all counts both. /api/event_count/ is still the separate quota-period tally.

Optional query parameters: bucket, plus scope, scope_mode and scope_empty, which restrict the tally the same way the events page's scope bar restricts the list.

Counting every row you own is expensive on a large account, so the value is cached per account, scope, bucket and quota, and refreshed in the background: repeat calls answer instantly and may be up to a minute behind.

Get your live / suppressed split

GET /api/event_visibility_counts/

How many of your stored events the sanitization policy currently shows, and how many it hides. The events page uses it for the count beside its "Show" control:

{ "live": 1200, "suppressed": 34 }

Suppression never deletes anything, so these two numbers always add up to every event you have stored. Nothing narrows them: no quota, no view, no scope, no filter, no bucket. That is why this is its own call rather than a field of /api/get_all_events_with_count/, whose answer depends on all of those.

Counting every row you own is expensive on a large account, so the value is cached per account and refreshed in the background: repeat calls answer instantly and may be up to a minute behind.

Export events

POST /api/events_export?format=csv|json

Downloads every event matching a filter, instead of one page of them. The body is exactly the body of /api/get_all_events_with_count/, and the two share the same request handling, so your monthly quota, the bucket you asked for, the tag predicate and every filter mean the same thing here as they do on the events page. The file covers the rows the page was showing you, not a different set.

format is csv (the default) or json; anything else is a 400 naming the two. A cursor in the body is ignored, since an export is the whole set rather than the page you had scrolled to.

Every row carries the heavy fields the events list leaves out, because a finding you cannot reproduce is not worth exporting: the page URL and call site, the origin, repr_created and the stack trace. taint_chain is the recorded chain of steps the value took, and it is blank unless you are an administrator, exactly as it is on GET /api/event/{id}. The column is there either way, so a non-admin file and an admin file line up column for column.

curl -s -X POST 'https://eyalsec.com/api/events_export?format=csv' \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"view":"browser","severity":["critical","high"]}' \
  -o findings.csv

An export stops at 50,000 rows. It always says so rather than ending quietly: CSV appends a final row beginning TRUNCATED, and JSON sets "truncated": true. Narrow the date range or the filter to get the rest.

Get event names

GET /api/event_names/
{ "names": ["socket.recv -> eval", "open -> exec", "…"] }

Saved searches

A saved search is a named events-page query string: the view, the scope, the toolbar filters and the advanced conditions, exactly as they appear in the page URL. Picking one puts the page back where it was. This is not the same thing as a filter: a filter is a standing rule applied to every events query, while a saved search does nothing until you choose it.

Searches are stored per view, because the searchable fields differ per agent, so every call names one view.

GET /api/event_searches/?view=python

view takes the same values as the events list's view and defaults to python. An unrecognised value is a 400, and a view your account does not have is a 403.

{ "searches": [ { "id": 3, "name": "my domain", "view": "browser",
                  "query": "view=browser&scope=shop.test" } ] }
POST /api/event_searches/

JSON body: name (1 to 80 characters, trimmed), view, and query (the query string without the leading ?, at most 4096 characters). Saving the same name again in the same view overwrites it, which is how you edit a search. Returns 201 and the view's whole list, so the new id is there without a second call.

curl -s https://eyalsec.com/api/event_searches/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"name":"my domain","view":"browser","query":"view=browser&scope=shop.test"}'
DELETE /api/event_searches/{id}

Returns 204. An id that belongs to another account deletes nothing and still returns 204, so the reply never confirms that someone else's row exists.

Machines

List your machines and perform every action on the Machines page.

List machines

GET /api/get_all_machines/
{
  "machines": [
    [
      "2026-05-20T09:00:00Z", "web-prod-01", "installed",
      "m_8f3c2a1b", "ubuntu_24_04_x86_64",
      "bc0ef13-20260604T120000Z", "bc0ef13-20260605T080000Z",
      "3.13", false, false, false, false, "python"
    ]
  ]
}

Each machine is a 13-element array:

Index Field Meaning
0 created_at When you added the machine
1 machine_name The name you gave it
2 status created, pending, or installed
3 public_id Safe machine handle used in other API calls
4 os_type Distro + arch target
5 installed_build Build version currently installed on the host
6 current_build Latest build available for this machine: for es-python, for its target and Python version
7 python_version The Python version this machine runs (e.g. 3.13)
8 socket_only Dormant. Machines created now are never socket-only, so this reads false; older machines may still carry true.
9 clone_locked true when a clone/tamper identity mismatch was flagged
10 stalled true when an install has sat in pending too long
11 stale_agent true when an old install's runtime (revoked credential) is still phoning in
12 agent_kind Which product this machine runs (one of the events list's view values)

The machine's secret is never exposed by the API. You reference machines by their public_id. When installed_build and current_build differ, an update is available (see Machines).

curl -s https://eyalsec.com/api/get_all_machines/ \
  -H "X-API-Key: es2_3f9a…"

Add a machine

POST /api/add_new_machine/

Form fields:

Returns 200:

{ "Name": "web-prod-02", "OsType": "ubuntu_24_04_x86_64",
  "Status": "created", "PublicID": "m_a1b2c3d4",
  "PythonVersion": "3.13", "SocketOnly": "false", "AgentKind": "python" }

An es-chromium machine omits the fields that are es-python's alone:

{ "Name": "test-browser", "OsType": "", "Status": "created",
  "PublicID": "m_a1b2c3d4", "AgentKind": "browser" }

Errors: 422 (bad/missing field, unknown target or Python version, a product that is not registered here), 409 (machine limit reached; an account with no plan yet has a limit of 0), 403 (CSRF, or a product that is not enabled on your account). SocketOnly always comes back "false": machines created now are never socket-only (the field is retained but dormant).

Your machine limit is counted per product, so a full es-python allowance does not consume your es-chromium one.

Set a machine's OS

POST /api/set_machine_os/

Form fields: machine_name, distro, arch. Returns 200:

{ "machine_name": "web-prod-02", "os_type": "debian_12_aarch64" }

Rename a machine

POST /api/rename_machine/

JSON body {"public_id": "m_…", "machine_name": "new-name"}200.

Delete a machine

POST /api/delete_machine/

JSON body {"public_id": "m_…"}204. This cascades: the machine and all of its events are removed (see Machines).

Get the install command

POST /api/get_eyalsec_install_script_data/

JSON body {"public_id": "m_…", "attested": true} returns a one-time install secret. attested must be true: it is your confirmation that you are authorized to monitor that machine, the same box the dashboard asks you to tick.

{ "secret": "…", "script_path": "https://eyalsec.com/install.sh", "os_type": "ubuntu_24_04_x86_64" }

The secret is single-use with a 10-minute TTL. This call serves es-python machines; an es-chromium machine gets its install command from the Machines page. Errors: 422 (attested missing or false, or no OS set yet), 404 (unknown machine), 403 (a product that is not enabled on your account).

Reinstall (update) a machine

There is no separate reinstall endpoint. Re-installing is just installing again: call the same POST /api/get_eyalsec_install_script_data/ shown above. If the machine is currently installed, pending or failed, the handler resets it to created for you (clearing installed_at, the recorded build and the clone identity, and deleting any outstanding install tokens) and then mints a fresh token. Your machine's name, public_id, configuration and events are untouched.

curl -s https://eyalsec.com/api/get_eyalsec_install_script_data/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"public_id":"m_8f3c2a1b","attested":true}'

The live credential is not rotated at this point, only when the new install actually starts, so the binary already on the host keeps reporting until it is replaced.

Get the uninstall command

POST /api/get_uninstall_script_data/

JSON body {"public_id": "m_…"} returns a one-time secret and the uninstall script path, the same command the Machines page shows:

{ "secret": "…", "script_path": "https://eyalsec.com/uninstall.sh" }

The secret is single-use with a short life.

es-python machines only. The command removes that product and nothing else, so asking for another product's machine returns 422; delete the machine instead.

Supported OS types (public)

GET /api/supported_os_types/

Needs no authentication:

{ "distros": [ { "id": "ubuntu_24_04", "label": "Ubuntu 24.04 LTS",
                 "archs": ["x86_64", "aarch64"] } ] }

Supported Python versions (public)

GET /api/supported_python_versions/

Also needs no authentication. Returns the versions you can pick when adding a machine, and the default:

{ "versions": ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"], "default": "3.13" }

Add ?os=<os_type> to get only the versions available for that target:

GET /api/supported_python_versions/?os=ubuntu_24_04_x86_64

A version can be supported in general and not yet published for a particular distribution and architecture, so a client that is populating a chooser should always pass os; otherwise it can offer a combination that POST /api/add_new_machine/ will refuse. The default in the response is always one of the versions in the same response. An unknown os is a 422.

Filters

Manage the global whitelist/blacklist filters from the Filters page.

List filters

GET /api/filters/
{ "filters": [ { "id": 7, "pattern": "^healthcheck$", "label": "noise",
                 "mode": "blacklist", "target": "where", "enabled": true,
                 "engine": "any" } ] }

Add ?engine=<view> to list only the filters that apply on one product's Filters page: those for that product plus those set to any.

Create a filter

POST /api/filters/create/

JSON body:

Returns 201 with the created filter. An empty or over-long pattern, an unknown mode, target, engine or impact class returns 400.

curl -s https://eyalsec.com/api/filters/create/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"pattern":"^healthcheck$","mode":"blacklist","label":"noise"}'

Update a filter

PATCH /api/filters/{id}/update/

Send a partial JSON body with just the fields you want to change → 200. A missing filter returns 404.

Delete a filter

DELETE /api/filters/{id}/delete/

Returns 204.

Machine rules

Machine rules drive the dashboard display (modes 1 and 2), machine-side event dropping (mode 4), and runtime Raise behavior (mode 3). For the full concept, see Machine configuration. The machine field is a public_id, or '' (empty string) to target the global scope.

List rules

GET /api/machine_rules/?machine=<public_id|>
{
  "rules": [
    { "ID": 12, "Source": "any", "EventPattern": ".*eval.*",
      "Mode": 3, "Action": "raise", "Polarity": "whitelist",
      "Label": "block eval", "Enabled": true, "sanitize_scope": "any" }
  ]
}

Note the capitalized field names in rule objects (all but sanitize_scope). Action and Polarity are legacy fields: Action is always raise, and Polarity follows the mode (blacklist for mode 4, whitelist otherwise). Mode is what decides what the rule does.

Create a rule

POST /api/machine_rules/create/

JSON body:

Returns 201 with the created rule (same shape as the list).

Mode 3 (Raise) has to be enabled for your account. It is a per-account setting rather than something you switch on yourself. Without it the request returns 403 with a message saying Raise rules are not enabled for your account, and who to ask. Check grant_raise on GET /api/account/ to see where you stand before you try.

curl -s https://eyalsec.com/api/machine_rules/create/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"machine":"m_8f3c2a1b","polarity":"whitelist","event_pattern":".*eval.*","mode":3,"label":"block eval"}'

Update a rule

PATCH /api/machine_rules/{id}/update/

Partial JSON (any of polarity, event_pattern, mode, source, sanitize_scope, label, enabled) → 200 with the updated rule. Changing a rule's mode to 3 re-checks that Raise is enabled for your account.

Delete a rule

DELETE /api/machine_rules/{id}/delete/

Returns 204.

Rule templates

A rule template is a named, ordered set of rules you can apply to a rule scope (your global rules, or one machine) in a single step. There are two kinds:

Applying a template copies its rules into the scope. The copies are ordinary rules from that moment on: edit them, disable them or delete them like any other. Editing the template afterwards does not reach back into a scope that has already applied it, and deleting a template does not remove rules applied from it.

List templates

GET /api/rule_templates/
{
  "templates": [
    {
      "id": 3,
      "owner": "eyalsec",
      "name": "OWASP basics",
      "description": "",
      "items": [
        { "source": "socket", "event_pattern": "exec|eval",
          "mode": 2, "sanitize_scope": "any", "position": 0 }
      ]
    }
  ]
}

owner is me for your own templates and eyalsec for the published ones. Items carry the same four fields a rule does: source, event_pattern, mode and sanitize_scope. They deliberately carry no enabled flag (an applied rule always lands enabled) and no polarity (it follows the mode, as it does on a rule).

Create a template

POST /api/rule_templates/create/

JSON body: name (required, unique among your own templates, compared without regard to case), description (optional), and items (the rule list). Each item is validated exactly the way Create a rule validates a rule, so a template can never hold something that would be refused as a rule.

At most 50 templates per account and 200 items per template. Returns 201 with the created template.

curl -s https://eyalsec.com/api/rule_templates/create/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"name":"My web app","items":[{"source":"socket","event_pattern":"exec|eval","mode":2}]}'

Update a template

PATCH /api/rule_templates/{id}/update/

Takes the same body as create and replaces the item list with the one you send, so include every item you want to keep. Returns 204. A template you do not own returns 404, which includes every EyalSec template.

Delete a template

DELETE /api/rule_templates/{id}/delete/

Returns 204. A template you do not own returns 404.

Apply a template

POST /api/rule_templates/{id}/apply/

JSON body: machine, a public_id or '' for the global scope. Works on your own templates and on EyalSec ones.

{ "added": 5, "skipped_raise": 2, "skipped_duplicate": 1 }

Nothing is replaced: the scope's existing rules are left alone and the template's rules are added to them. Two kinds of item are skipped rather than added, and both are counted so a partial apply is never silent:

Save a scope's rules as a template

POST /api/rule_templates/snapshot/

JSON body: name (required), description (optional) and machine (a public_id, or '' for global). Captures the rules currently in that scope as a new personal template and returns 201 with it. Disabled rules are captured too, since a template item has no enabled flag. The same 50-template and 200-item ceilings apply.

Taint

Read and set which taint sources are tracked, per machine or globally. For what the sources mean and when changes take effect, see Machine configuration.

Read taint settings

GET /api/machine_taint/?machine=<public_id|>

Every gate key is always present, for every product, including ones the machine's agent does not read (an agent ignores a key it has no counterpart for). The response below is shortened; the real one carries every key:

{ "taint": { "socket": "on", "file": "off", "stdin": "unset",
             "foreign": "unset", "env": "unset", "make_vuln": "unset",
             "argv": "unset", "weak_random": "unset", "hardcoded": "unset",
             "db_rows": "unset", "…": "unset" } }

Set a taint source

PUT /api/machine_taint/

JSON body:

Two of these keys are spelled differently as a rule source. The value you pass here is a gate key (underscores); the value you put in a rule's source field is a rule-source name. They differ in exactly two places: gate weak_random becomes rule source weak-random, and gate db_rows becomes rule source db. Passing the gate-key form to a rule fails validation and the rule is rejected.

Returns 204. An unknown key or state is a 400. Remember: taint changes take effect on the next process restart of es-python.

curl -s -X PUT https://eyalsec.com/api/machine_taint/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"machine":"m_8f3c2a1b","source":"socket","state":"on"}'

Event labels & notes

Triage tools for the events list: your own colored labels ("mark this event"), and one editable note per event.

Event ids are sequential and enumerable, so the routes that write to an event return 404, not 403, for an id that exists but is not yours: that keeps a foreign id indistinguishable from a nonexistent one. Detaching a label and deleting a note return 204 whether or not the event was yours, for the same reason from the other direction. Deleting is scoped by owner in the statement, so a foreign id deletes nothing, and reporting that it matched nothing would itself answer the question the 404 exists to withhold.

List labels

GET /api/event_labels/
{ "labels": [ { "id": 1, "name": "triage", "color": "amber" } ] }

Create a label

POST /api/event_labels/

JSON body:

Returns 201 with the created label.

curl -s -X POST https://eyalsec.com/api/event_labels/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"name":"triage","color":"amber"}'

Rename or recolor a label

PATCH /api/event_labels/{id}/

Send name (required) and optionally color204. The label is rewritten whole, so leaving color out sets it back to amber. A label you do not own returns 404.

Delete a label

DELETE /api/event_labels/{id}/

Returns 204. This also removes the label from every event it was attached to. A label you do not own returns 404.

Attach a label to an event

POST /api/event/{id}/labels/

JSON body: label_id (required). Returns 204. Re-attaching a label that is already on the event is a no-op, not an error, since the events page toggles labels on click.

Detach a label from an event

DELETE /api/event/{id}/labels/{labelID}/

Returns 204. Detaching a label that was not attached is also a no-op.

Write your note on an event

PUT /api/event/{id}/comment/

JSON body: body (required), at most 4096 bytes. There is one note per user per event; writing again replaces it. Returns 204.

curl -s -X PUT https://eyalsec.com/api/event/12345/comment/ \
  -H "X-API-Key: es2_3f9a…" \
  -H "Content-Type: application/json" \
  -d '{"body":"looks like a false positive"}'

Delete your note on an event

DELETE /api/event/{id}/comment/

Returns 204.

Activity

Read your own audit trail, the same data shown on the Activity log page.

GET /api/activity/?action=&outcome=&before_ts=&before_id=

Results are newest-first and keyset-paged, 50 per page. Filter with:

{ "entries": [ { "time": "2026-07-16T06:20:00Z", "id": 4812,
                 "action": "machine.create", "description": "Created machine web-prod-01",
                 "target": "web-prod-01", "source_ip": "203.0.113.4",
                 "auth_method": "apikey", "outcome": "success" } ] }

Activity is retained for 30 days.

curl -s "https://eyalsec.com/api/activity/?action=machine.create&outcome=success" \
  -H "X-API-Key: es2_3f9a…"

Your account

Everything on the Settings page is here too: your profile, password, two-factor auth, API keys and billing.

These endpoints ask for the same proof the dashboard asks for. Changing your password over the API needs your current password, exactly as the form does. Generating a key needs your password and a 2FA code. An API key on its own is never enough. That is deliberate: the API should not be an easier way into your account than the browser, so a leaked key still cannot take it over.

Send the credentials as form fields alongside the rest of the request: password, and totp when your account has two-factor auth on. A backup code works anywhere a totp code does.

Three of these destroy the key you are calling with. Read the warnings.

Read your profile

GET /api/account/
{
  "username": "alice",
  "email": "alice@example.com",
  "timezone": "Asia/Jerusalem",
  "email_verified": true,
  "totp_enabled": true,
  "email_2fa_enabled": false,
  "machine_limit": 5,
  "event_limit": 5000,
  "event_limit_monthly": true,
  "grant_raise": false
}

This is where you check what your account is actually allowed to do: machine_limit, event_limit (-1 means unlimited) and grant_raise. event_limit is the cap on each view, not on all of them added together, so the account total you can see is up to that number times the number of products on your account. It never returns your password, your two-factor secret, or any key.

Set your timezone

POST /api/account/timezone

Form field timezone: an IANA name such as Asia/Jerusalem. Send it empty to go back to automatic (your browser's zone). No password needed, same as the dashboard. Returns 200 with {"timezone": "Asia/Jerusalem"}; an unknown zone returns 422 and changes nothing.

Change your recovery email

POST /api/account/email

Needs password, plus totp when 2FA is on. Form field email.

A new address is not applied immediately. A confirmation link is emailed to it, and the change only lands once you click that link, exactly like the dashboard. This is what stops someone with a stolen session pointing your recovery address at themselves.

{
  "status": "confirmation_sent",
  "email": "new@example.com",
  "note": "not applied yet: click the link sent to the new address to confirm"
}

Sending an empty email removes your recovery address straight away. That also disables email 2FA (there would be nowhere to send the code) and signs out every session.

Change your password

POST /api/account/password

Needs current_password. Then new_password, and optionally confirm (if you send it, it has to match).

This revokes your API key. A password change signs out every session and deletes your key, so the key making this call stops working the instant it succeeds. Generate a new one afterwards.

{
  "status": "password_changed",
  "note": "your API key and all sessions were revoked; generate a new key"
}

Delete your account

POST /api/account/delete

Needs password. Returns 204 and removes the account and everything in it. There is no undo.

Set up an authenticator app

GET /api/account/2fa/setup
{
  "secret": "JBSWY3DPEHPK3PXP",
  "otpauth_url": "otpauth://totp/EyalSec:alice?secret=…",
  "expires_in_seconds": 600,
  "note": "call /api/account/2fa/enable with a code from this secret to activate it"
}

The secret is held for you server-side for 10 minutes. Turn otpauth_url into a QR code, or type secret into your app by hand. Calling this again starts over with a fresh secret.

Turn on the authenticator app

POST /api/account/2fa/enable

Needs password, and totp: a code generated from the secret you just got.

Note the difference: that totp proves you hold the authenticator, it is not a second factor on this request. The password is the re-auth.

Returns your backup codes, shown once:

{
  "status": "2fa_enabled",
  "backup_codes": ["…", "…"],
  "note": "backup codes are shown once; store them now. All sessions were revoked."
}

A wrong code returns 422 and enables nothing. Calling this without doing setup first returns 409.

Turn off the authenticator app

POST /api/account/2fa/disable

Needs password. Signs out every session, so your other devices have to log in again.

New backup codes

POST /api/account/2fa/backup/regenerate

Needs password. Replaces any codes you already had, so the old ones stop working. Shown once. Requires the authenticator app to be set up (409 if not).

Email second factor

POST /api/account/2fa/email/enable
POST /api/account/2fa/email/disable

Both need password. Enabling requires a recovery email on file, since that is where the code is sent (409 without one).

Look at your API key

GET /api/account/apikey
{
  "exists": true,
  "key_prefix": "es2_3f9a…",
  "scope": "full",
  "created_at": "2026-07-01T09:00:00Z",
  "expires_at": "2027-01-01T00:00:00Z",
  "last_used_at": "2026-07-16T06:20:00Z",
  "last_ip": "203.0.113.4"
}

Useful for checking whether a key is still in use, and from where. It never returns the key itself: only a hash is stored.

Generate a key

POST /api/account/apikey/generate

Needs password, plus totp when 2FA is on. Optional expires (a future YYYY-MM-DD; omit for a key that never expires) and read_only (any non-empty value).

This replaces your current key. You get one key per account, so generating a new one kills the one you are calling with. Copy api_key from the response or you will lock yourself out.

{
  "api_key": "es2_NEWKEY…",
  "note": "shown once, and it replaces any previous key for this account"
}

A past expires returns 422 and creates nothing.

Revoke your key

POST /api/account/apikey/revoke

Needs password. Returns 204. If you revoke the key you are calling with, that call is the last thing it does.

GET /api/account/billing/portal

Returns the URL behind the dashboard's Manage billing button:

{ "portal_url": "https://customer-portal.paddle.com/…" }

404 when there is no active subscription.

What is not here

Rate limits

Every response that hits a limit returns 429 with a Retry-After header. The per-key and password limits answer with a JSON body; the per-address limits answer in plain text.

Limit Applies to
300 requests/minute per API key Everything you call with a key. Counted against your account, so making a new key does not reset it.
600 requests/minute per IP address All traffic from one address, keyed or not.
120 requests/minute per IP address The events calls together: POST /api/get_all_events_with_count/ (the heaviest read), the export, GET /api/event/{id}, /api/event_view_counts/ and /api/event_visibility_counts/.
10 requests/minute Anything under /api/account/ that asks for your password.

The per-key and per-IP limits are counted separately and both apply, so whichever runs out first is the one you feel.

Getting your password wrong repeatedly locks the account. Five wrong attempts in a row and it locks for a minute, doubling up to 15 minutes. This is the same counter the login page uses, so a locked account is locked everywhere. While it is locked you get 429 even with the right password.

Agent endpoints

These endpoints are used automatically by the EyalSec agents on your monitored machines; you normally never call them by hand. They're listed here for completeness. They authenticate with the machine's secret, not your API key.

Report an event

POST /api/create_event/        (alias: POST /events)

The runtime sends a Secret: <machine-uuid> header and the event fields base64-encoded: where, repr, trace, location, and origin. The server responds 202 Accepted without waiting for the event to be stored; it's fire-and-forget. A machine sending faster than its allowance gets 429, and 503 means the server as a whole is shedding load. Events are aggregated and counted server-side, which is why the dashboard shows counts rather than one row per occurrence (see Events).

Fetch machine configuration

GET /api/machine_config

The runtime sends the Secret header and receives the machine's encrypted configuration (its taint settings and rules). The response is conditional: the agent sends an X-Config-Version header and gets 304 Not Modified when nothing has changed. es-python polls this at startup and roughly every 30 seconds (see Running es-python).

Install bootstrap (public)

POST /install.sh
POST /install/payload

These serve the self-extracting installer when you run the install command from the Machines page. They're public and always reachable so a new machine can bootstrap, and they support HTTP Range requests so an interrupted download can resume with curl -C -. The public GET /api/supported_os_types/ (documented under Machines) is also used here to list the available targets.

Errors

The API uses standard HTTP status codes. API-key requests always return a JSON body of the form {"error": "…"} rather than an HTML page or a redirect.

Status Meaning
400 Bad request: malformed JSON, an invalid regex, or an unparseable cursor or timestamp.
403 Forbidden: failed CSRF check, missing or invalid auth, a read-only key trying to change something, something that is not enabled for your account (Raise rules, or a product you do not have), or a write on a read-only account.
404 Not found: the object doesn't exist. This is also returned to hide objects that belong to another user.
409 Conflict: the action clashes with current state (the machine limit is reached, a label name is taken, or a 2FA step is out of order).
422 Unprocessable: a required field is missing or invalid (e.g. an unknown os_type or python_version, a name longer than 128 characters, a missing install attestation, or no OS set).
500 Server error: an unexpected failure on our side, including build or blob-storage problems.

A 401 Unauthorized is returned specifically when an API key is bad, revoked, or expired (see Authentication).

Glossary

A plain-English reference for the terms used throughout this guide. Each one is also defined where it first appears, but this is the quick lookup.

Untrusted data (taint)

Data that came from the outside world rather than from your own program, for example bytes read off a network socket, the contents of a file someone else can write, or text typed on standard input. EyalSec calls this untrusted data, and its internal name for it is taint. The two words mean the same thing. EyalSec tracks untrusted data as it flows through your program so it can tell when that data is about to be used in a dangerous way.

Source

Where untrusted data entered your program. For es-python the sources are:

Every source is off until you switch it on. You choose which sources EyalSec watches per machine; see Machine configuration.

Sink

A risky action: the dangerous thing untrusted data might end up doing. Common sinks are running a system command, executing code, or opening a file path built from outside input. EyalSec records (or blocks) the moment untrusted data reaches a sink. The pairing is the whole idea: a source brings data in, a sink could do harm with it, and EyalSec watches the path between them.

Report

The default behavior: when untrusted data from a source you switched on reaches a sink, EyalSec records an event on your dashboard and lets your program keep running. Nothing about how your app behaves changes. See Report mode vs Report and Raise mode.

Raise

The blocking behavior: instead of just recording, EyalSec stops the risky action by throwing a Python RuntimeError at that exact spot, so the dangerous operation never happens. Raise must be enabled on your account; see Plans & roles.

Modes 1 / 2 / 3 / 4

The four "modes" a dashboard rule can have:

Modes 1 and 2 only change what you see on the dashboard; they never affect what's collected or how your program runs. Modes 3 and 4 change what the machine does. Every mode acts on the events its pattern matches. See Machine configuration.

Severity

A per-event rating (critical, high, medium, low, or info) computed on the server from what the event shows, so the rows that deserve attention stand out. Shown as a colored word on the Events page, and filterable there and in the API.

Socket-only build

A slimmer version of the es-python runtime that watches network data only: it cannot watch the file, stdin, foreign, or env sources. It is no longer selectable when adding a machine (new machines always get the full build), but some older machines still run it and show a socket-only badge. See Machines.

Origin

The file metadata of the data's source: details about the file the untrusted data came from, such as its type, permissions, size, owner, and timestamps. Origin helps you judge whether a file should have been trusted in the first place. It appears in an event's detail pane.

Foreign code

Python code loaded from a file that another user on the machine can write to. If a file you import or run could be modified by someone other than you, that's a risk: they could change what your program does. When foreign-code detection is on, loading such a file emits a foreign-code:<path> event, and under Raise it aborts the load. See Running es-python.

public_id vs secret

Every machine has two identifiers:

Build version

A short label identifying exactly which build of the es-python runtime a machine is running. The dashboard compares the build installed on a machine against the latest available build and shows an "up to date" or "newer build available" badge. To take a newer build, reinstall the machine; see Machines.

API key

A token (it starts with es2_) that lets a program talk to the dashboard's JSON API on your behalf, without a browser login. You generate one in Settings; it's shown only once, so save it somewhere safe. An API key has the same access your account does.

CVE

Short for Common Vulnerabilities and Exposures: the industry-standard naming scheme for publicly known security flaws (for example, CVE-2021-44228). When people talk about a specific known vulnerability, they usually refer to it by its CVE identifier.

FAQ & troubleshooting

Quick answers to the questions that come up most. If yours isn't here, see Getting help.

Do I have to change my code?

No. You run the same programs the same way, just with es-python instead of python. EyalSec watches from the runtime; your source code stays exactly as it is.

Will Report and Raise mode break my app?

Only if untrusted data actually reaches a risky action. For normal traffic nothing changes; Report and Raise mode only steps in at the exact moment something dangerous is about to happen, and it does so by throwing a normal Python RuntimeError. If your code already catches exceptions around that operation, the attack is blocked and your app can carry on. Many people start in Report mode and switch to Raise once they're confident in what EyalSec is detecting.

I don't see any events. What's wrong?

Check these things in order:

  1. You ran your program with es-python, not plain python.
  2. The machine shows as installed (green) on the Machines page.
  3. You don't have a filter or rule hiding the events. Check Filters and the Events filter toolbar, and remember that new accounts start with two default "Don't send" drop rules (patterns re and write); events whose name matches either are dropped at the machine until you edit or remove those rules (see Filters).
  4. The sources you care about are switched on for that machine. Taint sources are off until you enable them; see Machine configuration. Remember that taint changes take effect on the next process restart.
  5. Your account has a plan. An account with no plan yet has no visible events; see Plans & roles.

How do I go back to normal Python?

Just use python as you always have. EyalSec is added alongside it, so your original Python is always still there. Switching between python and es-python takes nothing more than which command you type.

How do I update a machine to a newer build?

When the dashboard shows a "newer build available" badge on a machine, you update it by reinstalling it. On the Machines page, click Install on the machine's row. That mints a fresh install command for you to run on the host, which pulls down the latest es-python build. Your machine's internal identity is unchanged, so all your existing events stay attributed to it; nothing is lost. Reinstalling is the normal, expected way to take a newer build.

Why can't I create a Raise rule?

Raise rules (mode-3 rules that block an action at runtime) must be enabled on your account. If they aren't, you'll see:

Raise rules are not enabled for your account. Ask an administrator to enable them.

Report mode works for every account; only blocking (Raise) needs the capability turned on. To have it enabled, email sales@eyalsec.com. See Plans & roles.

I registered, but my dashboard says I have no active plan. What now?

Registering and verifying your email gives you a working sign-in, not an allowance. Until a plan is set up for your account it has no machines and no visible events, so adding a machine is refused and the events lists are empty. Email sales@eyalsec.com to book a live demo and have your plan sized. The limits apply the moment we set them, with nothing to reinstall. See Plans & roles.

How do I use the API?

EyalSec has a JSON API that mirrors what you can do in the dashboard. To use it:

  1. Generate an API key in Settings. It starts with es2_ and is shown only once, so copy it somewhere safe.

  2. Send it on each request in the X-API-Key header, like this:

    curl -H "X-API-Key: es2_3f9a…" https://eyalsec.com/api/get_all_machines/
    

API-key requests always return JSON and never redirect to a login page. A full-access key carries the same access your account has (a read-only key can only read), so keep it private. For the full list of endpoints, see the API reference.

I lost my 2FA device. How do I log in?

When you set up two-factor authentication, EyalSec showed you a set of backup codes (in the form xxxx-xxxx-xxxx-xxxx). Each one works exactly once in place of the 6-digit app code on the two-factor screen (choose Use a backup code instead), and they're not case-sensitive. If you turned on email sign-in codes, you can also have a code emailed to you from that screen. Use one to get in, then go to Settings to re-enroll your authenticator on your new device and regenerate a fresh set of backup codes. If you've also run out of backup codes and can't get in at all, email support@eyalsec.com. See Getting help.

Getting help

Still stuck, or want to talk to a person? We're here.

Before you write in, the FAQ & troubleshooting section answers the most common questions, and the Glossary explains any term you're unsure about. If you do reach out, telling us which machine and roughly when something happened helps us help you faster.

EyalSec Pricing Docs Security Contact Login Book a live demo