---
title: 'Phishing Types'
source: 'https://academia.sh/en/courses/attack-techniques/phishing-types'
course: 'Attack Techniques and the Threat Landscape'
language: en
updated: '2026-09-26T03:02:49+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts distinguishing indicator types across a message set and derives the per-class average'
    - 'Runs a threshold scan and reads the missed count together with the false-positive count'
    - 'Shows that indicator count falls as the target narrows, and where the threshold collapses'
---

# Phishing Types

Counting ten indicator types in a fictional set of twenty-six messages: the gap in average indicator count across bulk, spear, and whaling messages, the opposite directions of what a threshold scan misses versus what it falsely flags, legitimate messages flagged independent of any threshold, and the early-break gain of a five-step chain.

The previous lesson assumed the request had already reached the decision-maker and passed over the
chain's third link — making contact — in a single line. Yet the message that contact carries is
itself a measurable object: it carries countable indicators, and those indicators can be summed into
a single number. This lesson produces that number and records, together, what the chosen threshold
misses and what it falsely flags.

What is measured is not the message's text. Text varies endlessly; indicator type does not. A sender
coming from a domain never seen before, a sender verification record that fails, a link target that
diverges from the displayed text — each of these is an item a control **can see**. How sender
verification records are set up was measured in a different course; here, that record's result is
only one of ten indicators. The real question is this: why does phishing carry fewer indicators as
its target narrows, and what does that do to the threshold?

- **SE7.** The message set belongs to the field-crew mailbox of the fictional regional measurement
  network. No real message exists; the model carries only **indicator type and count** — no message
  text, subject line, or link ever appears.
- **SE8.** Every message's true class is known in the model. In reality, class is known only after
  review; it is assumed known here so that missed and falsely flagged messages can be counted
  separately.
- **SE9.** Indicator weights are fixed, and the score is the sum of the weights. In real systems
  weight is learned and drifts; a fixed weight simplifies the curve's shape and does not change the
  direction of either error.
- **SE10.** The set is not realistic in class ratio: there are sixteen fake and ten legitimate
  messages. In a real inbox the number of legitimate messages is far larger, and the number of false
  positives grows in the same proportion. The model therefore **understates** the false-positive
  rate.
- **SE11.** A message is either flagged or it passes. Intermediate states such as quarantine, a
  warning banner, or asking the user are not modeled.
- **SE12.** The chain was reduced to a single pattern: delivery, interaction, credential entry, use.
  Variants that proceed with an attachment or that directly request payment were not counted as
  separate chains.

## Indicator Types and Classes

```js
// messages.mjs — indicator types and each message's indicator set; model (no message content)
export const INDICATOR = { // indicator type: distinguishing weight
  'sender domain seen for the first time': 3, 'sender verification record fails': 3,
  'sender domain resembles a protected name': 3, 'link target diverges from displayed text': 3,
  'reply address differs from sender': 2, 'unexpected attachment type': 2,
  'wide recipient set': 1, 'unusual send time': 1,
  'first contact outside any reply thread': 1, 'time-pressure phrasing': 1,
};
const [F, V, R, L, P, A, W, U, C, T] = Object.keys(INDICATOR);
export const CLASS = ['bulk', 'spear', 'whaling', 'legitimate'];
// [message id, class, indicators it carries]
export const MESSAGE = [
  ['B1', 'bulk', [F, V, L, P, W, U, T]], ['B2', 'bulk', [F, V, L, A, W, T]],
  ['B3', 'bulk', [F, V, P, W, C, T]], ['B4', 'bulk', [F, R, L, W, U, T]],
  ['B5', 'bulk', [F, V, L, W, T]], ['B6', 'bulk', [V, L, P, W, U]],
  ['B7', 'bulk', [F, V, A, W, C, T]], ['B8', 'bulk', [F, L, W, U, C, T]],
  ['S1', 'spear', [R, L, C, T]], ['S2', 'spear', [F, L, C]],
  ['S3', 'spear', [R, P, T]], ['S4', 'spear', [V, L]], ['S5', 'spear', [R, A, C]],
  ['W1', 'whaling', [R, T]], ['W2', 'whaling', [L]], ['W3', 'whaling', [R, C]],
  ['L1', 'legitimate', [C]], ['L2', 'legitimate', [W, U]], ['L3', 'legitimate', []],
  ['L4', 'legitimate', [F, U, C]], ['L5', 'legitimate', [A]], ['L6', 'legitimate', [U, T]],
  ['L7', 'legitimate', [P]], ['L8', 'legitimate', [W, U, C, T]],
  ['L9', 'legitimate', [F, V, L, P, W]], ['L10', 'legitimate', [F, V, P, C]],
];
export const score = (m) => m[2].reduce((a, g) => a + INDICATOR[g], 0);
export const isFake = (m) => m[1] !== 'legitimate';
export function atThreshold(e) {
  const flagged = MESSAGE.filter((m) => score(m) >= e);
  return { missed: MESSAGE.filter((m) => isFake(m) && score(m) < e), falsePositives: flagged.filter((m) => !isFake(m)),
    caught: flagged.filter(isFake) };
}
// [step, precondition, control that sees the step, item dropped once the step completes]
export const CHAIN = [
  ['preparing the message', 'target address list', null, 0],
  ['delivery', 'a valid mail path', 'indicator threshold', 1],
  ['opening and interaction', "the recipient's interest", 'user reporting', 1],
  ['credential entry', 'a convincing collection point', 'second factor', 3],
  ['use of the collected information', 'a valid session', 'anomalous session detection', 6],
];
```

