---
title: 'Blue, Red, and Purple Teams'
source: 'https://academia.sh/en/courses/introduction-to-cybersecurity/blue-red-and-purple-teams'
course: 'Introduction to Cybersecurity'
language: en
updated: '2026-10-21T05:02:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts, separately, the paths the defense knows, the paths the attacking side finds, and the paths neither sees within the same set of paths'
    - 'Defines the information gap and measures how many paths purple work closes'
    - 'Computes the legitimate-work cost of closing a path and shows why closing a path that is not understood is expensive'
---

# Blue, Red, and Purple Teams

How three teams differ over the same set of paths: in a 30-path access graph, the 11 paths the defense knows, the 15 the attacking side finds, the 8 items where the two do not overlap, and the 13 paths neither sees; how purple work drives the information gap from 6 down to 0, how the closed paths climb from 17 to 22 while the legitimate requests they stop fall from 2,764 to 2,037, and how 8 out-of-scope paths remain.

The previous topic measured six defense principles over the same set of paths and wrote, side
by side, how many paths each one closed and how much legitimate work it stopped. Every one of
those measurements carried a quiet assumption: **the set of paths itself is known.** If a closed
path could be counted, that count was already written down somewhere beforehand. This lesson
lifts that assumption and asks a single question: how many items separate the set of paths an
organization knows from the set of paths that actually exists, and who finds that gap.

There are three working modes. **The blue team** runs the defense: it keeps the inventory,
builds the controls, reads the log. **The red team** looks for paths to the same asset within
written authorization and a defined scope; every path it finds may be an item that is not on the
defense's list. **Purple work** is not a separate roster — it is the two sides working together
over the same set of paths. This lesson separates all three into one number: **who sees how many
paths within the same set of paths.**

## Three Teams, One Set of Paths

The example is the course's fictional **regional measurement network**. The graph below is a
**model**; there is no real host, real address, or real service endpoint.

- **FR1.** The access graph is 12 nodes and 20 steps: 3 entry points, 2 protected assets. A
  **path** is a walk from an entry node to an asset with no repeated node.
- **FR2.** The defense knows a path only if **every** node on it is written in the asset
  inventory. A single node missing from the inventory makes every path through that node
  invisible.
- **FR3.** The attacking side finds a path only if every node is within **the written scope** and
  the step count does not exceed **the step budget**. In separate work the budget is 4 steps.
- **FR4.** In purple work the inventory is shared: every node the red side touches enters the
  inventory, and because no step is spent on discovery, the budget rises to 6.
- **FR5.** Each step carries a fixed number of legitimate requests per day; the numbers are
  fictional.
- **FR6.** A path closes when one step on it is cut. On a path it **understands**, the defense
  cuts the step carrying the fewest legitimate requests.
- **FR7.** On a path learned only from a report, whose intermediate nodes are not in the
  inventory, the defense cuts the one step it can name — the **entry step**.
- **FR8.** The cost to legitimate work is the total legitimate requests carried by the **distinct**
  set of cut steps; if the same step closes more than one path, it is counted once.

