---
title: 'Port Scanning'
source: 'https://academia.sh/en/courses/penetration-testing/port-scanning'
course: 'Security Tooling and Penetration Testing'
language: en
updated: '2026-09-26T03:02:55+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares scan types by the number of ports definitively distinguished, left ambiguous, and missed'
    - 'Calculates the network record, application record, and alert count each type leaves on the defense side'
    - 'Measures the balance between speed and trace, and how combining types contributes to coverage'
---

# Port Scanning

Comparing scan types on the trace axis: in a 96-port space, 23 open ports being definitively distinguished by type between 78.3% and 0%, the one-way probe producing 21 ambiguous candidates and 3 false positives, the trace per piece of evidence ranging from 3.13 to 8.00 records, the same type slowed down producing the same 18 pieces of evidence with the same 72 records but 0 alerts instead of 2, and 91.3% coverage requiring a combination of two types.

At the end of three lessons the externally visible name list is ready, but a name is not an asset.
A record resolves to a target; that target might accept no connections at all, or it might offer
three services at once. The two look identical at the record level. This lesson covers the step
that tests the difference, and the point where it departs from the previous three is this:
requests go to the target **directly** and **in large numbers**.

This changes the weight of the measurement too. The trace accumulated so far was 136 records and
5 alerts. In this lesson a single scan type on its own can exceed half that number. The comparison
is made on this axis: how many open ports each type definitively distinguishes, how many it
leaves ambiguous, how many it misses, and how many records and alerts it produces on the defense
side. This step can only be done against endpoints covered by written authorization and the scope
definition; even a single request sent to an out-of-scope endpoint invalidates the authorization.

## What Defines a Type Is What It Distinguishes

Scan types are referred to here not as a tool, a command, or an option, but as **discriminating
power**. What defines a type is which port state it definitively distinguishes and which it
leaves ambiguous.

**RE10:** the scan space is 9 connection-oriented and 3 connectionless ports per endpoint; the common port
list is 4 ports per endpoint. A port is in the `open`, `filtered`, or `closed` state. **RE11:** a
type is defined by which state of its space it **definitively** distinguishes and which it leaves
**ambiguous**; among the ports left ambiguous, the ones that are not actually open are false
positives. **RE12:** the trace is three items — every attempt is a network record, every open
port whose connection completes is an application record, and an alert follows from the speed
threshold and from whether an application record exists.

