---
title: 'Rules of Engagement'
source: 'https://academia.sh/en/courses/penetration-testing/rules-of-engagement'
course: 'Security Tooling and Penetration Testing'
language: en
updated: '2026-09-26T03:02:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Turns a written scope definition into a three-decision audit and separates out the borderline item'
    - 'Counts the out-of-scope items reachable by mistake through a shared front end'
    - 'Calculates the time window and communication protocol as measurable items'
---

# Rules of Engagement

A written scope definition for a 64-item fictional inventory is turned into a programmatic audit: 20 items come out in scope, 11 on the border, 33 out of scope. Through five shared front ends, 29 out-of-scope and borderline items can be reached by mistake; 11 of them are banned by name in the rules of engagement. The raw window exceeds the planned work by 4 hours, while subtracting the meter-reading interval leaves it 6 hours short; two of the four thresholds carry no on-call coverage, and for one of them the actual notification time comes to one and a half times what the contract states.

The previous topic showed what tools produce and what they cannot prove: the flow extracted from a packet set, the request an intercepting proxy altered, the behavior a sandbox could observe, the majority verdict of reputation services. All of them carried the same silent assumption — that there was a right to run those tools against the target they ran on. Where that right came from, where it ended, and who measured where it ended was never written down.

**Rules of engagement** are the written form of that right: they define what can be touched, when it can be touched, and who is notified within how much time when something goes wrong. But a written sentence is not an audit. The sentence "the subscriber portal is in scope, the branch network is out of scope" has to turn into a decision for every single item once there is an actual target list, and this lesson runs that turn. **Touching an out-of-scope target voids the authorization: from that moment on, the action taken stops being written permission and becomes unauthorized access — it carries legal consequences and it makes the entire assessment unusable.**

- **TP1.** The regional measurement network inventory has 64 items; it is spread across four units, three environments and eight front ends. Both the inventory and the scope definition are **fictional** — there is no real target.
- **TP2.** The scope definition consists of three allow criteria (network block, unit, environment) and two exclusion criteria; six allow entries and three exclusion entries in total.
- **TP3.** The audit produces three decisions: **in scope**, **borderline**, **out of scope**. Borderline means either that only part of the criteria matched, or that the item falls under both an allow rule and an exclusion rule; these items are set aside and cannot be touched without separate written approval.
- **TP4.** A **front end** is a shared entry point that publishes multiple items from the same point. A request sent to a front end can reach every item behind that front end.
- **TP5.** The test window is five nights, eight hours a night, two people. One hour of the night is the meter-reading interval, and no item is touched during that interval.
- **TP6.** The communication protocol defines four thresholds; the example incident occurs at 02:30 at night, and the workday starts at 09:00. All durations are fictional.

## Scope Definition as an Audit

