Guided pentest, phase by phase
Assign agents per phase and drive a pentest one phase at a time with full control.
What you’ll build
A guided pentest where you stay in the loop. Unlike automatic mode, guided mode hands control back to you between phases, so you choose which agents run in each phase and review activity before continuing. You will activate a Rules of Engagement, assign agents to phases 1 through 3, stream each phase, and then turn the raw agent output into structured vulnerabilities.
Without an active RoE the orchestrator will not start (no_active_roe).
Prerequisites
- A Rank account and an API token (see Authentication).
- Enough pentest agents available for each phase — a phase needs 3 to 4 agents on first assignment.
pip install rank-sdk
export RANK_API_KEY=rk_...
Steps
-
Create a chat session and a guided web pentest. The
chat_idcarries context across phases.import rank client = rank.Rank() chat = client.chats.create(nombre="Guided pentest - example.com") pentest = client.pentests.create( name="Guided pentest", url="https://example.com", type="web", mode="guided", methodology_id=1, assets=[ {"asset_type": "url", "asset_value": "https://example.com", "is_primary": True}, ], ) -
Activate a Rules of Engagement that covers the target.
activate=Trueis required before any phase can start.client.pentests.roe.create( pentest.id, allowed_domains=["example.com"], authorization_ref="ENG-2026-0042", escalation_contacts=[{"type": "email", "value": "security@example.com"}], activate=True, ) -
Assign at least three agents to each phase, each with an
execution_order.for phase_id in (1, 2, 3): available = client.agents.list(type="pentest", phase_id=phase_id) assignment = [ {"agent_id": a.id, "phase_id": phase_id, "execution_order": i + 1} for i, a in enumerate(available.items[:3]) ] client.pentests.agents.assign(pentest.id, agents=assignment) -
Stream each phase 1 -> 3, reusing the same
chat_id. Build the answer fromcontent; treat everyagent_eventas activity.for phase_id in (1, 2, 3): with client.ai.chat.stream( user_prompt=f"Execute phase {phase_id} against the configured targets", pentest_id=pentest.id, mode="guided", phase_id=phase_id, chat_id=chat.id, ) as stream: for event in stream: if event.type == "content": print(event.content, end="", flush=True) elif event.is_agent_event: ev = event.agent_event if ev and ev.event_type == rank.AgentEvent.TOOL_CALL: print(f"\n[tool] {ev.data.get('tool_name')}") -
Process the raw responses into vulnerabilities with an AI model. Processing is asynchronous:
process_vulnerabilitiesenqueues the job andwait_for_vulnerabilitiesblocks until it finishes.client.pentests.process_vulnerabilities(pentest.id, model_alias="gemini-2.5-flash") result = client.pentests.wait_for_vulnerabilities(pentest.id) print(result.status, result.total_created, result.summary) -
Finish the pentest and read the severity breakdown.
finished = client.pentests.finish(pentest.id) print(finished.vulnerabilities.by_severity)
-
Create a guided web pentest.
rank pentest create -n "Guided pentest" -u https://example.com -t web -m guidedAn active Rules of Engagement is required before
rank pentest run. Set it from the web app, the SDK or the REST API — the CLI does not create RoE. See Engagement & control. -
Assign at least three agents to each phase.
rank pentest assign 42 --phase 1 --agents 10 11 12 rank pentest assign 42 --phase 2 --agents 20 21 22 rank pentest assign 42 --phase 3 --agents 30 31 32 -
Run the guided pentest. The CLI streams each phase and pauses between them. When the phases are done it starts vulnerability processing and polls the backend until the job finishes — you don’t orchestrate it by hand.
rank pentest run 42 -
Review the findings the agents produced.
rank pentest vulns 42
Run it
Save the following as guided_pentest.py, set RANK_API_KEY, then run
python guided_pentest.py.
"""Drive a guided pentest phase by phase with the Rank SDK.
What this script does:
1. Creates a chat session and a guided web pentest.
2. Activates a Rules of Engagement that covers the target.
3. Assigns three pentest agents to each of the three phases.
4. Streams phases 1 -> 3 one at a time, reusing the same chat_id so the
agents keep context across phases.
5. Enqueues asynchronous vulnerability processing and waits for it to finish.
6. Marks the pentest as finished and prints the vulnerability breakdown.
Guided mode hands control back to you between phases, which is useful when
you want to review activity or adjust scope before continuing.
The streaming loop applies the golden rule: the assistant's answer is built
ONLY from `content` events. Every `agent_event` (thinking, tool_call, ...) is
internal activity rendered as a timeline and never concatenated to the answer.
Run:
pip install rank-sdk
export RANK_API_KEY=rk_...
python guided_pentest.py
Optional environment variables:
RANK_TARGET_URL Target URL to scan (default: https://example.com).
RANK_MODEL Model alias for vulnerability processing
(default: gemini-2.5-flash).
RANK_AUTH_REF RoE authorization_ref (default: ENG-2026-0042).
"""
from __future__ import annotations
import os
from urllib.parse import urlparse
import rank
TARGET_URL = os.environ.get("RANK_TARGET_URL", "https://example.com")
MODEL_ALIAS = os.environ.get("RANK_MODEL", "gemini-2.5-flash")
AUTH_REF = os.environ.get("RANK_AUTH_REF", "ENG-2026-0042")
PHASES = (1, 2, 3)
AGENTS_PER_PHASE = 3
def assign_phase_agents(client: rank.Rank, pentest_id: int, phase_id: int) -> None:
"""Pick the first agents available for a phase and assign them.
Each phase requires a minimum of 3 agents (maximum 4) on first
assignment.
"""
available = client.agents.list(type="pentest", phase_id=phase_id)
chosen = available.items[:AGENTS_PER_PHASE]
if len(chosen) < AGENTS_PER_PHASE:
raise SystemExit(
f"Phase {phase_id} needs at least {AGENTS_PER_PHASE} agents, "
f"only {len(chosen)} are available."
)
assignment = [
{"agent_id": agent.id, "phase_id": phase_id, "execution_order": order}
for order, agent in enumerate(chosen, start=1)
]
result = client.pentests.agents.assign(pentest_id, agents=assignment)
print(f"Phase {phase_id}: assigned {result.assigned_count} agents")
def stream_phase(client: rank.Rank, pentest_id: int, chat_id: int, phase_id: int) -> None:
"""Stream a single guided phase, separating the answer from activity."""
print(f"\n========== Phase {phase_id} ==========\n")
with client.ai.chat.stream(
user_prompt=f"Execute phase {phase_id} against the configured targets",
pentest_id=pentest_id,
mode="guided",
phase_id=phase_id,
chat_id=chat_id,
) as stream:
for event in stream:
if event.type == "content":
# The assistant's answer, token by token.
print(event.content, end="", flush=True)
elif event.is_agent_event:
ev = event.agent_event
if ev is None:
continue
# A compact activity timeline; never part of the answer.
if ev.event_type == rank.AgentEvent.TOOL_CALL:
print(f"\n [tool ->] {ev.data.get('tool_name')}")
elif ev.event_type == rank.AgentEvent.TOOL_RESULT:
print(f"\n [tool <-] {ev.data.get('tool_name')} "
f"({ev.data.get('duration_ms', 0)}ms)")
elif ev.event_type == rank.AgentEvent.AGENT_FINISHED:
print(f"\n [agent #{ev.agent_id} done] "
f"{ev.data.get('stop_reason')}")
elif event.type == "phase_complete":
print(f"\n--- Phase {event.metadata.get('phase_id')} complete "
f"({event.metadata.get('progress', '?')}%) ---")
elif event.type == "error":
print(f"\nERROR: {event.error}")
def main() -> None:
with rank.Rank() as client:
chat = client.chats.create(nombre=f"Guided pentest - {TARGET_URL}")
pentest = client.pentests.create(
name="Guided pentest",
url=TARGET_URL,
type="web",
mode="guided",
methodology_id=1,
assets=[
{"asset_type": "url", "asset_value": TARGET_URL, "is_primary": True},
],
)
print(f"Created pentest #{pentest.id} (status: {pentest.status})")
host = urlparse(TARGET_URL).hostname or "example.com"
roe = client.pentests.roe.create(
pentest.id,
allowed_domains=[host],
authorization_ref=AUTH_REF,
escalation_contacts=[{"type": "email", "value": "security@example.com"}],
activate=True,
)
print(f"RoE v{roe.version} status={roe.status}")
for phase_id in PHASES:
assign_phase_agents(client, pentest.id, phase_id)
for phase_id in PHASES:
stream_phase(client, pentest.id, chat.id, phase_id)
# Turn the agents' raw responses into structured vulnerabilities.
# The backend processes them asynchronously: enqueue the job, then
# block until it reaches a terminal state.
print("\n\nProcessing vulnerabilities with AI...")
client.pentests.process_vulnerabilities(pentest.id, model_alias=MODEL_ALIAS)
result = client.pentests.wait_for_vulnerabilities(pentest.id)
if result.status == "failed":
raise SystemExit(f"Vulnerability processing failed: {result.error}")
print(f" Detected {result.total_detected}, stored {result.total_created}")
finished = client.pentests.finish(pentest.id)
print(f"\n{finished.message}")
if finished.vulnerabilities is not None:
summary = finished.vulnerabilities
print(f" Total : {summary.total}")
print(f" By severity: {summary.by_severity}")
if summary.risk_score is not None:
print(f" Risk score : {summary.risk_score.score} "
f"({summary.risk_score.level})")
if __name__ == "__main__":
try:
main()
except rank.AuthenticationError:
print("ERROR: invalid or missing API key. Set RANK_API_KEY.")
except rank.APIError as exc:
print(f"API error ({exc.status_code}): {exc.message}")
Once you have findings, head to Triage vulnerabilities and gate CI.