```js
// recon/port.mjs — a fictional eight-endpoint port state and five scan types (a model): no real
// host, no real address, and no real service. No tool name, command, or flag sequence either:
// scan types are defined only by "which state it definitively distinguishes, which it leaves
// ambiguous."

// RE10: the scan space is 9 connection-oriented (conn) and 3 connectionless (disc) ports per endpoint; the
// common port list is 4 ports per endpoint. Only the non-closed ports are listed below, the rest
// are closed. "response" says whether the connectionless port answered the probe.
export const ENDPOINT = ["subscriber", "field", "vpn", "collector", "report", "shared", "monitoring", "log"];
export const SPACE = { conn: ENDPOINT.length * 9, disc: ENDPOINT.length * 3, common: ENDPOINT.length * 4 };

export const PORT = `
  subscriber   n1 conn open      1 -
  subscriber   n2 conn open      1 -
  subscriber   n3 conn open      0 -
  subscriber   n4 conn filtered  1 -
  field        n1 conn open      1 -
  field        n5 conn open      0 -
  field        n6 conn open      1 -
  field        b1 disc open      1 yes
  vpn          n1 conn filtered  1 -
  vpn          n6 conn open      1 -
  vpn          b2 disc open      0 no
  collector    n6 conn open      1 -
  collector    n7 conn open      0 -
  collector    n8 conn open      0 -
  collector    b1 disc open      1 yes
  report       n1 conn open      1 -
  report       n6 conn open      1 -
  report       n9 conn filtered  0 -
  shared       n1 conn open      1 -
  shared       n3 conn open      0 -
  monitoring   n1 conn open      1 -
  monitoring   n7 conn open      0 -
  monitoring   b1 disc open      1 yes
  log          n1 conn open      1 -
  log          n5 conn open      0 -
  log          b3 disc open      1 no`
  .trim().split("\n").map((r) => {
    const [endpoint, id, proto, state, common, response] = r.trim().split(/\s+/);
    return { endpoint, id, proto, state, common: +common, response };
  });

export const OPEN = PORT.filter((n) => n.state === "open");
const isOpen = (n) => n.state === "open";
const isResponsive = (n) => n.state === "open" && n.response === "yes";

// RE11: a scan type is defined by which state of its space it definitively distinguishes and
// which it leaves ambiguous. "unit" is a fictional time unit for one attempt, "app" is whether
// the connection completes and leaves a record at the application layer.
export const TYPE = [
  { name: "full connection", space: "conn", unit: 3, app: 1, confirmed: isOpen, ambiguous: () => false },
  { name: "half connection", space: "conn", unit: 1, app: 0, confirmed: isOpen, ambiguous: () => false },
  { name: "one-way probe", space: "conn", unit: 1, app: 0, confirmed: () => false,
    ambiguous: (n) => n.state !== "closed" },
  { name: "connectionless protocol", space: "disc", unit: 5, app: 0, confirmed: isResponsive,
    ambiguous: (n) => isOpen(n) && !isResponsive(n) },
  { name: "limited list", space: "common", unit: 3, app: 1, confirmed: isOpen,
    ambiguous: () => false },
  { name: "half connection (slow)", space: "conn", unit: 10, app: 0, confirmed: isOpen,
    ambiguous: () => false },
];

export const inScope = (t, n) =>
  t.space === "common" ? n.common === 1 : n.proto === t.space;

// RE12: the trace is three items — every attempt is a network record, every open port whose
// connection completes is an application record, and an alert follows from the speed threshold
// and from whether an application record exists.
export const measure = (t) => {
  const seen = PORT.filter((n) => inScope(t, n));
  const confirmed = seen.filter((n) => t.confirmed(n));
  const ambiguous = seen.filter((n) => t.ambiguous(n));
  const attempts = SPACE[t.space];
  const duration = attempts * t.unit;
  const rate = attempts / duration;
  const netRecords = attempts;
  const appRecords = t.app * confirmed.length;
  const alerts = (rate > 0.5 ? 2 : rate > 0.25 ? 1 : 0) + (appRecords > 0 ? 1 : 0);
  return { t, attempts, confirmed, ambiguous, duration, rate, netRecords, appRecords,
    records: netRecords + appRecords, alerts,
    falseP: ambiguous.filter((n) => n.state !== "open").length,
    missed: OPEN.length - confirmed.length - ambiguous.filter(isOpen).length };
};
```

```js
// recon/scan.mjs — scan types on the trace axis: how many candidates, how many missed, how many
// records, how many alerts.
import { PORT, OPEN, TYPE, SPACE, measure } from "./port.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 M = TYPE.map(measure);

console.log(`${SPACE.conn} connection-oriented + ${SPACE.disc} connectionless port space, ` +
  `${PORT.length} not-closed ports: ${OPEN.length} open, ` +
  `${PORT.filter((n) => n.state === "filtered").length} filtered`);

const A = [-23, 8, 8, 10, 9, 11, 10];
console.log("\n1. found and missed per scan type");
write(A, "scan type", "attempts", "conf", "ambiguous", "falseP", "missed", "cover %");
for (const o of M)
  write(A, o.t.name, o.attempts, o.confirmed.length, o.ambiguous.length, o.falseP, o.missed,
    ((o.confirmed.length / OPEN.length) * 100).toFixed(1));

const B = [-23, 9, 10, 8, 9, 8, 7];
console.log("\n2. the trace left on the defense side");
write(B, "scan type", "net rec", "app rec", "records", "duration", "rate", "alerts");
for (const o of M)
  write(B, o.t.name, o.netRecords, o.appRecords, o.records, o.duration, o.rate.toFixed(2), o.alerts);

const C = [-23, 12, 12, 12];
console.log("\n3. cost per piece of evidence");
write(C, "scan type", "records/conf", "dur/conf", "alerts/conf");
for (const o of M) {
  const k = o.confirmed.length;
  write(C, o.t.name, k ? (o.records / k).toFixed(2) : "-", k ? (o.duration / k).toFixed(2) : "-",
    k ? (o.alerts / k).toFixed(3) : "-");
}

const D = [-50, 8, 10, 8, 9, 8];
console.log("\n4. combining two scan types");
write(D, "combination", "conf", "cover %", "records", "duration", "alerts");
const pair = [[1, 3], [4, 3], [5, 3], [0, 3]];
for (const [i, j] of pair) {
  const k = new Set([...M[i].confirmed, ...M[j].confirmed]).size;
  write(D, `${M[i].t.name} + ${M[j].t.name}`, k, ((k / OPEN.length) * 100).toFixed(1),
    M[i].records + M[j].records, M[i].duration + M[j].duration, M[i].alerts + M[j].alerts);
}
const slow = M[5], fast = M[1];
console.log(`same type slowed down: confirmed ${fast.confirmed.length} -> ${slow.confirmed.length}, ` +
  `records ${fast.records} -> ${slow.records}, duration ${fast.duration} -> ${slow.duration}, ` +
  `alerts ${fast.alerts} -> ${slow.alerts}`);
```

