以下情境为教学假设,人物与数字不代表经核实的真实案例。
数据泄露的成本不是一个数字,而是四个口袋同时打开:取证、通知、客户补偿和罚款。四个口袋的付款时间不同、承担者不同、保险覆盖程度也不同。把它们加成一个总数,再减去保险赔付,才是企业真正要准备的现金。
LedgerLite 的一次泄露事件,用四个教学假设的数字来算:取证 $40,000、通知 $15,000、客户补偿 $60,000、罚款按三档期望值计。保险是网络安全险,保额 $1,000,000,免赔 $10,000。责任上限是 12 个月费用 $240,240。这一节把总成本、保险赔付和净敞口放在一张表上,再说明责任上限和保险为什么不能互相替代。它不判断任何辖区的通知义务是否适用;72 小时只作为商业时限引用。
泄露成本的四个口袋
四个口袋对应四类工作,不是四种会计科目。把它们分开,是因为每一类的可保性、可谈性和发生时间都不同。
| 口袋 | 具体内容 | LedgerLite 教学假设 | 谁承担 | 保险是否覆盖 |
|---|---|---|---|---|
| 取证 | 外部取证、日志分析、遏制与恢复 | $40,000 | 企业 | 通常覆盖 |
| 通知 | 邮件、客服、信用监测、说明材料 | $15,000 | 企业 | 通常覆盖,常有分项限额 |
| 客户补偿 | 折扣、违约金、和解 | $60,000 | 企业,受责任上限约束 | 通常覆盖 |
| 罚款 | 监管处罚的期望值 | $0 / $20,000 / $60,000 | 企业 | 通常除外 |
前三项合计 $115,000,这是无论罚款是否发生都要准备的钱。取证和通知是「事情已经发生」的成本,客户补偿是「事情影响到客户」的成本;罚款是「监管是否介入」的成本,发生概率最低,但一旦发生,通常最不受合同控制。
罚款一栏用期望值而不是固定金额:假设可能的处罚敞口是 $200,000,按低、中、高三档概率 0%、10%、30% 折算,期望值分别是 $0、$20,000、$60,000。这三个概率和 $200,000 都是教学假设,不是任何辖区的法定金额,也不是对某次执法的预测。这样处理的目的,是把「不确定的罚款」变成一个可以放进预算的数,而不是假装它不存在。
客户补偿是四个口袋里最容易被责任上限影响的一项。其余三项——取证、通知、罚款——不会因为合同里写了「赔偿上限 12 个月费用」就消失。责任上限管的是你对客户的赔偿,不管你自己请取证团队的钱,也不管监管罚多少。这一点在后面两节会反复出现。
72 小时是商业时钟,不是法律结论
企业客户的 DPA 或安全问卷里,常见的一条是「发现事件后 72 小时内通知」。本书不分析这条时限在哪个辖区、哪种情形下适用;只把它当成一个商业时钟:客户期待你在 72 小时内给出可用的通知,而你的内部流程必须留出缓冲。
把 72 小时拆成阶段,会看到真正的风险不在最后一封邮件,而在前面的发现和分级。
| 阶段 | 目标小时 | 谁负责 | 超时的后果 |
|---|---|---|---|
| 发现与分级 | 4 | on-call 或创始人 | 计时可能还没开始,也可能已经开始 |
| 取证与范围判断 | 8 | 外部取证方 | 无法判断通知对象和通知内容 |
| 法务与客户沟通准备 | 12 | 外部顾问 | 通知内容不完整,容易二次更正 |
| 通知发出 | 6 | 创始人 | 违约,客户信任受损 |
| 合计 | 30 | 留 42 小时缓冲 |
30 小时是教学假设下的目标流程,不是承诺。它说明一件事:72 小时看起来宽裕,但前提是你在 4 小时内发现、8 小时内判断范围、12 小时内准备好说法。如果发现本身花了 3 天,后面的流程再快也没有意义。通知时限的真正约束不在写邮件,而在「你什么时候知道自己出了事」。
对一人公司,这个时钟还有两个现实问题。第一,没有 24 小时值班,发现时间取决于客户、监控或第三方什么时候告诉你;第二,取证和法务都是外部资源,周末和假期的响应时间会更长。把 72 小时当成商业时限,意味着你要提前准备好联系人、模板和升级路径,而不是等事发后再找。
通知的对象也不只是一个。客户、受影响个人、监管、支付方、保险方,可能各有各的期待;本书不展开适用分析,只提醒一点:通知成本 $15,000 里,包含的是「把同一件事用不同版本说清楚」的工时,不是一封邮件的邮费。
保险赔多少:$1M 保额与 $10,000 免赔
网络安全险的保额是 $1,000,000,免赔是 $10,000。按第 6.2 节的公式,保险赔付 = max(0, min(可赔损失, 保额) − 免赔)。这里假设罚款不在可赔范围内,取证、通知和客户补偿在可赔范围内。
| 罚款档位 | 概率 | 期望罚款 | 总成本 | 保险赔付 | 净敞口 |
|---|---|---|---|---|---|
| 低 | 0% | $0 | $115,000 | $105,000 | $10,000 |
| 中 | 10% | $20,000 | $135,000 | $105,000 | $30,000 |
| 高 | 30% | $60,000 | $175,000 | $105,000 | $70,000 |
保险赔付固定为 $105,000:可赔损失 $115,000 低于保额 $1,000,000,所以封顶不生效,只扣免赔 $10,000。净敞口 = 总成本 − 赔付,低档是 $10,000,中档是 $30,000,高档是 $70,000。换句话说,保险把 $115,000 的可赔成本压到 $10,000 自留,但罚款每增加 $20,000,净敞口就跟着增加 $20,000。
这张表还说明保额不是唯一的约束。$1,000,000 的保额在这次事件里只用了 $105,000,真正决定净敞口的是免赔和罚款是否可保。如果保单把通知费用设了 $10,000 的分项限额,通知成本 $15,000 只能赔 $10,000,净敞口会再多 $5,000;如果客户补偿被列为除外,$60,000 会全部留在企业一侧。保额、分项限额、除外责任三者一起看,才是实际赔付。
保险赔付还有时间维度。取证和通知要先付钱,保险后报销;客户补偿可能要等和解或判决。对现金流紧张的公司,即使最终能赔,前几个月的现金流出仍然要自己垫。LedgerLite 年末现金 $95,000,一次 $115,000 的先付成本已经超过现金余额;保险赔付的时间差本身就是风险。
责任上限与保险是两道不同的墙
责任上限和保险经常被放在同一句话里说:「我们有 12 个月费用上限,也有 $1M 保险,应该够了。」这是两道不同的墙,作用对象、触发条件和覆盖范围都不同。
| 工具 | 作用对象 | 数值 | 是否直接支付现金 | 不覆盖什么 |
|---|---|---|---|---|
| 责任上限 | 你对客户的赔偿 | 12 个月费用 $240,240 | 否,减少负债 | 取证、通知、罚款 |
| 网络安全险 | 你已支付的可赔成本 | $1,000,000 − $10,000 | 是,报销 | 罚款、免赔、超限部分 |
责任上限不产生现金,它只是把「你欠客户多少」封顶。保险不减少负债,它只是把你已经付出去的可赔成本报销回来。两者叠加,才是「客户能拿多少」和「你能收回多少」两个不同问题的答案。
用两个索赔金额看差别。第一个是 $60,000 的客户补偿,低于上限 $240,240,上限不生效;第二个是 $400,000,超过上限,上限省下 $159,760。
| 场景 | 客户索赔 | 计入成本的索赔 | 上限省下 | 可赔成本 | 保险赔付 | 净敞口 |
|---|---|---|---|---|---|---|
| 索赔 $60,000 | $60,000 | $60,000 | $0 | $115,000 | $105,000 | $30,000 |
| 索赔 $400,000 | $400,000 | $240,240 | $159,760 | $295,240 | $285,240 | $30,000 |
第二行里,净敞口仍然是 $30,000:$10,000 免赔加 $20,000 期望罚款。上限省下的 $159,760 没有变成企业的现金,而是变成了客户拿不到的部分。这就是责任上限的真实作用:它把损失从「企业赔给客户」转移到「客户自己承担」,但客户不一定接受,可能表现为关系恶化、续约流失或诉讼。
这也解释了为什么不能只用责任上限做风险预算。上限挡得住客户索赔,挡不住取证、通知和罚款;保险挡得住一部分可赔成本,挡不住罚款和免赔。两道墙之间有缝隙,缝隙的大小就是净敞口。对 LedgerLite,这个缝隙在中档罚款下是 $30,000;如果罚款概率升到高档,缝隙变成 $70,000,而责任上限仍然是 $240,240,不会跟着变。
代码:泄露成本与净敞口
下面的 breach-liability.ts 把这一节的口径固化成两个函数。breachCost 把取证、通知、客户补偿和罚款期望值加总;settleBreach 先用责任上限压客户索赔,再把可赔成本交给第 6.2 节的 settleClaim 计算保险赔付,最后用「总成本 − 赔付」得到净敞口。金额用整数美分,概率用基点。
// file: src/contract/breach-liability.ts
// tier: L1
// verify: node --test test/contract/breach-liability.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
import { settleClaim } from "./coverage.ts";
export type BreachCostInput = {
forensicsCents: Cents;
notificationCents: Cents;
customerClaimCents: Cents;
assumedFineCents: Cents;
fineProbabilityBps: Bps;
};
export type BreachCost = {
directCostCents: Cents;
expectedFineCents: Cents;
grossCostCents: Cents;
};
export type BreachSettlementInput = {
cost: BreachCostInput;
liabilityCapCents: Cents;
insuranceLimitCents: Cents;
deductibleCents: Cents;
};
export type BreachSettlement = {
cappedCustomerClaimCents: Cents;
capSavingCents: Cents;
insurableCents: Cents;
expectedFineCents: Cents;
grossCostCents: Cents;
insurancePayoutCents: Cents;
netExposureCents: Cents;
};
function validAmount(value: Cents): boolean {
return Number.isInteger(value) && value >= 0;
}
/**
* A fine is not a fixed number in a plan. It is an assumed exposure multiplied
* by a probability tier; the result is an expected cost, not a prediction of
* any regulator's decision.
*/
export function breachCost(input: BreachCostInput): Result<BreachCost> {
if (!validAmount(input.forensicsCents)) {
return fail("forensics must be a non-negative integer amount");
}
if (!validAmount(input.notificationCents)) {
return fail("notification must be a non-negative integer amount");
}
if (!validAmount(input.customerClaimCents)) {
return fail("customer claim must be a non-negative integer amount");
}
if (!validAmount(input.assumedFineCents)) {
return fail("assumed fine must be a non-negative integer amount");
}
if (!Number.isInteger(input.fineProbabilityBps) || input.fineProbabilityBps < 0 || input.fineProbabilityBps > 10_000) {
return fail("fine probability must be an integer between 0 and 10000 bps");
}
const directCostCents =
input.forensicsCents + input.notificationCents + input.customerClaimCents;
const expectedFineCents = applyRate(
input.assumedFineCents,
input.fineProbabilityBps,
);
return ok({
directCostCents,
expectedFineCents,
grossCostCents: directCostCents + expectedFineCents,
});
}
/**
* The liability cap and the insurance policy are two different walls. The cap
* decides how much of the customer claim the business owes; insurance decides
* how much of the paid, insurable cost comes back. Fines sit outside both: the
* cap does not remove them and the policy is assumed not to pay them.
*/
export function settleBreach(
input: BreachSettlementInput,
): Result<BreachSettlement> {
const cost = breachCost(input.cost);
if (!cost.ok) {
return fail(cost.error);
}
if (!validAmount(input.liabilityCapCents)) {
return fail("liability cap must be a non-negative integer amount");
}
if (!validAmount(input.insuranceLimitCents)) {
return fail("insurance limit must be a non-negative integer amount");
}
if (!validAmount(input.deductibleCents)) {
return fail("deductible must be a non-negative integer amount");
}
const cappedCustomerClaimCents = Math.min(
input.cost.customerClaimCents,
input.liabilityCapCents,
);
const capSavingCents = input.cost.customerClaimCents - cappedCustomerClaimCents;
const insurableCents =
input.cost.forensicsCents +
input.cost.notificationCents +
cappedCustomerClaimCents;
const claim = settleClaim({
lossCents: insurableCents,
limitCents: input.insuranceLimitCents,
deductibleCents: input.deductibleCents,
});
if (!claim.ok) {
return fail(claim.error);
}
const grossCostCents = insurableCents + cost.value.expectedFineCents;
return ok({
cappedCustomerClaimCents,
capSavingCents,
insurableCents,
expectedFineCents: cost.value.expectedFineCents,
grossCostCents,
insurancePayoutCents: claim.value.payoutCents,
netExposureCents: grossCostCents - claim.value.payoutCents,
});
}
代码里有四个口径值得注意。第一,settleBreach 只把取证、通知和「上限后的客户索赔」放进可赔成本,罚款单独加在总成本里,不进入保险计算;这对应「罚款通常除外」的假设。第二,capSavingCents 是客户索赔减去上限后的差额,它不是现金收入,只是企业少欠客户的部分。第三,netExposureCents 等于总成本减保险赔付,包含免赔、罚款和任何未投保部分;它不是「损失减保额」这么简单。第四,代码复用 settleClaim,是为了保证封顶和免赔的顺序与第 6.2 节一致。
代码不判断罚款是否真的会发生,也不判断保单是否真的覆盖某一类成本。它只把「四个口袋 + 两道墙」变成一个可复算的净敞口。实际做预算时,把每一类成本的假设换成自己的数字,再跑一遍,就能看到哪一类假设最影响结果。
小结
- 泄露成本分四个口袋:取证 $40,000、通知 $15,000、客户补偿 $60,000、罚款期望值 $0 到 $60,000。
- 前三项合计 $115,000;罚款按 $200,000 敞口 × 0%/10%/30% 折算,是教学假设,不是法定金额。
- 72 小时是商业时钟,不是法律结论;目标流程 30 小时,留 42 小时缓冲,但发现时间才是真正的约束。
- $1M 保额、$10,000 免赔下,保险赔付 $105,000;净敞口低档 $10,000、中档 $30,000、高档 $70,000。
- 责任上限 12 个月费用 $240,240 只压客户索赔;$400,000 索赔被压到 $240,240,省下 $159,760,但不产生现金。
- 责任上限和保险是两道不同的墙:一个减少负债,一个报销成本;罚款和免赔在两道墙之间。
- 净敞口不是「损失 − 保额」,而是「总成本 − 实际赔付」,要按事故类型和保单结构分别算。
阅读导航:上一节:11.1 DPA、SCC 与跨境传输 · 下一节:11.3 客户审查与安全问卷 。
常见错误与边界
- 把罚款当成固定金额。 罚款应用「假设敞口 × 概率档位」折算成期望值,并标注为教学假设;不要把它写成某个辖区的确定金额。
- 把责任上限当成保险。 上限减少你欠客户的钱,不支付取证、通知或罚款;它不产生现金。
- 把保险当成责任上限。 保险报销已支付的可赔成本,不减少你对客户的负债;两者作用对象不同。
- 用「损失 − 保额」算净敞口。 正确口径是「总成本 − 实际赔付」,还要扣免赔、加罚款和未投保部分。
- 忽略分项限额。 通知、取证、抗辩可能各有上限;总保额不等于每一项都能赔到。
- 把 72 小时当成从发现开始的保证。 发现时间、分级时间和外部资源响应都会吃掉缓冲;时钟的真正风险在流程前端。
- 假设罚款可保。 很多保单把罚款列为除外;做预算时先把罚款放在保险之外,再看保单是否真的覆盖。
- 只看最终赔付,不看先付现金。 取证、通知、客户补偿通常要先付;保险后报销,时间差会压现金流。
- 把客户补偿和客户关系分开算。 责任上限让客户拿不到的部分,可能变成续约流失或诉讼;这部分成本不在净敞口里,但真实存在。
- 把一次事件的净敞口当成年度预算。 净敞口是单次事件的口径;年度预算还要乘以事件频率,频率本身是教学假设。
数据泄露的财务问题,最后会落到两个数字上:这次事件要付多少现金,保险能报回多少。责任上限改变的是「客户能要多少」,保险改变的是「你能收回多少」,两者都不是「事情没发生」。下一节换一个方向:在事故发生之前,客户会用安全问卷来判断要不要和你签约。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。