Skip to content
academia.sh

Lesson 05 / 15

Attack Surface

Converting surface into a countable unit: in a 13-node, 21-edge access graph, four entry points to two targets produce 35 separate paths; the entry point carrying only 1.4% of total legitimate work opens 37.1% of the paths; a single edge can close 14 paths; one edge stops 4 legitimate work items and closes no path at all; greedy narrowing by ratio zeroes the paths at a cost of 31 legitimate work items, and closing all entry points reaches the same result at a cost of 27.

Published Updated

In this lesson

  • Models attack surface as a set of paths in an access graph and runs the path count
  • Measures, for every edge, both the number of paths it closes and the number of legitimate work items it stops
  • Calculates the cost of reducing paths to zero and compares two reduction strategies
Contents

Every measurement in the previous lesson stood on a single assumption: that access is one step. All twelve of the twelve chains started from that step, and that step was never opened up. But reaching an asset is not passing through a single door. This lesson opens that step up and establishes the unit of measure the rest of the curriculum will stand on.

Attack surface is the whole of the entry points that can be used to reach an asset, plus the crossings between them. “The surface is wide” carries no decision. The surface’s countable counterpart is the count of attack paths: how many separate sequences go from source to target. From this lesson on, every defense will be measured by the same question — how many paths does it close, how many remain, how much legitimate work stops.

Surface Is a Path Count

The graph below is a model: there is no real network, no real hostname, no real service endpoint. Running a count like this on a real network without written authorization and a defined scope is both illegal and unprofessional.

SC21: every edge is an access crossing and carries the count of fictional legitimate-work items that pass through it per day. SC22: a path is a sequence from the source to a target that visits no node twice; the path count below is actually run with the depth-first walk that follows. SC23: when an edge is removed, every path that goes through it closes, and the legitimate work that edge carried stops.

// security/graph.mjs — fictional measurement network's access graph (model): there is no real
// network, real hostname, or real service endpoint. The graph is directed and acyclic. No randomness.

// SC21: an edge is written "source target legitimate-work"; legitimate work is the count of
// fictional work items passing through that edge per day.
export const EDGE = `
  outer-network         branch-endpoint       12
  outer-network         field-app              9
  outer-network         shared-interface       4
  outer-network         management-endpoint    2
  branch-endpoint       network-gateway       12
  field-app             network-gateway        9
  shared-interface      billing                4
  management-endpoint   network-gateway        2
  management-endpoint   validator              1
  network-gateway       collector             15
  network-gateway       billing                6
  network-gateway       work-order             5
  network-gateway       report-endpoint        4
  collector             validator             15
  collector             measurement-store     14
  validator             measurement-store     13
  validator             billing                7
  billing               measurement-store      5
  billing               identity-store         3
  work-order            validator              2
  work-order            identity-store         4`
  .trim().split("\n").map((r) => {
    const [a, b, work] = r.trim().split(/\s+/);
    return { from: a, to: b, work: +work, id: `${a}->${b}` };
  });

export const SOURCE = "outer-network";
export const TARGET = ["measurement-store", "identity-store"];
export const NODE = [...new Set(EDGE.flatMap((e) => [e.from, e.to]))];

// SC22: a path is a sequence from the source to a target that visits no node twice; the path
// count below is actually run with the depth-first walk that follows.
export function paths(edges = EDGE) {
  const adj = {};
  for (const e of edges) (adj[e.from] ||= []).push(e.to);
  const found = [], trail = [SOURCE], seen = new Set([SOURCE]);
  (function walk(d) {
    if (TARGET.includes(d)) return found.push([...trail]);
    for (const n of adj[d] || []) {
      if (seen.has(n)) continue;
      seen.add(n); trail.push(n);
      walk(n);
      trail.pop(); seen.delete(n);
    }
  })(SOURCE);
  return found;
}

export const PATH = paths();
export const remainingPaths = (removed) =>
  paths(EDGE.filter((e) => !removed.includes(e.id))).length;
export const stoppedWork = (removed) =>
  EDGE.filter((e) => removed.includes(e.id)).reduce((s, e) => s + e.work, 0);