```
72 connection-oriented + 24 connectionless port space, 26 not-closed ports: 23 open, 3 filtered

1. found and missed per scan type
scan type              attempts    conf ambiguous   falseP     missed   cover %
full connection              72      18         0        0          5      78.3
half connection              72      18         0        0          5      78.3
one-way probe                72       0        21        3          5       0.0
connectionless protocol      24       3         2        0         18      13.0
limited list                 32      15         0        0          8      65.2
half connection (slow)       72      18         0        0          5      78.3

2. the trace left on the defense side
scan type                net rec   app rec records duration    rate alerts
full connection               72        18      90      216    0.33      2
half connection               72         0      72       72    1.00      2
one-way probe                 72         0      72       72    1.00      2
connectionless protocol       24         0      24      120    0.20      0
limited list                  32        15      47       96    0.33      2
half connection (slow)        72         0      72      720    0.10      0

3. cost per piece of evidence
scan type              records/conf    dur/conf alerts/conf
full connection                5.00       12.00       0.111
half connection                4.00        4.00       0.111
one-way probe                     -           -           -
connectionless protocol        8.00       40.00       0.000
limited list                   3.13        6.40       0.133
half connection (slow)         4.00       40.00       0.000

4. combining two scan types
combination                                           conf   cover % records duration  alerts
half connection + connectionless protocol               21      91.3      96      192       2
limited list + connectionless protocol                  15      65.2      71      216       2
half connection (slow) + connectionless protocol        21      91.3      96      840       0
full connection + connectionless protocol               21      91.3     114      336       2
same type slowed down: confirmed 18 -> 18, records 72 -> 72, duration 72 -> 720, alerts 2 -> 0
```

The numbers belong to the **measurement** class; their inputs are RE10–RE12.

## Zero Confirmed, Twenty-One Ambiguous

The most striking row of the first table is the third one. The one-way probe makes 72 attempts
and **cannot definitively distinguish a single port**: its coverage is 0%. Instead it produces 21
ambiguous candidates. The reason lies in the type's definition — in this type, no response
arriving is the same outcome for both an open and a filtered port; the two cannot be told apart.
**3 of the 21 candidates produced are false positives**; the three filtered ports sit in the same
list as the open ones.

This row is the plainest example of this course's unit of measure. The sentence "the scan found
twenty-one open ports" is false for this type: twenty-one **candidates** were found, zero pieces
of evidence. Closing that gap requires each candidate to be confirmed with a separate step, and
the trace of those steps does not appear in this table at all.

At the other end, the full and half connection types definitively distinguish 18 ports, leave
nothing ambiguous, and their coverage is **78.3%**. The five ports they miss are on the
connectionless protocol: a type that scans the connection-oriented space cannot see a single port in the
connectionless space. The limited list finds 15 ports with 32 attempts (65.2%) and misses 8 — the
ones it misses are the ports outside the common list, and those are exactly the ones most
interesting in an assessment.

## Same Evidence, Different Trace

The second table opens up the trace column. Full and half connection produce the same 18 pieces
of evidence, but full connection leaves **90 records** and half connection leaves **72 records**.
The difference is in the application record: full connection completes the connection at every
open port, and the application at that port writes a line. Half connection does not complete the
connection; 72 lines remain at the network layer, none at the application layer.

This distinction also determines the **value** of the two records from the blue team's side. A
network record only says that an attempt was made; an application record says who connected to
which port on which endpoint, often together with an identity or a request line. A type chosen on
the scanning side for "leaving less trace" means, on the defense side, **trace that carries less
information**.

