Anatomy of a run
Scope, agent, seal, report — what happens between submitting an engagement and downloading evidence.
One run is called an engagement. This page follows a single engagement from the moment it is started to the moment an auditor can verify it. Terms in bold are defined on Concepts and glossary.
Scope¶
An engagement carries a list of targets, an in-scope list, an out-of-scope
list, rules of engagement, and exactly one framework. The create request
also accepts an optional credentials list, which the engagement does not
keep. The in-scope list is what the agent may touch. The two lists do not
combine the same way for names and for addresses:
- An out-of-scope name is compared as an exact string. It excludes that one name from an in-scope wildcard, and nothing beneath it.
- An out-of-scope address or network that falls inside an in-scope address or network, or encloses one, is an overlap. So is a name that appears on both lists. An overlap aborts the run in its first phase.
Scope describes how the two lists combine.
The rules of engagement have three fields: max_duration_minutes,
allowed_actions and denied_actions. The wizard offers four actions: scan,
enumerate, exploit and post_exploit. In the first phase the run renders
the rules, the scope and the framework into a rules-of-engagement document. The
document is stored with the run and reproduced in the report. It is not sent to
the model, which receives the targets and the two scope lists.
The scope guard checks every command and every URL before it is executed. It
refuses one aimed outside the scope, and a command whose first token is a tool
associated with a denied action. Only dos has such a list of tools, so denying
any of the four wizard actions refuses no command. The guard extracts addresses
and hostnames from the command string, and a command in which it finds no target
is allowed through as a local command.
Rules of engagement sets out what the
guard enforces.
The maximum duration is not enforced, and the agent is not told it. Inside the
run, max_duration_minutes is read only to write one line of the document, and
no timer stops the run at that mark. Four ceilings are enforced inside the
container. A 500-step ceiling on the run and an abort after five consecutive
failing steps end the run. A five-minute timeout ends a single command, and a
ten-step cap ends the threat-modeling phase.
Ownership is checked when an engagement is started, not when it is created.
Starting one re-checks every gated identifier — the targets plus the in-scope
list, not the out-of-scope list — against the assets registered to the
organization. Each identifier needs a covering asset whose status is verified
(a DNS TXT record or a well-known file that Pentr fetches) or attested (a
written claim by an organization owner, valid for 90 days). The status alone
does not clear it: the proof behind a verified asset is fetched again and must
pass, and an attested asset must not have expired. An engagement with no
targets and no in-scope entries is refused. If any identifier fails, the
engagement is marked blocked, the response names each identifier and the
reason, and nothing is queued.
blocked is not terminal. Publish the missing record or renew the attestation,
and submit the same engagement again through the API. The console shows
Start pentest only on a pending engagement.
Lifecycle lists the transitions.
Queue¶
A cleared engagement is written to a Redis list and its status becomes queued.
One consumer — the scheduler process on the agent host — takes jobs off that list
in order, one at a time: it launches a container, waits for that run to exit,
and only then takes the next. There is no priority and no preemption.
The plan tier of the user who created a schedule sets how many engagements the organization may have queued or running: free 1, pro 3, enterprise 10. An unrecognised tier gets the free allowance, never an unlimited one. That cap governs how much may be in flight, not how fast the queue drains. It is checked only when a schedule comes due: if the organization is already at its limit, the occurrence is skipped, the reason is recorded against the schedule, and the next run time still advances rather than backfilling. A manually submitted engagement is not refused for the cap. Caps and skips describes the cap and the skip, and Plans and limits the tier.
Run¶
The scheduler starts a new container for each engagement, named after the engagement id. No container is reused for a second run. Runs share the agent host's Docker daemon, the agent image and the Docker network the containers are started on. Isolation describes the controls on the run container.
Inside, the agent works through seven phases in a fixed order:
- Pre-engagement — renders the rules-of-engagement document and checks the two scope lists for overlap. No model is called, and the phase advances on the first step of the run, which is why the console pipeline shows six stages rather than seven.
- Intelligence — passive information gathering. The prompt tells the agent not to send packets to in-scope targets in this phase.
- Threat modeling — the prompt asks for a structured model: assets, threat actors, ranked attack paths and hypotheses to test next.
- Vulnerability analysis — active probing: version scans, CVE matching, and the OWASP WSTG checklist for HTTP services.
- Exploitation — attempts the unverified findings. The prompt asks the agent to mark each finding verified, false positive or not exploitable by the result; no code checks the status against the evidence.
- Post-exploitation — for findings that gave access, the prompt asks for privilege escalation, other reachable in-scope hosts, readable data, and whether persistence would be possible, documented rather than planted.
- Reporting — the prompt asks the agent to review its findings against a deliverable checklist and fix gaps. The payload the server persists — the rules-of-engagement document, the threat model, the intelligence report, the findings and a summary — is assembled by code when the run loop ends.
In the six model-driven phases, the agent ends a phase by calling
phase_complete. Two advances happen without it: pre-engagement advances on its
first step, and threat modeling advances when it reaches its step cap. No branch
skips a phase or goes back. Phases describes each one.
Progress streams to the engagement page over server-sent events: phase changes, the text of each command the agent runs, refused commands, errors, and each finding as it is recorded. The stream does not carry command output; stdout and stderr go into the artifact. Running an engagement lists the events.
Capture¶
Evidence is captured inside the container as it is produced. Four kinds of
artifact reach the run: command logs (the command, stdout, stderr, exit code
and timing), request/response pairs, browser screenshots, and other — the kind
the model's own payload and file evidence items are stored under. Each one is
hashed with SHA-256 when it is captured and stored under that hash, so identical
bytes are stored once per run. One artifact cannot exceed 10 MiB; a capture over
that size is dropped rather than truncated, and the step that produced it fails.
Artifacts sets out the four kinds and where each comes
from.
Each artifact records the phase it was captured in, its position in the run, and the finding it was captured for, if any. When the same bytes are captured again for another finding, the stored artifact keeps its first phase and position and gains a link to the second finding, so one artifact can support several findings.
Seal¶
When the run completes, the artifacts are ordered and hashed into a chain: each entry's hash covers the previous entry's hash, the artifact's own hash, and the entry's metadata. The last entry's hash is the chain head. A manifest records every entry, the chain head, the total count, the framework, and a digest of the scope and rules the run was started with. The manifest itself is hashed, and that hash goes into a small canonical signature block alongside the chain head, the key id and the signing time. That block is signed with the organization's active Ed25519 signing key, which Pentr holds (Key custody).
Sealing is fail-closed. If the key-encryption key is missing or any step of the signature fails, sealing raises and no seal row is written; the run is left unsealed rather than recorded with a half-made seal. Sealing describes what is written before that point.
Sealing happens once, at completion
A run is sealed when the container exits zero and returns a results
payload. That is a weaker condition than finishing the work: a run that hit
the 500-step ceiling, and a run that aborted after five consecutive failing
steps, both exit that way and are both sealed. So is a run rejected for a
scope overlap before it captured anything: its locker verifies over an empty
chain, and the manifest's entry_count of 0 is what says so. There is no
re-seal, so an engagement that crashed, returned no payload, or whose seal
attempt failed stays unsealed.
An unsealed run is not lost. Its findings are stored, the report renders, and
PDF, SARIF and CSV all export — but the evidence hashes are absent. The PDF
states that the run was not sealed, SARIF reports
evidence_integrity.sealed as false, and the CSV has no column for it. An
unsealed run has no locker, so there is nothing for an auditor to verify.
Report¶
Findings map to framework controls through the identifiers they carry. The agent is asked for MITRE ATT&CK technique ids on every finding, and any it gives are checked against the ATT&CK catalog included with Pentr; some findings also carry OWASP WSTG ids. The mapper looks both up in the tables the chosen framework declares. Nothing obliges a finding to carry identifiers at all, so two kinds of finding fall back to the older path, keyword matching on the finding's text — SQL injection, weak credentials, TLS problems and so on. Those two are findings tagged with nothing, and findings whose tags appear in no table.
The report model carries severity, CVSS where the agent produced one, CVEs, MITRE ATT&CK technique and tactic ids, OWASP WSTG ids, and the sealed artifact hashes backing each finding. The SARIF and CSV exports add CWE identifiers derived from the finding's text.
A report downloads in three formats, built by the same pipeline:
- PDF — the human-readable report.
- SARIF 2.1.0 — one run, one result per finding, for code-scanning and ticketing pipelines.
- CSV — one row per finding with fixed columns, for spreadsheets.
They are not built at the same moment. The PDF is rendered once, when the report is generated; SARIF and CSV are rebuilt at every download. A change to the framework file between the two can give them different control ids, and the PDF can list findings in a different order from the exports. Findings & reports describes the formats.
Separately from the report, a sealed engagement can be exported as an evidence
locker: a zip holding the manifest, the signature block, the signature, the
public key, an unsigned summary header, a README, and every artifact named by its
hash. The report is not in it. Exporting a locker lists
each file. pentr-verify checks a locker offline, without access to Pentr.
Your first engagement walks through producing one and
verifying it.
Last updated 2026-09-24