Get started

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.