```js
// scope.mjs — the model that turns the scope definition into a programmatic audit.
// THIS IS A MODEL; both the inventory and the scope are fictional, there is no real target.
export const SEED = 20260806;
const makeRng = (t) => { let s = t >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };
const r = makeRng(SEED);

// Each item belongs to a unit, an environment, an internal network block and a front end.
const UNIT = ["subscriber-portal", "field-app", "branch-network", "meter-collector"];
const ENVIRONMENT = ["production", "staging", "development"];
const POOL = {                                      // which unit uses which front end
  "subscriber-portal": ["front-a1", "front-a2", "front-shared-1"],
  "field-app": ["front-s1", "front-shared-1", "front-shared-2"],
  "branch-network": ["front-b1", "front-shared-2"],
  "meter-collector": ["front-t1", "front-t2", "front-shared-2"],
};
export const TARGET = Array.from({ length: 64 }, (_, i) => {
  const unit = UNIT[Math.floor(r() * 4)], pool = POOL[unit];
  return { name: `${unit}-${String(i + 1).padStart(2, "0")}`, unit,
    environment: ENVIRONMENT[Math.floor(r() * 3)],
    block: `10.${20 + Math.floor(r() * 4)}.${Math.floor(r() * 6)}.0/24`,
    front: pool[Math.floor(r() * pool.length)] };
});

// The scope definition written in the rules of engagement: three allow criteria, two exclusion criteria.
export const SCOPE = {
  allowedBlock: ["10.20.", "10.21."],
  allowedUnit: ["subscriber-portal", "field-app"],
  allowedEnvironment: ["production", "staging"],
  excludedName: ["meter-collector", "branch-network"],
  excludedEnvironment: ["development"],
};

// Audit: the three criteria are checked separately. All match -> in scope, none match ->
// out of scope, some match or it collides with an exclusion -> BORDERLINE.
export function decide(h, k = SCOPE) {
  const criteria = [k.allowedBlock.some((b) => h.block.startsWith(b)),
    k.allowedUnit.includes(h.unit), k.allowedEnvironment.includes(h.environment)];
  const excluded = k.excludedName.some((a) => h.name.startsWith(a)) ||
    k.excludedEnvironment.includes(h.environment);
  const matched = criteria.filter(Boolean).length;
  if (excluded) return matched === 3 ? "borderline" : "out-of-scope";
  return matched === 3 ? "in-scope" : matched === 0 ? "out-of-scope" : "borderline";
}
export const DECISION = new Map(TARGET.map((h) => [h.name, decide(h)]));

// Shared front end: if an out-of-scope item uses the same front end as an in-scope
// item, a request sent to that front end reaches the out-of-scope item too.
export function sharedRisk() {
  const fronts = new Map();
  for (const h of TARGET) fronts.set(h.front, [...(fronts.get(h.front) ?? []), h]);
  const risky = [];
  for (const [name, list] of fronts) {
    const d = (k) => list.filter((h) => DECISION.get(h.name) === k).length;
    const banned = list.filter((h) => SCOPE.excludedName.some((a) => h.name.startsWith(a))).length;
    if (d("in-scope") && list.length > d("in-scope")) {
      risky.push({ front: name, inScope: d("in-scope"), outOfScope: d("out-of-scope"),
        borderline: d("borderline"), banned });
    }
  }
  return risky.sort((a, b) => b.outOfScope - a.outOfScope || a.front.localeCompare(b.front));
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const gen = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j])
    : String(v).padEnd(B[0]))).join("");
  const yz = gen([19, 10, 12, 14, 7]);
  const count = (d) => TARGET.filter((h) => DECISION.get(h.name) === d).length;
  console.log(`model (seed ${SEED}): ${TARGET.length} targets, ${UNIT.length} units, ` +
    `${new Set(TARGET.map((h) => h.front)).size} front ends`);
  console.log("\n" + yz(["unit", "in scope", "borderline", "out of scope", "total"]));
  for (const u of UNIT) {
    const l = TARGET.filter((h) => h.unit === u);
    const c = (d) => l.filter((h) => DECISION.get(h.name) === d).length;
    console.log(yz([u, c("in-scope"), c("borderline"), c("out-of-scope"), l.length]));
  }
  console.log(yz(["TOTAL", count("in-scope"), count("borderline"), count("out-of-scope"), TARGET.length]));

  const risky = sharedRisk(), yz2 = gen([16, 10, 14, 12, 16]);
  console.log("\n" + yz2(["front end", "in scope", "out of scope", "borderline", "banned by name"]));
  for (const x of risky) console.log(yz2([x.front, x.inScope, x.outOfScope, x.borderline, x.banned]));
  const t = (field) => risky.reduce((a, x) => a + x[field], 0);
  console.log(`\nshared front ends ${risky.length}; a request sent to these front ends ` +
    `can also reach ${t("outOfScope") + t("borderline")} out-of-scope and borderline items, ` +
    `${t("banned")} of which are banned by name in the rules of engagement`);
}
```

```
model (seed 20260806): 64 targets, 4 units, 8 front ends

unit                 in scope  borderline  out of scope  total
subscriber-portal          11           5             1     17
field-app                   9           6             6     21
branch-network              0           0             9      9
meter-collector             0           0            17     17
TOTAL                      20          11            33     64

front end         in scope  out of scope  borderline  banned by name
front-shared-2           1            14           2              11
front-shared-1           7             3           5               0
front-a1                 3             1           0               0
front-a2                 5             0           3               0
front-s1                 4             0           1               0

shared front ends 5; a request sent to these front ends can also reach 29 out-of-scope and borderline items, 11 of which are banned by name in the rules of engagement
```

The table above writes out what a two-sentence scope definition corresponds to across 64 items. The written text takes two units in and leaves two units out; the number, however, spreads across three columns. **20 items are in scope, 33 are out of scope, 11 are borderline.** All 11 borderline items come from allowed units: the unit criterion matches, but the network block or environment criterion does not. The written definition produces no decision for these items, because whoever wrote it did not account for a unit having items in three different blocks and three different environments. The measurement reduces this gap to a name: the number of items whose scope is uncertain is a little more than half the number of items in scope.