```js
// team/network.mjs — the access graph of a fictional regional measurement network (MODEL). There is
// no real host, real address, or real service endpoint; every number is fictional.

// node: inventory = is it listed in the defense's asset list;
//       scope     = is it within the bounds of written authorization (a third-party endpoint is outside).
export const NODE = `
  field-terminal       entry  yes  yes
  subscriber-portal    entry  yes  yes
  supplier-endpoint     entry  no   no
  branch-switch          mid   no   yes
  collector               mid   yes  yes
  validator                mid   yes  yes
  identity-directory        mid   yes  yes
  management-network         mid   yes  yes
  work-order                   mid   yes  yes
  backup-endpoint                mid   no   yes
  reading-store                   asset yes  yes
  billing                          asset yes  yes`
  .trim().split("\n").map((r) => {
    const [name, type, inv, scp] = r.trim().split(/\s+/);
    return { name, type, inventory: inv === "yes", scope: scp === "yes" };
  });

// edge: a step, and how many legitimate requests pass through it per day (fictional).
export const EDGE = `
  field-terminal     collector             420
  field-terminal     branch-switch          35
  subscriber-portal  validator             900
  subscriber-portal  identity-directory    760
  supplier-endpoint  management-network     18
  supplier-endpoint  backup-endpoint         6
  branch-switch      management-network     24
  branch-switch      collector             110
  collector          validator             380
  collector          reading-store         540
  validator          reading-store         610
  validator          billing               290
  identity-directory management-network    44
  identity-directory work-order           150
  management-network collector             60
  management-network backup-endpoint       12
  management-network identity-directory    38
  backup-endpoint    reading-store           9
  work-order         validator             130
  work-order         billing                95`
  .trim().split("\n").map((r) => {
    const [a, b, m] = r.trim().split(/\s+/);
    return { a, b, legit: Number(m) };
  });

const find = (name) => NODE.find((d) => d.name === name);
export const ENTRY = NODE.filter((d) => d.type === "entry").map((d) => d.name);
export const ASSET = NODE.filter((d) => d.type === "asset").map((d) => d.name);

// All simple paths: from an entry node to an asset, with no repeated node.
export function paths() {
  const out = {};
  for (const e of EDGE) (out[e.a] ??= []).push(e.b);
  const all = [];
  const walk = (d, trail) => {
    if (ASSET.includes(d)) { all.push([...trail]); return; }
    for (const s of out[d] ?? []) if (!trail.includes(s)) walk(s, [...trail, s]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return all.map((trail) => ({
    trail,
    step: trail.length - 1,
    edges: trail.slice(0, -1).map((_, i) => `${trail[i]}>${trail[i + 1]}`),
  }));
}

export const edgeLegitimate = (e) => {
  const [a, b] = e.split(">");
  return EDGE.find((k) => k.a === a && k.b === b).legit;
};

// The defense knows a path only if every node in it is listed in its inventory.
export const blueKnows = (y) => y.trail.every((d) => find(d).inventory);
// The attacking side finds a path only if every node is in scope and the step count does not
// exceed its budget; touching an out-of-scope node steps outside the authorization.
export const redFinds = (y, budget) =>
  y.trail.every((d) => find(d).scope) && y.step <= budget;
```

## Who Knows What

```js
// team/measure.mjs — within the same set of paths, the path the defense knows, the path the
// attacking side finds, and the path neither sees; then the legitimate-work cost of closing them.
import { paths, NODE, EDGE, edgeLegitimate, blueKnows, redFinds } from "./network.mjs";

const P = paths();
const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const INV0 = new Set(NODE.filter((d) => d.inventory).map((d) => d.name));
const knows = (y, inv) => y.trail.every((d) => inv.has(d));
const BUDGET = 4, PURPLE_BUDGET = 6;   // fictional: purple work spends no step on discovery

// Purple work: every node the red side touches enters the inventory, and red reads the inventory.
const TOUCHED = new Set();
for (const y of P) if (redFinds(y, BUDGET)) y.trail.forEach((d) => TOUCHED.add(d));
const INV1 = new Set([...INV0, ...TOUCHED]);

const status = (inv, b) => {
  const m = P.filter((y) => knows(y, inv)), k = P.filter((y) => redFinds(y, b));
  const cut = m.filter((y) => k.includes(y));
  return { blue: m.length, red: k.length, common: cut.length, blueOnly: m.length - cut.length,
    redOnly: k.length - cut.length, neither: P.length - (m.length + k.length - cut.length) };
};
const SOLO = status(INV0, BUDGET), PURPLE = status(INV1, PURPLE_BUDGET);

console.log(`access graph MODEL: ${NODE.length} nodes, ${EDGE.length} steps, ` +
  `${P.length} simple paths; step budget is ${BUDGET} working separately, ${PURPLE_BUDGET} in purple work.`);
const W1 = [-38, 10, 8];
console.log("\n1. who knows what in the same set of paths");
write(W1, "measure", "separate", "purple");
for (const [ad, f] of [["path the defense knows", (r) => r.blue],
  ["path the attacking side finds", (r) => r.red], ["  known by both", (r) => r.common],
  ["  known by defense only", (r) => r.blueOnly],
  ["  known by attack only (gap)", (r) => r.redOnly],
  ["path neither sees", (r) => r.neither]]) write(W1, ad, f(SOLO), f(PURPLE));

// Closing: the step carrying the fewest legitimate requests is cut on a known path; on a path
// learned only from the report, the defense cuts the one step it can name — the entry step.
const cheapest = (y) => y.edges.reduce((a, e) => (edgeLegitimate(e) < edgeLegitimate(a) ? e : a));
const cost = (cut) => ({ step: cut.size,
  legit: [...cut].reduce((a, e) => a + edgeLegitimate(e), 0) });
const knownSolo = P.filter((y) => knows(y, INV0));
const knownPaired = P.filter((y) => knows(y, INV0) || redFinds(y, BUDGET));
const knownPurple = P.filter((y) => knows(y, INV1) || redFinds(y, PURPLE_BUDGET));
const CUT1 = new Set(knownSolo.map(cheapest));
const CUT2 = new Set([...knownSolo.map(cheapest), ...knownPaired.filter((y) => !knows(y, INV0)).map((y) => y.edges[0])]);
const CUT3 = new Set(knownPurple.map(cheapest));

const W2 = [-30, 10, 8, 10, 16];
console.log("\n2. legitimate-work cost of closing (legitimate requests: per day, fictional)");
write(W2, "work mode", "closed", "open", "cut", "legit stopped");
for (const [ad, y, k] of [["defense only", knownSolo, CUT1], ["defense + separate testing", knownPaired, CUT2],
  ["purple work", knownPurple, CUT3]]) {
  const b = cost(k);
  write(W2, ad, y.length, P.length - y.length, b.step, b.legit);
}
const costliest = [...CUT2].filter((e) => !CUT3.has(e))
  .reduce((a, e) => (edgeLegitimate(e) > edgeLegitimate(a) ? e : a));
const thatPath = knownPaired.find((y) => y.edges[0] === costliest && !knows(y, INV0));
console.log(`\nmost expensive cut in separate testing: ${costliest} (${edgeLegitimate(costliest)} legitimate requests),`);
console.log(`  path it closes: ${thatPath.trail.join(" > ")}`);
console.log(`  in purple work the same path closes by cutting ${cheapest(thatPath)}: ` +
  `${edgeLegitimate(cheapest(thatPath))} legitimate requests.`);
const outOfScope = P.filter((y) => !y.trail.every((d) => NODE.find((x) => x.name === d).scope));
console.log(`of the ${PURPLE.neither} paths still remaining after purple work, ${outOfScope.length} ` +
  `pass through an out-of-scope node.`);
```

