Triage a capture over HTTP
RAM Triage is a SkillSafe app, so everything the page does is available as a REST API: post the text output of your Volatility 3 plugin runs and get back one structured JSON report — suspicious processes each with a verbatim evidence quote, injected-memory findings, network indicators, persistence, an IOC table, a timeline, containment steps and the follow-up commands to run next. This page documents the exact request and response shapes the app itself uses; nothing here is a simplification of them.
Base URL and the response envelope
Every endpoint below is relative to this base, and every call carries a bearer token:
BASE https://api.skillsafe.ai/v1/app-api
Success and failure share one envelope. A success body is
{"ok":true,"data":{...}}; a failure is
{"ok":false,"error":{"code":"...","message":"...","details":{...}}}.
Read data on success and error.code on failure — the HTTP
status mirrors the code but the body is the authoritative description.
Endpoints used on this page
POST /guest GET /me POST /estimate POST /run GET /jobs/{job_id} POST /run-stream POST /collections/cases/query
Error codes
| Code | HTTP | What it means and what to do |
|---|---|---|
unauthorized | 401 | Missing, malformed or expired token. Mint a new one (step 1). Guest tokens expire; personal tokens do not, but can be rotated. |
forbidden | 403 | The token is valid but not for this app. Tokens are scoped per app slug. |
payment_required | 402 | Balance is below min_credits. Call /estimate first and compare against /me so you never reach this. |
validation_error | 400 | The input object is the wrong shape. error.details names the field. dump_output must be a non-empty string. |
rate_limited | 429 | Back off and retry. Vector search on the cases collection is limited to 30 requests per minute per IP, tighter than the other data endpoints. |
not_found | 404 | Unknown job_id, record id, or collection name. |
sponsor_exhausted | 402 | Returned in error.details.reason when a guest run exceeds the publisher's sponsored allowance. Sign in and run on your own credits. |
internal_error | 500 | Retry once with the same Idempotency-Key; the platform will not double-bill a repeat of a key it has already settled. |
1. Get a token
The simplest path is the point-and-click one: open
the token page, sign in if you want the run billed to your own
account, and use Copy shell export. It hands you
export SKILLSAFE_TOKEN="aut_..." ready to paste. You never need to open a
browser console to find a token.
For a fully scripted flow, mint a guest token instead. A guest can call /me
and /estimate freely; /run works only if this app sponsors guest
usage, which it does not by default, so a triage run needs a personal token.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # from /tokens.html, or the guest call below
curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"ram-triage"}' | jq -r '.data.token'
import json, urllib.request
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from /tokens.html
req = urllib.request.Request(
API + "/guest",
data=json.dumps({"slug": "ram-triage"}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
print(TOKEN)
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from /tokens.html
const res = await fetch(API + "/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "ram-triage" }),
});
TOKEN = (await res.json()).data.token;
console.log(TOKEN);
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
func main() {
token := os.Getenv("SKILLSAFE_TOKEN") // or the guest call below
body, _ := json.Marshal(map[string]string{"slug": "ram-triage"})
res, err := http.Post(API+"/guest", "application/json", bytes.NewReader(body))
if err != nil {
panic(err)
}
defer res.Body.Close()
var env struct {
Data struct{ Token string `json:"token"` } `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
if env.Data.Token != "" {
token = env.Data.Token
}
fmt.Println(token)
}
import java.net.URI;
import java.net.http.*;
public class Step1 {
static final String API = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] args) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN"); // or the guest call below
HttpClient http = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"ram-triage\"}"))
.build();
String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(body); // {"ok":true,"data":{"token":"aut_..."}}
}
}
require "json"
require "net/http"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV["SKILLSAFE_TOKEN"] || "YOUR_TOKEN"
uri = URI("#{API}/guest")
res = Net::HTTP.post(uri, { slug: "ram-triage" }.to_json,
"Content-Type" => "application/json")
puts JSON.parse(res.body)["data"]["token"]
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
$ch = curl_init(API . "/guest");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => "ram-triage"]),
CURLOPT_RETURNTRANSFER => true,
]);
$env = json_decode(curl_exec($ch), true);
echo $env["data"]["token"], "\n";
using System.Net.Http.Json;
using System.Text.Json;
const string API = "https://api.skillsafe.ai/v1/app-api";
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
var res = await http.PostAsJsonAsync(API + "/guest", new { slug = "ram-triage" });
var env = await res.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(env.GetProperty("data").GetProperty("token").GetString());
2. A tiny client helper
Every later step assumes this helper: it prefixes the base URL, attaches the bearer token,
and unwraps the data envelope so calling code deals in plain objects. It also
raises on ok:false instead of silently returning a failure body.
# cURL has no helper - the pattern below is used verbatim in every step:
# curl -s "$API/PATH" -H "Authorization: Bearer $TOKEN" [-d '{json}']
# and jq pulls fields out of the {"data": ...} envelope:
# ... | jq '.data'
# ... | jq -r '.error.code // empty' # non-empty means the call failed
import json, urllib.error, urllib.request
def api(method, path, body=None, extra_headers=None):
headers = {"Authorization": "Bearer " + TOKEN}
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode()
headers.update(extra_headers or {})
req = urllib.request.Request(API + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
except urllib.error.HTTPError as e:
env = json.load(e)
err = env.get("error", {})
raise RuntimeError("%s: %s" % (err.get("code"), err.get("message")))
async function api(method, path, body, extraHeaders) {
const headers = { Authorization: "Bearer " + TOKEN, ...(extraHeaders || {}) };
if (body !== undefined) headers["Content-Type"] = "application/json";
const res = await fetch(API + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const env = await res.json();
if (!env.ok) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
func api(method, path string, body any, extra map[string]string, out any) error {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, API+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
for k, v := range extra {
req.Header.Set(k, v)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
if !env.OK {
return fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return json.Unmarshal(env.Data, out)
}
static String api(String method, String path, String jsonBody, Map<String, String> extra)
throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + token);
if (jsonBody == null) {
b.method(method, HttpRequest.BodyPublishers.noBody());
} else {
b.header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(jsonBody));
}
if (extra != null) extra.forEach(b::header);
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// Parse with your JSON library of choice; read .data on ok, .error.code otherwise.
return res.body();
}
def api(method, path, body = nil, extra = {})
uri = URI("#{API}#{path}")
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
extra.each { |k, v| req[k] = v }
if body
req["Content-Type"] = "application/json"
req.body = body.to_json
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
<?php
function api(string $method, string $path, $body = null, array $extra = []) {
global $token;
$headers = array_merge(["Authorization: Bearer {$token}"], $extra);
$opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
$opts[CURLOPT_POSTFIELDS] = json_encode($body);
}
$opts[CURLOPT_HTTPHEADER] = $headers;
$ch = curl_init(API . $path);
curl_setopt_array($ch, $opts);
$env = json_decode(curl_exec($ch), true);
if (empty($env["ok"])) {
throw new RuntimeException($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
static async Task<JsonElement> Api(string method, string path, object? body = null,
Dictionary<string, string>? extra = null) {
using var req = new HttpRequestMessage(new HttpMethod(method), API + path);
req.Headers.Add("Authorization", "Bearer " + token);
if (extra != null) foreach (var kv in extra) req.Headers.Add(kv.Key, kv.Value);
if (body != null) req.Content = JsonContent.Create(body);
var res = await http.SendAsync(req);
var env = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!env.GetProperty("ok").GetBoolean()) {
var e = env.GetProperty("error");
throw new Exception(e.GetProperty("code").GetString() + ": " +
e.GetProperty("message").GetString());
}
return env.GetProperty("data");
}
3. Who am I, and what is my balance
/me returns subject_type ("user" or
"guest") and credits. Compare the balance against
/estimate before running — that is what turns a 402 after submit into a
clear message before it.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
# {"subject_type":"user","credits":184320,...}
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int64 `json:"credits"`
}
if err := api("GET", "/me", nil, nil, &me); err != nil {
panic(err)
}
fmt.Println(me.SubjectType, me.Credits)
System.out.println(api("GET", "/me", null, null));
// {"ok":true,"data":{"subject_type":"user","credits":184320}}
me = api("GET", "/me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await Api("GET", "/me");
Console.WriteLine($"{me.GetProperty("subject_type").GetString()} " +
$"{me.GetProperty("credits").GetInt64()}");
10000 credits is one US dollar. The
app renders them as dollars, and so should you.
4. Price the run — free, no job, no charge
/estimate takes the same input object as /run and
returns what a run would reserve. It creates no job and costs nothing, so it is also the
cheapest way to prove your token, your input shape and the app's model binding are all
valid.
This app is bound to the model alias gpt-terra, which
currently resolves to gpt-5.6-terra, at
markup_bps: 1000 (a 10% publisher markup). The response
fields worth reading:
model— the concrete model,gpt-5.6-terra.model_alias— the tier alias,gpt-terra.markup_bps—1000.hold_credits— the worst-case amount reserved before the run. It prices the full output cap, so the actual charge is usually far lower. Never show it as the price.min_credits— the floor needed to start a run at all. Betweenmin_creditsandhold_creditsa run still executes with a reduced output cap and comes back withtruncated: true.sponsor_enabled— whether guests may run on the publisher's wallet.falsefor this app.
The input object is exactly what the page submits. dump_output is the only
required field; everything else has a default. Note the tabs inside
dump_output are real tab characters, written here as \t because
that is how JSON encodes them.
cat > input.json <<'JSON'
{
"case": {
"name": "WKSTN-4471",
"os_hint": "windows",
"acquisition": "winpmem",
"scenario": "ransomware"
},
"dump_output": "$ vol -f mem.raw windows.pslist\nPID\tPPID\tImageFileName\tOffset(V)\n4\t0\tSystem\t0x8e0f4b8d3900\n624\t524\tservices.exe\t0x8e0f4b9b0640\n3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n$ vol -f mem.raw windows.malfind\nPID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n",
"analyst_note": "EDR fired on PID 3288 at 09:40.",
"prescan_facts": null,
"current_datetime": "2026-08-12T14:00:00+00:00 (Wednesday)"
}
JSON
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @input.json | jq '.data | {model, model_alias, markup_bps, hold_credits, min_credits}'
DUMP = (
"$ vol -f mem.raw windows.pslist\n"
"PID\tPPID\tImageFileName\tOffset(V)\n"
"4\t0\tSystem\t0x8e0f4b8d3900\n"
"624\t524\tservices.exe\t0x8e0f4b9b0640\n"
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n"
"$ vol -f mem.raw windows.malfind\n"
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n"
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n"
)
payload = {
"case": {"name": "WKSTN-4471", "os_hint": "windows",
"acquisition": "winpmem", "scenario": "ransomware"},
"dump_output": DUMP,
"analyst_note": "EDR fired on PID 3288 at 09:40.",
"prescan_facts": None,
"current_datetime": "2026-08-12T14:00:00+00:00 (Wednesday)",
}
est = api("POST", "/estimate", payload)
print(est["model"], est["model_alias"], est["markup_bps"])
print("reserved:", est["hold_credits"], "floor:", est["min_credits"])
assert est["model_alias"] == "gpt-terra"
const DUMP =
"$ vol -f mem.raw windows.pslist\n" +
"PID\tPPID\tImageFileName\tOffset(V)\n" +
"4\t0\tSystem\t0x8e0f4b8d3900\n" +
"624\t524\tservices.exe\t0x8e0f4b9b0640\n" +
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n" +
"$ vol -f mem.raw windows.malfind\n" +
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n" +
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n";
const payload = {
case: { name: "WKSTN-4471", os_hint: "windows",
acquisition: "winpmem", scenario: "ransomware" },
dump_output: DUMP,
analyst_note: "EDR fired on PID 3288 at 09:40.",
prescan_facts: null,
current_datetime: "2026-08-12T14:00:00+00:00 (Wednesday)",
};
const est = await api("POST", "/estimate", payload);
console.log(est.model, est.model_alias, est.markup_bps);
console.log("reserved:", est.hold_credits, "floor:", est.min_credits);
dump := "$ vol -f mem.raw windows.pslist\n" +
"PID\tPPID\tImageFileName\tOffset(V)\n" +
"4\t0\tSystem\t0x8e0f4b8d3900\n" +
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n" +
"$ vol -f mem.raw windows.malfind\n" +
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n" +
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n"
payload := map[string]any{
"case": map[string]string{
"name": "WKSTN-4471", "os_hint": "windows",
"acquisition": "winpmem", "scenario": "ransomware",
},
"dump_output": dump,
"analyst_note": "EDR fired on PID 3288 at 09:40.",
"current_datetime": "2026-08-12T14:00:00+00:00 (Wednesday)",
}
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
Hold int64 `json:"hold_credits"`
Min int64 `json:"min_credits"`
}
if err := api("POST", "/estimate", payload, nil, &est); err != nil {
panic(err)
}
fmt.Println(est.Model, est.ModelAlias, est.MarkupBps, est.Hold, est.Min)
String dump = String.join("\n",
"$ vol -f mem.raw windows.pslist",
"PID\tPPID\tImageFileName\tOffset(V)",
"4\t0\tSystem\t0x8e0f4b8d3900",
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
"",
"$ vol -f mem.raw windows.malfind",
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found");
// Build the JSON with your library of choice; the shape is:
// {"case":{...},"dump_output":"...","analyst_note":"...","current_datetime":"..."}
String body = mapper.writeValueAsString(Map.of(
"case", Map.of("name", "WKSTN-4471", "os_hint", "windows",
"acquisition", "winpmem", "scenario", "ransomware"),
"dump_output", dump,
"analyst_note", "EDR fired on PID 3288 at 09:40.",
"current_datetime", "2026-08-12T14:00:00+00:00 (Wednesday)"));
System.out.println(api("POST", "/estimate", body, null));
// data.model == "gpt-5.6-terra", data.model_alias == "gpt-terra", data.markup_bps == 1000
DUMP = [
"$ vol -f mem.raw windows.pslist",
"PID\tPPID\tImageFileName\tOffset(V)",
"4\t0\tSystem\t0x8e0f4b8d3900",
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
"",
"$ vol -f mem.raw windows.malfind",
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found"
].join("\n")
payload = {
case: { name: "WKSTN-4471", os_hint: "windows",
acquisition: "winpmem", scenario: "ransomware" },
dump_output: DUMP,
analyst_note: "EDR fired on PID 3288 at 09:40.",
current_datetime: "2026-08-12T14:00:00+00:00 (Wednesday)"
}
est = api("POST", "/estimate", payload)
puts "#{est['model']} #{est['model_alias']} #{est['markup_bps']}"
puts "reserved: #{est['hold_credits']} floor: #{est['min_credits']}"
<?php
$dump = implode("\n", [
'$ vol -f mem.raw windows.pslist',
"PID\tPPID\tImageFileName\tOffset(V)",
"4\t0\tSystem\t0x8e0f4b8d3900",
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
"",
'$ vol -f mem.raw windows.malfind',
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found",
]);
$payload = [
"case" => ["name" => "WKSTN-4471", "os_hint" => "windows",
"acquisition" => "winpmem", "scenario" => "ransomware"],
"dump_output" => $dump,
"analyst_note" => "EDR fired on PID 3288 at 09:40.",
"current_datetime" => "2026-08-12T14:00:00+00:00 (Wednesday)",
];
$est = api("POST", "/estimate", $payload);
echo $est["model"], " ", $est["model_alias"], " ", $est["markup_bps"], "\n";
echo "reserved: ", $est["hold_credits"], " floor: ", $est["min_credits"], "\n";
var dump = string.Join("\n", new[] {
"$ vol -f mem.raw windows.pslist",
"PID\tPPID\tImageFileName\tOffset(V)",
"4\t0\tSystem\t0x8e0f4b8d3900",
"3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
"",
"$ vol -f mem.raw windows.malfind",
"PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
"3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found"
});
var payload = new {
@case = new { name = "WKSTN-4471", os_hint = "windows",
acquisition = "winpmem", scenario = "ransomware" },
dump_output = dump,
analyst_note = "EDR fired on PID 3288 at 09:40.",
current_datetime = "2026-08-12T14:00:00+00:00 (Wednesday)"
};
var est = await Api("POST", "/estimate", payload);
Console.WriteLine(est.GetProperty("model_alias").GetString()); // gpt-terra
Console.WriteLine(est.GetProperty("hold_credits").GetInt64());
case key is a reserved word in some languages. C# escapes it as
@case; in Java or Go build the map explicitly, as above.
5. Run it, then poll the job
/run is metered. It returns a job_id immediately; poll
/jobs/{job_id} until status is terminal
(succeeded, failed or canceled). The report text is
in output.output on success, alongside
charged_credits — the amount actually billed, usually well under the
hold — and truncated.
Always send an Idempotency-Key. Derive it from the input, not
from a random value or a clock: if a connection drops after the platform accepted the run,
your retry must carry the same key so it joins the existing job instead of
starting a second billable one. The app uses
ram-triage:{hash of dump_output + case + analyst_note}:a{attempt}, and its
automatic reformat-retry reuses the same hash with the attempt number bumped, so a
malformed first reply cannot double-charge.
KEY="ram-triage:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--data-binary @input.json | jq -r '.data.job_id')
while :; do
J=$(curl -s "$API/jobs/$JOB" -H "Authorization: Bearer $TOKEN" | jq '.data')
S=$(echo "$J" | jq -r '.status')
[ "$S" = "succeeded" ] || [ "$S" = "failed" ] || [ "$S" = "canceled" ] && break
sleep 2
done
echo "$J" | jq -r '.output.output' > report.json
echo "$J" | jq '{status, charged_credits, truncated}'
import hashlib, json, time
def idem_key(payload, attempt=1):
basis = json.dumps({
"d": payload["dump_output"],
"c": payload["case"],
"n": payload.get("analyst_note", ""),
}, sort_keys=True).encode()
return "ram-triage:%s:a%d" % (hashlib.sha256(basis).hexdigest()[:16], attempt)
job = api("POST", "/run", payload, {"Idempotency-Key": idem_key(payload)})
job_id = job["job_id"]
while True:
j = api("GET", "/jobs/" + job_id)
if j["status"] in ("succeeded", "failed", "canceled"):
break
time.sleep(2)
if j["status"] != "succeeded":
raise RuntimeError("run " + j["status"])
report = json.loads(j["output"]["output"])
print("charged:", j["charged_credits"], "truncated:", j.get("truncated"))
print(report["posture"], "-", report["verdict"])
import { createHash } from "node:crypto";
function idemKey(payload, attempt = 1) {
const basis = JSON.stringify({
d: payload.dump_output, c: payload.case, n: payload.analyst_note || "",
});
const h = createHash("sha256").update(basis).digest("hex").slice(0, 16);
return `ram-triage:${h}:a${attempt}`;
}
const { job_id } = await api("POST", "/run", payload,
{ "Idempotency-Key": idemKey(payload) });
let j;
for (;;) {
j = await api("GET", "/jobs/" + job_id);
if (["succeeded", "failed", "canceled"].includes(j.status)) break;
await new Promise((r) => setTimeout(r, 2000));
}
if (j.status !== "succeeded") throw new Error("run " + j.status);
const report = JSON.parse(j.output.output);
console.log("charged:", j.charged_credits, "truncated:", j.truncated);
console.log(report.posture, "-", report.verdict);
func idemKey(dump string, attempt int) string {
sum := sha256.Sum256([]byte(dump))
return fmt.Sprintf("ram-triage:%x:a%d", sum[:8], attempt)
}
var started struct{ JobID string `json:"job_id"` }
extra := map[string]string{"Idempotency-Key": idemKey(dump, 1)}
if err := api("POST", "/run", payload, extra, &started); err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Charged int64 `json:"charged_credits"`
Truncated bool `json:"truncated"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
for {
if err := api("GET", "/jobs/"+started.JobID, nil, nil, &job); err != nil {
panic(err)
}
if job.Status == "succeeded" || job.Status == "failed" || job.Status == "canceled" {
break
}
time.Sleep(2 * time.Second)
}
fmt.Println(job.Status, job.Charged, job.Truncated)
fmt.Println(job.Output.Output) // the JSON report
MessageDigest md = MessageDigest.getInstance("SHA-256");
String hash = HexFormat.of().formatHex(md.digest(dump.getBytes())).substring(0, 16);
String key = "ram-triage:" + hash + ":a1";
String started = api("POST", "/run", body, Map.of("Idempotency-Key", key));
String jobId = mapper.readTree(started).at("/data/job_id").asText();
JsonNode job;
while (true) {
job = mapper.readTree(api("GET", "/jobs/" + jobId, null, null)).get("data");
String st = job.get("status").asText();
if (st.equals("succeeded") || st.equals("failed") || st.equals("canceled")) break;
Thread.sleep(2000);
}
System.out.println(job.get("status").asText() + " " + job.get("charged_credits").asLong());
System.out.println(job.at("/output/output").asText()); // the JSON report
require "digest"
def idem_key(payload, attempt = 1)
basis = { d: payload[:dump_output], c: payload[:case],
n: payload[:analyst_note].to_s }.to_json
"ram-triage:#{Digest::SHA256.hexdigest(basis)[0, 16]}:a#{attempt}"
end
started = api("POST", "/run", payload, { "Idempotency-Key" => idem_key(payload) })
job_id = started["job_id"]
loop do
@job = api("GET", "/jobs/#{job_id}")
break if %w[succeeded failed canceled].include?(@job["status"])
sleep 2
end
raise "run #{@job['status']}" unless @job["status"] == "succeeded"
report = JSON.parse(@job["output"]["output"])
puts "charged: #{@job['charged_credits']} truncated: #{@job['truncated']}"
puts "#{report['posture']} - #{report['verdict']}"
<?php
function idem_key(array $payload, int $attempt = 1): string {
$basis = json_encode([
"d" => $payload["dump_output"],
"c" => $payload["case"],
"n" => $payload["analyst_note"] ?? "",
]);
return "ram-triage:" . substr(hash("sha256", $basis), 0, 16) . ":a{$attempt}";
}
$started = api("POST", "/run", $payload,
["Idempotency-Key: " . idem_key($payload)]);
$jobId = $started["job_id"];
do {
$job = api("GET", "/jobs/{$jobId}");
if (in_array($job["status"], ["succeeded", "failed", "canceled"], true)) break;
sleep(2);
} while (true);
$report = json_decode($job["output"]["output"], true);
echo "charged: ", $job["charged_credits"], "\n";
echo $report["posture"], " - ", $report["verdict"], "\n";
using System.Security.Cryptography;
using System.Text;
static string IdemKey(string dump, int attempt = 1) {
var h = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(dump)));
return $"ram-triage:{h[..16].ToLowerInvariant()}:a{attempt}";
}
var started = await Api("POST", "/run", payload,
new Dictionary<string, string> { ["Idempotency-Key"] = IdemKey(dump) });
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true) {
job = await Api("GET", "/jobs/" + jobId);
var st = job.GetProperty("status").GetString();
if (st is "succeeded" or "failed" or "canceled") break;
await Task.Delay(2000);
}
var reportText = job.GetProperty("output").GetProperty("output").GetString();
Console.WriteLine(job.GetProperty("charged_credits").GetInt64());
Console.WriteLine(reportText);
truncated comes back true, the balance sat between
min_credits and hold_credits and the output cap was reduced. The
report you got is real but short — render what parsed and say so, rather than
presenting a clipped answer as complete.
6. Stream it instead
/run-stream is the same billable operation delivered as Server-Sent Events, so
a long triage shows progress instead of a spinner. Events carry
event: job when the job is accepted, event: delta with a
{"text":"..."} chunk, and event: done with the settled totals.
Send the same Idempotency-Key you would send to /run.
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: $KEY" \
--data-binary @input.json
import json, urllib.request
req = urllib.request.Request(
API + "/run-stream",
data=json.dumps(payload).encode(),
headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Idempotency-Key": idem_key(payload),
},
method="POST",
)
raw, event = "", None
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
data = json.loads(line[6:])
if event == "delta":
raw += data.get("text", "")
print("\rreceived %d chars" % len(raw), end="")
elif event == "done":
print("\ncharged:", data.get("charged_credits"))
report = json.loads(raw[raw.index("{"): raw.rindex("}") + 1])
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
Authorization: "Bearer " + TOKEN,
"Content-Type": "application/json",
Accept: "text/event-stream",
"Idempotency-Key": idemKey(payload),
},
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
if (event === "delta") raw += data.text || "";
else if (event === "done") console.log("charged:", data.charged_credits);
}
}
}
const report = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", idemKey(dump, 1))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var raw strings.Builder
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: "):
var d struct {
Text string `json:"text"`
Charged int64 `json:"charged_credits"`
}
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
if event == "delta" {
raw.WriteString(d.Text)
} else if event == "done" {
fmt.Println("charged:", d.Charged)
}
}
}
fmt.Println(raw.String())
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
StringBuilder raw = new StringBuilder();
String[] event = { null };
HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.forEach(line -> {
if (line.startsWith("event: ")) {
event[0] = line.substring(7);
} else if (line.startsWith("data: ")) {
try {
JsonNode d = mapper.readTree(line.substring(6));
if ("delta".equals(event[0])) raw.append(d.path("text").asText());
else if ("done".equals(event[0]))
System.out.println("charged: " + d.path("charged_credits").asLong());
} catch (Exception ignored) { }
}
});
System.out.println(raw);
uri = URI("#{API}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Idempotency-Key"] = idem_key(payload)
req.body = payload.to_json
raw = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ")
event = line[7..]
elsif line.start_with?("data: ")
data = JSON.parse(line[6..])
raw << data.fetch("text", "") if event == "delta"
puts "charged: #{data['charged_credits']}" if event == "done"
end
end
end
end
end
report = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
<?php
$raw = "";
$event = null;
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Content-Type: application/json",
"Accept: text/event-stream",
"Idempotency-Key: " . idem_key($payload),
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ")) {
$d = json_decode(substr($line, 6), true);
if ($event === "delta") $raw .= $d["text"] ?? "";
if ($event === "done") echo "charged: ", $d["charged_credits"] ?? 0, "\n";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
$start = strpos($raw, "{");
$report = json_decode(substr($raw, $start, strrpos($raw, "}") - $start + 1), true);
using var req = new HttpRequestMessage(HttpMethod.Post, API + "/run-stream");
req.Headers.Add("Authorization", "Bearer " + token);
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Idempotency-Key", IdemKey(dump));
req.Content = JsonContent.Create(payload);
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var sr = new StreamReader(stream);
var raw = new StringBuilder();
string? evt = null, line;
while ((line = await sr.ReadLineAsync()) != null) {
if (line.StartsWith("event: ")) {
evt = line[7..];
} else if (line.StartsWith("data: ")) {
var d = JsonDocument.Parse(line[6..]).RootElement;
if (evt == "delta" && d.TryGetProperty("text", out var t))
raw.Append(t.GetString());
else if (evt == "done" && d.TryGetProperty("charged_credits", out var c))
Console.WriteLine($"charged: {c.GetInt64()}");
}
}
Console.WriteLine(raw.ToString());
"suspicious_processes",
"iocs", "next_commands" and so on) and showing the raw text
rather than discarding the run you already paid for.
7. The result contract
The model returns one JSON object as text. Strip any code fence, take the
substring from the first { to the last }, and parse — that
is exactly what the app's parseResult() does. Its
normalize() then coerces every field and throws on four
conditions, which are therefore the only hard requirements:
- The reply must be a JSON object at all —
reply was not a JSON object. verdictmust be a non-empty string.exec_summarymust be a non-empty string.next_commandsmust contain at least one entry with a non-emptycommand.
Everything else degrades safely: unknown enum values fall back
(posture to suspicious-needs-followup, a
severity to medium, an iocs[].type to
filename, an injection_findings[].verdict to
inconclusive), missing arrays become empty arrays, and a
pid given as a string is coerced to a number. Empty arrays are valid and are
what a clean capture should produce.
| Field | Type | Notes |
|---|---|---|
case_name | string | Defaults to "Memory triage". |
posture | enum | One of compromise-confirmed, suspicious-needs-followup, no-evidence-found. |
verdict | string | Required. One actionable sentence. |
exec_summary | string | Required. What the capture shows and does not show. |
assumptions, open_questions, containment_steps, next_steps | string[] | Plain lists. |
suspicious_processes | object[] | pid, ppid (numbers), name, path, severity (critical|high|medium|low), technique, why, evidence_quote. |
injection_findings | object[] | pid, process, region, protection, verdict (injected|likely-benign|inconclusive), why. |
network_indicators | object[] | pid, process, local, remote, state, severity, why. |
persistence | object[] | mechanism (dropped if empty), detail, severity, why. |
iocs | object[] | type (ip|domain|path|filename|hash|registry|mutex|port), value (dropped if empty), context, confidence (high|medium|low). |
timeline | object[] | when (defaults to "unknown"), what (dropped if empty), source. |
next_commands | object[] | Required, non-empty. command, plugin, why. |
coverage_check | object | addressed (string[] of prescan flag ids), not_addressed ([{id, why}]), extra_notes (string). |
summary | string | One closing paragraph. |
# report.json was written in step 5. Pull the parts you want:
jq -r '.posture, .verdict' report.json
jq -r '.suspicious_processes[]
| "\(.severity)\t\(.pid)\t\(.name)\t\(.technique)"' report.json
# indicators as a CSV blocklist
jq -r '["type","value","confidence","context"], (.iocs[]
| [.type, .value, .confidence, .context]) | @csv' report.json > iocs.csv
# the follow-up commands, ready to paste into a shell
jq -r '.next_commands[].command' report.json
REQUIRED_ENUMS = {"compromise-confirmed", "suspicious-needs-followup", "no-evidence-found"}
def parse_report(text):
t = text.strip()
if t.startswith("```"):
t = t.split("\n", 1)[1].rsplit("```", 1)[0]
obj = json.loads(t[t.index("{"): t.rindex("}") + 1])
for field in ("verdict", "exec_summary"):
if not str(obj.get(field, "")).strip():
raise ValueError(field + " must be a non-empty string")
if not obj.get("next_commands"):
raise ValueError("next_commands must contain at least one command")
if obj.get("posture") not in REQUIRED_ENUMS:
obj["posture"] = "suspicious-needs-followup"
return obj
report = parse_report(j["output"]["output"])
for p in report["suspicious_processes"]:
print(p["severity"], p["pid"], p["name"], "-", p["technique"])
print(len(report["iocs"]), "indicators;", len(report["next_commands"]), "commands")
const POSTURES = ["compromise-confirmed", "suspicious-needs-followup", "no-evidence-found"];
function parseReport(text) {
let t = String(text).trim().replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "");
const i = t.indexOf("{"), k = t.lastIndexOf("}");
if (i < 0 || k <= i) throw new Error("no JSON object found");
const obj = JSON.parse(t.slice(i, k + 1));
if (!String(obj.verdict || "").trim()) throw new Error("verdict must be non-empty");
if (!String(obj.exec_summary || "").trim()) throw new Error("exec_summary must be non-empty");
if (!(obj.next_commands || []).length) throw new Error("next_commands must be non-empty");
if (!POSTURES.includes(obj.posture)) obj.posture = "suspicious-needs-followup";
return obj;
}
const report = parseReport(j.output.output);
report.suspicious_processes.forEach((p) =>
console.log(p.severity, p.pid, p.name, "-", p.technique));
type Process struct {
PID int `json:"pid"`
PPID int `json:"ppid"`
Name string `json:"name"`
Path string `json:"path"`
Severity string `json:"severity"`
Technique string `json:"technique"`
Why string `json:"why"`
Evidence string `json:"evidence_quote"`
}
type IOC struct {
Type string `json:"type"`
Value string `json:"value"`
Context string `json:"context"`
Confidence string `json:"confidence"`
}
type Report struct {
CaseName string `json:"case_name"`
Posture string `json:"posture"`
Verdict string `json:"verdict"`
ExecSummary string `json:"exec_summary"`
Processes []Process `json:"suspicious_processes"`
IOCs []IOC `json:"iocs"`
NextCommands []struct {
Command string `json:"command"`
Plugin string `json:"plugin"`
Why string `json:"why"`
} `json:"next_commands"`
Summary string `json:"summary"`
}
text := job.Output.Output
var report Report
if err := json.Unmarshal([]byte(text[strings.Index(text, "{"):strings.LastIndex(text, "}")+1]), &report); err != nil {
panic(err)
}
if report.Verdict == "" || report.ExecSummary == "" || len(report.NextCommands) == 0 {
panic("report is missing a required field")
}
for _, p := range report.Processes {
fmt.Printf("%s\t%d\t%s\t%s\n", p.Severity, p.PID, p.Name, p.Technique)
}
String text = job.at("/output/output").asText();
String json = text.substring(text.indexOf('{'), text.lastIndexOf('}') + 1);
JsonNode report = mapper.readTree(json);
if (report.path("verdict").asText().isBlank()
|| report.path("exec_summary").asText().isBlank()
|| !report.path("next_commands").elements().hasNext()) {
throw new IllegalStateException("report is missing a required field");
}
System.out.println(report.get("posture").asText() + " - " + report.get("verdict").asText());
for (JsonNode p : report.path("suspicious_processes")) {
System.out.printf("%s\t%d\t%s\t%s%n",
p.path("severity").asText(), p.path("pid").asInt(),
p.path("name").asText(), p.path("technique").asText());
}
POSTURES = %w[compromise-confirmed suspicious-needs-followup no-evidence-found].freeze
def parse_report(text)
t = text.strip.sub(/\A```[a-z]*\s*/i, "").sub(/```\s*\z/, "")
obj = JSON.parse(t[t.index("{")..t.rindex("}")])
%w[verdict exec_summary].each do |f|
raise "#{f} must be a non-empty string" if obj[f].to_s.strip.empty?
end
raise "next_commands must be non-empty" if Array(obj["next_commands"]).empty?
obj["posture"] = "suspicious-needs-followup" unless POSTURES.include?(obj["posture"])
obj
end
report = parse_report(@job["output"]["output"])
report["suspicious_processes"].each do |p|
puts [p["severity"], p["pid"], p["name"], p["technique"]].join("\t")
end
<?php
const POSTURES = ["compromise-confirmed", "suspicious-needs-followup", "no-evidence-found"];
function parse_report(string $text): array {
$t = trim(preg_replace('/^```[a-z]*\s*/i', "", $text));
$t = preg_replace('/```\s*$/', "", $t);
$start = strpos($t, "{");
$obj = json_decode(substr($t, $start, strrpos($t, "}") - $start + 1), true);
foreach (["verdict", "exec_summary"] as $f) {
if (trim($obj[$f] ?? "") === "") throw new RuntimeException("$f must be non-empty");
}
if (empty($obj["next_commands"])) throw new RuntimeException("next_commands must be non-empty");
if (!in_array($obj["posture"] ?? "", POSTURES, true)) {
$obj["posture"] = "suspicious-needs-followup";
}
return $obj;
}
$report = parse_report($job["output"]["output"]);
foreach ($report["suspicious_processes"] as $p) {
echo implode("\t", [$p["severity"], $p["pid"], $p["name"], $p["technique"]]), "\n";
}
static readonly string[] Postures =
{ "compromise-confirmed", "suspicious-needs-followup", "no-evidence-found" };
static JsonElement ParseReport(string text) {
var t = text.Trim();
if (t.StartsWith("```")) t = t[(t.IndexOf('\n') + 1)..t.LastIndexOf("```")];
var i = t.IndexOf('{');
var doc = JsonDocument.Parse(t[i..(t.LastIndexOf('}') + 1)]);
var root = doc.RootElement;
if (string.IsNullOrWhiteSpace(root.GetProperty("verdict").GetString()))
throw new Exception("verdict must be non-empty");
if (root.GetProperty("next_commands").GetArrayLength() == 0)
throw new Exception("next_commands must be non-empty");
return root;
}
var report = ParseReport(reportText!);
foreach (var p in report.GetProperty("suspicious_processes").EnumerateArray()) {
Console.WriteLine($"{p.GetProperty("severity").GetString()}\t" +
$"{p.GetProperty("pid").GetInt32()}\t" +
$"{p.GetProperty("name").GetString()}");
}
evidence_quote is a verbatim substring of the
dump_output you sent, and no pid appears that is absent from it.
The app checks both and surfaces violations rather than trusting the answer. If you drive
the API directly, do the same — it is a two-line check and it catches the failure
mode that matters.
8. Send prescan_facts
The browser app parses the Volatility tables client-side before running anything and passes
the result in as prescan_facts. It is ground truth produced by a
deterministic parser, not by a model, and the prompt requires the report to reconcile
every flags[].id in coverage_check —
confirmed as a finding, or explicitly dismissed with a reason.
Sending null is legal and the run still works; you simply lose the audit. If
you can compute even a few facts, send them. The shape:
stats—processes,hidden,connections,external,rwx,iocs(all numbers).plugins/missing_plugins— short plugin keys such aspslist,psscan,cmdline,netscan,malfind.flags—[{id, label, detail}], whereidisfamily:key. Families:hidden,masquerade,inject,duplicate,parent,orphan,net,port,lolbin,encoded,destructive,tempexec.hidden_pids— numbers present in psscan but not pslist.clipped—{chars_removed, sections:[{key, kept, dropped}]}. Non-zero means rows were dropped to fit the budget, and the report is told not to read absence as evidence.
jq '.prescan_facts = {
"stats": {"processes":3,"hidden":0,"connections":0,"external":0,"rwx":1,"iocs":0},
"plugins": ["pslist","malfind"],
"missing_plugins": ["psscan","cmdline","netscan"],
"flags": [{"id":"inject:pid3288-0","label":"svchost.exe (PID 3288) 0x1f0000",
"detail":"PAGE_EXECUTE_READWRITE, MZ header present in the region."}],
"hidden_pids": [],
"clipped": {"chars_removed":0,"sections":[]}
}' input.json > input-with-facts.json
payload["prescan_facts"] = {
"stats": {"processes": 3, "hidden": 0, "connections": 0,
"external": 0, "rwx": 1, "iocs": 0},
"plugins": ["pslist", "malfind"],
"missing_plugins": ["psscan", "cmdline", "netscan"],
"flags": [{
"id": "inject:pid3288-0",
"label": "svchost.exe (PID 3288) 0x1f0000",
"detail": "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
}],
"hidden_pids": [],
"clipped": {"chars_removed": 0, "sections": []},
}
report = parse_report(api("POST", "/run", payload,
{"Idempotency-Key": idem_key(payload)}) and "")
# Then assert the audit actually happened:
sent = {f["id"] for f in payload["prescan_facts"]["flags"]}
cov = report["coverage_check"]
handled = set(cov["addressed"]) | {n["id"] for n in cov["not_addressed"]}
assert not (sent - handled), "unreconciled flags: %s" % (sent - handled)
payload.prescan_facts = {
stats: { processes: 3, hidden: 0, connections: 0, external: 0, rwx: 1, iocs: 0 },
plugins: ["pslist", "malfind"],
missing_plugins: ["psscan", "cmdline", "netscan"],
flags: [{
id: "inject:pid3288-0",
label: "svchost.exe (PID 3288) 0x1f0000",
detail: "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
}],
hidden_pids: [],
clipped: { chars_removed: 0, sections: [] },
};
// After the run, assert the audit happened:
const sent = new Set(payload.prescan_facts.flags.map((f) => f.id));
const cov = report.coverage_check;
const handled = new Set([...cov.addressed, ...cov.not_addressed.map((n) => n.id)]);
const missed = [...sent].filter((id) => !handled.has(id));
if (missed.length) console.warn("unreconciled flags:", missed);
payload["prescan_facts"] = map[string]any{
"stats": map[string]int{
"processes": 3, "hidden": 0, "connections": 0,
"external": 0, "rwx": 1, "iocs": 0,
},
"plugins": []string{"pslist", "malfind"},
"missing_plugins": []string{"psscan", "cmdline", "netscan"},
"flags": []map[string]string{{
"id": "inject:pid3288-0",
"label": "svchost.exe (PID 3288) 0x1f0000",
"detail": "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
}},
"hidden_pids": []int{},
"clipped": map[string]any{"chars_removed": 0, "sections": []any{}},
}
Map<String, Object> facts = Map.of(
"stats", Map.of("processes", 3, "hidden", 0, "connections", 0,
"external", 0, "rwx", 1, "iocs", 0),
"plugins", List.of("pslist", "malfind"),
"missing_plugins", List.of("psscan", "cmdline", "netscan"),
"flags", List.of(Map.of(
"id", "inject:pid3288-0",
"label", "svchost.exe (PID 3288) 0x1f0000",
"detail", "PAGE_EXECUTE_READWRITE, MZ header present in the region.")),
"hidden_pids", List.of(),
"clipped", Map.of("chars_removed", 0, "sections", List.of()));
// merge `facts` into the payload map under "prescan_facts" before serialising
payload[:prescan_facts] = {
stats: { processes: 3, hidden: 0, connections: 0, external: 0, rwx: 1, iocs: 0 },
plugins: %w[pslist malfind],
missing_plugins: %w[psscan cmdline netscan],
flags: [{
id: "inject:pid3288-0",
label: "svchost.exe (PID 3288) 0x1f0000",
detail: "PAGE_EXECUTE_READWRITE, MZ header present in the region."
}],
hidden_pids: [],
clipped: { chars_removed: 0, sections: [] }
}
sent = payload[:prescan_facts][:flags].map { |f| f[:id] }
cov = report["coverage_check"]
handled = cov["addressed"] + cov["not_addressed"].map { |n| n["id"] }
warn "unreconciled: #{sent - handled}" unless (sent - handled).empty?
<?php
$payload["prescan_facts"] = [
"stats" => ["processes" => 3, "hidden" => 0, "connections" => 0,
"external" => 0, "rwx" => 1, "iocs" => 0],
"plugins" => ["pslist", "malfind"],
"missing_plugins" => ["psscan", "cmdline", "netscan"],
"flags" => [[
"id" => "inject:pid3288-0",
"label" => "svchost.exe (PID 3288) 0x1f0000",
"detail" => "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
]],
"hidden_pids" => [],
"clipped" => ["chars_removed" => 0, "sections" => []],
];
$sent = array_column($payload["prescan_facts"]["flags"], "id");
$cov = $report["coverage_check"];
$handled = array_merge($cov["addressed"], array_column($cov["not_addressed"], "id"));
$missed = array_diff($sent, $handled);
if ($missed) fwrite(STDERR, "unreconciled: " . implode(", ", $missed) . "\n");
var facts = new {
stats = new { processes = 3, hidden = 0, connections = 0,
external = 0, rwx = 1, iocs = 0 },
plugins = new[] { "pslist", "malfind" },
missing_plugins = new[] { "psscan", "cmdline", "netscan" },
flags = new[] { new {
id = "inject:pid3288-0",
label = "svchost.exe (PID 3288) 0x1f0000",
detail = "PAGE_EXECUTE_READWRITE, MZ header present in the region."
}},
hidden_pids = Array.Empty<int>(),
clipped = new { chars_removed = 0, sections = Array.Empty<object>() }
};
var payloadWithFacts = new {
@case = payload.@case, dump_output = payload.dump_output,
analyst_note = payload.analyst_note, prescan_facts = facts,
current_datetime = payload.current_datetime
};
9. The cases collection
The app declares one collection, cases, with
acl_read: "owner" and acl_write: "user", so each subject sees
only its own records. Declared (queryable) fields:
| Field | Type | Embedded |
|---|---|---|
title | string | yes |
posture | string | no |
findings | number | no |
ran_at | timestamp | no |
host | string | yes |
processes | string | yes |
summary | string | yes |
The full report round-trips inside the document under an undeclared report
key: stored and returned intact, just not filterable. Two rules that cost real debugging
time if missed:
- Every
whereentry must be an operator object.{"posture":{"eq":"compromise-confirmed"}}works; the bare-value shorthand{"posture":"compromise-confirmed"}is rejected. Operators:eq,ne,lt,lte,gt,gte,in,contains. - The sort key is
sort, an object.order_byis silently ignored — the query still succeeds and quietly falls back tocreated_at desc, which only coincidentally matches.
Records nest under doc: read records[i].doc.title, never
records[i].title. Semantic search over the embedded fields lives at
/collections/cases/similar and is limited to 30 requests per minute per IP;
it only ever finds records written after the embed set was declared, because
the platform does not backfill vectors.
# confirmed compromises with at least three findings, newest first
curl -s -X POST "$API/collections/cases/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"where": {"posture": {"eq": "compromise-confirmed"}, "findings": {"gte": 3}},
"sort": {"field": "ran_at", "dir": "desc"},
"limit": 20
}' | jq -r '.data.records[] | "\(.doc.ran_at)\t\(.doc.host)\t\(.doc.title)"'
# semantic search over title, host, processes and summary
curl -s -X POST "$API/collections/cases/similar" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"the workstation with the fake svchost","limit":8}' \
| jq -r '.data.records[] | "\(.score)\t\(.doc.title)"'
res = api("POST", "/collections/cases/query", {
"where": {"posture": {"eq": "compromise-confirmed"}, "findings": {"gte": 3}},
"sort": {"field": "ran_at", "dir": "desc"}, # NOT order_by
"limit": 20,
})
for rec in res["records"]:
doc = rec["doc"] # records nest under `doc`
print(doc["ran_at"], doc["host"], doc["title"], doc["findings"])
hits = api("POST", "/collections/cases/similar",
{"text": "the workstation with the fake svchost", "limit": 8})
for rec in hits["records"]:
print(round(rec["score"], 3), rec["doc"]["title"])
const res = await api("POST", "/collections/cases/query", {
where: { posture: { eq: "compromise-confirmed" }, findings: { gte: 3 } },
sort: { field: "ran_at", dir: "desc" }, // NOT order_by
limit: 20,
});
for (const rec of res.records) {
const doc = rec.doc; // records nest under `doc`
console.log(doc.ran_at, doc.host, doc.title, doc.findings);
}
const hits = await api("POST", "/collections/cases/similar",
{ text: "the workstation with the fake svchost", limit: 8 });
hits.records.forEach((r) => console.log(r.score.toFixed(3), r.doc.title));
query := map[string]any{
"where": map[string]any{
"posture": map[string]string{"eq": "compromise-confirmed"},
"findings": map[string]int{"gte": 3},
},
"sort": map[string]string{"field": "ran_at", "dir": "desc"},
"limit": 20,
}
var page struct {
Records []struct {
RecordID string `json:"record_id"`
Score float64 `json:"score"`
Doc struct {
Title string `json:"title"`
Host string `json:"host"`
Posture string `json:"posture"`
Findings int `json:"findings"`
RanAt string `json:"ran_at"`
} `json:"doc"`
} `json:"records"`
}
// NOTE: the cursor is NOT in `data`. It lives at meta.pagination.next_cursor,
// alongside meta.pagination.has_more, so a paging client must read the full
// envelope rather than just the unwrapped data object.
if err := api("POST", "/collections/cases/query", query, nil, &page); err != nil {
panic(err)
}
for _, r := range page.Records {
fmt.Println(r.Doc.RanAt, r.Doc.Host, r.Doc.Title)
}
String query = """
{"where":{"posture":{"eq":"compromise-confirmed"},"findings":{"gte":3}},
"sort":{"field":"ran_at","dir":"desc"},
"limit":20}
""";
JsonNode page = mapper.readTree(
api("POST", "/collections/cases/query", query, null)).get("data");
for (JsonNode rec : page.path("records")) {
JsonNode doc = rec.path("doc"); // records nest under `doc`
System.out.printf("%s\t%s\t%s%n",
doc.path("ran_at").asText(), doc.path("host").asText(),
doc.path("title").asText());
}
res = api("POST", "/collections/cases/query", {
where: { posture: { eq: "compromise-confirmed" }, findings: { gte: 3 } },
sort: { field: "ran_at", dir: "desc" }, # NOT order_by
limit: 20
})
res["records"].each do |rec|
doc = rec["doc"] # records nest under `doc`
puts [doc["ran_at"], doc["host"], doc["title"], doc["findings"]].join("\t")
end
hits = api("POST", "/collections/cases/similar",
{ text: "the workstation with the fake svchost", limit: 8 })
hits["records"].each { |r| puts "#{r['score'].round(3)}\t#{r['doc']['title']}" }
<?php
$res = api("POST", "/collections/cases/query", [
"where" => ["posture" => ["eq" => "compromise-confirmed"],
"findings" => ["gte" => 3]],
"sort" => ["field" => "ran_at", "dir" => "desc"], // NOT order_by
"limit" => 20,
]);
foreach ($res["records"] as $rec) {
$doc = $rec["doc"]; // records nest under `doc`
echo implode("\t", [$doc["ran_at"], $doc["host"], $doc["title"]]), "\n";
}
$hits = api("POST", "/collections/cases/similar",
["text" => "the workstation with the fake svchost", "limit" => 8]);
foreach ($hits["records"] as $r) {
printf("%.3f\t%s\n", $r["score"], $r["doc"]["title"]);
}
var page = await Api("POST", "/collections/cases/query", new {
where = new {
posture = new { eq = "compromise-confirmed" },
findings = new { gte = 3 }
},
sort = new { field = "ran_at", dir = "desc" }, // NOT order_by
limit = 20
});
foreach (var rec in page.GetProperty("records").EnumerateArray()) {
var doc = rec.GetProperty("doc"); // records nest under `doc`
Console.WriteLine($"{doc.GetProperty("ran_at").GetString()}\t" +
$"{doc.GetProperty("host").GetString()}\t" +
$"{doc.GetProperty("title").GetString()}");
}
var hits = await Api("POST", "/collections/cases/similar",
new { text = "the workstation with the fake svchost", limit = 8 });
foreach (var r in hits.GetProperty("records").EnumerateArray()) {
Console.WriteLine($"{r.GetProperty("score").GetDouble():F3}\t" +
$"{r.GetProperty("doc").GetProperty("title").GetString()}");
}
limit capped at 100, 20 values per in, 10
where fields. The app trims the stored paste to about 14 KB and drops it
entirely if the report alone would breach the document cap — the report is what a
restore needs, the paste is a convenience.
What this app will not do
RAM Triage reads plugin output that you paste or post. It does not acquire memory, does not parse a raw image, and does not execute Volatility or anything else — the runtime is a static bundle under a strict CSP with no server. The report is instructed never to claim it ran a command, never to cite a PID absent from your input, and never to attribute activity to a named threat actor. Triage is only as wide as the plugins you pasted, and a clean result across five plugins is not a clean host.