The third decision being a separate column is not a detail — it is the actual information the audit carries. If a borderline item is counted as "in scope," the authorization limit is exceeded; if it is counted as "out of scope," the assessment keeps its own scope narrower than it should and items that genuinely need testing are never seen. Of the two mistakes, the first carries legal consequences, the second wastes the organization's money. The correct action is to flag the item with a third decision and send it back to whoever wrote the definition. This is also where the value of a scope audit is measured: not how many items it auto-approves, but **how many items it sends back to a human**.

The table below gives a harsher result. Five of the eight front ends collect in-scope and out-of-scope items at the same point. The worst is `front-shared-2`: behind it there is a **single** in-scope item, against which stand 14 out-of-scope and 2 borderline items, and 11 of those 14 are items banned by unit name in the rules of engagement. For that one item, a request sent to that front end also reaches the 11 items banned by name. In total, across five front ends, **29 items** are reachable by mistake.

This shows why writing a scope definition as a list of items is not enough. Scope intersects not with the item but with the **access point**; how many items stand behind a single point is a decision made by the infrastructure, not the inventory. The correct action here is to exclude the single item behind `front-shared-2` from the assessment: the value of that one item is immeasurably small next to the risk of touching 11 banned items. The 11 borderline items, meanwhile, are tied to a separate written approval, and no step runs before that approval arrives.

## Time Window and Communication Protocol

```js
// window.mjs — the time window and communication protocol counted as measurable items.
import { TARGET, DECISION } from "./scope.mjs";

const inScopeCount = TARGET.filter((h) => DECISION.get(h.name) === "in-scope").length;
const STEP = [["recon", 0.4], ["scan", 1.1], ["validation", 2.3]];   // hours/item, fictional
const NIGHTS = 5, HOURS = 8, PEOPLE = 2, BLACKOUT = 1;      // one hour a night is the meter-reading interval

const gen = (B) => (h) => h.map((v, j) => (j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0]))).join("");
const y1 = gen([12, 7, 12, 13]);
const planned = STEP.reduce((a, [, s]) => a + s * inScopeCount, 0);
const raw = NIGHTS * HOURS * PEOPLE, net = NIGHTS * (HOURS - BLACKOUT) * PEOPLE;

console.log(`${inScopeCount} in-scope items, ${STEP.length} step classes`);
console.log(y1(["step", "items", "hours/item", "total hours"]));
for (const [name, s] of STEP) {
  console.log(y1([name, inScopeCount, s.toFixed(1), (s * inScopeCount).toFixed(1)]));
}
console.log(y1(["PLANNED", "", "", planned.toFixed(1)]));
console.log(`window: ${NIGHTS} nights x ${HOURS} hours x ${PEOPLE} people = ${raw} person-hours; ` +
  `subtracting the meter-reading interval (${BLACKOUT} hour/night) leaves ${net} person-hours`);
console.log(`the raw window exceeds the planned work by ${(raw - planned).toFixed(1)} hours, ` +
  `the net window falls ${(planned - net).toFixed(1)} hours short`);

// Communication protocol: contract duration, on-call coverage and parties for each threshold.
const THRESHOLD = [
  ["critical finding", 1, true, ["test team", "org security"], false],
  ["service outage", 0.25, true, ["test team", "org security", "system owner"], true],
  ["scope excursion", 4, false, ["test team", "org security", "system owner",
    "legal"], true],
  ["third-party data", 24, false, ["test team", "org security", "legal",
    "data owner", "regulator"], true],
];
const INCIDENT = 2.5, WORKDAY = 9;                        // incident hour and workday start
const y2 = gen([19, 10, 9, 9, 9, 7]);
console.log("\n" + y2(["threshold", "contract", "on-call", "actual", "parties", "stops"]));
let contractTotal = 0, actualTotal = 0;
for (const [name, contract, onCall, parties, stops] of THRESHOLD) {
  const actual = onCall ? contract : Math.max(contract, WORKDAY - INCIDENT);
  contractTotal += contract; actualTotal += actual;
  console.log(y2([name, `${contract} h`, onCall ? "yes" : "no", `${actual.toFixed(2)} h`,
    parties.length, stops ? "yes" : "no"]));
}
const allParties = new Set(THRESHOLD.flatMap(([, , , t]) => t));
console.log(`\n${THRESHOLD.length} thresholds, ${allParties.size} distinct parties, ` +
  `testing stops at ${THRESHOLD.filter((e) => e[4]).length} thresholds`);
console.log(`for an incident occurring at ${INCIDENT.toFixed(2)} at night, the contract promises ` +
  `${contractTotal.toFixed(2)} hours total, actual comes to ${actualTotal.toFixed(2)} hours`);
```

