fraud-sim 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +462 -0
  2. package/README.md +621 -81
  3. package/bin/fraud-sim.js +625 -0
  4. package/examples/account-drain-demo/index.js +1 -1
  5. package/examples/adaptive-hardened-demo/index.js +106 -0
  6. package/examples/basic-credential-stuffing/index.js +4 -2
  7. package/examples/express-hardened-target/server.js +230 -0
  8. package/examples/express-mock-target/server.js +153 -2
  9. package/package.json +40 -8
  10. package/src/adapters/http.js +49 -7
  11. package/src/index.js +126 -13
  12. package/src/llm/anthropic.js +7 -0
  13. package/src/llm/deterministic.js +21 -7
  14. package/src/llm/openai.js +7 -0
  15. package/src/locales/index.js +193 -0
  16. package/src/reporting/aggregate.js +338 -0
  17. package/src/reporting/html.js +424 -0
  18. package/src/reporting/index.js +241 -0
  19. package/src/reporting/junit.js +171 -0
  20. package/src/reporting/money.js +105 -0
  21. package/src/runner.js +295 -12
  22. package/src/scenarios/account-drain.js +10 -2
  23. package/src/scenarios/adaptive-attacker.js +33 -11
  24. package/src/scenarios/card-testing.js +174 -0
  25. package/src/scenarios/credential-stuffing.js +146 -20
  26. package/src/scenarios/index.js +37 -6
  27. package/src/scenarios/legitimate-users.js +290 -0
  28. package/src/scenarios/mule-network.js +201 -0
  29. package/src/scenarios/promo-abuse.js +168 -0
  30. package/src/scenarios/session-hijack.js +234 -0
  31. package/src/scenarios/sim-swap-takeover.js +253 -0
  32. package/src/scenarios/slow-drain.js +171 -0
  33. package/src/scenarios/velocity-evasion.js +179 -0
  34. package/src/types.js +160 -0
  35. package/src/utils/clock.js +77 -0
  36. package/src/utils/faker-helpers.js +87 -2
  37. package/src/utils/index.js +69 -0
  38. package/src/utils/random-source.js +104 -0
  39. package/src/utils/random.js +257 -44
  40. package/src/utils/results.js +84 -0
  41. package/src/utils/rng.js +3 -4
  42. package/src/utils/throttle.js +127 -0
  43. package/types/adapters/http.d.ts +6 -0
  44. package/types/adapters/index.d.ts +3 -0
  45. package/types/adapters/logging.d.ts +19 -0
  46. package/types/adapters/mock.d.ts +32 -0
  47. package/types/index.d.ts +79 -0
  48. package/types/llm/anthropic.d.ts +17 -0
  49. package/types/llm/deterministic.d.ts +11 -0
  50. package/types/llm/index.d.ts +3 -0
  51. package/types/llm/openai.d.ts +17 -0
  52. package/types/locales/index.d.ts +146 -0
  53. package/types/reporting/aggregate.d.ts +126 -0
  54. package/types/reporting/html.d.ts +1 -0
  55. package/types/reporting/index.d.ts +99 -0
  56. package/types/reporting/junit.d.ts +13 -0
  57. package/types/reporting/money.d.ts +20 -0
  58. package/types/runner.d.ts +10 -0
  59. package/types/scenarios/account-drain.d.ts +25 -0
  60. package/types/scenarios/adaptive-attacker.d.ts +22 -0
  61. package/types/scenarios/card-testing.d.ts +28 -0
  62. package/types/scenarios/credential-stuffing.d.ts +23 -0
  63. package/types/scenarios/index.d.ts +261 -0
  64. package/types/scenarios/legitimate-users.d.ts +49 -0
  65. package/types/scenarios/mule-network.d.ts +17 -0
  66. package/types/scenarios/promo-abuse.d.ts +28 -0
  67. package/types/scenarios/session-hijack.d.ts +15 -0
  68. package/types/scenarios/sim-swap-takeover.d.ts +16 -0
  69. package/types/scenarios/slow-drain.d.ts +28 -0
  70. package/types/scenarios/velocity-evasion.d.ts +31 -0
  71. package/types/streaming/index.d.ts +1 -0
  72. package/types/streaming/sse.d.ts +34 -0
  73. package/types/types.d.ts +191 -0
  74. package/types/utils/clock.d.ts +39 -0
  75. package/types/utils/faker-helpers.d.ts +114 -0
  76. package/types/utils/index.d.ts +8 -0
  77. package/types/utils/random-source.d.ts +39 -0
  78. package/types/utils/random.d.ts +89 -0
  79. package/types/utils/results.d.ts +46 -0
  80. package/types/utils/rng.d.ts +36 -0
  81. package/types/utils/sleep.d.ts +7 -0
  82. package/types/utils/throttle.d.ts +33 -0
