Security graph REST API
The API is plain HTTP JSON, served by om serve and by the Compose or Helm
container. The console is the same process at / and /ui/. There is
no GraphQL endpoint.
Every /v1 route except GET /v1/health checks
OM_API_SECRET. Send it as Authorization: Bearer … or
X-API-Key. The console HTML does not. Treat the API as an internal service.
Details are on Security.
Reads
| Method and path | Notes |
|---|---|
GET /v1/health | {"status":"ok","service":"opensourceom-api"}. Does not check the API secret. Helm probes this path. |
GET /v1/graph/stats | nodes, edges, and by_type. |
GET /v1/graph/queries | Named queries and their descriptions. |
GET /v1/graph/query?name= | Run one named query. Unknown names return 400. |
GET /v1/graph/nodes?type=&limit= | Nodes. type filters by node type. Default limit 500. |
GET /v1/graph/edges?limit= | Edges. Default limit 2000. |
GET /v1/graph/snapshot?limit= | Nodes (default 500) and edges (default 2000) together. |
GET /v1/findings?limit= | Finding views with the affected resource. Default limit 200. |
GET /v1/rules | Rule id, name, and description. |
GET /v1/identity/blast-radius | identity_id or name. See Blast radius. |
limit falls back to the default when it is missing, zero, or not an integer.
There is no server-side maximum above the number you pass.
Writes
| Method and path | Body |
|---|---|
POST /v1/ingest | A graph batch: {"nodes":[],"edges":[]}. Response 202 with counts. |
POST /v1/rules/run | No body. Optional ?id=rule-id runs one rule. Response includes matches and findings_created. |
POST /v1/export/slack | JSON body {"webhook":"https://hooks.slack.com/..."}. A webhook query parameter is rejected. Response {"exported":N}. |
curl -s http://localhost:8080/v1/health
curl -s -H "Authorization: Bearer $OM_API_SECRET" \
http://localhost:8080/v1/graph/stats
curl -s -H "Authorization: Bearer $OM_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{"nodes":[],"edges":[]}' \
http://localhost:8080/v1/ingest
Ingest upserts by node and edge id. It does not run plugin validation, so callers are
responsible for type strings and for edges whose endpoints already exist or are in the same
body. The CLI plugin path does validate. Prefer om scan plugin for external
collectors.
Response shapes
GET /v1/graph/stats:
{
"nodes": 12,
"edges": 9,
"by_type": { "Workload": 3, "Datastore": 2, "Identity": 2 }
} GET /v1/graph/query?name=internet-to-datastore returns query,
summary, and paths. Each path is an array of nodes. When a
CloudTrail, Activity Log, or Admin Activity event’s resource node is on a path, audits lists that
event with index set to the path’s position. Unknown names
are 400. The six names are listed by GET /v1/graph/queries as
{"queries":[{"name":"...","description":"..."}]}. See
Attack paths for depth and row limits.
GET /v1/findings returns findings and, when another page remains,
next_cursor. Each item has finding (the node)
plus affected_resource_id, affected_resource_name, and
affected_resource_type from the VIOLATES edge. An attack-path
finding also includes path, the ordered node ids from the internet to the
datastore. Rows are ordered by normalized_score descending. Default limit 200.
POST /v1/rules/run returns matches and findings_created.
findings_created counts upserts, including updates of a finding that already
existed for that rule and resource. An unknown id is 400.
GET /v1/identity/blast-radius returns the identity, reachable
(at most 200 nodes), max_depth (6), and summary. Pass
identity_id or name. A name that matches no identity is 400.
This route requires the API secret, like the other /v1 reads.
curl -s -X POST -H "Authorization: Bearer $OM_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{"webhook":"https://hooks.slack.com/services/..."}' \
http://localhost:8080/v1/export/slack
Slack export loads up to 500 findings and posts the same digest as
om export findings run --format slack. Put the webhook in the JSON body.
Query strings are rejected so the URL does not land in access logs.
Errors
Failures are JSON {"error":"..."} with 400, 401, or 500. A missing or wrong
secret on any /v1 route except health is 401 unauthorized. Invalid JSON on ingest is 400
invalid json body. There is no request id. A list that has another page returns
next_cursor; pass it back as cursor. Raise
limit when a list is truncated. There is no server-side maximum above the
number you pass, except the fixed caps inside path queries, blast radius, and exports.
Copyright © 2026 OpenSourceOM. Licensed under Apache-2.0.