```
20 in-scope items, 3 step classes
step          items  hours/item  total hours
recon            20         0.4          8.0
scan             20         1.1         22.0
validation       20         2.3         46.0
PLANNED                                 76.0
window: 5 nights x 8 hours x 2 people = 80 person-hours; subtracting the meter-reading interval (1 hour/night) leaves 70 person-hours
the raw window exceeds the planned work by 4.0 hours, the net window falls 6.0 hours short

threshold            contract  on-call   actual  parties  stops
critical finding          1 h      yes   1.00 h        2     no
service outage         0.25 h      yes   0.25 h        3    yes
scope excursion           4 h       no   6.50 h        4    yes
third-party data         24 h       no  24.00 h        5    yes

4 thresholds, 6 distinct parties, testing stops at 3 thresholds
for an incident occurring at 2.50 at night, the contract promises 29.25 hours total, actual comes to 31.75 hours
```

The time window is enough on paper, and it is not enough in reality. Five nights, two people and eight hours a night give 80 person-hours; the planned work is 76 hours, so the window exceeds the work by four hours. Once the meter-reading interval is subtracted, the usable time drops to 70 hours and the same plan falls **six hours short**. The difference is not a management decision, it is arithmetic: the untouchable interval is one-eighth of the window, and the plan's share drops by that same proportion. The only way to reclaim time is to cut the number of steps, and since validation is the most expensive class at 46 hours, that is where the cut has to come from — which is the question the next lesson asks.

The second table measures communication. Two of the four thresholds carry on-call coverage, and for those two the actual notification time equals what the contract states. For the remaining two, the other party can only be reached during working hours; for a suspected scope excursion arising at 02:30 at night, the contract states four hours while the actual time is 6.5 hours. This means the protocol's two slowest thresholds are also the thresholds that stop testing: two of the three test-stopping thresholds depend on working hours. The four thresholds together involve six distinct parties, and the threshold that involves the most parties is also the longest, because the protocol requires reaching all of them.

The stop column also deserves its own reading. Testing stops at three thresholds, and the moment it stops is not a decision but an obligation: once a suspected scope excursion arises, no new step runs until that suspicion is cleared. This directly increases the hours lost from the window — if a test that stops at 02:30 at night waits until 09:00, the remaining 3.5 hours of that night amount to seven person-hours for two people, added on top of a plan that is already six hours short. The time and communication sections of the rules of engagement are not separate documents for this reason; one is the input to the other.

The conclusion comes down to a single sentence: the rules of engagement produce three separate numbers — the number of items that can be touched, the number of usable hours, and how long notification actually takes. None of the three shows up in the written text; all three appear only once it is run against the inventory and the calendar.

## Summary

- In a 64-item inventory, a two-sentence scope definition leaves 20 items in scope, 33 out of scope, and **11 items borderline** — undecided; all of the borderline items come from allowed units with a disallowed block or environment.
- Five of the eight front ends collect in-scope and out-of-scope items at the same point; 29 items are reachable by mistake through these front ends, and 11 of them are banned by name in the rules of engagement.
- `front-shared-2` carries the risk of 11 banned items for the sake of a single in-scope item behind it; the correct decision is to exclude that one item from the assessment.
- In the raw calculation the window exceeds the planned work by 4 hours, but once the meter-reading interval is subtracted it falls 6 hours short; the cut has to come from the 46-hour validation class.
- Two of the four communication thresholds carry no on-call coverage; a suspected scope excursion at 02:30 at night takes 4 hours by contract and 6.5 hours in practice, and involves four distinct parties; the four thresholds together reach six parties.

## Next Step

Scope has been turned into numbers, the window has been calculated, and notification times have been written down. These three numbers determine what can be done, but not **how** it will be done. An assessment over the same 20 items can be run with no prior information at all, or with credentials and architecture documents supplied. The two paths do not spend the same time and do not produce the same number of candidates. The next lesson compares black-, gray- and white-box assessments in terms of candidate production: how many candidates each type produces on the same target, how many turn into evidence, and what the time difference between them costs.
