# PLAYBOOK — Scope Templates and Hand-Off Patterns

## Per-engagement-type scope templates

### External web app pentest

```yaml
in_scope_targets:
  - host: app.acme.example
    notes: production web app
  - host: api.acme.example
    notes: REST API; rate-limit 60 req/min observed
  - url: https://app.acme.example/admin/
    notes: admin UI (test as authorized admin user)
out_of_scope_targets:
  - host: payments.acme.example
    reason: PCI scope, separate authz required
  - host: legal.acme.example
    reason: third-party hosted; not customer's
```

### Internal network pentest

```yaml
in_scope_targets:
  - cidr: 10.50.0.0/16
    notes: corporate user range
  - cidr: 10.51.0.0/16
    notes: developer subnet
  - cidr: 10.52.0.0/24
    notes: test infrastructure
out_of_scope_targets:
  - cidr: 10.99.0.0/16
    reason: production DB tier, separate authz
  - cidr: 10.250.0.0/24
    reason: physical surveillance VLAN
  - cidr: 10.251.0.0/24
    reason: VOIP (avoid call disruption)
```

### Red-team engagement

```yaml
in_scope_targets:
  - cidr: 203.0.113.0/24
    notes: customer ASN block 1
  - cidr: 198.51.100.0/24
    notes: customer ASN block 2
  - host: "*.acme.example"
    notes: any subdomain of acme.example
  - cloud_account: aws:123456789012
    notes: production AWS account
out_of_scope_targets:
  - host: ceo.acme.example
    reason: executive personal device
  - cidr: 203.0.113.250/32
    reason: SOC bastion (would tip off SOC)
```

### Cloud account pentest (AWS)

```yaml
in_scope_targets:
  - cloud_account: aws:123456789012
    notes: prod-1 account; full read scope
  - cloud_account: aws:210987654321
    notes: prod-2 account; identity-services only
out_of_scope_targets:
  - cloud_account: aws:999999999999
    reason: management account; out of agreed scope
```

### SaaS tenant pentest

```yaml
in_scope_targets:
  - saas_tenant: okta:acme-corp
    notes: customer Okta tenant; SSO config audit
  - saas_tenant: auth0:acme
    notes: customer Auth0 tenant
out_of_scope_targets:
  - saas_tenant: salesforce:acme
    reason: separate vendor-authz required
```

Many SaaS vendors require explicit prior notification before
pentesting (Salesforce, Okta, etc. all have their own pentest
authorization forms). The skill flags `saas:` entries informationally;
the operator should verify the vendor's own authz separately.

## Allowlist emission patterns per scanner

### nmap

```bash
python3 ./scripts/define_scope.py --roe roe.yaml --emit-allowlist /tmp/allowed.txt
nmap -iL /tmp/allowed.txt -sV -oN nmap-scan.txt
```

### Burp Suite Target → Scope

Burp uses regex-based scope rules. Convert the allowlist by
escaping dots and accepting any port:

```python
# Quick converter
import re
with open("/tmp/allowed.txt") as f:
    for line in f:
        entry = line.strip()
        # Convert hostname/CIDR to Burp regex
        print(re.escape(entry))
```

Paste output into Burp's "Include in scope" with "Use advanced
scope control" enabled.

### AWS WAF allowlist

```bash
python3 - <<EOF
import json
ips = open("/tmp/allowed.txt").read().splitlines()
v4 = [ip for ip in ips if ":" not in ip]
print(json.dumps({"Addresses": v4}, indent=2))
EOF
```

Pipe the output to `aws wafv2 update-ip-set` to install the
allowlist.

### Cloud security tools (ScoutSuite, Prowler, CloudSploit)

Cloud scope is by account ID, not IP. Read the `cloud_account`
entries from the normalized targets JSON:

```bash
jq -r '.[] | select(.type=="cloud") | .normalized' /tmp/targets.json
```

## Scope-extension protocol

When testing reveals a target that should have been in scope but
wasn't:

1. Halt testing of the extension target.
2. Request a scope amendment from the authorizer.
3. Authorizer signs an extension YAML:

   ```yaml
   amendment_id: ACME-2026-Q2-PENTEST-001-AMEND-01
   amendment_to: ACME-2026-Q2-PENTEST-001
   amendment_date: 2026-06-15T10:00:00Z
   in_scope_targets:
     - host: newly-discovered.acme.example
       notes: discovered during port-scan of cidr 203.0.113.0/24; serves admin UI
   signature_block:
     signer: jane.doe@acme.example
     signed_at: 2026-06-15T11:00:00Z
     signature: |
       <signature>
   ```

4. Re-run this skill with `--extension`.
5. Verify the new scope is clean before resuming testing.

## Allowlist file format

The `--emit-allowlist` output is one entry per line:

```
203.0.113.10
203.0.113.0/24
2001:db8::10
2001:db8::/64
```

Hostnames are NOT in the allowlist (DNS resolves at scan time;
see THEORY.md). The normalized targets JSON is the source of
truth for hostnames + URLs + cloud accounts + SaaS tenants.

## Normalized targets JSON format

```json
[
  {"raw": "app.acme.example", "type": "hostname", "normalized": "app.acme.example"},
  {"raw": "203.0.113.0/24", "type": "cidrv4", "normalized": "203.0.113.0/24"},
  {"raw": "https://api.acme.example/v2", "type": "url", "normalized": "https://api.acme.example/v2"},
  {"raw": "aws:123456789012", "type": "cloud", "normalized": "aws:123456789012"}
]
```

Downstream skills consume this JSON via `--targets-file` argument
patterns (planned across the cluster 1-4 skills).

## Common scope-mistake patterns

| Pattern | Fix |
|---|---|
| `host: 203.0.113.0/24` | Use `cidr:` not `host:` |
| `host: https://app.acme.example` | Use `url:` not `host:` |
| `cidr: 203.0.113.10` | Single IP without mask; CIDR /32 implied but explicit is better |
| Hostname with trailing dot (`acme.example.`) | DNS-canonical form; strip the dot |
| `*.acme.example/admin` | Mixes wildcard + path; not supported |
| Cloud account with hyphenated number `aws:1234-5678-9012` | Use bare number, no hyphens |
| Unicode in hostname (`münich.acme.example`) | Convert to Punycode (`xn--mnich-rta.acme.example`) |

## Pre-scan gate (CI)

```bash
python3 ./scripts/define_scope.py --roe roe.yaml --min-severity high \
    --format json --output scope-issues.json
jq -e '. == []' scope-issues.json || {
  echo "::error::Scope issues block testing"
  exit 1
}
```

This pattern works for engagements where the ROE is checked into
a private engagement repo with CI.

## Scope drift detection between engagements

When running multiple engagements for the same customer over time,
diff this engagement's scope against the previous:

```bash
python3 ./scripts/define_scope.py --roe engagements/acme-2026-q1/roe.yaml \
    --emit-targets engagements/acme-2026-q1/targets.json
python3 ./scripts/define_scope.py --roe engagements/acme-2026-q2/roe.yaml \
    --emit-targets engagements/acme-2026-q2/targets.json
diff <(jq -S .[].normalized engagements/acme-2026-q1/targets.json) \
     <(jq -S .[].normalized engagements/acme-2026-q2/targets.json)
```

Scope changes between engagements are common (customer added new
infra, decommissioned old) — but they should be explicit and
documented, not silent.