```
access graph MODEL: 12 nodes, 20 steps, 30 simple paths; step budget is 4 working separately, 6 in purple work.

1. who knows what in the same set of paths
measure                                 separate  purple
path the defense knows                        11      22
path the attacking side finds                 15      22
  known by both                                9      22
  known by defense only                        2       0
  known by attack only (gap)                   6       0
path neither sees                             13       8

2. legitimate-work cost of closing (legitimate requests: per day, fictional)
work mode                         closed    open       cut   legit stopped
defense only                          11      19         7            1969
defense + separate testing            17      13         9            2764
purple work                           22       8        10            2037

most expensive cut in separate testing: subscriber-portal>identity-directory (760 legitimate requests),
  path it closes: subscriber-portal > identity-directory > management-network > backup-endpoint > reading-store
  in purple work the same path closes by cutting backup-endpoint>reading-store: 9 legitimate requests.
of the 8 paths still remaining after purple work, 8 pass through an out-of-scope node.
```

The `separate` column of the first table reads the setup where the three teams work
independently. The graph has 30 paths. The defense knows **11** of them; the attacking side
finds **15**. The two numbers look close, but they do not point at the same paths: the
intersection is only **9** items.

Two non-overlapping items are not the same kind of gap. **The 2 paths known only to the defense**
are harmless: they are 5 steps long, and the attacking side's budget is 4 steps. The defense knows
them and can close them; they do not stop existing just because testing did not find them. **The 6
paths found only by the attacking side**, though, are the **information gap**: they pass through
two nodes — the branch switch and the backup endpoint — that are not in the inventory, so the
defense's list never had them. The size of what an organization does not know about its own
network can only be written down once an outside test measures that number.

The last row gives what neither team sees: **13 paths**. This is nearly half the total, and it
has two separate causes. Part of it runs through nodes the defense's inventory does not have and
is too long for the attacking side's budget to reach; the rest starts from the third-party
endpoint, which is **outside the scope**. Testing a system without written authorization and a
defined scope is both illegal and unprofessional; the reason no one touches the out-of-scope node
is not a technical barrier but this boundary.

## What Purple Work Closes