@@ -0,0 +1,261 @@
1
+ export const scenarios: ({
2
+ id: string;
3
+ name: string;
4
+ traffic: string;
5
+ description: string;
6
+ defaultOptions: {
7
+ targetUserIds: any;
8
+ warmupLogins: number;
9
+ sessionsPerUser: number;
10
+ sessionGapHours: number;
11
+ withdrawalRate: number;
12
+ newDeviceRate: number;
13
+ travelRate: number;
14
+ newPayeeRate: number;
15
+ amountMinor: number;
16
+ delayMs: number;
17
+ };
18
+ run(ctx: any): Promise<{
19
+ customers: number;
20
+ customersBlocked: number;
21
+ falsePositiveRate: number;
22
+ frictionRate: number;
23
+ valueBlockedMinor: number;
24
+ byStage: {
25
+ [k: string]: {
26
+ events: number;
27
+ allowed: number;
28
+ stepUp: number;
29
+ blocked: number;
30
+ };
31
+ };
32
+ falsePositiveReasons: any;
33
+ warmup: {
34
+ attempts: number;
35
+ blocked: number;
36
+ stepUp: number;
37
+ allowed: number;
38
+ errors: number;
39
+ };
40
+ simulatedTime: boolean;
41
+ simulatedSpan: string;
42
+ attempts: number;
43
+ blocked: number;
44
+ stepUp: number;
45
+ allowed: number;
46
+ errors: number;
47
+ }>;
48
+ } | {
49
+ id: string;
50
+ name: string;
51
+ description: string;
52
+ defaultOptions: {
53
+ targetUserIds: any;
54
+ targetUserId: any;
55
+ attackerCount: number;
56
+ attemptsPerIp: number;
57
+ delayMs: number;
58
+ maxTotalAttempts: number;
59
+ ipPersona: string;
60
+ };
61
+ run(ctx: any): Promise<{
62
+ attempts: number;
63
+ blocked: number;
64
+ stepUp: number;
65
+ allowed: number;
66
+ errors: number;
67
+ accountsTargeted: number;
68
+ }>;
69
+ } | {
70
+ id: string;
71
+ name: string;
72
+ description: string;
73
+ defaultOptions: {
74
+ targetUserId: any;
75
+ attackerIp: any;
76
+ withdrawalCount: number;
77
+ amountMinor: number;
78
+ delayBetweenWithdrawalsMs: number;
79
+ postLoginPauseMs: number;
80
+ skipPasswordChange: boolean;
81
+ };
82
+ run(ctx: any): Promise<{
83
+ attempts: any;
84
+ blocked: any;
85
+ stepUp: any;
86
+ allowed: any;
87
+ errors: any;
88
+ loginDecision: any;
89
+ withdrawalDecisions: any;
90
+ attackerSucceeded: any;
91
+ }>;
92
+ } | {
93
+ id: string;
94
+ name: string;
95
+ description: string;
96
+ defaultOptions: {
97
+ targetUserId: any;
98
+ maxRounds: number;
99
+ baselineIp: string;
100
+ delayMs: number;
101
+ };
102
+ run(ctx: any): Promise<{
103
+ attempts: number;
104
+ blocked: number;
105
+ stepUp: number;
106
+ allowed: number;
107
+ errors: number;
108
+ rounds: number;
109
+ outcome: string;
110
+ strategyHistory: any[];
111
+ }>;
112
+ } | {
113
+ id: string;
114
+ name: string;
115
+ description: string;
116
+ defaultOptions: {
117
+ attackerUserId: string;
118
+ cardCount: number;
119
+ binCount: number;
120
+ microAmountMinor: number;
121
+ largeAmountMinor: number;
122
+ escalateAfter: number;
123
+ delayMs: number;
124
+ maxTotalAttempts: number;
125
+ ipPersona: string;
126
+ reuseIp: boolean;
127
+ };
128
+ run(ctx: any): Promise<{
129
+ estimatedLossKobo: number;
130
+ attackerSucceeded: boolean;
131
+ binsUsed: string[];
132
+ attempts: number;
133
+ blocked: number;
134
+ stepUp: number;
135
+ allowed: number;
136
+ errors: number;
137
+ }>;
138
+ } | {
139
+ id: string;
140
+ name: string;
141
+ description: string;
142
+ defaultOptions: {
143
+ accountCount: number;
144
+ bonusMinor: number;
145
+ deviceCount: number;
146
+ subnetCount: number;
147
+ sharedPayoutAccount: any;
148
+ claimBonus: boolean;
149
+ delayMs: number;
150
+ maxTotalAttempts: number;
151
+ };
152
+ run(ctx: any): Promise<{
153
+ estimatedLossKobo: number;
154
+ attackerSucceeded: boolean;
155
+ sharedPayoutAccount: any;
156
+ devicesUsed: number;
157
+ subnetsUsed: number;
158
+ attempts: number;
159
+ blocked: number;
160
+ stepUp: number;
161
+ allowed: number;
162
+ errors: number;
163
+ }>;
164
+ } | {
165
+ id: string;
166
+ name: string;
167
+ description: string;
168
+ defaultOptions: {
169
+ sourceUserId: any;
170
+ muleCount: number;
171
+ amountPerMuleMinor: number;
172
+ retentionRatio: number;
173
+ finalDestinationAccount: any;
174
+ forwardDelayMinutes: number;
175
+ delayMs: number;
176
+ maxTotalAttempts: number;
177
+ };
178
+ run(ctx: any): Promise<any>;
179
+ } | {
180
+ id: string;
181
+ name: string;
182
+ description: string;
183
+ defaultOptions: {
184
+ targetUserId: any;
185
+ withdrawalCount: number;
186
+ amountMinor: number;
187
+ amountJitterMinor: number;
188
+ intervalMinutes: number;
189
+ sameDevice: boolean;
190
+ delayMs: number;
191
+ maxTotalAttempts: number;
192
+ };
193
+ run(ctx: any): Promise<{
194
+ estimatedLossKobo: any;
195
+ attackerSucceeded: boolean;
196
+ simulatedTime: boolean;
197
+ simulatedSpan: string;
198
+ drainedBeforeFirstBlockMinor: any;
199
+ attempts: number;
200
+ blocked: number;
201
+ stepUp: number;
202
+ allowed: number;
203
+ errors: number;
204
+ }>;
205
+ } | {
206
+ id: string;
207
+ name: string;
208
+ description: string;
209
+ defaultOptions: {
210
+ targetUserId: any;
211
+ benignActionsBeforeHijack: number;
212
+ amountMinor: number;
213
+ withdrawAfterHijack: boolean;
214
+ hijackFromPersona: string;
215
+ delayMs: number;
216
+ };
217
+ run(ctx: any): Promise<any>;
218
+ } | {
219
+ id: string;
220
+ name: string;
221
+ description: string;
222
+ defaultOptions: {
223
+ targetUserId: any;
224
+ minutesSincePort: number;
225
+ amountMinor: number;
226
+ reportPortEvent: boolean;
227
+ attemptPasswordReset: boolean;
228
+ ipPersona: string;
229
+ delayMs: number;
230
+ };
231
+ run(ctx: any): Promise<any>;
232
+ } | {
233
+ id: string;
234
+ name: string;
235
+ description: string;
236
+ defaultOptions: {
237
+ targetUserId: any;
238
+ attemptCount: number;
239
+ assumedLimit: number;
240
+ assumedWindowSeconds: number;
241
+ safetyMargin: number;
242
+ jitterRatio: number;
243
+ rotateDevice: boolean;
244
+ ipPersona: string;
245
+ delayMs: number;
246
+ maxTotalAttempts: number;
247
+ };
248
+ run(ctx: any): Promise<{
249
+ attackerSucceeded: any;
250
+ pacingMs: number;
251
+ effectiveRatePerWindow: number;
252
+ assumedLimit: any;
253
+ simulatedTime: boolean;
254
+ simulatedSpan: string;
255
+ attempts: number;
256
+ blocked: number;
257
+ stepUp: number;
258
+ allowed: number;
259
+ errors: number;
260
+ }>;
261
+ })[];
@@ -0,0 +1,49 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let traffic: string;
5
+ let description: string;
6
+ namespace defaultOptions {
7
+ let targetUserIds: any;
8
+ let warmupLogins: number;
9
+ let sessionsPerUser: number;
10
+ let sessionGapHours: number;
11
+ let withdrawalRate: number;
12
+ let newDeviceRate: number;
13
+ let travelRate: number;
14
+ let newPayeeRate: number;
15
+ let amountMinor: number;
16
+ let delayMs: number;
17
+ }
18
+ function run(ctx: any): Promise<{
19
+ customers: number;
20
+ customersBlocked: number;
21
+ falsePositiveRate: number;
22
+ frictionRate: number;
23
+ valueBlockedMinor: number;
24
+ byStage: {
25
+ [k: string]: {
26
+ events: number;
27
+ allowed: number;
28
+ stepUp: number;
29
+ blocked: number;
30
+ };
31
+ };
32
+ falsePositiveReasons: any;
33
+ warmup: {
34
+ attempts: number;
35
+ blocked: number;
36
+ stepUp: number;
37
+ allowed: number;
38
+ errors: number;
39
+ };
40
+ simulatedTime: boolean;
41
+ simulatedSpan: string;
42
+ attempts: number;
43
+ blocked: number;
44
+ stepUp: number;
45
+ allowed: number;
46
+ errors: number;
47
+ }>;
48
+ }
49
+ export default _default;
@@ -0,0 +1,17 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let description: string;
5
+ namespace defaultOptions {
6
+ let sourceUserId: any;
7
+ let muleCount: number;
8
+ let amountPerMuleMinor: number;
9
+ let retentionRatio: number;
10
+ let finalDestinationAccount: any;
11
+ let forwardDelayMinutes: number;
12
+ let delayMs: number;
13
+ let maxTotalAttempts: number;
14
+ }
15
+ function run(ctx: any): Promise<any>;
16
+ }
17
+ export default _default;
@@ -0,0 +1,28 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let description: string;
5
+ namespace defaultOptions {
6
+ let accountCount: number;
7
+ let bonusMinor: number;
8
+ let deviceCount: number;
9
+ let subnetCount: number;
10
+ let sharedPayoutAccount: any;
11
+ let claimBonus: boolean;
12
+ let delayMs: number;
13
+ let maxTotalAttempts: number;
14
+ }
15
+ function run(ctx: any): Promise<{
16
+ estimatedLossKobo: number;
17
+ attackerSucceeded: boolean;
18
+ sharedPayoutAccount: any;
19
+ devicesUsed: number;
20
+ subnetsUsed: number;
21
+ attempts: number;
22
+ blocked: number;
23
+ stepUp: number;
24
+ allowed: number;
25
+ errors: number;
26
+ }>;
27
+ }
28
+ export default _default;
@@ -0,0 +1,15 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let description: string;
5
+ namespace defaultOptions {
6
+ let targetUserId: any;
7
+ let benignActionsBeforeHijack: number;
8
+ let amountMinor: number;
9
+ let withdrawAfterHijack: boolean;
10
+ let hijackFromPersona: string;
11
+ let delayMs: number;
12
+ }
13
+ function run(ctx: any): Promise<any>;
14
+ }
15
+ export default _default;
@@ -0,0 +1,16 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let description: string;
5
+ namespace defaultOptions {
6
+ let targetUserId: any;
7
+ let minutesSincePort: number;
8
+ let amountMinor: number;
9
+ let reportPortEvent: boolean;
10
+ let attemptPasswordReset: boolean;
11
+ let ipPersona: string;
12
+ let delayMs: number;
13
+ }
14
+ function run(ctx: any): Promise<any>;
15
+ }
16
+ export default _default;
@@ -0,0 +1,28 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let description: string;
5
+ namespace defaultOptions {
6
+ let targetUserId: any;
7
+ let withdrawalCount: number;
8
+ let amountMinor: number;
9
+ let amountJitterMinor: number;
10
+ let intervalMinutes: number;
11
+ let sameDevice: boolean;
12
+ let delayMs: number;
13
+ let maxTotalAttempts: number;
14
+ }
15
+ function run(ctx: any): Promise<{
16
+ estimatedLossKobo: any;
17
+ attackerSucceeded: boolean;
18
+ simulatedTime: boolean;
19
+ simulatedSpan: string;
20
+ drainedBeforeFirstBlockMinor: any;
21
+ attempts: number;
22
+ blocked: number;
23
+ stepUp: number;
24
+ allowed: number;
25
+ errors: number;
26
+ }>;
27
+ }
28
+ export default _default;
@@ -0,0 +1,31 @@
1
+ declare namespace _default {
2
+ let id: string;
3
+ let name: string;
4
+ let description: string;
5
+ namespace defaultOptions {
6
+ let targetUserId: any;
7
+ let attemptCount: number;
8
+ let assumedLimit: number;
9
+ let assumedWindowSeconds: number;
10
+ let safetyMargin: number;
11
+ let jitterRatio: number;
12
+ let rotateDevice: boolean;
13
+ let ipPersona: string;
14
+ let delayMs: number;
15
+ let maxTotalAttempts: number;
16
+ }
17
+ function run(ctx: any): Promise<{
18
+ attackerSucceeded: any;
19
+ pacingMs: number;
20
+ effectiveRatePerWindow: number;
21
+ assumedLimit: any;
22
+ simulatedTime: boolean;
23
+ simulatedSpan: string;
24
+ attempts: number;
25
+ blocked: number;
26
+ stepUp: number;
27
+ allowed: number;
28
+ errors: number;
29
+ }>;
30
+ }
31
+ export default _default;
@@ -0,0 +1 @@
1
+ export { sseStream } from "./sse.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Server-Sent Events streaming helper.
3
+ *
4
+ * Wraps an Express-style response object to stream simulation events to a
5
+ * browser dashboard as they happen, instead of waiting for the full report.
6
+ *
7
+ * @example
8
+ * import { sseStream } from 'fraud-sim/streaming';
9
+ *
10
+ * app.get('/simulate/:scenario/stream', async (req, res) => {
11
+ * const stream = sseStream(res);
12
+ * await run(req.params.scenario, {
13
+ * target,
14
+ * onEvent: (event) => stream.send(event),
15
+ * });
16
+ * stream.close();
17
+ * });
18
+ */
19
+ /**
20
+ * Set up SSE headers on `res` and return a `{ send, close }` handle.
21
+ *
22
+ * Handles backpressure: if `res.write` reports the internal buffer is full
23
+ * (returns `false`), further events queue in memory until the response
24
+ * emits `drain`, then flush in order.
25
+ *
26
+ * @param {object} res - Any Express-style response with `write`, `end`,
27
+ * `writeHead` (or settable `.statusCode`/`.setHeader`), and an
28
+ * EventEmitter-style `on('drain', ...)`.
29
+ * @returns {{ send: (event: object) => void, close: () => void }}
30
+ */
31
+ export function sseStream(res: object): {
32
+ send: (event: object) => void;
33
+ close: () => void;
34
+ };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * What a target says about one scored event.
3
+ *
4
+ * `decision` is deliberately typed loosely: a target may return a value outside
5
+ * the three canonical ones, and a scenario counts anything unrecognised as an
6
+ * error rather than guessing. `'ERROR'` is produced by the adapters themselves
7
+ * for failures that never reached a decision.
8
+ */
9
+ export type TargetResponse = {
10
+ decision: "ALLOW" | "STEP_UP" | "BLOCK" | "ERROR" | string;
11
+ riskScore?: number | null;
12
+ reasons?: Array<{
13
+ code: string;
14
+ label?: string;
15
+ weight?: number;
16
+ }>;
17
+ /**
18
+ * - The target's unmapped response body.
19
+ */
20
+ raw?: object;
21
+ httpStatus?: number;
22
+ /**
23
+ * - True when the call failed rather than
24
+ * being scored: rate limiting, a 5xx, a timeout. Excluded from block rates.
25
+ */
26
+ infraError?: boolean;
27
+ retryAfter?: string | null;
28
+ };
29
+ /**
30
+ * The bridge between scenarios and the system under test.
31
+ *
32
+ * Not every target implements every method. A scenario that needs one you have
33
+ * not provided fails with a message naming it, rather than silently skipping
34
+ * that part of the attack.
35
+ */
36
+ export type TargetAdapter = {
37
+ scoreLogin?: (payload: object) => Promise<TargetResponse>;
38
+ scoreWithdrawal?: (payload: object) => Promise<TargetResponse>;
39
+ scoreAction?: (payload: object) => Promise<TargetResponse>;
40
+ reportEvent?: (eventType: string, data: object) => Promise<void>;
41
+ };
42
+ /**
43
+ * One thing that happened during a run.
44
+ */
45
+ export type ScenarioEvent = {
46
+ type: "attempt" | "response" | "milestone" | "warning" | "error" | "summary" | "attacker_adapts" | string;
47
+ timestamp: number;
48
+ round?: number;
49
+ /**
50
+ * - Which phase of a multi-stage attack this belongs to.
51
+ */
52
+ stage?: string;
53
+ payload?: object;
54
+ response?: TargetResponse;
55
+ latencyMs?: number;
56
+ message?: string;
57
+ /**
58
+ * - Declared geo/reputation for the payload's IP,
59
+ * present only for pool-backed addresses.
60
+ */
61
+ ipIntel?: object;
62
+ };
63
+ /**
64
+ * What the runner provides to a scenario.
65
+ */
66
+ export type ScenarioContext = {
67
+ /**
68
+ * - Defaults merged with the caller's options.
69
+ */
70
+ options: object;
71
+ /**
72
+ * - Already wrapped with the run's rate limit.
73
+ */
74
+ target: TargetAdapter;
75
+ /**
76
+ * - Present only for scenarios that use one.
77
+ */
78
+ llm?: object;
79
+ emit: (event: ScenarioEvent) => void;
80
+ signal: AbortSignal;
81
+ };
82
+ /**
83
+ * An attack. Everything concrete about how traffic is generated lives here.
84
+ *
85
+ * `defaultOptions` is not documentation — it is the scenario's option surface.
86
+ * The runner rejects any option key it does not declare, so a typo fails
87
+ * loudly rather than silently falling back to a default.
88
+ */
89
+ export type Scenario = {
90
+ id: string;
91
+ name: string;
92
+ description: string;
93
+ defaultOptions?: object;
94
+ run: (ctx: ScenarioContext) => Promise<object>;
95
+ };
96
+ /**
97
+ * Scenario metadata, as returned by `listScenarios()`.
98
+ */
99
+ export type ScenarioMetadata = {
100
+ id: string;
101
+ name: string;
102
+ description: string;
103
+ defaultOptions?: object;
104
+ };
105
+ /**
106
+ * The result of one run.
107
+ *
108
+ * Note the denominators. `blockRate` and `stepUpRate` are computed over
109
+ * `scoredAttempts`, not `attempts`: an attempt that failed before reaching a
110
+ * decision is not evidence about the defence either way, so counting it as a
111
+ * block would let a target look better the more it fell over. Check
112
+ * `errorRate` before reading anything into a run.
113
+ *
114
+ * Scenarios add their own fields — `attackerSucceeded`, `estimatedLossKobo`,
115
+ * `blockedAtStage` and so on — which is why this is not a closed shape.
116
+ */
117
+ export type ScenarioReport = {
118
+ scenarioId: string;
119
+ scenarioName: string;
120
+ startedAt: number;
121
+ completedAt: number;
122
+ durationMs: number;
123
+ /**
124
+ * - Everything the scenario tried.
125
+ */
126
+ attempts: number;
127
+ /**
128
+ * - Attempts that came back with a decision.
129
+ */
130
+ scoredAttempts: number;
131
+ blocked: number;
132
+ stepUp: number;
133
+ allowed: number;
134
+ /**
135
+ * - Attempts that never reached a decision.
136
+ */
137
+ errors: number;
138
+ /**
139
+ * - `blocked / scoredAttempts`.
140
+ */
141
+ blockRate: number;
142
+ /**
143
+ * - `stepUp / scoredAttempts`.
144
+ */
145
+ stepUpRate: number;
146
+ /**
147
+ * - `errors / attempts`.
148
+ */
149
+ errorRate: number;
150
+ /**
151
+ * - Present only when the run was seeded.
152
+ */
153
+ seed?: number;
154
+ /**
155
+ * - True when the scenario advanced
156
+ * timestamps rather than sleeping; results are only faithful if the target
157
+ * derives its windows from the event timestamp.
158
+ */
159
+ simulatedTime?: boolean;
160
+ simulatedSpan?: string;
161
+ attackerSucceeded?: boolean;
162
+ estimatedLossKobo?: number;
163
+ };
164
+ /**
165
+ * Totals across a suite. Same denominator rule as a single run.
166
+ */
167
+ export type SuiteAggregate = {
168
+ attempts: number;
169
+ scoredAttempts: number;
170
+ blocked: number;
171
+ stepUp: number;
172
+ allowed: number;
173
+ errors: number;
174
+ blockRate: number;
175
+ stepUpRate: number;
176
+ errorRate: number;
177
+ };
178
+ /**
179
+ * The result of `runSuite()`.
180
+ */
181
+ export type SuiteReport = {
182
+ suiteName: string;
183
+ startedAt: string;
184
+ completedAt: string;
185
+ /**
186
+ * - How many scenarios ran at once.
187
+ */
188
+ concurrency: number;
189
+ scenarioReports: ScenarioReport[];
190
+ aggregate: SuiteAggregate;
191
+ };