Drive Intrusion Phase Map from your own code
The page does its free work in the browser: it reads the intrusion timeline (one event per line with a timestamp, or a
pipe, tab or CSV table with a time and an event column) and the controls list, and works out each event's intrusion
phase and ATT&CK tactics, whether it was detected, blocked or missed, the indicators it names, each phase's status,
the furthest phase, the first detection, the dwell time, each control's phases and course-of-action kinds and flags
KC-01..KC-14. Its two paid lanes are available over HTTP. You send one JSON object with task set to
"map" or "coa", and the model replies with one JSON object. The easiest way to build the
facts string is the page itself:
ChainKit.buildInput(lane, ChainKit.analyze(timeline, controls, {incident, question}), {handoff})
in chainkit.js, which also runs under Node.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt. The output analyses the timeline as written and recommends defensive measures only.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"map" | How far the adversary got: the seven intrusion phases in order, each with a status, its events, ATT&CK tactics, a word-for-word quote, what detected or stopped it and the defensive gap; every event placed once or unplaced with a reason; the verdict, the furthest phase, the first detection, every page flag answered, the break point and the intrusion told in order. |
"coa" | The courses of action: a seven-by-six matrix (detect, deny, disrupt, degrade, deceive, destroy per phase) marking controls already in place, and a roadmap of 3-12 actions sorted now, next quarter, later, every phase passed unseen answered by a detect or deny action. |
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | map or coa |
incident | optional | A name for the incident. |
timeline | required | The events as the page read them, one per line: E1: 2026-09-09 08:14 Phishing email .... Up to 120 events; past that the middle is dropped and a marker line says how many. |
controls | required (may be empty) | The controls, numbered C1: ... by the page; up to 40. |
facts | required | A JSON string: incident, events[{id, ts, phase, also[], tactics[], detection, iocs}], phases[{id, status, events[], detected[], blocked[], controls[]}], furthest, verdict, first_detection{phase, event, ts}, dwell_hours, dwell_until, sequence, controls[{id, state, phases[], coas[]}], grid[], flags[{id, severity, events[], text}], notes[]. |
question | optional | What you want to know; answered in summary. |
map_handoff | coa, optional | A JSON string of the map the plan follows: verdict, furthest_phase, first_detection, phases[{phase, status, events[], detection, gap}], break_point. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contracts
Both lanes share an envelope: task, task_note, headline, questions[], summary. Phase ids: recon, weaponization, delivery, exploitation, installation, c2, actions.
task: "map"
verdict: objectives reached|stopped late|stopped early|still open|unclear, furthest_phase, first_detection{phase, event}, flags[{flag, response: confirmed|disputed, note}] (one per page flag, in order), phases[{phase, status: completed undetected|completed detected|stopped here|still open|no evidence|not reached, events[], tactics[], techniques[], evidence, detection, gap}] (exactly 7, in order), unplaced[{event, why}], break_point{phase, why}, narrative[] (3-8 sentences).
task: "coa"
source (map handoff|from the input), matrix[{phase, detect, deny, disrupt, degrade, deceive, destroy, in_place[]}] (exactly 7, in order), actions[{id, phase, coa, action, why, events[], controls[], cost, effect, horizon: now|next quarter|later}] (3-12, A1.., sorted by horizon).
Worked examples
These are the page's own bundled examples (invented organisations, hosts and addresses), with the input as app.js builds it and the saved model reply the page replays for free: a map of a freight company's ransomware intrusion and its courses of action after the handoff, a map of a VPN appliance compromise, and a plan after a password spray.
Map: ransomware at a freight company
Input (each pasted text shown as its lines; facts shortened here):
{
"task": "map",
"incident": "Vantolle Freight ransomware, September 2026",
"timeline": [
"E1: 2026-09-02 Threat intel later showed the look-alike domain vantolle-invoices.example[.]com was registered on this date",
"E2: 2026-09-09 08:14 Phishing email with attachment Invoice_4471.docm delivered to j.alvarez from billing@vantolle-invoices.example[.]com; the email gateway scored it clean",
"E3: 2026-09-09 08:31 J. Alvarez opened the attachment and enabled macros; WINWORD.EXE spawned powershell.exe with an encoded command",
"E4: 2026-09-09 08:32 powershell.exe downloaded a DLL from hxxps://cdn-sync-update.example[.]net/a.dll and ran it with rundll32",
"E5: 2026-09-09 08:33 Scheduled task \"OneDriveSyncCheck\" created on FIN-WS-22 to run the DLL at logon",
"... (13 lines in all)"
],
"controls": [
"C1: Secure email gateway with attachment sandboxing (macro documents are allowed through)",
"C2: Office macros are not blocked for files from the internet",
"C3: EDR on all Windows endpoints in detect-only mode; alerts below high are auto-closed by a tuning rule",
"C4: Web proxy logs all outbound HTTPS; uncategorised sites are allowed",
"C5: No egress filtering on servers",
"... (9 lines in all)"
],
"facts": "(a JSON string; decoded: 13 events, furthest phase actions, page reading objectives reached, first detection E7, dwell 53.8 hours; phases completed undetected: weaponization, delivery, exploitation, installation, c2; 9 controls; flags KC-01, KC-03, KC-04, KC-05, KC-08)",
"question": "Where was the earliest point we could realistically have stopped this?"
}
Reply:
{
"task": "map",
"task_note": "",
"verdict": "objectives reached",
"furthest_phase": "actions",
"first_detection": {
"phase": "actions",
"event": "E7"
},
"flags": [
{
"flag": "KC-01",
"response": "confirmed",
"note": "E10 staged about 38 GB, E11 uploaded the archives to cloud storage and E12 encrypted FILE-SRV-01 and 41 workstations before E13 isolated FILE-SRV-01."
},
{
"flag": "KC-03",
"response": "confirmed",
"note": "E7 is the first event that says anything was detected; E2 to E6 show no alert."
},
{
"flag": "KC-04",
"response": "confirmed",
"note": "From E2 to E7 is 53.8 hours; the E7 alert was auto-closed, so no analyst acted until E13."
},
{
"flag": "KC-05",
"response": "confirmed",
"note": "The E7 EDR alert was auto-closed by the C3 tuning rule; E8 followed the same afternoon and the attack ran on to E12."
},
{
"flag": "KC-08",
"response": "confirmed",
"note": "E3 is placed in exploitation only; C1 scored E2 clean, C3 is detect-only, C4 allowed the uncategorised C2 traffic in E6."
}
],
"phases": [
{
"phase": "recon",
"status": "no evidence",
"events": [],
"tactics": [],
"techniques": [],
"evidence": "",
"detection": "",
"gap": ""
},
{
"phase": "weaponization",
"status": "completed undetected",
"events": [
"E1"
],
"tactics": [
"Resource Development (TA0042)"
],
"techniques": [
"T1583.001 Acquire Infrastructure: Domains"
],
"evidence": "Threat intel later showed the look-alike domain vantolle-invoices.example[.]com was registered on this date",
"detection": "",
"gap": "No listed control covers this phase; the look-alike domain was only found later through threat intel, so there was no monitoring of newly registered domains resembling the company's."
},
{
"phase": "delivery",
"status": "completed undetected",
"events": [
"E2"
],
"tactics": [
"Initial Access (TA0001)"
],
"techniques": [
"T1566.001 Phishing: Spearphishing Attachment"
],
"evidence": "Phishing email with attachment Invoice_4471.docm delivered to j.alvarez from billing@vantolle-invoices.example[.]com; the email gateway scored it clean",
"detection": "",
"gap": "C1 lets macro documents through and scored the .docm from a look-alike sender domain clean; C8 MFA does not act on a malicious attachm
... (shortened here; the page shows it whole)
Courses of action: the same timeline, with the map handed over
Input (with the map handed over; texts shown as lines):
{
"task": "coa",
"incident": "Vantolle Freight ransomware, September 2026",
"timeline": [
"E1: 2026-09-02 Threat intel later showed the look-alike domain vantolle-invoices.example[.]com was registered on this date",
"E2: 2026-09-09 08:14 Phishing email with attachment Invoice_4471.docm delivered to j.alvarez from billing@vantolle-invoices.example[.]com; the email gateway scored it clean",
"E3: 2026-09-09 08:31 J. Alvarez opened the attachment and enabled macros; WINWORD.EXE spawned powershell.exe with an encoded command",
"E4: 2026-09-09 08:32 powershell.exe downloaded a DLL from hxxps://cdn-sync-update.example[.]net/a.dll and ran it with rundll32",
"E5: 2026-09-09 08:33 Scheduled task \"OneDriveSyncCheck\" created on FIN-WS-22 to run the DLL at logon",
"... (13 lines in all)"
],
"controls": [
"C1: Secure email gateway with attachment sandboxing (macro documents are allowed through)",
"C2: Office macros are not blocked for files from the internet",
"C3: EDR on all Windows endpoints in detect-only mode; alerts below high are auto-closed by a tuning rule",
"C4: Web proxy logs all outbound HTTPS; uncategorised sites are allowed",
"C5: No egress filtering on servers",
"... (9 lines in all)"
],
"facts": "(a JSON string; decoded: 13 events, furthest phase actions, page reading objectives reached, first detection E7, dwell 53.8 hours; phases completed undetected: weaponization, delivery, exploitation, installation, c2; 9 controls; flags KC-01, KC-03, KC-04, KC-05, KC-08)",
"question": "Where was the earliest point we could realistically have stopped this?",
"map_handoff": "{\"verdict\":\"objectives reached\",\"furthest_phase\":\"actions\",\"first_detection\":{\"phase\":\"actions\",\"event\":\"E7\"},\"phases\":[{\"phase\":\"recon\",\"status\":\"no evidence\",\"events\":[],\"detection\":\"\",\"gap\":\"\"},{\"phase\":\"weaponization\",\"status\":\"completed undetected\",\"events\":[\"E1\"],\"detection\":\"\",\"gap\":\"No listed control covers thi..."
}
Reply:
{
"task": "coa",
"task_note": "",
"source": "map handoff",
"matrix": [
{
"phase": "recon",
"detect": "Use threat intel to watch for exposure of finance staff addresses such as j.alvarez that invoice lures target",
"deny": "Limit public listing of finance staff names and mailbox addresses used for invoice lures",
"disrupt": "",
"degrade": "",
"deceive": "Publish decoy finance contact addresses and alert on any mail sent to them",
"destroy": "",
"in_place": []
},
{
"phase": "weaponization",
"detect": "Monitor newly registered domains resembling vantolle-freight; E1 was registered before E2 and found only later",
"deny": "In place: C1 gateway; add a block on newly registered look-alike domains such as vantolle-invoices.example[.]com",
"disrupt": "",
"degrade": "",
"deceive": "",
"destroy": "Ask the registrar to take down vantolle-invoices.example[.]com and refer it to law enforcement",
"in_place": []
},
{
"phase": "delivery",
"detect": "In place: C1 gateway scored E2 clean; alert on external .docm attachments from newly seen sender domains",
"deny": "In place: C1 sandboxing; change policy to quarantine macro documents such as Invoice_4471.docm from external senders",
"disrupt": "Add an external-sender banner and a one-click phishing report button for finance staff such as j.alvarez",
"degrade": "Strip macros or convert macro attachments to macro-free formats at the gateway before delivery",
"deceive": "Seed decoy finance mailboxes so invoice lures sent to them raise an alert",
"destroy": "Report billing@vantolle-invoices.example[.]com to the registrar and mail provider for takedown",
"in_place": [
"C1",
"C4",
"C8"
]
},
{
"phase": "exploitation",
"detect": "In place: C3 EDR detect-only; raise WINWORD.EXE spawning encoded powershell.exe and rundll32 running a downloaded DLL to high",
"deny": "Block macros in Office files from the internet by group policy so E3 could not run",
"disrupt": "Block Office apps from creating child processes and constrain PowerShell on finance workstations",
"degrade": "Remove local admin rights from finance workstations to limit what the E4 DLL can do",
"deceive": "",
"destroy": "Report cdn-sync-update.example[.]net to its host and registrar for takedown",
"in_place": [
"C1",
"C3",
"C6"
]
},
... (shortened here; the page shows it whole)
Map: a VPN appliance exploited, with an instruction hidden in the controls
Input (texts shown as lines):
{
"task": "map",
"incident": "Dunmarra Clinic VPN appliance compromise",
"timeline": [
"E1: 2026-08-28 Port scan of our VPN gateway on 443 and 8443 from 198.51.100.23 seen in the perimeter firewall logs",
"E2: 2026-09-03 22:41 Exploitation of CVE-2026-9990417 on the VPN appliance (vendor advisory published 2026-08-30, patch not yet applied)",
"E3: 2026-09-03 22:43 A web shell help.jsp was dropped in the appliance's web root",
"E4: 2026-09-03 23:10 The attacker ran commands through the web shell to list local users and the appliance configuration",
"E5: 2026-09-03 23:12 Created a local admin account \"vpnsupport\" on the appliance",
"... (9 lines in all)"
],
"controls": [
"C1: Perimeter firewall: default-deny egress from the DMZ, deny events forwarded to the SIEM",
"C2: VPN appliance patching within 30 days of a vendor advisory",
"C3: No web application firewall in front of the VPN portal",
"C4: No file integrity monitoring on the appliance",
"C5: SIEM with a 24x7 on-call analyst",
"... (7 lines in all)"
],
"facts": "(a JSON string; decoded: 9 events, furthest phase c2, page reading stopped late, first detection E6, dwell 1.4 hours; phases completed undetected: recon, exploitation, installation; 7 controls; flags KC-03, KC-06, KC-08, KC-09, KC-11)"
}
Reply:
{
"task": "map",
"task_note": "",
"verdict": "stopped late",
"furthest_phase": "c2",
"first_detection": {
"phase": "c2",
"event": "E6"
},
"flags": [
{
"flag": "KC-03",
"response": "confirmed",
"note": "E6 is the first event that says anything was blocked or detected; E2 to E5 ran unseen before it."
},
{
"flag": "KC-06",
"response": "confirmed",
"note": "No separate delivery event is listed. E2, the exploitation of CVE-2026-9990417 on the internet-facing VPN appliance, most likely was the delivery as well; the timeline does not show it separately."
},
{
"flag": "KC-08",
"response": "confirmed",
"note": "C2 allows patching within 30 days of a vendor advisory; the advisory was published 2026-08-30 and E2 came on 2026-09-03 with the patch not yet applied, so C2 was followed as written but its window was too long to stop E2."
},
{
"flag": "KC-09",
"response": "confirmed",
"note": "C4 states there is no file integrity monitoring on the appliance, and no listed control watches for local account creation (E5) or web shell commands (E4)."
},
{
"flag": "KC-11",
"response": "confirmed",
"note": "E7, E8 and E9 are the defender's own triage, containment and recovery actions; they are listed under unplaced."
}
],
"phases": [
{
"phase": "recon",
"status": "completed undetected",
"events": [
"E1"
],
"tactics": [
"Reconnaissance (TA0043)"
],
"techniques": [
"T1595 Active Scanning"
],
"evidence": "Port scan of our VPN gateway on 443 and 8443 from 198.51.100.23 seen in the perimeter firewall logs",
"detection": "",
"gap": "The scan from 198.51.100.23 is in the perimeter firewall logs, but the timeline does not show it raising an alert or any response. C6 (quarterly external attack surface scanning) finds your own exposure, not inbound scanning, so no listed control turns a scan of the VPN gateway into action."
},
{
"phase": "weaponization",
"status": "no evidence",
"events": [],
"tactics": [],
"techniques": [],
"evidence": "",
"detection": "",
"gap": ""
},
{
"phase": "delivery",
"status": "no evidence",
"events": [],
"tactics": [],
"techniques": [],
"evidence": "",
"detection": "",
"gap": ""
},
{
"phase": "exploitation",
"status": "completed undetected",
"events": [
"E2"
],
"t
... (shortened here; the page shows it whole)
Courses of action: a password spray on a VPN with no MFA
Input (texts shown as lines):
{
"task": "coa",
"incident": "Pellwick Software VPN intrusion",
"timeline": [
"E1: 2026-09-20 03:12 | VPN portal | Password spray against the VPN portal from 192.0.2.15: 2,300 attempts over 40 minutes, no lockout",
"E2: 2026-09-20 03:52 | VPN portal | Successful VPN login as m.chen from 192.0.2.15 (no MFA on the VPN)",
"E3: 2026-09-20 04:05 | DEV-07 | RDP to DEV-07 as m.chen; the attacker enumerated the domain users and groups",
"E4: 2026-09-20 04:20 | DEV-07 | Installed a remote access tool as a new service \"UpdaterSvc\"",
"E5: 2026-09-20 04:22 | DEV-07 | Outbound TLS connections to sync-cdn.example[.]org every 5 minutes",
"... (7 lines in all)"
],
"controls": [
"C1: VPN login: password only, no MFA, no account lockout",
"C2: EDR on workstations in block mode for known malware",
"C3: NDR sensor at the internet edge, alerts to the SOC",
"C4: No application allowlisting on developer machines",
"C5: DNS filtering on the corporate resolver (categorised domains only)",
"... (6 lines in all)"
],
"facts": "(a JSON string; decoded: 7 events, furthest phase actions, page reading stopped late, first detection E6, dwell 2.5 hours; phases completed undetected: delivery, installation; 6 controls; flags KC-01, KC-03, KC-06, KC-07, KC-08, KC-09, KC-11, KC-12)",
"question": "What should we fix first with one engineer for a month?"
}
Reply:
{
"task": "coa",
"task_note": "",
"source": "from the input",
"matrix": [
{
"phase": "recon",
"detect": "Use external threat intelligence on 192.0.2.15 and similar sources probing the VPN portal before a spray starts",
"deny": "Keep staff usernames used for VPN login out of public view where possible",
"disrupt": "",
"degrade": "",
"deceive": "",
"destroy": "",
"in_place": []
},
{
"phase": "weaponization",
"detect": "",
"deny": "",
"disrupt": "",
"degrade": "",
"deceive": "",
"destroy": "",
"in_place": []
},
{
"phase": "delivery",
"detect": "Alert the SOC on many failed VPN logins from one source, such as 2,300 attempts from 192.0.2.15 over 40 minutes",
"deny": "Require MFA on the VPN portal and add account lockout after repeated failed logins",
"disrupt": "Automatically block a source IP at the VPN portal once it sprays passwords across accounts",
"degrade": "Rate-limit VPN login attempts per source and per account to slow password sprays",
"deceive": "Create decoy VPN accounts that alert on any login attempt",
"destroy": "Send an abuse report for 192.0.2.15 to its hosting provider",
"in_place": []
},
{
"phase": "exploitation",
"detect": "Alert when a new VPN session is followed within minutes by RDP to a developer machine such as DEV-07",
"deny": "Allow RDP from VPN sessions to developer machines only for named accounts that need it",
"disrupt": "",
"degrade": "",
"deceive": "",
"destroy": "",
"in_place": []
},
{
"phase": "installation",
"detect": "In place: C2 EDR, blocks known malware only; add alerts on new services such as \"UpdaterSvc\" on developer machines",
"deny": "Add application allowlisting on developer machines so unapproved remote access tools cannot run",
"disrupt": "Have EDR isolate a host when an unapproved remote access tool is installed as a service",
"degrade": "Remove local admin rights from developer accounts so they cannot install services",
"deceive": "",
"destroy": "",
"in_place": [
"C2"
]
},
{
"phase": "c2",
"detect": "In place: C3 NDR, alerted on beaconing at 05:40 but the SOC triaged it at 06:10; set a shorter triage target",
"deny": "In place: C5 DNS filtering, categorised domains only; also block uncategorised domains such as sync-cdn.example[.]org",
"disrupt": "Le
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js). For a map: the seven phases in order; every event placed exactly once, in a phase or unplaced with a reason; each status consistent with its events and the furthest phase; the verdict the phases allow; the first detection a real event placed where the reply says; ATT&CK tactics written Name (TA0000) and fitting their phase; technique ids well formed; a gap for every phase completed undetected; one answer per page flag, in order; the break point a phase with events. For courses of action: the source as sent; seven matrix rows in order; every "In place" cell naming a control id; 3 to 12 actions numbered A1.. sorted now, next quarter, later; every phase passed unseen answered by a detect or deny action; nothing that attacks or hacks back. In both, quotes are checked word for word, every event and control id must be the page's, and no figure the input or the page's reading does not contain may appear. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same input into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://intrusion-phase-map.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://intrusion-phase-map.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// intrusion-phase-map.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://intrusion-phase-map.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://intrusion-phase-map.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// IntrusionPhaseMap.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class IntrusionPhaseMap {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://intrusion-phase-map.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://intrusion-phase-map.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://intrusion-phase-map.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://intrusion-phase-map.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"intrusion-phase-map"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "intrusion-phase-map"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "intrusion-phase-map" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "intrusion-phase-map"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "intrusion-phase-map"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "intrusion-phase-map" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "intrusion-phase-map"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "intrusion-phase-map" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "map", "incident": "...", "timeline": "...", "controls": "...", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "map", "incident": "...", "timeline": "...", "controls": "...", "facts": "...", ...}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, incident, timeline, controls, facts, question, ...}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses intrusion-phase-map:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="intrusion-phase-map:map:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "intrusion-phase-map:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("headline"))
import { createHash } from "node:crypto";
const key = `intrusion-phase-map:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.headline);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "intrusion-phase-map:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "intrusion-phase-map:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "intrusion-phase-map:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["headline"]].inspect
$key = "intrusion-phase-map:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["headline"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"intrusion-phase-map:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream (coa)
The same run as Server-Sent Events. coa.json is a courses-of-action input, with map_handoff when it follows a map.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: intrusion-phase-map:coa:$(shasum -a 256 coa.json | cut -c1-16):a1" --data @coa.json
rev = json.load(open("coa.json")) # {"task": "coa", "incident": "...", "timeline": "...", "controls": "...", "facts": "...", "map_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "intrusion-phase-map:coa:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("coa.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `intrusion-phase-map:coa:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("coa.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "intrusion-phase-map:coa:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("coa.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "intrusion-phase-map:coa:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("coa.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "intrusion-phase-map:coa:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("coa.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: intrusion-phase-map:coa:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("coa.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"intrusion-phase-map:coa:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);