The third table gives cost per piece of evidence. The limited list is the cheapest type at
**3.13 records** per piece of evidence, but it never sees eight ports at all. The
connectionless-protocol scan spends **8.00 records** and **40.00 duration** units per piece of
evidence; it is the most expensive in the records column, ties the slow half connection in the
duration column, and gives only 3 pieces of evidence in return. In this table, the cheapness
ranking and the coverage ranking are **each other's reverse**.

## The Balance Between Speed and Trace

The last line of the second table is this lesson's main finding. When the half connection type is
slowed down tenfold, **the confirmed count stays at 18, records stay at 72, and alerts drop from
2 to 0**. Duration rises from 72 to 720.

In other words, the relationship between speed and trace is not direct. Slowing down **does not
reduce the number of records** the defense sees at all; what it reduces is those records crossing
a threshold and turning into an alert. The same 72 lines get written to the log in both runs; in
one it triggers the rate rule because it fits inside a minute, in the other it does not because
it is spread over a window ten times wider.

The defense side's takeaway reads directly from this: **a detection based on a speed threshold
will never see a slow scan**. The way to see a slow scan is to accumulate attempts from the same
source over a wide window and tie the threshold not to speed but to **spread** — how many
separate endpoints and how many separate ports were touched. In this run, both types touch 8
endpoints and 72 ports; the spread is the same in both runs.

The `alerts/conf` column of the third table writes the same thing from the reverse direction. The
slow half connection and the connectionless-protocol scan produce **0.000** alerts; full and half
connection produce 0.111, the limited list 0.133. The type producing the most alerts is not the
type making the most attempts — the limited list gives the highest rate with 32 attempts, while
the connectionless-protocol scan produces no alerts at all with 24 attempts; because an alert depends
not on the number of attempts but on the attempt's **density and layer**.

## Twenty-One of Twenty-Three Ports

The fourth table shows that a single type is not enough. The highest single-type coverage of the
twenty-three open ports is 78.3%; climbing to **91.3%** requires combining two types that scan the
connection-oriented and connectionless spaces separately. The remaining two ports are not found by any
combination: the two open ports in the connectionless space that do not respond to the probe cannot
be tied to a definitive result by any of these types.

The combinations' cost differs. Half connection with connectionless protocol spends 96 records, 192
duration, and 2 alerts for 21 pieces of evidence. When the same evidence is obtained with full
connection, records rise to 114 and duration to 336; alerts stay at 2. When obtained with the slow
half connection, records stay at 96, duration is **840**, and alerts are **0**. The three options
give the same 21 pieces of evidence; the difference between them is only in cost and trace.

## Summary

- What determines a scan type is the state it distinguishes: for 23 open ports in a 96-port
  space, the coverages are 78.3%, 78.3%, 0%, 13.0%, and 65.2%.
- The one-way probe cannot definitively distinguish a single port; it produces 21 ambiguous
  candidates, 3 of them false positives — this type produces candidates, not evidence.
- The same 18 pieces of evidence leave 90 records with full connection and 72 with half
  connection; the difference is at the application layer, and an application record carries more
  information on the defense side.
- Cost per piece of evidence ranges from 3.13 to 8.00 records; the cheapest type never sees 8
  ports, the most expensive gives only 3 pieces of evidence.
- When the same type is slowed down tenfold, evidence stays at 18, records stay at 72, and alerts
  drop from 2 to 0: slowing down does not erase the trace, it keeps the trace from crossing the
  threshold.
- 21 of the twenty-three open ports (91.3%) can only be reached by combining two types; two ports
  cannot be tied to a definitive result in any combination.

## Next Step

This lesson definitively distinguished that a port is **open**: 21 ports were marked at the
evidence level. Yet a port being open does not say what is running there. Each of the twenty-one
open ports is, for now, only a number; what software sits behind it, which version it is running,
and whether that version is associated with a known vulnerability have not been asked at all.

The step that asks this question is fingerprinting, and it brings a new measure: **precision**.
The signatures collected from an endpoint point to a version set; the set sometimes has a single
member, sometimes contains dozens of versions, and sometimes is the **wrong** set because of a
banner the target deliberately leaves misleading. The next lesson counts how many candidates can
be narrowed down to a single version, how much extra probing raises precision and how much it
grows the trace, and how many false positives incorrect version information carries into the
finding list. It is the topic's closing lesson.
