Machines map API
These endpoints read and change the Map view of the Machines page: the zones you draw, the policies you reuse, and the wires that connect them. Anything you can do on the Map, you can do from here, with the same checks and the same preview.
How settings reach a machine
A machine takes its settings from six levels. From the farthest to the closest: your account settings, policies wired to your account, policies wired to Not placed (only while the machine has no place on the map), policies wired to each zone the machine is in (the outermost zone first), policies wired to the machine itself, and the machine's own settings from its Configure window. For a single setting, such as whether a source is watched, the closest level that sets it wins. Rules add up instead: a machine applies the rules of every level, and checks outbound rules from the closest level outward.
When two policies wired at the same level disagree, the stricter value wins: on over off, deny over allow, block over report, the lower rate limit. The map lists each of these conflicts. For a setting with no stricter value, the oldest policy wins, and that is listed too.
A machine picks a change up on its next configuration check, within about 30 seconds (see when a change reaches the machine).
Every id in a request is checked against your account. An id that belongs to another account answers 404, exactly like one that does not exist.
Preview a change
Add ?dry_run=1 to any write to see what it would do without saving anything. The answer has the same shape as the real write's, with "dry_run": true and no id, since nothing is created.
A write that can change what a machine runs (moving a machine into another zone, moving or deleting a zone, changing or deleting a policy's settings, adding or removing a wire, clearing a machine's own setting) answers with the change on every machine it reaches:
{ "ok": true, "dry_run": false,
"diff": { "machines": [ { "public_id": "7c1e4f0a-0000-4000-8000-000000000000", "name": "web-01",
"changes": [ { "kind": "outbound", "key": "default_action", "from": "allow", "to": "deny" } ],
"rules_added": 3, "rules_removed": 0 } ],
"summary": "web-01 gains 3 rules, outbound default becomes deny" } }
changes lists each setting whose value in force changes, with the value before (from) and after (to). A built-in default shows as its value; for a source no level sets, that is unset. The outbound rules change lists the outbound rules in the order they are checked, as action target, separated by commas, and is empty when there are none. rules_added and rules_removed count rules. summary says the same in one sentence of at most 120 characters, ending with "and 4 more machines" when the change reaches more machines than fit. A change that reaches no machine has an empty machines list and an empty summary.
A write that only names, moves, resizes or collapses something answers {"ok": true}, with the new id when it creates one.
Once in a while a change is saved but the list of what changed cannot be put together. The write still succeeded, so the answer is 200 with "diff_unavailable": true in place of diff, the id when it created something, and a summary asking you to reload:
{ "ok": true, "dry_run": false, "diff_unavailable": true,
"summary": "Saved. Reload the map to see what changed." }
Reload the map (GET /api/machines_map/) to see the change. Sending the write again is not needed.
Errors
An error answers {"error": "..."}: one sentence saying what happened and what to do next.
| Status | When |
|---|---|
| 400 | The body is not one valid JSON object, it has a field the endpoint does not take, a name is empty, or a value is not allowed. |
| 403 | A cookie call without its CSRF token; or a unique-event policy that names a product your account does not have. |
| 404 | The zone, policy, wire or machine is not on your map. |
| 409 | A limit: 200 zones, 200 policies, 2000 wires, zones 8 deep, a zone placed inside itself or one of its own zones, or a wire that already exists. Also a policy whose settings changed while you were editing it: reload the map and try again. |
| 413 | The body is too large. |
Read the map
GET /api/machines_map/ returns the whole map and answers 200.
GET /api/machines_map/
{ "zones": [ { "id": 1, "parent_id": null, "name": "Production", "x": 0, "y": 0, "w": 320, "h": 220, "collapsed": false } ],
"machines": [ { "public_id": "7c1e4f0a-0000-4000-8000-000000000000", "name": "web-01", "status": "installed",
"agent_kind": "python", "os_type": "ubuntu_24_04_x86_64", "zone_id": 1, "x": 10, "y": 20, "local_overrides": 2,
"note": "Payments API, production" } ],
"policies": [ { "id": 3, "name": "Block outbound", "kind": "outbound", "body": { "default_action": "deny", "rules": [] },
"x": 600, "y": 120, "summary": "deny by default" } ],
"bindings": [ { "id": 7, "policy_id": 3, "zone_id": 1, "machine": null, "target": "zone" } ],
"conflicts": [ { "kind": "watch", "key": "file", "level": "zone", "zone_id": 1,
"winner_policy_id": 2, "loser_policy_id": 4, "binding_id": 9 } ],
"limits": { "zones": 200, "policies": 200, "bindings": 2000, "depth": 8 },
"can_raise": true,
"raise_web_keys": [ "block", "rate.action" ],
"templates": [ { "id": 12, "name": "OWASP Top 10 (es-python)", "owner": "eyalsec", "product": "python", "rules": 14 } ] }
A machine whose zone_id is null is in no zone, and one whose x is null is not placed yet; both take your account settings and what is wired to your account or to the machine itself, and one not placed yet also takes what is wired to Not placed. local_overrides counts the settings the machine's own Configure window sets; those always win. note is the machine's note, empty when it has none. A wire's target is account, unplaced (Not placed), zone (with zone_id) or machine (with the machine's public_id). templates lists the templates a config policy can link: EyalSec's and your own. In conflicts, binding_id is the wire that lost, and zone_id is left out for a conflict between policies wired to your account. can_raise is always true. raise_web_keys names the web protection settings whose blocking values put a machine on the Raise rate.
Zones
A zone is a box on the map. Zones nest, each inside at most one other, up to 8 deep.
Create a zone
POST /api/machines_map/zones/ creates a zone and answers 200 with its id.
curl -s -X POST https://eyalsec.com/api/machines_map/zones/ \
-H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"name":"Production","parent_id":null,"x":0,"y":0,"w":320,"h":220}'
name is required, at most 80 characters. parent_id is the zone to put it in, or null for the top level.
Change a zone
PATCH /api/machines_map/zones/{id}/ changes any of name, x, y, w, h, collapsed, severity and parent_id, and answers 200. Only the fields you send change. severity is critical, high, medium, low, or "" for none; it colours the zone on the map and changes nothing a machine inherits.
PATCH /api/machines_map/zones/1/
Sending a different parent_id moves the zone, and everything in it, into another zone (null for the top level). That changes what those machines inherit, so the answer carries the diff.
Delete a zone
DELETE /api/machines_map/zones/{id}/ deletes a zone and its wires, and answers 200 with the diff. The zones and machines inside it move up into its parent.
DELETE /api/machines_map/zones/1/
Place a machine
PATCH /api/machines_map/machines/{public_id}/ sets where a machine sits, and answers 200. Send zone_id (null for no zone) with x and y. Leaving x and y out, or sending them as null, takes the machine off the map and out of its zone, whatever zone_id says.
curl -s -X PATCH https://eyalsec.com/api/machines_map/machines/7c1e4f0a-0000-4000-8000-000000000000/ \
-H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"zone_id":1,"x":10,"y":20}'
The public_id is the one the machine list gives. A placement that puts the machine in another zone answers with the diff; one that leaves it in the same zone answers {"ok": true}.
Policies
A policy is a named group of settings of one kind that you wire to your account, to zones or to machines. Change it once and every machine it reaches follows. Each kind's body is checked the way the matching endpoint checks the same settings:
| Kind | Body |
|---|---|
rules |
{"rules": [{"pattern": "^noise", "source": "any", "mode": 1, "sanitize_scope": "any", "label": "", "enabled": true}]}, each rule as in create a rule, at most 500. |
watch |
{"gates": {"socket": "on", "file": "off"}}, keys as in set a taint source. A key left out is not set by this policy. |
outbound |
{"mode": "all", "default_action": "deny", "on_violation": "block", "rules": [{"action": "allow", "target": "*.stripe.com:443", "note": ""}]}, as in set the outbound policy. Every field is optional. |
web |
{"settings": {"block": "on", "rate.limit": "100"}}, as in web protection. autoblock and autoblock.hours stay account settings and cannot go in a policy. |
unique |
{"axes": {"python": ["source", "detail"]}}, for each product the fields that make two detections different events, as in the unique-event definition. An empty list [] keeps only the baseline. |
config |
{"templates": [12, 15], "rules": [...], "watch": {...}, "outbound": {...}, "web": {...}, "unique": {...}}: every kind above in one policy, each section in that kind's body shape, plus the ids of the templates it links. A linked template's rules and settings reach every machine the policy reaches, and so do later changes to it; the policy's own settings come first. A template id your account cannot see is left out. Every section is optional. |
A rules policy with a Raise rule (mode 3), or a web policy that blocks, puts every machine it reaches on the Raise rate. A unique-event policy can only name products your account has.
Create a policy
POST /api/machines_map/policies/ creates a policy and answers 200 with its id. It reaches no machine until you wire it.
curl -s -X POST https://eyalsec.com/api/machines_map/policies/ \
-H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"name":"Watch env","kind":"watch","body":{"gates":{"env":"on"}},"x":600,"y":120}'
Change a policy
PATCH /api/machines_map/policies/{id}/ changes any of name, x, y and body, and answers 200. A new body replaces the old one whole and reaches every machine the policy is wired to, so the answer carries the diff.
PATCH /api/machines_map/policies/3/?dry_run=1
Delete a policy
DELETE /api/machines_map/policies/{id}/ deletes a policy and all its wires, and answers 200 with the diff.
DELETE /api/machines_map/policies/3/
Wires
A wire connects one policy to one target. A policy can have many wires, but only one per target.
Wire a policy
POST /api/machines_map/bindings/ adds a wire and answers 200 with its id and the diff.
curl -s -X POST https://eyalsec.com/api/machines_map/bindings/ \
-H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"policy_id":3,"target":"zone","zone_id":1}'
target is account (every machine), unplaced (every machine not placed on the map, new ones included), zone (with zone_id: every machine in that zone and in the zones inside it) or machine (with machine, a public_id). Send only the id the target needs; the other one is a 400. A wire puts the policy in force on more machines, so the same checks apply as for the policy's settings.
Remove a wire
DELETE /api/machines_map/bindings/{id}/ removes one wire and answers 200 with the diff.
DELETE /api/machines_map/bindings/7/
Effective settings
GET /api/machines_map/effective/ answers 200 with what one machine runs and where each value comes from. Pass the machine's public_id as machine.
GET /api/machines_map/effective/?machine=7c1e4f0a-0000-4000-8000-000000000000
{ "machine": "7c1e4f0a-0000-4000-8000-000000000000", "name": "web-01",
"settings": [ { "kind": "watch", "key": "env", "value": "on", "level": "zone",
"zone_id": 1, "zone_name": "Production", "policy_id": 2, "policy_name": "Watch env" } ],
"rules": [ { "mode": 1, "source": "any", "pattern": "^noise", "sanitize_scope": "any", "label": "", "match_on": [],
"prov": { "kind": "rules", "key": "^noise", "value": "1", "level": "zone",
"zone_id": 1, "zone_name": "Production", "policy_id": 5, "policy_name": "Hide noise" } } ],
"outbound_rules": [ { "action": "deny", "target": "*", "note": "",
"prov": { "kind": "outbound", "key": "rules", "value": "deny *", "level": "account" } } ],
"local": [ { "kind": "watch", "key": "socket", "value": "on", "hides": null } ],
"conflicts": [] }
level is default, account, root (a policy wired to your account), unplaced (a policy wired to Not placed), zone, machine_policy (a policy wired to the machine) or local (the machine's own setting); the zone and policy fields are left out where they do not apply. rules lists the rules in force and outbound_rules the outbound rules in the order the machine checks them, each with where it comes from as prov. local lists each of the machine's own settings and what it hides; hides is null when it hides nothing.
Clear a machine's own setting
POST /api/machines_map/clear_local/ deletes one setting the machine sets for itself, so it inherits again, and answers 200 with the diff. It is the same as choosing unset in the machine's Configure window.
curl -s -X POST https://eyalsec.com/api/machines_map/clear_local/ \
-H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"machine":"7c1e4f0a-0000-4000-8000-000000000000","kind":"watch","key":"socket"}'
kind and key name the setting: watch with a taint source, web with a web protection setting, unique with a product such as python, or outbound with default_action, mode, on_violation, or rules for the machine's own rule list. A machine with no such setting of its own answers 404.
Set a machine's own setting
POST /api/machines_map/set_local/ sets one setting on the machine itself, as its Configure window would, so it wins over what the map wires to it. It answers 200 with the diff; add ?dry_run=1 to see the diff without saving.
curl -s -X POST https://eyalsec.com/api/machines_map/set_local/ \
-H "X-API-Key: es2_EXAMPLEKEYdoNotUse0000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"machine":"7c1e4f0a-0000-4000-8000-000000000000","kind":"watch","key":"socket","value":"on"}'
kind and key are as for clear_local; the outbound rule list is changed in Configure. value is on or off for watch, a web protection value for web, a comma list of source, detail, trace, machine and origin for unique, and for outbound one of untrusted or all (mode), allow or deny (default_action), block or report (on_violation). A setting the machine's agent does not read answers 400.