```js
// run1.mjs — per-indicator class distribution and the indicator-count gap across three attack types
import { INDICATOR, MESSAGE, CLASS, score, isFake } from './messages.mjs';

console.log(`${MESSAGE.length} messages, ${MESSAGE.filter(isFake).length} fake; ${Object.keys(INDICATOR).length} indicator types, ` +
  `highest possible score ${Object.values(INDICATOR).reduce((a, b) => a + b, 0)}\n`);
console.log(`${'indicator'.padEnd(40)} ${'wt.'.padStart(3)} ${'bulk'.padStart(5)} ${'spear'.padStart(7)} ` +
  `${'whaling'.padStart(8)} ${'legitimate'.padStart(10)}`);
for (const [g, a] of Object.entries(INDICATOR)) {
  const c = CLASS.map((s) => MESSAGE.filter((m) => m[1] === s && m[2].includes(g)).length);
  console.log(`${g.padEnd(40)} ${String(a).padStart(3)} ${String(c[0]).padStart(5)} ${String(c[1]).padStart(7)} ` +
    `${String(c[2]).padStart(8)} ${String(c[3]).padStart(10)}`);
}
console.log(`\n${'class'.padEnd(10)} ${'messages'.padStart(8)} ${'avg. indicators'.padStart(15)} ${'avg. score'.padStart(10)} ` +
  `${'min score'.padStart(13)} ${'max score'.padStart(14)}`);
for (const s of CLASS) {
  const k = MESSAGE.filter((m) => m[1] === s), p = k.map(score);
  console.log(`${s.padEnd(10)} ${String(k.length).padStart(8)} ` +
    `${(k.reduce((a, m) => a + m[2].length, 0) / k.length).toFixed(2).padStart(15)} ` +
    `${(p.reduce((a, b) => a + b, 0) / k.length).toFixed(2).padStart(10)} ${String(Math.min(...p)).padStart(13)} ` +
    `${String(Math.max(...p)).padStart(14)}`);
}
const whl = MESSAGE.filter((m) => m[1] === 'whaling'), leg = MESSAGE.filter((m) => m[1] === 'legitimate');
console.log(`\naverage indicator count of whaling messages (${(whl.reduce((a, m) => a + m[2].length, 0) / whl.length).toFixed(2)}), ` +
  `lower than that of legitimate messages (${(leg.reduce((a, m) => a + m[2].length, 0) / leg.length).toFixed(2)})`);
console.log(`4 highest-scoring messages: ${[...MESSAGE].sort((a, b) => score(b) - score(a)).slice(0, 4).map((m) => `${m[0]}/${m[1]} (${score(m)})`).join(', ')}`);
```

```
26 messages, 16 fake; 10 indicator types, highest possible score 20

indicator                                wt.  bulk   spear  whaling legitimate
sender domain seen for the first time      3     7       1        0          3
sender verification record fails           3     6       1        0          2
sender domain resembles a protected name   3     1       3        2          0
link target diverges from displayed text   3     6       3        1          1
reply address differs from sender          2     3       1        0          3
unexpected attachment type                 2     2       1        0          1
wide recipient set                         1     8       0        0          3
unusual send time                          1     4       0        0          4
first contact outside any reply thread     1     3       3        1          4
time-pressure phrasing                     1     7       2        1          2

class      messages avg. indicators avg. score     min score      max score
bulk              8            5.88      11.50            10             14
spear             5            3.00       6.60             6              8
whaling           3            1.67       3.67             3              4
legitimate       10            2.30       3.90             0             12

average indicator count of whaling messages (1.67), lower than that of legitimate messages (2.30)
4 highest-scoring messages: B1/bulk (14), B2/bulk (13), B4/bulk (12), L9/legitimate (12)
```

