Engagement & RoE API
HTTP reference for Rules of Engagement, the kill switch, and in-run approval decisions that keep a pentest fail-closed.
All paths are relative to https://api.aleex-rank.ai/api/v2 and authenticate with X-API-Key: rk_... (see REST API). These endpoints bind a pentest to an active Rules of Engagement (RoE), stop it immediately with the kill switch, and decide in-run approvals. The model is described in Engagement, RoE & kill switch.
The orchestrator is fail-closed: it will not start a pentest that has no active RoE, and a version cannot be activated unless every pentest asset falls inside that scope. Widening the RoE of a running pentest requires an approved scope_expansion.
Rules of Engagement
GET /pentests/{id}/roe
POST /pentests/{id}/roe
PUT /pentests/{id}/roe
POST creates a new version. PUT replaces the current draft. activate is a control flag on write — it is not stored on the object. Send activate: true to make the version active once validation passes.
At least one of allowed_cidrs or allowed_domains must be a non-empty list. Public IPv4 entries in allowed_cidrs must be /16 or more specific; public IPv6 must be /32 or more specific. Private ranges are exempt from that floor.
| Field | Type | Required | Notes |
|---|---|---|---|
allowed_cidrs | string[] | Conditional | CIDRs in scope; at least one allow list must be non-empty |
allowed_domains | string[] | Conditional | Domains in scope |
denied_cidrs | string[] | No | Always excluded, even if also allowed |
denied_domains | string[] | No | Always excluded |
allowed_techniques | string[] | No | ATT&CK ids Txxxx or Txxxx.xxx |
forbidden_techniques | string[] | No | ATT&CK ids the engagement must not use |
source_ips | string[] | No | Source addresses the orchestrator may use |
time_windows | object[] | No | Each {start, end, days}; start/end are HH:MM |
timezone | string | Conditional | IANA zone; required when time_windows is set |
asset_criticality | object | No | Map of asset → critical | high | medium | low | info |
max_rps | int | No | Request-rate ceiling |
max_concurrency | int | No | Parallel-action ceiling |
authorization_ref | string | Yes | Ticket, memo or contract id that authorizes the test |
escalation_contacts | object[] | Yes | Non-empty. type is email, slack or webhook; value is the target |
requires_approval_for | string[] | No | shell, script, destructive, exploit, scope_expansion, low_scope_confidence |
auto_approve | bool | No | When true, listed classes may auto-approve — never scope_expansion |
activate | bool | No | Write-only control flag; omitted on reads |
days on a window is all or an array of mon…sun.
{
"allowed_cidrs": ["203.0.113.0/24"],
"allowed_domains": ["acme.example.com"],
"denied_cidrs": ["203.0.113.10/32"],
"denied_domains": ["admin.acme.example.com"],
"allowed_techniques": ["T1190", "T1059.001"],
"forbidden_techniques": ["T1498", "T1565"],
"source_ips": ["198.51.100.20"],
"time_windows": [
{"start": "09:00", "end": "18:00", "days": ["mon", "tue", "wed", "thu", "fri"]}
],
"timezone": "Europe/Madrid",
"asset_criticality": {"acme.example.com": "high", "203.0.113.10": "critical"},
"max_rps": 10,
"max_concurrency": 4,
"authorization_ref": "ROE-2026-014",
"escalation_contacts": [
{"type": "email", "value": "soc@acme.example"},
{"type": "slack", "value": "#security-escalations"}
],
"requires_approval_for": ["shell", "destructive", "scope_expansion"],
"auto_approve": false,
"activate": true
}
POST returns 201 with the stored version (no activate field). Activation that would leave an asset outside scope, or a public allow-list that is too broad, fails with 400.
GET returns the active RoE, the version history, and the approval classes this pentest may request:
{
"success": true,
"data": {
"active": {
"version": 2,
"allowed_cidrs": ["203.0.113.0/24"],
"allowed_domains": ["acme.example.com"],
"denied_cidrs": ["203.0.113.10/32"],
"denied_domains": ["admin.acme.example.com"],
"authorization_ref": "ROE-2026-014",
"escalation_contacts": [
{"type": "email", "value": "soc@acme.example"}
],
"requires_approval_for": ["shell", "destructive", "scope_expansion"],
"auto_approve": false,
"timezone": "Europe/Madrid",
"activated_at": "2026-03-02 09:00:00"
},
"versions": [
{"version": 1, "status": "superseded", "created_at": "2026-03-01 18:12:00"},
{"version": 2, "status": "active", "created_at": "2026-03-02 09:00:00"}
],
"available_approval_classes": [
{"id": "shell", "description": "Interactive or one-shot shell commands (shell_tool)"},
{"id": "script", "description": "Interpreter scripts (script_tool)"},
{"id": "destructive", "description": "Shell or script whose payload looks destructive"},
{"id": "exploit", "description": "Active exploitation tools (sqlmap, commix, hydra, metasploit, and similar)"},
{"id": "scope_expansion", "description": "Widening the active RoE of a pentest that is already running"},
{"id": "low_scope_confidence", "description": "An action whose target could not be confidently matched to the authorised scope"}
]
}
}
Class ids match GET /catalogs/approval-classes. Adding CIDRs or domains to a running pentest is a widening: submit the new version, then approve the resulting scope_expansion request before it can become active.
Kill switch
POST /pentests/{id}/kill
Stops the engagement immediately on the REST control plane. reason is optional, max 500 characters:
{"reason": "Traffic observed against an out-of-scope host"}
{
"success": true,
"data": {
"message": "Kill requested",
"pentest_id": 15,
"kill_requested": true,
"kill_requested_at": "2026-03-02T11:20:00+01:00",
"reason": "Traffic observed against an out-of-scope host"
}
}
This is not POST https://aleex.aleex-rank.ai/pentest/{id}/cancel, which asks the streaming backend to cancel an in-flight run. Use /kill when the RoE is breached or you need a hard stop regardless of stream state.
Approvals
GET /pentests/{id}/approvals
POST /pentests/{id}/approvals/{approvalId}/decide
GET lists in-run approval requests. Filter with ?status=pending (also approved, denied).
{
"success": true,
"data": {
"approvals": [
{
"id": 88,
"pentest_id": 15,
"action_class": "scope_expansion",
"action_summary": "Add 198.51.100.0/24 to allowed_cidrs",
"target": "198.51.100.0/24",
"status": "pending",
"requested_at": "2026-03-02 11:04:00",
"auto_approved": false
}
],
"total": 1
}
}
Decide with decision approve or deny; reason is optional:
{"decision": "approve", "reason": "Change ticket CHG-441 covers the extra subnet"}
{
"success": true,
"data": {
"id": 88,
"action_class": "scope_expansion",
"status": "approved",
"decided_at": "2026-03-02 11:12:00"
}
}
scope_expansion is never auto-approved, even when auto_approve is true.