Skip to content
Reference

API reference

The falak-neo-core REST and JSON API — authentication, tenancy, findings, events, agents, policies and the event stream.

Core exposes a REST/JSON API. The console is a client of it, and so can you be.

Base URL and versioning

https://api.eu.falakneo.example/v1

The version is in the path. Breaking changes get a new version; additive changes do not. Unknown fields in a response should be ignored rather than treated as an error.

Authentication

curl -H "Authorization: Bearer $FALAK_API_KEY" \
     https://api.eu.falakneo.example/v1/findings

Create keys under Settings → API keys. Keys are stored as SHA-256 digests — the plaintext is shown exactly once, at creation.

There is no tenant parameter

The tenant is derived from the key. No endpoint accepts a tenant id, and passing one has no effect. This is deliberate: a tenant that can be named is a tenant that can be guessed.

Scopes

ScopeGrants
findings:readRead findings and their evidence
findings:writeSet disposition, status and comments
events:readQuery the OCSF event store
agents:readRead the sensor fleet
agents:writeModify agent labels and policy binding
policies:read / policies:writeRead and publish sensor policy
rules:read / rules:writeRead and modify detection content
enrollment:writeCreate enrollment tokens
response:executeRequest signed response actions

Rate limits

PlanRequests / minBurst
Team60060
Business3,000300
Enterprisenegotiatednegotiated

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 carries Retry-After.

Findings

List

GET /v1/findings
  ?severity=4,5
  &status=NEW,TRIAGING
  &technique=T1620
  &from=1763372400000&to=1763458800000
  &limit=100&cursor=
{
  "rows": [
    {
      "findingUid": "b7f1c9e2-…",
      "title": "Fileless execution from an anonymous memory file descriptor",
      "severityId": 5,
      "riskScore": 88,
      "status": "NEW",
      "disposition": "UNKNOWN",
      "techniqueUids": ["T1620", "T1059.004"],
      "hostname": "ip-10-40-118-14.eu-central-1.compute.internal",
      "k8sNamespace": "payments",
      "processName": "memfd:payload",
      "firstSeenAt": 1763458204119,
      "lastSeenAt": 1763458204119,
      "occurrences": 1
    }
  ],
  "total": 41,
  "nextCursor": "eyJvIjoxMDB9"
}

Get one, with evidence

GET /v1/findings/{findingUid}

Returns the full OCSF 2004 Detection Finding document plus the correlated evidence events, the reconstructed process tree, comments and response history.

Triage

PATCH /v1/findings/{findingUid}
Content-Type: application/json
 
{ "status": "RESOLVED",
  "disposition": "TRUE_POSITIVE",
  "note": "Confirmed. Host isolated, image pulled from the registry." }

Every mutation is written to the tenant audit log with the API key as actor.

Events

POST /v1/hunt/query
Content-Type: application/json
 
{
  "classUids": [1007],
  "conditions": [
    { "field": "process.name", "operator": "in", "value": "sh,bash,dash" },
    { "field": "actor.process.name", "operator": "in", "value": "nginx,php-fpm" }
  ],
  "join": "and",
  "from": 1763372400000,
  "to": 1763458800000,
  "limit": 500
}

The response includes the compiled ClickHouse query core executed, which is useful both for debugging and for understanding cost.

The tenant predicate is added by core

It is always the leading term in the compiled WHERE, so the primary key prefix is used. You cannot remove it, and you cannot widen past it.

Live stream

Server-Sent Events. One-directional, survives HTTP/2 multiplexing and egress proxies far better than a socket upgrade, and reconnects on its own.

curl -N -H "Authorization: Bearer $FALAK_API_KEY" \
  "https://api.eu.falakneo.example/v1/events/stream?class_uid=2004&min_severity=4"
event: ready
data: {"tenant":"northwind","minSeverity":4}
 
event: finding
id: 1
data: {"findingUid":"b7f1c9e2-…","severityId":5,…}
 
: keepalive

Agents

GET  /v1/agents                     # fleet, with probe state and drift
GET  /v1/agents/{agentUid}          # detail, host facts, SensorStats series
PATCH /v1/agents/{agentUid}         # labels, policy binding

Policies

GET  /v1/policies
POST /v1/policies/{id}/publish      # compiles, signs, increments generation

Publishing returns the new generation. Convergence is observable through the agent list — each agent reports the generation it has applied.

Response actions

POST /v1/actions
Content-Type: application/json
 
{ "findingUid": "b7f1c9e2-…",
  "agentUid": "9f2c1ab4-…",
  "typeId": 1,
  "params": { "pid": "41290" },
  "dryRun": false }
typeIdAction
1Kill process
2Isolate host
3Quarantine file
4Collect artifact
5Stop container
6Block network
7Run diagnostic

Core signs the action and stamps an expiry. The sensor verifies the signature before executing and refuses anything unsigned, expired, or addressed to a different agent. 202 Accepted means the action was dispatched, not that it succeeded — poll the finding for the result.

Errors

{ "error": "forbidden",
  "message": "API key lacks scope \"response:execute\"",
  "requestId": "req_9c2f1ab4" }
StatusMeaning
400Malformed request or failed validation
401Missing or invalid key
403Key lacks the required scope
404Not found in your tenant
409Conflict, e.g. publishing a policy concurrently
429Rate limited
5xxOur fault; requestId is what support will ask for

A 404 is returned for objects that exist in another tenant. Distinguishing "does not exist" from "not yours" would itself be a disclosure.

Something wrong or missing? Edit this page