export const TOTAL_WORK = EDGE.reduce((s, e) => s + e.work, 0);
// security/surface.mjs — the surface is a path count: how many paths, what closes them, how much legitimate work stands.
import { EDGE, NODE, TARGET, PATH, SOURCE, TOTAL_WORK, remainingPaths, stoppedWork } from "./graph.mjs";

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 len = PATH.map((p) => p.length - 1);
const entry = [...new Set(EDGE.filter((e) => e.from === SOURCE).map((e) => e.to))];

console.log(`${NODE.length} nodes, ${EDGE.length} edges, ${entry.length} entry points, ` +
  `${TARGET.length} targets; path count ${PATH.length}, path length ` +
  `${Math.min(...len)}..${Math.max(...len)}, total legitimate work ${TOTAL_WORK}`);

const A = [-21, 9, 10, 11];
console.log("\n1. where paths enter, where they exit");
write(A, "point", "paths", "shortest", "longest");
for (const e of entry) {
  const p = PATH.filter((x) => x[1] === e);
  write(A, e, p.length, Math.min(...p.map((x) => x.length - 1)),
    Math.max(...p.map((x) => x.length - 1)));
}
for (const t of TARGET) {
  const p = PATH.filter((x) => x.at(-1) === t);
  write(A, t, p.length, Math.min(...p.map((x) => x.length - 1)),
    Math.max(...p.map((x) => x.length - 1)));
}

const B = [-38, 11, 9, 8, 11];
console.log("\n2. if an edge is removed (path/work ratio, high to low)");
write(B, "edge", "leg. work", "closed", "left", "path/work");
const measured = EDGE.map((e) => ({ ...e, closes: PATH.length - remainingPaths([e.id]) }));
for (const e of [...measured].sort((x, y) => y.closes / y.work - x.closes / x.work))
  write(B, e.id, e.work, e.closes, PATH.length - e.closes, (e.closes / e.work).toFixed(2));

const C = [-36, 11, 12, 14, 13];
console.log("\n3. greedy narrowing: each round removes the highest path/work edge");
write(C, "removed edge", "leg. work", "paths left", "work stopped", "work left %");
const removed = [];
while (remainingPaths(removed) > 0) {
  const pick = EDGE.filter((e) => !removed.includes(e.id))
    .map((e) => ({ e, closes: remainingPaths(removed) - remainingPaths([...removed, e.id]) }))
    .sort((x, y) => y.closes / y.e.work - x.closes / x.e.work)[0];
  removed.push(pick.e.id);
  write(C, pick.e.id, pick.e.work, remainingPaths(removed), stoppedWork(removed),
    (((TOTAL_WORK - stoppedWork(removed)) / TOTAL_WORK) * 100).toFixed(1));
}

const D = [-30, 12, 14, 13];
console.log("\n4. three ways to shrink the surface");
write(D, "approach", "paths left", "work stopped", "work left %");
const entryEdge = EDGE.filter((e) => e.from === SOURCE).map((e) => e.id);
const free = measured.filter((e) => e.closes === 0).map((e) => e.id);
for (const [name, set] of [["nothing", []], ["edges that close zero paths", free],
  ["all entry points", entryEdge], ["greedy pick", removed]])
  write(D, name, remainingPaths(set), stoppedWork(set),
    (((TOTAL_WORK - stoppedWork(set)) / TOTAL_WORK) * 100).toFixed(1));
console.log(`edge that closes zero paths: ${free.join(", ") || "none"}`);

const E = [-21, 16, 14, 14, 14];
console.log("\n5. what counts as the surface measure (if an entry point is removed)");
write(E, "entry point", "share of pts %", "paths closed", "path share %", "work share %");
for (const e of entry) {
  const edge = EDGE.find((x) => x.from === SOURCE && x.to === e);
  const closes = PATH.length - remainingPaths([edge.id]);
  write(E, e, (100 / entry.length).toFixed(1), closes, ((closes / PATH.length) * 100).toFixed(1),
    ((edge.work / TOTAL_WORK) * 100).toFixed(1));
}
13 nodes, 21 edges, 4 entry points, 2 targets; path count 35, path length 3..6, total legitimate work 148

1. where paths enter, where they exit
point                    paths  shortest    longest
branch-endpoint             10         4          6
field-app                   10         4          6
shared-interface             2         3          3
management-endpoint         13         3          6
measurement-store           21         3          6
identity-store              14         3          6