The `purple` column is the result of two changes, and both are an exchange of information. The
defense adds the nodes testing touches to its inventory; the branch switch and the backup
endpoint are now on the list. Because the testing side reads the inventory, it spends no step on
discovery, and its budget rises from 4 to 6. No new control was built, no new tool was bought, not
a single step in the graph changed.

The result changes across four rows at once. The path the defense knows goes from 11 to **22**,
the path the attacking side finds goes from 15 to **22**, and both are **the same 22 paths**: the
non-overlapping item drops to zero. The information gap falls **from 6 to 0**. The path neither
sees drops from 13 to **8**.

This is the measured contribution of purple work: **11 paths** the defense could not reach alone
enter the shared list. None of these paths are new — all 30 were in the graph from the start.
What closes is not a vulnerability; it is **an information gap**.

## The Cost of Closing

The second table reads the same three setups in terms of the defense's actual job: how many
paths close, and how many legitimate requests are stopped in exchange.

When the defense works alone, 11 paths close, 19 stay open, and cutting 7 steps stops **1,969**
legitimate requests a day. Once testing is added, closed paths rise to 17, but stopped legitimate
requests jump to **2,764**: closing 6 more paths costs 795 more requests, or 132 per path. In
purple work, closed paths are **22** and stopped legitimate requests are **2,037**: compared to
the paired setup, **5 more paths close while 727 fewer legitimate requests are stopped.**

This reversal is explained in the last three rows. In a setup working separately, the defense has
to close a path it reads from a report, but that path's intermediate nodes are not in its
inventory; the only step it can name is the entry step. It cuts the step from the subscriber
portal to the identity directory, and **760** legitimate requests are stopped a day. The same
path closes in purple work by cutting the step from the backup endpoint to the reading store: **9**
legitimate requests. The path that closes is identical; the cost differs by a factor of **84**.

The rule follows from this: **a path closed without being understood closes, but closes
expensively.** A finding list says which path is open; it does not say which step should be cut.
Choosing that step requires knowing what job every node on the path does, and that knowledge sits
with the defense. What purple work closes is a two-way gap: the defense learns that the path
exists, and the testing side learns the legitimate work running over it.

## Where Testing Is Not Enough

Even after purple work, **8 paths** are not on anyone's list, and the output's last line writes
that all 8 of these pass through an **out-of-scope** node. The third-party endpoint is neither in
the defense's inventory nor within the area testing is authorized for.

This remainder does not close with better testing. Raising the budget from 6 to 10 finds no new
path, because the obstacle is not the step count. Widening the inventory does not help either,
because that endpoint belongs to another organization, and authority to test it is granted only
by a separate written agreement. Part of the set of paths stays unmeasured not from a technical
gap but from **the boundary of authorization** — and an unmeasured path is an unclosed path.

## Summary

- In the 30-path graph, the defense sees 11 paths and the attacking side sees 15; the
  intersection is 9. Of the 8 non-overlapping items, 2 are the long path known to the defense, and
  **6 are the information gap** — paths the defense never knew about because they pass through
  two nodes missing from the inventory.
- Working separately, 13 paths sit outside both teams' view; after purple work that number falls
  to 8, and the information gap drops from 6 to 0. Not one step in the graph changes — only
  information is shared.
- Purple work raises closed paths from 17 to 22 while lowering stopped legitimate requests from
  2,764 to 2,037: more paths, less cost.
- A path that is not understood closes expensively. On a path learned from a report, the defense
  cuts the entry step (760 legitimate requests); the same path in purple work closes with a step
  carrying 9 legitimate requests.
- All of the remaining 8 paths pass through an out-of-scope node. This remainder can only be
  measured by changing the boundary of authorization, not by growing the budget.

## Next Step

This lesson separated three working modes and tied the difference between them to a single
number: who sees how many paths within the same set of paths. The measurement showed one thing —
the defense is not a roster, it is the sum of complementary **jobs**. The job that keeps the
inventory and the job that looks for the path sit in separate hands, and when the two do not talk,
11 paths never enter the shared list.

Still, "blue" and "red" are team names, not job names. Who keeps the inventory, who chooses which
step to cut, who reads the record left after a cut, who makes the architectural decision? The
next lesson divides the same set of paths this time by **role**, and looks for two numbers: how
many paths sit on no role at all, and how many sit on two roles at once.
