Scope

What the agent may touch: identifiers, subdomains, and the in-scope and out-of-scope lists.

An engagement carries three lists of identifiers: its targets, its in-scope list and its out-of-scope list.

A target is a host, address range or application named in the scope of one run. An asset is something your organization owns and has proven it owns, registered once and reused across runs. Targets and in-scope entries have to be covered by an asset; out-of-scope entries do not, because excluding something is not a claim to own it.

Identifier kinds

Four kinds exist. The same four are used for assets and for scope entries.

Kind Written as Stored as
domain Example.COM. example.com
ip 10.0.0.5 the canonical form of the address, v4 or v6
cidr 10.0.0.5/24 10.0.0.0/24
url https://app.example.com:443/login/?next=%2Fa#top https://app.example.com/login

Domains are lowercased, stripped of a trailing dot and IDNA-encoded. A domain must contain a dot, and its last label must not be all digits — 1.2.3.4 is an address, not a name. A CIDR keeps its prefix length and drops its host bits, so 10.0.0.5/24 and 10.0.0.0/24 are the same asset. A URL must be http or https, and only its scheme, host, non-default port and path survive: the default port for the scheme, a trailing slash, the query string and the fragment are all dropped. Two URLs differing only in their query are the same asset.

When a bare string arrives in a target or scope list, its kind is inferred: anything containing :// is a URL, anything else containing / is a CIDR, anything that parses as an address is an IP, and everything else is treated as a domain. Registering an asset is different — there you name the kind, and the API rejects any kind outside those four.

Subdomains

Two separate mechanisms answer to the word "subdomain".

Ownership coverage is the include_subdomains flag on a domain asset. With it set, the asset covers any name beneath it: the launch check walks a target's ancestors and accepts a domain asset that matches one of them and carries the flag. Without it, the asset covers its exact value and nothing else. The flag is forced off for ip, cidr and url assets.

Run scope is the in-scope list, matched inside the container by shell-style glob. example.com matches example.com alone; *.example.com matches sub.example.com and not the bare domain. To reach both, list both. The matching rules are on Rules of engagement.

A domain asset with include_subdomains authorizes you to name a subdomain. It does not put that subdomain in scope.

Address ranges

A cidr asset covers an ip target that falls inside it, and a cidr target that is a subnet of it. Both v4 and v6 ranges are accepted and stored.

Two limits apply past that point. No automated verifier applies to a cidr asset, so the only way to prove one is an owner attestation — see Ownership verification. And the scope guard inside the container reads in-scope entries as IPv4 networks only: an IPv6 range on the in-scope list is not recognised as a range, falls through to the hostname list, and matches no address the agent might target.

How the two lists combine

Out-of-scope is checked before in-scope, for addresses and for names alike, so an excluded target is refused even where an in-scope entry would otherwise cover it.

The lists are not matched the same way:

  • In-scope names are matched by glob, so *.example.com works.
  • Out-of-scope names are matched by exact string equality. A wildcard on the out-of-scope list excludes nothing. Write each excluded host out in full.
  • In-scope addresses are read as networks. Out-of-scope addresses may be a single address or a network — and a single out-of-scope address inside an in-scope range is read as its own /32, a subnet of that range, so it is treated as an overlap and aborts the run rather than narrowing the range. Only names narrow an in-scope entry, and only when written out in full.

Overlap between the lists

Overlap is detected once, in the container, when the rules-of-engagement document is rendered in the first phase. Two shapes are detected: an out-of-scope network that is a subnet of an in-scope network or the reverse, and a domain string that appears verbatim on both lists.

Nothing checks for it at submit time. The engagement is queued, waits its turn, and gets a container; the render then raises every time the first phase is attempted, and the run aborts after five consecutive failures without ever leaving pre-engagement. That costs a queue slot and a container. The run still ends completed and is sealed, over an empty chain with no findings and no artifacts — see Sealing. Fix the lists and create a new engagement; a completed engagement cannot be submitted again.

Because the domain check is an exact-string comparison, *.example.com in scope and secret.example.com out of scope is not an overlap, and the run starts.

A wildcard in scope does not stop at the host you excluded

In-scope names are matched with fnmatch, whose * crosses dots. Out-of-scope names are matched by exact string. With *.example.com in scope and secret.example.com out of scope, secret.example.com itself is refused — and a.secret.example.com matches the wildcard, does not equal the exclusion, and is allowed.

An exclusion removes the exact names you write and nothing beneath them. To keep a subtree out, either list every host in it on the out-of-scope list or narrow the in-scope entries so the wildcard never reaches them.

Targets

A target is submitted as a host plus a type. The type is discarded: the engagement record stores hosts only, and the type the agent is eventually told comes from the kind of the asset that covered that target at launch. A target spelled Example.COM is normalised before it is matched against your inventory, and the raw spelling is what reaches the agent.

parent_asset_id on an asset records a relationship between two assets you registered. It is validated to name an asset in your own inventory and returned in the API response; it plays no part in coverage.

Authenticated testing

Pentr has no way to pass credentials to the agent. The engagement create schema accepts a credentials list and the agent's config model has a field for it, but the server never reads the one or fills the other: the engagement record drops the list, the job payload leaves the credential slot empty, and the environment variable the container would read is never set. Section 6 of every rules-of-engagement document reads "No credentials provided — black-box testing only".

Runs are unauthenticated. Anything behind a login is out of the agent's reach unless it can get in by attacking the login itself.

Last updated 2026-09-24