Read row by row, the indicator table shows which item belongs to which class. A wide recipient set
appears in eight of eight bulk messages and in none of the spear or whaling messages; a sender domain
that resembles a protected name runs the other way — it appears in one bulk message and in five
narrowly targeted ones. Cheap indicators are a scale signal; expensive indicators are a preparation
signal.

The class table gives the lesson's central number. Bulk phishing carries 5.88 indicators per
message, spear phishing 3.00, whaling 1.67. The drop is not chance; it is the definition itself: as
the target narrows, **cheap** indicators such as a wide recipient set, an unusual hour, and generic
sending infrastructure disappear. What remains is only the expensive indicators, and there are few of
those too.

The last line shows the measure's limit. The average indicator count of whaling messages is 1.67,
that of legitimate messages 2.30. A fake message carries **fewer** indicators than a legitimate one.
No threshold that looks only at indicator count can separate these two sets in the right direction; a
threshold that tries flags legitimate messages before it flags fake ones.

The top of the score ranking shows the same problem on the weight side. Fourth place is a legitimate
message, sitting at twelve points in the same band as three bulk phishing messages. This message is a
subscriber notice sent through an external delivery service: it carries a domain seen for the first
time, a verification record that fails, a reply address that diverges from the sender, and a link
target that diverges from the displayed text. Every one of these indicators is counted correctly; the
message not being fake does not change that.

## Where the Threshold Collapses

```js
// run2.mjs — threshold scan, structural false positives, and the chain's early-break gain
import { MESSAGE, CHAIN, score, isFake, atThreshold } from './messages.mjs';

const FAKE = MESSAGE.filter(isFake).length;
console.log(`${'threshold'.padStart(9)} ${'caught'.padStart(9)} ${'missed'.padStart(6)} ${'false pos.'.padStart(10)} ` +
  `${'total wrong'.padStart(11)} ${'class distribution of missed'.padStart(30)}`);
const result = [];
for (let e = 13; e >= 2; e--) {
  const r = atThreshold(e); result.push({ e, r });
  const d = ['bulk', 'spear', 'whaling'].map((s) => `${s[0].toUpperCase()}${r.missed.filter((m) => m[1] === s).length}`).join(' ');
  console.log(`${String(e).padStart(9)} ${`${r.caught.length}/${FAKE}`.padStart(9)} ${String(r.missed.length).padStart(6)} ` +
    `${String(r.falsePositives.length).padStart(10)} ${String(r.missed.length + r.falsePositives.length).padStart(11)} ${d.padStart(30)}`);
}
const least = result.reduce((a, s) => (s.r.missed.length + s.r.falsePositives.length < a.r.missed.length + a.r.falsePositives.length ? s : a));
const clean = result.filter((s) => s.r.falsePositives.length === 0);
const full = result.filter((s) => s.r.missed.length === 0);
console.log(`\nthreshold minimizing total wrong ${least.e}: missed ${least.r.missed.length}, falsely flagged ${least.r.falsePositives.length}`);
console.log(`lowest threshold with zero false positives ${Math.min(...clean.map((s) => s.e))}: missed there ${clean.reduce((a, s) => (s.e < a.e ? s : a)).r.missed.length}`);
console.log(`highest threshold that misses nothing ${Math.max(...full.map((s) => s.e))}: falsely flagged there ${full.reduce((a, s) => (s.e > a.e ? s : a)).r.falsePositives.length}`);
const always = MESSAGE.filter((m) => !isFake(m) && score(m) >= 9);
console.log(`legitimate messages flagged independent of threshold ${always.length}: ${always.map((m) => `${m[0]} (${score(m)} points, ${m[2].length} indicators)`).join('; ')}`);

let cum = 0; const cumul = CHAIN.map((a) => (cum += a[3]));
const breakable = CHAIN.map((a, i) => (a[2] ? i : -1)).filter((i) => i >= 0);
console.log(`\nchain has ${CHAIN.length} steps, with a control ${breakable.length}, with no control ${CHAIN.length - breakable.length}`);
console.log(`${'#'.padStart(2)} ${'step'.padEnd(34)} ${'precondition'.padEnd(31)} ${'control'.padEnd(29)} ${'cum.'}`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(34)} ${a[1].padEnd(31)} ${(a[2] ?? '-').padEnd(29)} ${cumul[i]}`));
console.log(`earliest breakable link ${breakable[0] + 1} (items dropped ${breakable[0] ? cumul[breakable[0] - 1] : 0}), ` +
  `latest ${breakable[breakable.length - 1] + 1} (items dropped ${cumul[breakable[breakable.length - 1] - 1]})`);