2. if an edge is removed (path/work ratio, high to low)
edge                                    leg. work   closed    left  path/work
outer-network->management-endpoint              2       13      22       6.50
management-endpoint->network-gateway            2       10      25       5.00
work-order->validator                           2        9      26       4.50
billing->identity-store                         3       11      24       3.67
management-endpoint->validator                  1        3      32       3.00
network-gateway->work-order                     5       12      23       2.40
billing->measurement-store                      5       11      24       2.20
validator->billing                              7       14      21       2.00
outer-network->field-app                        9       10      25       1.11
field-app->network-gateway                      9       10      25       1.11
network-gateway->billing                        6        6      29       1.00
outer-network->branch-endpoint                 12       10      25       0.83
branch-endpoint->network-gateway               12       10      25       0.83
network-gateway->collector                     15       12      23       0.80
work-order->identity-store                      4        3      32       0.75
collector->validator                           15        9      26       0.60
validator->measurement-store                   13        7      28       0.54
outer-network->shared-interface                 4        2      33       0.50
shared-interface->billing                       4        2      33       0.50
collector->measurement-store                   14        3      32       0.21
network-gateway->report-endpoint                4        0      35       0.00

3. greedy narrowing: each round removes the highest path/work edge
removed edge                          leg. work  paths left  work stopped  work left %
outer-network->management-endpoint            2          22             2         98.6
work-order->validator                         2          16             4         97.3
billing->identity-store                       3          11             7         95.3
billing->measurement-store                    5           6            12         91.9
work-order->identity-store                    4           4            16         89.2
network-gateway->collector                   15           0            31         79.1

4. three ways to shrink the surface
approach                        paths left  work stopped  work left %
nothing                                 35             0        100.0
edges that close zero paths             35             4         97.3
all entry points                         0            27         81.8
greedy pick                              0            31         79.1
edge that closes zero paths: network-gateway->report-endpoint

5. what counts as the surface measure (if an entry point is removed)
entry point            share of pts %  paths closed  path share %  work share %
branch-endpoint                  25.0            10          28.6           8.1
field-app                        25.0            10          28.6           6.1
shared-interface                 25.0             2           5.7           2.7
management-endpoint              25.0            13          37.1           1.4

Numbers belong to the measurement class; their inputs are SC21–SC23.

Thirty-Five Paths

Thirteen nodes and twenty-one edges produce 35 paths. This is the number the surface will use in this lesson and through the rest of the curriculum. Neither node count nor edge count can stand in for it: if edges grow by 5%, the path count can double, because path count depends on how the edges combine.

The first table separates where paths enter and where they exit. 21 paths go to measurement-store, 14 to identity-store. On the entry side, though, there is an unexpected distribution: branch-endpoint and field-app each carry ten paths, shared-interface only two, management-endpoint thirteen — the highest of the four. The management endpoint is the network’s smallest gate, and it opens the most paths, because it connects to two of the graph’s middle nodes at once.

The shortest path is three steps and leaves from two endpoints — shared-interface and management-endpoint; the longest is six steps. The length distribution changes where a defense should go: a three-step path has two points to place an intermediate control, a six-step path has five.

An Edge’s Two Numbers

The second table is this lesson’s central measurement, and it names every edge with two numbers: how many paths it closes, how many legitimate work items it stops. The ordering is by the ratio between them.

The top row is the outer-network->management-endpoint edge: it closes 13 paths and stops only 2 legitimate work items — a ratio of 6.50. This is not the edge that closes the most paths; that is validator->billing, which closes 14 paths but stops 7 legitimate work items (ratio 2.00). Judged by “closes the most” alone, the second edge wins; once the cost is counted too, the first is three times as efficient.

The bottom of the table shows the opposite direction. collector->measurement-store closes only 3 paths and stops 14 legitimate work items — a ratio of 0.21. This edge is the network’s actual job: readings get written to the store through it. Removing it narrows the surface almost not at all, and stops a tenth of the work.

The last row is the item every surface-reduction list should look for. network-gateway->report-endpoint closes no path at all — 35 paths stay 35 — and stops 4 legitimate work items. The reporting endpoint connects to no target, so it does not sit on any path. In a simplification made without counting the surface, this edge could get removed as an “open end”; once measured, what shows up is a decision that has a cost and a return of zero.

What Counts as the Surface Measure

