26. Quantitative Security
Level: Expert You'll use this for: turning "this feels risky" into a defensible, numeric argument.
Password/key strength, connected end-to-end
Combining Ch. 15 (keyspace), Ch. 18 (brute-force time), and Ch. 23 (entropy) into one practical workflow:
import math
def crack_time_estimate(entropy_bits, guesses_per_second):
total_guesses = 2 ** entropy_bits
avg_guesses = total_guesses / 2 # attacker expected to find it halfway through
seconds = avg_guesses / guesses_per_second
return seconds / (60*60*24*365) # in years
# Offline attack against a fast, unsalted-hash-cracking rig (10^11 guesses/sec)
print(crack_time_estimate(40, 1e11), "years") # weak: cracked in seconds
print(crack_time_estimate(80, 1e11), "years") # strong: computationally infeasible
# Online attack, rate-limited to 10 guesses/sec (a login form with lockout)
print(crack_time_estimate(40, 10), "years") # even "weak" 40-bit entropy holds up when rate-limited
The takeaway that changes real security architecture decisions: entropy alone doesn't determine practical security — entropy combined with attacker guess rate does. This is why rate-limiting a login endpoint is often more impactful than forcing users into unmemorable passwords, and why offline-crackable hash leaks (fast hash, no rate limit possible) are categorically worse than online-only exposure.
Detection system quality: precision, recall, ROC/AUC
Building directly on Bayes' theorem (Ch. 16):
Precision = TP / (TP + FP) "of things I flagged, how many were real?"
Recall = TP / (TP + FN) "of real things, how many did I catch?"
A tiny worked example, by hand. Say a detector runs against 20 events: 8 real attacks, 12 benign. It flags 10 events, and of those 10, 6 turn out to be real attacks (and it missed 2 real attacks entirely):
TP (flagged AND real) = 6
FP (flagged but benign) = 10 - 6 = 4
FN (real but NOT flagged) = 8 - 6 = 2
Precision = TP / (TP+FP) = 6 / (6+4) = 6/10 = 0.60 "60% of alerts were real"
Recall = TP / (TP+FN) = 6 / (6+2) = 6/8 = 0.75 "caught 75% of real attacks"
If the threshold were loosened to flag more events, more of those missed 2 attacks would likely get caught (recall rises) — but some of the 12 benign events would likely get swept in too (precision falls). That push-pull, made concrete with real counts, is what the ROC curve below plots across every possible threshold at once.
These trade off against each other — lowering your alert threshold increases recall (catch more real attacks) but decreases precision (more false alarms), and vice versa. An ROC curve plots this tradeoff across every possible threshold; AUC (area under the curve) summarizes overall detector quality independent of any one threshold choice.
from sklearn.metrics import roc_curve, roc_auc_score
y_true = [0,0,0,0,1,0,0,1,0,1] # 1 = actual attack
y_scores = [0.1,0.4,0.2,0.3,0.8,0.05,0.6,0.9,0.15,0.7] # detector's risk score
auc = roc_auc_score(y_true, y_scores)
fpr, tpr, thresholds = roc_curve(y_true, y_scores)
print("AUC:", auc)
Choosing the operating threshold on that curve is a business decision, not a math one — but the math tells you exactly what tradeoff you're accepting at each point.
Risk = Likelihood × Impact (and why this needs distributions, not point estimates)
The classic security risk formula is often applied with single-number guesses, which hides massive uncertainty. A more rigorous approach (used in quantitative risk frameworks like FAIR) treats likelihood and impact as probability distributions and runs a Monte Carlo simulation:
import numpy as np
np.random.seed(0)
# Simulate 100,000 possible "years" of risk for a given threat scenario
n_sims = 100_000
# Likelihood: how many incidents per year (Poisson-distributed, Ch. 14)
incidents_per_year = np.random.poisson(lam=0.3, size=n_sims)
# Impact per incident: log-normally distributed (heavy-tailed cost, common for breach costs)
cost_per_incident = np.random.lognormal(mean=11, sigma=1.2, size=n_sims) # ~$60k median
annual_loss = incidents_per_year * cost_per_incident
print("Median annual loss estimate: $", np.median(annual_loss))
print("95th percentile (tail risk): $", np.percentile(annual_loss, 95))
This Monte Carlo approach (repeatedly sampling from the input distributions and tabulating outcomes) directly generalizes Ch. 14's distributions and Ch. 9's percentile thinking into a decision-support tool — instead of a single scary/reassuring number, you get a full distribution of plausible outcomes, including the tail risk that a single "expected value" calculation would hide.
k-anonymity — quantifying anonymization strength
A dataset satisfies k-anonymity if every record is indistinguishable
from at least k-1 other records on the "quasi-identifying" fields (zip
code, birth date, gender, etc.). This is a combinatorics/counting
question (Ch. 15): how many records share each combination of
quasi-identifiers?
import pandas as pd
df = pd.DataFrame({
"zip": ["02138","02138","02139","02138","02139"],
"age": [29, 29, 45, 29, 45],
"disease": ["flu","cold","flu","flu","cold"],
})
group_sizes = df.groupby(["zip","age"]).size()
k = group_sizes.min()
print("k-anonymity level:", k) # smallest group size = the anonymity guarantee
A famous real-world result of this exact math: 87% of the US population is
uniquely identifiable from just {zip code, birth date, gender} alone
(Sweeney, 2000) — a striking demonstration that "anonymized" data with
insufficient k-anonymity often isn't anonymous at all.
The confusion matrix, every rate named
| Actually malicious | Actually benign | |
|---|---|---|
| Alerted | TP (true positive) | FP (false positive) |
| Not alerted | FN (false negative) | TN (true negative) |
| Metric | Formula | Question it answers | Also called |
|---|---|---|---|
| TPR | TP / (TP + FN) |
of real attacks, what fraction did we catch? | recall, sensitivity, detection rate |
| FPR | FP / (FP + TN) |
of benign events, what fraction did we wrongly flag? | fall-out, false-alarm rate |
| Precision | TP / (TP + FP) |
of alerts, what fraction were real? | positive predictive value |
| F1 | 2·P·R / (P + R) |
one number balancing precision P and recall R |
harmonic mean |
For the 20-event example above (P = 0.6, R = 0.75),
F1 = 2·0.6·0.75 / 1.35 ≈ 0.67. F1 uses the harmonic mean so that a
detector can't hide a terrible precision behind a great recall. If either
one goes to 0, so does F1.
Precision depends on the base rate, not just the detector
TPR and FPR are properties of the detector. Precision also depends on
the environment, through the base rate π = the fraction of events that
are malicious. Bayes' theorem (Ch. 16) gives:
Precision = TPR · π / ( TPR · π + FPR · (1 - π) )
| Symbol | Meaning |
|---|---|
π |
base rate: P(event is malicious), before any detection |
TPR · π |
the fraction of all events that are caught attacks |
FPR · (1 - π) |
the fraction of all events that are false alarms |
With TPR = 0.99, FPR = 0.01, π = 10⁻⁴, precision is 0.0098. That's
Ch. 16's "under 1% of alerts are real", now as a formula. Solve it the other
way to get the design target: for half of all alerts to be real, you need
FPR ≈ TPR · π / (1 - π) ≈ 10⁻⁴. The false-positive rate has to be about as
small as the base rate. That's a 100× improvement over "99% accurate", and
it's why production detection stacks layer cheap high-recall filters in
front of expensive high-precision ones. Each stage raises the base rate
seen by the next stage.
Choosing the alert threshold from costs
If a false alarm costs C_FP (analyst time) and a missed attack costs
C_FN (breach losses), alerting on an event is worth it when the expected
cost of staying silent exceeds the expected cost of alerting:
alert if P(attack | evidence) · C_FN > (1 - P(attack | evidence)) · C_FP
<=> P(attack | evidence) > C_FP / (C_FP + C_FN)
Example: triaging an alert costs about $50 of analyst time, and a missed
intrusion costs about $100,000. The threshold is
50 / (50 + 100,000) ≈ 0.0005. You should investigate anything with more than
a 0.05% chance of being real. That shows why "only page on high-confidence
alerts" can be the wrong policy when misses are expensive, and the formula
lets you defend the threshold with numbers in a design review. The same
derivation sets the decision threshold of any classifier, from fraud to
medical screening.
What AUC actually measures
AUC has a clean probabilistic meaning (the Mann–Whitney U statistic):
AUC = P( score(random attack) > score(random benign event) )
Count it for the sklearn example above. There are 3 attacks, scored
0.8, 0.9, 0.7, and 7 benign events, the highest of which scores 0.6. All
3 × 7 = 21 attack/benign pairs are ranked correctly, so AUC = 21/21 = 1.0.
Random guessing gives 0.5. AUC is threshold-free and base-rate-free, which
makes it good for comparing detectors and bad for predicting alert volume. Use
precision at your real π for that (above).
Password hashing: buying bits with work
A deliberately slow hash adds "free" entropy. If a hash is W times slower
for the attacker, it adds log₂ W bits of effective strength:
effective bits = entropy_bits + log₂(slowdown factor)
A single high-end GPU computes fast unsalted hashes like MD5 at tens of
billions per second, but only tens of thousands of bcrypt hashes per second at
a moderate cost setting. That's a slowdown of roughly a million, so about
log₂(10⁶) ≈ 20 extra bits. A 40-bit password behind bcrypt resists an offline
attacker about as well as a 60-bit password behind MD5. Each +1 to bcrypt's
cost parameter doubles the work (+1 bit). Memory-hard functions (scrypt,
Argon2) also require a large block of RAM per guess, which removes the GPU's
and ASIC's advantage from massive parallelism. That's why OWASP recommends
Argon2id. Salts don't add bits per hash. They stop one computation from
attacking every account at once, so precomputed rainbow tables become useless.
Differential privacy: a mathematical definition of "anonymous"
k-anonymity (above) can be broken by combining it with outside data. Differential
privacy (Dwork et al., 2006) gives a guarantee that holds against any
auxiliary information. A randomized query M is ε-differentially private
if, for any two datasets D and D' that differ in one person's record, and
any set of outputs S:
P[ M(D) ∈ S ] ≤ e^ε · P[ M(D') ∈ S ]
| Symbol | Meaning |
|---|---|
D, D' |
"neighbouring" datasets: identical except for one individual |
M |
the randomized mechanism (the query plus noise) |
ε |
"epsilon", the privacy budget. Smaller is more private. e^ε ≈ 1 + ε for small ε |
In words: whether you are in the dataset changes the probability of any
answer by at most a factor of e^ε, so nobody can learn much about you from
the output.
The Laplace mechanism achieves this for numeric queries. Add noise drawn
from a Laplace distribution with scale b = Δf / ε, where Δf (the
sensitivity) is the most one person can change the true answer.
Example: "how many employees clicked the phishing link?" One person
changes a count by at most 1, so Δf = 1. With ε = 0.5, the scale is
b = 2, which gives a noise standard deviation of √2 · b ≈ 2.8. The published
count is true count ± about 3, which is useful for a company of 5,000 and
protective for any individual.
Privacy losses add up: answering k such queries costs k·ε in total
(the composition theorem). That's why deployments track a budget. The 2020
US Census, Apple's keyboard and emoji telemetry, and Google's RAPPOR all use
differential privacy, and DP-SGD (clip each example's gradient, add Gaussian
noise) trains ML models with the same guarantee, so they can't memorize any
single training record.
Real-life engineering ties
- Security: this entire chapter is core Sr.-security-engineer vocabulary — precision/recall/AUC for evaluating any detection system, entropy+rate-limiting reasoning for authentication design, Monte Carlo risk quantification for prioritizing a remediation backlog defensibly instead of by gut feeling.
- AI: the same precision/recall/ROC math applies to any binary classifier, security-related or not (fraud detection, content moderation, spam filtering).
- SWE/Network: capacity/incident risk modeling (Monte Carlo over failure-rate distributions) generalizes directly to reliability engineering (Ch. 14's exponential/Poisson distributions feeding in here).
More real-life examples:
- The "Have I Been Pwned" Pwned Passwords API (built by researcher Troy Hunt) lets you check whether a password appears in a breach corpus without ever sending the full password or its full hash — the client sends only the first 5 characters of a SHA-1 hash and receives back all matching suffixes, directly applying k-anonymity thinking in a widely deployed production tool.
- The FAIR (Factor Analysis of Information Risk) model is an industry-standard quantitative risk framework that formalizes "risk = probability distribution of loss frequency x probability distribution of loss magnitude" — a standardized, auditable version of the Monte Carlo simulation in this chapter, used for real enterprise risk reporting to boards and regulators.
- Credit scoring and insurance underwriting use the same precision/recall and cost-weighted tradeoff reasoning as fraud/intrusion detection: too permissive approves bad loans (false negatives), too strict rejects good customers (false positives).
- Bug bounty programs and CVSS (Common Vulnerability Scoring System) scores are attempts to formalize "likelihood x impact" into a repeatable number for prioritizing a remediation backlog.
Common mistake
Optimizing a detection system purely for accuracy or purely for recall in isolation. In a Bayes-driven rare-event context (Ch. 16), a 100%-recall detector that flags everything is worthless (zero precision); the right metric is almost always a threshold chosen on a precision/recall or cost-weighted curve, not a single global metric.