console.log(`early-break gain ${cumul[breakable[breakable.length - 1] - 1] - (breakable[0] ? cumul[breakable[0] - 1] : 0)} items; if never broken ${cumul[CHAIN.length - 1]}`);
```

```
threshold    caught missed false pos. total wrong   class distribution of missed
       13      2/16     14          0          14                       B6 S5 W3
       12      3/16     13          1          14                       B5 S5 W3
       11      6/16     10          1          11                       B2 S5 W3
       10      8/16      8          1           9                       B0 S5 W3
        9      8/16      8          2          10                       B0 S5 W3
        8      9/16      7          2           9                       B0 S4 W3
        7     10/16      6          2           8                       B0 S3 W3
        6     13/16      3          2           5                       B0 S0 W3
        5     13/16      3          3           6                       B0 S0 W3
        4     15/16      1          4           5                       B0 S0 W1
        3     16/16      0          4           4                       B0 S0 W0
        2     16/16      0          8           8                       B0 S0 W0

threshold minimizing total wrong 3: missed 0, falsely flagged 4
lowest threshold with zero false positives 13: missed there 14
highest threshold that misses nothing 3: falsely flagged there 4
legitimate messages flagged independent of threshold 2: L9 (12 points, 5 indicators); L10 (9 points, 4 indicators)

chain has 5 steps, with a control 4, with no control 1
 # step                               precondition                    control                       cum.
 1 preparing the message              target address list             -                             0
 2 delivery                           a valid mail path               indicator threshold           1
 3 opening and interaction            the recipient's interest        user reporting                2
 4 credential entry                   a convincing collection point   second factor                 5
 5 use of the collected information   a valid session                 anomalous session detection   11
earliest breakable link 2 (items dropped 0), latest 5 (items dropped 5)
early-break gain 5 items; if never broken 11
```

The class-distribution-of-missed column repeats the same thing throughout the scan. As the threshold
falls from ten to six, bulk phishing is already at zero and spear phishing begins to erode, but all
three whaling messages still evade the threshold entirely at six. The W column only moves once the
threshold reaches four, and it hits zero at three. A threshold that drops from thirteen to three
catches every fake message; in exchange, four legitimate messages get flagged — two-fifths of the ten
legitimate messages.

Two of the false positives are not a threshold problem. The messages scoring twelve and nine points
are flagged at every threshold that catches whaling messages — even at most thresholds that fail to
catch them. The two sets are interleaved in the same score band. This is not fixed by adjusting
weights; fixing it requires legitimate bulk mail to **lose its own indicators**: leaving its own
domain, having its verification record pass, having its link target match the text. The thing to fix
is not the threshold but the legitimate flow itself.

The chain table closes three columns. The first of the five steps is a preparation step no control
sees. The earliest of the four breakable links is delivery, where the dropped-item count is zero; the
latest is the fifth step, by which point five items have already dropped. The early-break gain is
five items; if the chain never breaks, the dropped-item count reaches eleven. Raising the threshold to
loosen the second link is therefore not just a matter of one number changing: it shifts the load onto
the third and fourth links — user reporting and the second factor.

## Summary

- 10 indicator types were counted across a set of 26 messages. Bulk phishing carries 5.88 indicators
  per message, spear phishing 3.00, whaling 1.67; cheap indicators disappear as the target narrows.
- The average indicator count of whaling messages (1.67) is lower than that of legitimate messages
  (2.30). A measure that looks only at indicator count separates these two sets in the wrong
  direction.
- At threshold 13, false positives are zero and 14 of 16 fake messages evade it. At threshold 3, none
  evade it and 4 of 10 legitimate messages are flagged. The threshold that minimizes total error is 3
  (0 + 4).
- The class distribution of missed messages evades all 3 whaling messages entirely up to threshold 6;
  this column only moves at threshold 4.
- 2 legitimate messages are flagged independent of threshold (12 and 9 points). This is not a
  threshold-tuning problem; it is legitimate bulk mail carrying the same indicators.
- The chain has 5 steps, 1 without a control. The earliest breakable link is step 2 (0 items), the
  latest is step 5 (5 items); the early-break gain is 5 items, and 11 items drop if the chain is
  never broken.

## Next Step

Every indicator in this lesson belonged to a single channel, and that channel had one thing in
common: the message is stored. Headers can be read, the domain can be queried, the link target can be
compared with the text, and all of this can be done before the message is opened. In text messages
and voice calls, most of these items do not exist; in a voice call, the one verifiable item that
remains is usually the caller's number, and even that may not be what it appears to be. That is the
next lesson's question: how many verification items exist per channel, at which link the chain can be
broken in each channel, and how many chains a callback verification procedure breaks, channel by
channel.