The fifth table shows why no other measure can substitute for path count. Each of the four entry points is 25% of the entry-point count; the share of paths they close is, respectively, 28.6%, 28.6%, 5.7%, and 37.1%. Four gates of the same size carry shares of the surface that differ by six and a half times.

A surface assessment made by counting entry points writes these four items as equal and produces the sentence “we have four open ends.” An assessment that counts edges falls into the same error: paths coming through shared-interface use two edges, so do paths starting at management-endpoint, but one opens two paths and the other thirteen. The one countable thing is combination, and the count of combination is the path.

The last column reads the same table in the opposite direction. management-endpoint carries only 1.4% of total legitimate work and opens 37.1% of the paths; branch-endpoint carries 8.1% of the work and 28.6% of the paths. Reading the two ratios together gives the order for surface reduction directly: the entry point carrying the least work while opening the most paths comes first. The three columns gathered through this lesson — paths closed, paths remaining, cost in legitimate work — turn, for the first time in this table, into a ranking rule.

The Cost of Reducing to Zero

The third table tries to reduce the paths to zero: each round removes the highest-ratio edge and the paths are counted again. After six edges, the paths remaining are zero and the legitimate work stopped is 31; 79.1% of the network’s work stays standing.

The order is instructive on its own. The first removal takes 35 paths down to 22, the second to 16, the third to 11. The first three edges close 68.6% of the paths at a total cost of 7 legitimate work items. The cost paid for the remaining eleven paths is 24 legitimate work items. The return on surface reduction is concentrated in the first steps; closing the last paths gets exponentially more expensive. The last row is the clearest form of this: network-gateway->collector alone stops 15 legitimate work items and closes the four paths that remain.

The fourth table compares the greedy choice against two alternatives. Removing the edges that close zero paths spends 4 legitimate work items and closes none of the 35 paths. Cutting all entry points off from the outer network zeroes the paths and stops 27 legitimate work items. The greedy choice by ratio also zeroes the paths, but stops 31 legitimate work items.

This last comparison is this lesson’s most striking result: choosing the edge that looks most efficient at every step gives a more expensive total in the end. The greedy choice spends the cheap edges in the first rounds and ends up having to remove an expensive one. The cheapest complete cut is not an edge ranking but a cut selection — which set of edges together separates the source from the targets. When a defense list is ordered item by item on efficiency, unless what the chosen items do together is counted, the total cost comes out four units higher.

Summary

  • Attack surface’s countable counterpart is path count: in a 13-node, 21-edge graph, 35 paths run from four entry points to two targets, with lengths between 3 and 6 steps.
  • Each of the four entry points is 25% of the entry-point count, but they open 28.6%, 28.6%, 5.7%, and 37.1% of the paths; the entry point carrying the least legitimate work (1.4%) carries the most paths. Counting entry points or edges does not measure the surface.
  • Every edge is named with two numbers: the edge that closes the most paths is 14 paths / 7 work (ratio 2.00), the most efficient is 13 paths / 2 work (ratio 6.50), and among the path-closing edges the least efficient is 3 paths / 14 work (ratio 0.21).
  • One edge stops 4 legitimate work items and closes no path; because it connects to no target, it does not sit on any path.
  • The first three removals close 68.6% of the paths at a cost of 7 legitimate work items; the cost paid for the remaining eleven paths is 24 legitimate work items.
  • Zeroing the paths costs 31 legitimate work items by greedy selection and 27 by cutting all entry points; the most efficient choice item by item is not the cheapest choice overall.

Next Step

This topic established security’s units of measure. The three axes were overlapped on the same asset, asset, threat, and vulnerability were split into separate lists, risk was ranked as the composition of probability and impact, the chain from vulnerability to impact was broken into steps, and this lesson reduced surface to a single number: 35 paths. Every question now has a unit.

Yet through all of these measurements, one thing was never actually done. Paths were counted, removing edges was tried, and its cost was written down — but removal is not a defense; it is stopping work. A real defense leaves the edge in place and puts a control on top of it: adds a layer, narrows privilege, splits into compartments, removes location-based trust, separates authentication from authorization, turns defaults off. Each of these principles can be run over the same 35 paths, and the same three numbers can be written for each: how many paths closed, how many remain, how much legitimate work stopped. The next topic — Defense Principles — measures six principles one at a time with these same three columns.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close